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
13 changes: 2 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,18 +154,9 @@ When `WEBLATE_URL_PREFIX` is set (e.g. `/weblate`), all paths are prefixed accor

Weblate's `urls.py` does **not** auto-discover URLconfs from arbitrary `INSTALLED_APPS` entries. It builds a single `real_patterns` list by hand and only extends it for known built-in apps (legal, SAML, git-export, etc.) via explicit `if "app" in settings.INSTALLED_APPS:` guards — there is no generic plugin scan.

This plugin handles registration in `BoostEndpointConfig.ready()` (`src/boost_weblate/endpoint/apps.py`), which runs once at Django startup and appends to `weblate.urls.real_patterns`:
This plugin handles registration in `BoostEndpointConfig.ready()` (`src/boost_weblate/endpoint/apps.py`), which delegates to `register_boost_endpoint_urls()` in `src/boost_weblate/endpoint/weblate_urls_adapter.py`. That adapter appends to `weblate.urls.real_patterns` after fail-fast layout checks (raises `WeblateUrlLayoutError` if `real_patterns` is missing or Weblate is below the supported version).

```python
wl_urls.real_patterns.append(
path(
"boost-endpoint/",
include(("boost_weblate.endpoint.urls", "boost_endpoint")),
),
)
```

The operation is idempotent (guarded by a `_cppa_boost_weblate_urls_registered` attribute on the module). Routes sit under Weblate's `URL_PREFIX` handling because `real_patterns` is used before the prefix wrapper is applied.
Registration is idempotent via `functools.lru_cache` on the adapter function (not a sentinel on Weblate's module). Routes sit under Weblate's `URL_PREFIX` handling because `real_patterns` is used before the prefix wrapper is applied.

### Request / response for `POST /boost-endpoint/add-or-update/`

Expand Down
13 changes: 2 additions & 11 deletions docs/boost-endpoint-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,9 @@ The Boost Endpoint is the HTTP API surface of this plugin. It provides three rou

Adding the app to `INSTALLED_APPS` is required but not sufficient for routes to be active. Weblate's `urls.py` builds its route list by hand (`real_patterns`) and does not auto-discover URLconfs from arbitrary apps.

`BoostEndpointConfig.ready()` (`src/boost_weblate/endpoint/apps.py`) appends to `weblate.urls.real_patterns` at Django startup:

```python
wl_urls.real_patterns.append(
path(
"boost-endpoint/",
include(("boost_weblate.endpoint.urls", "boost_endpoint")),
),
)
```
`BoostEndpointConfig.ready()` (`src/boost_weblate/endpoint/apps.py`) delegates to `register_boost_endpoint_urls()` in `src/boost_weblate/endpoint/weblate_urls_adapter.py`, which appends to `weblate.urls.real_patterns` at Django startup after verifying Weblate's URL layout (raises `WeblateUrlLayoutError` on incompatibility).

This is idempotent — a module-level flag (`_cppa_boost_weblate_urls_registered`) prevents double-registration. The routes inherit Weblate's `URL_PREFIX` handling because `real_patterns` is processed before the prefix wrapper is applied.
Registration is idempotent via `functools.lru_cache` on the adapter. The routes inherit Weblate's `URL_PREFIX` handling because `real_patterns` is processed before the prefix wrapper is applied.

For `INSTALLED_APPS` registration, use `settings_override.py` (recommended) or the `WEBLATE_ADD_APPS` Docker environment variable — **not both**. See the main [README](../README.md#weblate_add_apps) for the full comparison.

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ classifiers = [
"Topic :: Software Development :: Localization"
]
dependencies = [
"packaging==26.2",
"Weblate[all]==2026.5"
]
description = "Standalone Weblate plugin for Boost documentation translation."
Expand Down
50 changes: 9 additions & 41 deletions src/boost_weblate/endpoint/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,58 +4,26 @@

from __future__ import annotations

import logging

from django.apps import AppConfig
from django.urls import include, path

logger = logging.getLogger(__name__)

_PLUGIN_URLS_ATTR = "_cppa_boost_weblate_urls_registered"
from boost_weblate.endpoint.weblate_urls_adapter import register_boost_endpoint_urls


def register_plugin_urls() -> None:
"""Append this app's routes to Weblate's pattern list.
"""Register Boost endpoint routes with Weblate.

This is the supported plugin path: at process
startup, append a single ``path("boost-endpoint/", ...)`` entry to
``weblate.urls.real_patterns`` so routes stay under Weblate's ``URL_PREFIX``
handling.
Delegates to
:func:`~boost_weblate.endpoint.weblate_urls_adapter.register_boost_endpoint_urls`,
which appends a single ``path("boost-endpoint/", ...)`` entry to
``weblate.urls.real_patterns`` after fail-fast layout checks.

Exposed HTTP paths (relative to ``/boost-endpoint/``): ``info/``,
``add-or-update/``, and ``plugin-ping/`` (see ``boost_weblate.endpoint.urls``).

Weblate builds ``urlpatterns`` from module-level ``real_patterns`` (see
``weblate.urls``). Optional plugins append to ``real_patterns`` before
the ``URL_PREFIX`` wrapper is applied, so mutating that list keeps routes
consistent when a path prefix is configured.
Raises :class:`~boost_weblate.endpoint.weblate_urls_adapter.WeblateUrlLayoutError`
when Weblate's URL module layout is incompatible.
"""
try:
import weblate.urls as wl_urls # noqa: PLC0415
except ModuleNotFoundError as exc:
logger.debug(
"boost_weblate.endpoint: skipping URL registration (import error: %s)",
exc,
)
return

if getattr(wl_urls, _PLUGIN_URLS_ATTR, False):
return

if not hasattr(wl_urls, "real_patterns"):
logger.warning(
"boost_weblate.endpoint: weblate.urls has no real_patterns; "
"URL registration skipped (unexpected Weblate layout)."
)
return

wl_urls.real_patterns.append(
path(
"boost-endpoint/",
include(("boost_weblate.endpoint.urls", "boost_endpoint")),
),
)
setattr(wl_urls, _PLUGIN_URLS_ATTR, True)
register_boost_endpoint_urls()


class BoostEndpointConfig(AppConfig):
Expand Down
113 changes: 113 additions & 0 deletions src/boost_weblate/endpoint/weblate_urls_adapter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# SPDX-FileCopyrightText: 2026 Andrew Zhang <whisper67265@outlook.com>
#
# SPDX-License-Identifier: BSL-1.0

"""Adapter for registering Boost endpoint routes on Weblate's URL pattern list."""

from __future__ import annotations

import importlib.metadata
import logging
from functools import lru_cache
from types import ModuleType

from django.urls import URLResolver, include, path
from packaging.version import InvalidVersion, Version

logger = logging.getLogger(__name__)

_MIN_WEBLATE_VERSION = Version("2026.5")
_BOOST_ENDPOINT_PREFIX = "boost-endpoint/"


class WeblateUrlLayoutError(RuntimeError):
"""Raised when ``weblate.urls`` lacks the layout this plugin expects."""


def _weblate_version() -> str:
try:
return importlib.metadata.version("Weblate")
except importlib.metadata.PackageNotFoundError:
return "unknown"


def _assert_weblate_url_layout(wl_urls: ModuleType) -> None:
"""Verify Weblate exposes the ``real_patterns`` list before mutation."""
version = _weblate_version()
if not hasattr(wl_urls, "real_patterns"):
msg = (
"weblate.urls.real_patterns is missing; "
f"Weblate {version} URL layout is incompatible with cppa-weblate-plugin"
)
raise WeblateUrlLayoutError(msg)
if not isinstance(wl_urls.real_patterns, list):
msg = (
"weblate.urls.real_patterns is not a list; "
f"Weblate {version} URL layout is incompatible with cppa-weblate-plugin"
)
raise WeblateUrlLayoutError(msg)
if version == "unknown":
msg = (
"Weblate package version could not be determined; "
"cppa-weblate-plugin cannot verify minimum version compatibility"
)
raise WeblateUrlLayoutError(msg)
try:
parsed = Version(version)
except InvalidVersion as exc:
msg = (
f"Weblate version string {version!r} could not be parsed; "
"cppa-weblate-plugin cannot verify minimum version compatibility"
)
raise WeblateUrlLayoutError(msg) from exc
if parsed < _MIN_WEBLATE_VERSION:
msg = (
f"Weblate {version} is below the minimum supported version "
f"{_MIN_WEBLATE_VERSION}; cppa-weblate-plugin requires Weblate "
f"{_MIN_WEBLATE_VERSION} or newer"
)
raise WeblateUrlLayoutError(msg)


def _boost_endpoint_route() -> URLResolver:
return path(
_BOOST_ENDPOINT_PREFIX,
include(("boost_weblate.endpoint.urls", "boost_endpoint")),
)


def _route_already_registered(real_patterns: list) -> bool:
return any(
str(getattr(entry, "pattern", "")) == _BOOST_ENDPOINT_PREFIX
for entry in real_patterns
)


@lru_cache(maxsize=1)
def register_boost_endpoint_urls() -> None:
"""Append Boost endpoint routes to ``weblate.urls.real_patterns`` once.

Weblate builds ``urlpatterns`` from module-level ``real_patterns``. Appending
before the ``URL_PREFIX`` wrapper keeps routes under prefix configuration.

Idempotent via ``lru_cache``; safe to call from ``AppConfig.ready()``.

Note: if ``weblate.urls`` is not importable at call time, the no-op result
is still cached; retrying after the module becomes importable requires an
explicit ``register_boost_endpoint_urls.cache_clear()`` call.
"""
try:
import weblate.urls as wl_urls # noqa: PLC0415
except ModuleNotFoundError as exc:
logger.debug(
"boost_weblate.endpoint: skipping URL registration (import error: %s)",
exc,
)
return

_assert_weblate_url_layout(wl_urls)

if _route_already_registered(wl_urls.real_patterns):
return

wl_urls.real_patterns.append(_boost_endpoint_route())
53 changes: 28 additions & 25 deletions tests/endpoint/test_apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,36 @@
from __future__ import annotations

import builtins
import sys
import types

import pytest

from boost_weblate.endpoint.apps import register_plugin_urls
from boost_weblate.endpoint import apps
from boost_weblate.endpoint.weblate_urls_adapter import register_boost_endpoint_urls


@pytest.fixture(autouse=True)
def _clear_cache() -> None:
register_boost_endpoint_urls.cache_clear()
yield
register_boost_endpoint_urls.cache_clear()


def test_register_plugin_urls_delegates_to_adapter(
monkeypatch: pytest.MonkeyPatch,
) -> None:
called = False

def fake_register() -> None:
nonlocal called
called = True

monkeypatch.setattr(
apps,
"register_boost_endpoint_urls",
fake_register,
)
apps.register_plugin_urls()
assert called is True
Comment thread
coderabbitai[bot] marked this conversation as resolved.


def test_register_plugin_urls_skips_when_weblate_urls_missing(
Expand All @@ -24,25 +48,4 @@ def fake_import(name: str, *args, **kwargs): # type: ignore[no-untyped-def]
return real_import(name, *args, **kwargs)

monkeypatch.setattr(builtins, "__import__", fake_import)
# Should not raise; no fake module to inspect.
register_plugin_urls()


def test_register_plugin_urls_skips_without_real_patterns(
monkeypatch: pytest.MonkeyPatch,
) -> None:
fake = types.ModuleType("weblate.urls")
monkeypatch.setitem(sys.modules, "weblate.urls", fake)
register_plugin_urls()
assert not hasattr(fake, "real_patterns")


def test_register_plugin_urls_appends_once(monkeypatch: pytest.MonkeyPatch) -> None:
fake = types.ModuleType("weblate.urls")
fake.real_patterns = []
monkeypatch.setitem(sys.modules, "weblate.urls", fake)

register_plugin_urls()
register_plugin_urls()

assert len(fake.real_patterns) == 1
apps.register_plugin_urls()
Loading
Loading