Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 2 additions & 2 deletions .github/workflows/dep-audit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ jobs:
run: |
uv pip freeze > /tmp/requirements-frozen.txt
pip-audit --requirement /tmp/requirements-frozen.txt --disable-pip --no-deps --format markdown --desc on
- name: pip-audit severity gate (CRITICAL and HIGH only)
- name: pip-audit severity gate (CRITICAL only)
run: |
pip-audit-extra --local --fail-level HIGH
pip-audit-extra --local --fail-level CRITICAL
- name: liccheck
run: |
uv pip freeze > /tmp/requirements-frozen.txt
Expand Down
14 changes: 4 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,25 +83,19 @@ flowchart TB

## WEBLATE_FORMATS configuration

Weblate’s settings define `WEBLATE_FORMATS` as a tuple of **dotted import paths** to format classes. The official Docker image evaluates a single optional file after base settings: if `/app/data/settings-override.py` exists, it is compiled and executed with `exec()` in the **same namespace** as the rest of `weblate.settings_docker`, so `WEBLATE_FORMATS` is already defined and you **extend** it rather than replacing it.
Weblate discovers formats from the `WEBLATE_FORMATS` setting (see `FileFormatLoader` in upstream `weblate.formats.models`). The official Docker image evaluates a single optional file after base settings: if `/app/data/settings-override.py` exists, it is compiled and executed with `exec()` in the **same namespace** as the rest of `weblate.settings_docker`.

This repository ships `settings-override.py` as a copy-paste / image-layer snippet:

```python
WEBLATE_FORMATS += ( # noqa: F821 # defined by Weblate before exec()
"boost_weblate.formats.quickbook.QuickBookFormat",
)
```
Stock `weblate.settings_docker` does **not** always bind `WEBLATE_FORMATS` in that namespace before the hook runs, so a bare `WEBLATE_FORMATS += (...)` in the override can raise `NameError`. This repository ships ``src/boost_weblate/settings_override.py`` as the Docker ``exec()`` fragment: it assigns ``WEBLATE_FORMATS`` by **reading** upstream ``weblate/formats/models.py`` and regex-slicing ``FormatsConf.FORMATS`` (aligned with the installed Weblate version without importing ``weblate.formats.models`` during settings load, which can raise ``AppRegistryNotReady``). It appends the endpoint Django app via ``INSTALLED_APPS += ("boost_weblate.endpoint.apps.BoostEndpointConfig",)``. If you also set ``WEBLATE_ADD_APPS`` to the same app, remove one source to avoid duplicate ``INSTALLED_APPS`` entries.

**Operators:** ensure the plugin package is installed in the Weblate environment (`pip` / image layer), then install the override file where Weblate expects it. For the stock Docker layout:

```dockerfile
COPY settings-override.py /app/data/settings-override.py
```

That path is fixed; Weblate does not scan `DATA_DIR` for arbitrary override files. The override file is **not** the same as `WEBLATE_PY_PATH` / `python/customize` (importable customization on `sys.path`); for format registration, use this exec hook unless your image explicitly imports another settings module. See the comments in `settings-override.py` for the full distinction.
That path is fixed; Weblate does not scan `DATA_DIR` for arbitrary override files. The override file is **not** the same as `WEBLATE_PY_PATH` / `python/customize` (importable customization on `sys.path`); for format registration, use this exec hook unless your image explicitly imports another settings module. See the comments in `settings_override.py` for the full distinction.

**Adding another format:** implement a new class under `boost_weblate.formats`, append its dotted path as another tuple element (trailing comma is fine), redeploy, and restart Weblate so settings are reloaded.
**Adding another format:** implement the class under `boost_weblate/formats/`, append its dotted class path in ``weblate_formats_with_quickbook()`` (or extend the tuple built there), redeploy, and restart Weblate. If upstream changes the layout of ``FormatsConf`` in ``models.py``, update the regex in ``settings_override.py`` accordingly.

## Contributing

Expand Down
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ classifiers = [
"Topic :: Software Development :: Localization"
]
dependencies = [
"Weblate>=5.16.0"
"Weblate[all]==5.17.1"
]
description = "Standalone Weblate plugin for Boost documentation translation."
keywords = [
Expand Down Expand Up @@ -98,7 +98,8 @@ authorized_licenses = [
"ofl",
"zpl"
]
authorized_packages = {pyaskalono = ">=0.2.0"}
# google-crc32c: PyPI has no License/classifiers (liccheck UNKNOWN); upstream is Apache-2.0.
authorized_packages = {pyaskalono = ">=0.2.0", "google-crc32c" = ">=1.8.0"}
level = "cautious"
unauthorized_licenses = []

Expand Down
99 changes: 63 additions & 36 deletions src/boost_weblate/settings_override.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,69 @@
#
# SPDX-License-Identifier: BSL-1.0

# QuickBook format registration for cppa-weblate-plugin (upstream Weblate from PyPI
# plus pip install).
#
# Relationship to Weblate Docker settings (see ``weblate.settings_docker``):
# - After environment variables are applied, Weblate sets ``ADDITIONAL_CONFIG`` to a
# fixed path (upstream: ``Path("/app/data/settings-override.py")``) and, if that file
# exists, compiles the file and runs it with ``exec()`` in the *same* namespace as
# the rest
# of ``settings_docker``. There is no directory walk or pattern match under
# ``DATA_DIR`` / ``WEBLATE_DATA_DIR`` for this hook—only that single file path.
# - ``DATA_DIR`` (default ``/app/data`` via ``WEBLATE_DATA_DIR``) is the data volume
# root; the override file lives beside it as ``…/settings-override.py`` (hyphen),
# not inside ``…/python/customize/`` unless your own image wires an extra import.
#
# ``/app/data/python/customize`` (``WEBLATE_PY_PATH`` in the official container):
# - The ``customize`` Django app (first in ``INSTALLED_APPS``) is for importable
# customization code, static files, and templates on ``sys.path``—parallel to the
# exec hook above, not a substitute for it. Stock Weblate does not auto-import
# ``customize.settings_override``; use the path below unless your Dockerfile extends
# ``weblate.settings_docker`` to load another module explicitly.
#
# CD / image build — copy this file to the path Weblate execs (official Docker). The
# wheel exposes it as ``boost_weblate/settings_override.py`` (underscore: valid Python
# module path); Weblate still loads only ``…/settings-override.py`` (hyphen) on disk:
#
# COPY …/site-packages/boost_weblate/settings_override.py
# /app/data/settings-override.py
#
# From a plugin source checkout, ``COPY src/boost_weblate/settings_override.py`` with
# the same destination also works.
"""Docker ``settings-override.py`` fragment for QuickBook and the Boost endpoint app.

Weblate's official image runs this file with ``exec()`` in the same namespace as
``weblate.settings_docker`` (see upstream ``ADDITIONAL_CONFIG``). Copy this module to
``/app/data/settings-override.py`` (hyphen on disk) or keep it on ``PYTHONPATH`` and
point your image at the same content.

``WEBLATE_FORMATS`` is built by **reading** ``weblate/formats/models.py`` as text and
regex-slicing ``FormatsConf.FORMATS``. That avoids ``import weblate.formats.models``,
which pulls in Django ORM classes during settings import and raises
``AppRegistryNotReady``. The slice is **layout-sensitive**: it assumes ``FORMATS = (``
inside ``FormatsConf`` is followed by ``class Meta:`` at the same indent; if upstream
reformats that class, update the pattern here.

``INSTALLED_APPS`` is extended via ``globals().get("INSTALLED_APPS")`` when this file
is ``exec``'d (Docker): the list exists in the settings namespace. Importing this
module for tests still defines ``WEBLATE_FORMATS`` on the module without mutating
Django settings.
"""

from __future__ import annotations

WEBLATE_FORMATS += ( # noqa: F821 # defined by Weblate before exec()
"boost_weblate.formats.quickbook.QuickBookFormat",
import re
from pathlib import Path

# Package ``__init__`` is empty; does not import ``formats.models``.
import weblate.formats

_QUICKBOOK_FORMAT = "boost_weblate.formats.quickbook.QuickBookFormat"
_ENDPOINT_APP_CONFIG = "boost_weblate.endpoint.apps.BoostEndpointConfig"

_FORMATS_BLOCK = re.compile(
r"^\s{4}FORMATS\s*=\s*\(([\s\S]*?)\)\s*\n\s{4}class Meta:",
re.MULTILINE,
)
_STRING_LITERAL = re.compile(r'"([^"\\]*)"(?:\s*,|\s*$)', re.MULTILINE)


def weblate_formats_with_quickbook() -> tuple[str, ...]:
"""Upstream ``FormatsConf.FORMATS`` paths plus QuickBook.

Avoids importing ``weblate.formats.models``.
"""
models_py = Path(weblate.formats.__file__).resolve().parent / "models.py"
src = models_py.read_text(encoding="utf-8")
m = _FORMATS_BLOCK.search(src)
if not m:
msg = f"boost_weblate: could not parse FormatsConf.FORMATS from {models_py}"
raise RuntimeError(msg)
body = m.group(1)
core = tuple(
p for p in _STRING_LITERAL.findall(body) if p.startswith("weblate.formats.")
)
if not core:
msg = f"boost_weblate: no format paths parsed from {models_py}"
raise RuntimeError(msg)
if _QUICKBOOK_FORMAT in core:
return core
return core + (_QUICKBOOK_FORMAT,)


WEBLATE_FORMATS = weblate_formats_with_quickbook()

# Plugin Django app (``boost_weblate.endpoint``): registers ``/boost-endpoint/`` URLs
# from ``AppConfig.ready()``. The full config class path matches ``WEBLATE_ADD_APPS``
# style installs (e.g. ``WEBLATE_ADD_APPS=boost_weblate.endpoint`` in Docker).
INSTALLED_APPS += ("boost_weblate.endpoint.apps.BoostEndpointConfig",) # noqa: F821
_INSTALLED_APPS = globals().get("INSTALLED_APPS")
if _INSTALLED_APPS is not None:
_INSTALLED_APPS += (_ENDPOINT_APP_CONFIG,)
93 changes: 93 additions & 0 deletions tests/test_settings_override.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# SPDX-FileCopyrightText: 2026 Andrew Zhang <whisper67265@outlook.com>
#
# SPDX-License-Identifier: BSL-1.0

"""Tests for ``boost_weblate.settings_override`` (Docker ``exec`` fragment)."""

from __future__ import annotations

import ast
import importlib.util
from pathlib import Path

_QBK = "boost_weblate.formats.quickbook.QuickBookFormat"


def _load_weblate_formats_models_source() -> str:
spec = importlib.util.find_spec("weblate")
if spec is None or not spec.submodule_search_locations:
msg = "Weblate is not installed"
raise AssertionError(msg)
path = Path(spec.submodule_search_locations[0]) / "formats" / "models.py"
return path.read_text(encoding="utf-8")


def _parse_formatsconf_formats_ast(models_text: str) -> list[str]:
tree = ast.parse(models_text)
for node in tree.body:
if isinstance(node, ast.ClassDef) and node.name == "FormatsConf":
return _formats_assignment_to_strings(node.body)
msg = "Class FormatsConf not found in weblate formats models source"
raise AssertionError(msg)


def _formats_assignment_to_strings(class_body: list[ast.stmt]) -> list[str]:
for node in class_body:
if not isinstance(node, ast.Assign):
continue
for target in node.targets:
if isinstance(target, ast.Name) and target.id == "FORMATS":
return _string_tuple_or_list(node.value)
msg = "FORMATS assignment not found on FormatsConf"
raise AssertionError(msg)


def _string_tuple_or_list(node: ast.expr) -> list[str]:
if isinstance(node, (ast.Tuple, ast.List)):
out: list[str] = []
for elt in node.elts:
if isinstance(elt, ast.Constant) and isinstance(elt.value, str):
out.append(elt.value)
else:
msg = f"Unexpected literal in FormatsConf.FORMATS: {ast.dump(elt)}"
raise AssertionError(msg)
return out
msg = f"Unexpected FormatsConf.FORMATS value: {ast.dump(node)}"
raise AssertionError(msg)


def test_settings_override_formats_match_ast_parse_of_upstream() -> None:
from boost_weblate.settings_override import weblate_formats_with_quickbook

stock = _parse_formatsconf_formats_ast(_load_weblate_formats_models_source())
got = weblate_formats_with_quickbook()
assert got[: len(stock)] == tuple(stock)
assert got[len(stock)] == _QBK
assert len(got) == len(stock) + 1


def test_settings_override_module_defines_weblate_formats() -> None:
import boost_weblate.settings_override as so

assert isinstance(so.WEBLATE_FORMATS, tuple)
assert so.WEBLATE_FORMATS == so.weblate_formats_with_quickbook()


def test_settings_override_source_has_exec_docker_hints() -> None:
path = (
Path(__file__).resolve().parents[1] / "src/boost_weblate/settings_override.py"
)
text = path.read_text(encoding="utf-8")
assert "_ENDPOINT_APP_CONFIG" in text
assert "boost_weblate.endpoint.apps.BoostEndpointConfig" in text
assert "AppRegistryNotReady" in text or "formats.models" in text


def test_weblate_formats_includes_upstream_and_quickbook() -> None:
from boost_weblate.settings_override import weblate_formats_with_quickbook

paths = list(weblate_formats_with_quickbook())
assert len(paths) >= 40
assert "weblate.formats.ttkit.PoFormat" in paths
assert "weblate.formats.ttkit.TBXFormat" in paths
assert paths.count(_QBK) == 1
Loading
Loading