diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml new file mode 100644 index 0000000..dfffd28 --- /dev/null +++ b/.github/workflows/integration-test.yml @@ -0,0 +1,88 @@ +name: Integration test + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +env: + FORCE_COLOR: 1 + +jobs: + integration: + name: Translate, build & install on Python ${{ matrix.python }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python: [3.13, 3.14, 3.15] + + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: ${{ matrix.python }} + allow-prereleases: true + + - name: Install build dependencies + run: python -m pip install babel hatchling build + + - name: Create a new translation + run: | + python scripts/i18n.py extract + python scripts/i18n.py init eo + + - name: Translate one docstring + run: | + python - <<'PY' + import sys + sys.path.insert(0, "scripts") + import i18n + + path = i18n.PO_DIR / "eo.po" + catalog = i18n.read_catalog(path) + for message in catalog: + if "turtle.Turtle.forward" in message.auto_comments: + message.string = "Movu la testudon antaŭen." + break + # A nonexistent name to check it is dropped at import. + catalog.add("Not a real docstring.", "Ne vera dokumentaĵo.", + auto_comments=["turtle.Turtle.nonexistent"]) + i18n.write_catalog(catalog, path) + PY + python scripts/i18n.py stats + + - name: Build and install the wheel + run: | + python -m build --wheel + python -m pip install dist/*.whl + + - name: Check the translated docstring is used + working-directory: ${{ runner.temp }} + run: | + echo "language = eo" > turtle.cfg + cat > check.py <<'PY' + import contextlib + import io + + # turtle prints errors for names it cannot resolve. + with contextlib.redirect_stdout(io.StringIO()) as output: + import turtle + assert output.getvalue() == "" + + assert turtle.Turtle.forward.__doc__ == "Movu la testudon antaŭen." + assert turtle.forward.__doc__ == "Movu la testudon antaŭen." + assert "Move the turtle backward" in turtle.back.__doc__ + + import turtle_docstringdict_eo + assert "Turtle.nonexistent" not in turtle_docstringdict_eo.docsdict + assert not hasattr(turtle.Turtle, "nonexistent") + PY + python check.py diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index a9058e3..ab191b2 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,6 +1,10 @@ name: Lint -on: [push, pull_request, workflow_dispatch] +on: + push: + branches: [main] + pull_request: + workflow_dispatch: permissions: {} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 58f6bae..8b85569 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -2,6 +2,7 @@ name: Upload package on: push: + branches: [main] pull_request: release: types: diff --git a/.gitignore b/.gitignore index 6e7ae53..b56693b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ # Compiled from po/*.po -/turtle_translations/*.py -!/turtle_translations/__init__.py +/turtle_docstringdict_*.py # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/README.md b/README.md index 984b44a..8397c4d 100644 --- a/README.md +++ b/README.md @@ -52,12 +52,16 @@ pl 42/103 translated (40%), 3 fuzzy ### Compiling -Each PO file compiles to a `turtle_translations/.py` module containing a -`docsdict`, which is what `turtle` loads. The generated modules are built -automatically when the wheel is built, so you normally only need this to test -locally: +Each PO file compiles to a top-level `turtle_docstringdict_.py` module +containing a `docsdict`, which is what `turtle` imports. Untranslated and fuzzy +entries are left out so their help stays English. When imported, the module drops +entries for names that do not exist in the running version of `turtle`, so a +single module built from the newest Python works on older ones too. + +The modules are built automatically when the wheel is built, so you normally +only need this to test locally: ```console $ python scripts/i18n.py compile -Compiled: turtle_translations/pl.py +Compiled: turtle_docstringdict_pl.py ``` diff --git a/scripts/hook.py b/scripts/hook.py index 47963c8..31fdf25 100644 --- a/scripts/hook.py +++ b/scripts/hook.py @@ -1,6 +1,7 @@ """Hatchling build hook to compile the PO catalogs when the wheel is built.""" import sys +import tempfile from pathlib import Path from hatchling.builders.hooks.plugin.interface import BuildHookInterface @@ -11,7 +12,9 @@ class CustomBuildHook(BuildHookInterface): def initialize(self, version, build_data): - # The generated modules are gitignored, so hatch need to be told to ship them. - build_data["artifacts"] = [ - str(path.relative_to(self.root)) for path in i18n._compile_catalogs() - ] + self._build_dir = tempfile.TemporaryDirectory() + for path in i18n._compile_catalogs(self._build_dir.name): + build_data["force_include"][str(path)] = path.name + + def finalize(self, version, build_data, artifact_path): + self._build_dir.cleanup() diff --git a/scripts/i18n.py b/scripts/i18n.py index c8f470d..2359cae 100755 --- a/scripts/i18n.py +++ b/scripts/i18n.py @@ -10,7 +10,6 @@ ROOT = Path(__file__).resolve().parent.parent PO_DIR = ROOT / "po" -PACKAGE_DIR = ROOT / "turtle_translations" POT = PO_DIR / "turtle.pot" PROJECT = "turtle-translations" BUGS_ADDRESS = "https://github.com/python/turtle-translations/issues" @@ -115,16 +114,38 @@ def _load_docsdict(path): } -def _compile_catalogs(): +MODULE_FOOTER = """ +# turtle imports this module after defining its classes, so drop entries for +# names this version of turtle does not have to avoid stdout reports. +import turtle + +for _key in list(docsdict): + _obj = turtle + for _attr in _key.split("."): + _obj = getattr(_obj, _attr, None) + if _obj is None: + del docsdict[_key] +""" + + +def _render_module(source, docsdict): + lines = [f"# Generated from {source.name}.", + "", + "docsdict = {" + ] + for key, doc in sorted(docsdict.items()): + lines.append(f" {key!r}: {doc!r},") + lines.append("}") + return "\n".join(lines) + "\n" + MODULE_FOOTER + + +def _compile_catalogs(output_dir=ROOT): + """Write a `turtle_docstringdict_.py` module for each PO file.""" written = [] for path in po_files(): # turtle lowercases the language before importing it! - target = PACKAGE_DIR / f"{path.stem.lower()}.py" - lines = [f"# Generated from {path.name}.", "", "docsdict = {"] - for key, doc in sorted(_load_docsdict(path).items()): - lines.append(f" {key!r}: {doc!r},") - lines.append("}") - target.write_text("\n".join(lines) + "\n", encoding="utf-8") + target = Path(output_dir) / f"turtle_docstringdict_{path.stem.lower()}.py" + target.write_text(_render_module(path, _load_docsdict(path)), encoding="utf-8") written.append(target) return written diff --git a/turtle_translations/__init__.py b/turtle_translations/__init__.py deleted file mode 100644 index 7c02458..0000000 --- a/turtle_translations/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -"""Docstring translations for the Python 'turtle' module.""" - -import pkgutil - - -def available(): - """Return a list of the available languages.""" - return sorted( - info.name for info in pkgutil.iter_modules(__path__) - if not info.name.startswith("_") - )