Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release/include qm #85

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/configuration/exclude.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ If you want to avoid some files to be shipped with your plugin, create a `.gitat

```gitignore
resources.qrc export-ignore
*.ts export-ignore
*.pro export-ignore
```
41 changes: 31 additions & 10 deletions qgispluginci/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def create_archive(
raise_min_version: str = None,
disable_submodule_update: bool = False,
):

plugin_folder_path = Path(parameters.plugin_path)
repo = git.Repo()

top_tar_handle, top_tar_file = mkstemp(suffix=".tar")
Expand All @@ -53,7 +53,7 @@ def create_archive(
initial_stash = None
diff = repo.index.diff(None)
if diff:
print("Uncommitted changes:")
print("\n*** Uncommitted changes:")
for diff in diff:
print(diff)
if not allow_uncommitted_changes:
Expand Down Expand Up @@ -129,9 +129,11 @@ def create_archive(
stash = "HEAD"
if stash == "" or stash is None:
stash = "HEAD"

# create TAR archive
print("archive plugin with stash: {}".format(stash))
print("\nArchive plugin with stash: {}".format(stash))
repo.git.archive(stash, "-o", top_tar_file, parameters.plugin_path)

# adding submodules
for submodule in repo.submodules:
_, sub_tar_file = mkstemp(suffix=".tar")
Expand Down Expand Up @@ -160,11 +162,10 @@ def create_archive(
# add translation files
if add_translations:
with tarfile.open(top_tar_file, mode="a") as tt:
print("adding translations")
for file in glob("{}/i18n/*.qm".format(parameters.plugin_path)):
print(" adding translation: {}".format(os.path.basename(file)))
# https://stackoverflow.com/a/48462950/1548052
tt.add(file)
print("\nAdding translations...")
for qm in plugin_folder_path.glob("**/*.qm"):
tt.add(qm)
print(f"Translation added: {qm}")

# compile qrc files
pyqt5ac.main(
Expand All @@ -189,6 +190,7 @@ def create_archive(
# adding the content of TAR archive
with tarfile.open(top_tar_file, mode="r:") as tt:
for m in tt.getmembers():
print(m)
if m.isdir():
continue
f = tt.extractfile(m)
Expand Down Expand Up @@ -382,6 +384,7 @@ def release(
release_tag: str = None,
github_token: str = None,
upload_plugin_repo_github: bool = False,
local_translation: bool = False,
transifex_token: str = None,
osgeo_username: str = None,
osgeo_password: str = None,
Expand Down Expand Up @@ -423,12 +426,30 @@ def release(
parser = ChangelogParser()
release_version = parser.latest_version()

# check parameters conflicts about translation
if local_translation and transifex_token:
print(
"Translation source can't be both from Transifex and local. "
"Transifex will be preferred."
)
local_translation = False

if transifex_token is not None:
tr = Translation(
parameters, create_project=False, transifex_token=transifex_token
parameters=parameters,
source_translation="transifex",
transifex_create_project=False,
transifex_token=transifex_token,
)
tr.process_translation_transifex()
tr.pull()
tr.compile_strings()
elif local_translation:
print("Using local translations")
tr = Translation(parameters=parameters, source_translation="local")
tr.process_translation_local()
else:
print("No translation operations")

archive_name = parameters.archive_name(parameters.plugin_path, release_version)

Expand All @@ -441,7 +462,7 @@ def release(
parameters,
release_version,
archive_name,
add_translations=transifex_token is not None,
add_translations=any([transifex_token, local_translation]),
allow_uncommitted_changes=allow_uncommitted_changes,
is_prerelease=is_prerelease,
disable_submodule_update=disable_submodule_update,
Expand Down
216 changes: 190 additions & 26 deletions qgispluginci/translation.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import glob
import subprocess
from pathlib import Path
from typing import Union

from pytransifex import Transifex

Expand All @@ -15,49 +16,73 @@

class Translation:
def __init__(
self, parameters: Parameters, transifex_token: str, create_project: bool = True
self,
parameters: Parameters,
source_translation: str = "transifex",
# transifex specific
transifex_token: str = None,
transifex_create_project: bool = True,
):
"""Manage translation files for the plugin.

Args:
parameters (Parameters): CLI parameters
source_translation (str, optional): source of translation to use. \
Implemented: local or transifex. Defaults to "transifex".
transifex_token (str, optional): Transifex API token. Defaults to None.
transifex_create_project (bool, optional): if True, it will create the \
project, resource and language on Transifex. Defaults to True.

Raises:
TranslationFailed: [description]
"""
Parameters
----------
parameters:
# store parameters as attributes
self.parameters = parameters

transifex_token:
Transifex API token
if source_translation == "transifex":
self.transifex_create_project = transifex_create_project
self._t = Transifex(
api_token=transifex_token,
organization=parameters.transifex_organization,
i18n_type="QT",
)
assert self._t.ping()

create_project:
if True, it will create the project, resource and language on Transifex
elif source_translation == "local":
pass
else:
raise NotImplementedError("Source of translation not implemented")

def process_translation_transifex(self):
"""Process translation using Transifxe.

Raises:
TranslationFailed: if something went wrong
"""
self.parameters = parameters
self._t = Transifex(
transifex_token, parameters.transifex_organization, i18n_type="QT"
)
assert self._t.ping()
self.ts_file = "{dir}/i18n/{res}_{lan}.ts".format(
dir=self.parameters.plugin_path,
res=self.parameters.transifex_resource,
lan=self.parameters.translation_source_language,
)

if self._t.project_exists(parameters.transifex_project):
if self._t.project_exists(self.parameters.transifex_project):
print(
"Project {o}/{p} exists on Transifex".format(
o=self.parameters.transifex_organization,
p=self.parameters.transifex_project,
)
)
elif create_project:
elif self.transifex_create_project:
print(
"project does not exists on Transifex, creating one as {o}/{p}".format(
o=self.parameters.transifex_organization,
p=self.parameters.transifex_project,
)
)
self._t.create_project(
self._t.transifex_create_project(
slug=self.parameters.transifex_project,
repository_url=self.parameters.repository_url,
source_language_code=parameters.translation_source_language,
source_language_code=self.parameters.translation_source_language,
)
self.update_strings()
print(
Expand All @@ -82,6 +107,88 @@ def __init__(
)
)

def process_translation_local(self):
"""Process the local translation files."""
# check if there is a *.pro file
project_file = self.get_qmake_project_file()
if not project_file:
project_file = self.create_qmake_project_file_on_fly()
temp_pro_file = True
else:
temp_pro_file = False

# update strings
self.run_lupdate(project_file=project_file, verbose=True)
self.run_lrelease()

# clean up
if temp_pro_file:
project_file.unlink()

def get_qmake_project_file(self) -> Union[Path, None]:
"""Look for a *.pro file in the plugin directory.

Raises:
Warning: if more than one *.pro file is found.

Returns:
Union[Path, None]: path to the *.pro file or None if not found.
"""
plugin_folder_path = Path(self.parameters.plugin_path)
qmake_project_files = sorted(list(plugin_folder_path.glob("**/*.pro")))

if not len(qmake_project_files):
print("No QMake project file (.pro) found.")
return None
elif len(qmake_project_files) == 1:
print(f"QMake project file (.pro) found: {qmake_project_files[0]}")
return qmake_project_files[0]
else:
raise Warning(
"There are more than one QMake project file (.pro) file in the plugin "
f"folder. The first will be used as fallback: {qmake_project_files[0]}"
)

def create_qmake_project_file_on_fly(self) -> Path:
"""Generate a QMake project file (*.pro) on the fly from forms (*.ui), \
sources (*.py) and selected languages for translation (*.ts).

Returns:
Path: project file (*.pro) path
"""
plugin_folder_path = Path(self.parameters.plugin_path)

# listing relevant files in the plugin folder
source_py_files = sorted(
str(src.relative_to(plugin_folder_path))
for src in plugin_folder_path.glob("**/*.py")
)
source_ui_files = sorted(
str(src.relative_to(plugin_folder_path))
for src in plugin_folder_path.glob("**/*.ui")
)
ts_files = sorted(
str(src.relative_to(plugin_folder_path))
for src in plugin_folder_path.glob("**/*.ts")
)

# if no ts_files, create empty files for select translations
for lang in self.parameters.translation_languages:
ts_file = f"{plugin_folder_path}/i18n/{self.parameters.transifex_resource}_{lang}.ts"
print(ts_file)

# write output project file
project_file = plugin_folder_path / f"{self.parameters.plugin_slug}.pro"

with project_file.open(mode="w", encoding="UTF8") as f:
f.write("CODECFORTR = UTF-8\n\n")
f.write("FORMS = {} \n\n".format(" ".join(source_ui_files)))
f.write("SOURCES = {} \n\n".format(" ".join(source_py_files)))
f.write("TRANSLATIONS = {}\n\n".format("\n".join(ts_files)))

print(f"QMake project file temporarily created on the fly: {project_file}")
return project_file

def update_strings(self):
"""
Update TS files from plugin source strings
Expand Down Expand Up @@ -120,17 +227,10 @@ def update_strings(self):
f.flush()
f.close()

cmd = [self.parameters.pylupdate5_path, "-noobsolete", str(project_file)]

output = subprocess.run(cmd, capture_output=True, text=True)

# run (py)lupdate(5) path
self.run_lupdate(project_file=project_file, drop_obsoletes=True)
project_file.unlink()

if output.returncode != 0:
raise TranslationFailed(output.stderr)
else:
print("Successfully run pylupdate5: {}".format(output.stdout))

def compile_strings(self):
"""
Compile TS file into QM files
Expand All @@ -146,6 +246,7 @@ def compile_strings(self):
else:
print("Successfully run lrelease: {}".format(output.stdout))

# -- TRANSIFEX ---------------------------------------------------------------------
def pull(self):
"""
Pull TS files from Transifex
Expand Down Expand Up @@ -219,3 +320,66 @@ def __get_resource(self) -> dict:
)
)
return resources[0]

# -- SUBPROCESSES ------------------------------------------------------------------
def run_lrelease(self):
"""Execute the Qt Linguist release tool to compile TS file into QM files.

Raises:
subprocess.CalledProcessError: if the subprocess fails.
"""
plugin_folder_path = Path(self.parameters.plugin_path)
# cmd base
cmd = [self.parameters.lrelease_path]

# append translation files
for ts in plugin_folder_path.glob("**/*.ts"):
cmd.append(str(ts))

print(f"Running Qt Linguist release command: {cmd}")
output = subprocess.run(cmd, capture_output=True, text=True)

if output.returncode != 0:
raise subprocess.CalledProcessError(output.stderr)
else:
print(
"Successfully run lrelease: {}".format(output.stdout or output.stderr)
)

def run_lupdate(
self, project_file: Path, drop_obsoletes: bool = True, verbose: bool = False
):
"""Execute the Qt Linguist CLI defined in parameters (typically lupdate or \
pylupdate5) to update *.ts files from a project file (.pro).

Args:
project_file (Path): QMake project file (*.pro)
drop_obsoletes (bool, optional): add '-noobsolete' option to drop all \
obsolete and vanished strings. Defaults to True.
verbose (bool, optional): add verbose option. Defaults to False.

Raises:
subprocess.CalledProcessError: if the subprocess fails.
"""
# cmd base
cmd = [self.parameters.pylupdate5_path]

# options
if drop_obsoletes:
cmd.append("-noobsolete")

if verbose:
cmd.append("-verbose")

# add project file
cmd.append(str(project_file))

print(f"Running Qt Linguist update command: {cmd}")
output = subprocess.run(cmd, capture_output=True, text=True)

if output.returncode != 0:
raise subprocess.CalledProcessError(output.stderr)
else:
print(
"Successfully run pylupdate5: {}".format(output.stdout or output.stderr)
)
Loading