From c251c5ff7f303ce1b7adc3c215bbc1f837452d9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Sat, 31 Oct 2026 17:15:06 +0100 Subject: [PATCH 1/2] feat(kotlin-sdk): integrate translation files CDN installation Offer Translation files CDN alongside Kotlin SDK CDN so Android clients can consume original XML translations. Provide installation for existing users and generate separate Gradle CDN blocks with updated documentation. --- docs/changes.rst | 2 + docs/conf.py | 1 + docs/devel/kotlin-sdk.rst | 26 ++++- docs/snippets/addons-autogenerated.rst | 10 +- weblate/addons/base.py | 5 +- weblate/addons/cdn.py | 10 +- weblate/addons/fedora_messaging.py | 5 +- weblate/addons/views.py | 6 +- weblate/addons/webhooks.py | 10 +- weblate/kotlin_sdk/addons.py | 79 ++++++++++++- weblate/kotlin_sdk/tests.py | 154 ++++++++++++++++++++++++- weblate/templates/addons/kotlin.html | 12 ++ 12 files changed, 296 insertions(+), 24 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 4318869b999c..519e8673b25c 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -30,6 +30,8 @@ Weblate 2026.11 .. rubric:: Improvements +* :doc:`Kotlin SDK integration ` now offers Translation files CDN installation and Gradle configuration for original XML downloads. + * :ref:`fedora-messaging` now publishes using Kombu, removing the Twisted dependency. Broker URLs no longer accept query strings or fragments; configure delivery timing using the add-on fields. * Sped up source-string linking during :ref:`project backup restoration `. * Sped up :ref:`Git export URL updates ` for projects with categorized components. diff --git a/docs/conf.py b/docs/conf.py index 9f39db704a19..b3b5112bb3b8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -323,6 +323,7 @@ def setup(app: Sphinx) -> None: ("py:class", "Form"), # Autodoc renders these type annotations from wlc signatures, but their # reference targets are not available in the configured inventories. + ("py:class", "HttpRequest"), ("py:class", "Path"), ("py:class", "JSONValue"), ("py:class", "RequestParams"), diff --git a/docs/devel/kotlin-sdk.rst b/docs/devel/kotlin-sdk.rst index 9eb81b066349..8cdb4eefaf50 100644 --- a/docs/devel/kotlin-sdk.rst +++ b/docs/devel/kotlin-sdk.rst @@ -10,8 +10,9 @@ Kotlin SDK for Android The official Kotlin SDK for Weblate delivers translation updates to Android applications without rebuilding or redistributing them. Its Gradle plugin generates and uploads resource metadata for each build. The Android library -downloads updated translations from the public CDN configured by the -:ref:`addon-weblate.cdn.kotlin` add-on. +downloads updated translations from the public CDNs configured by the +:ref:`addon-weblate.cdn.kotlin` and :ref:`addon-weblate.cdn.files` add-ons. +The latter supplies original Android XML translation files. The SDK is maintained in the `Kotlin SDK repository `_. The plugin is published on the @@ -39,8 +40,18 @@ string resources. For self-hosted Weblate, configure :setting:`LOCALIZE_CDN_URL` and :setting:`LOCALIZE_CDN_PATH` first. See :ref:`addon-weblate.cdn.kotlin` for public CDN publication, supported resources, and build retention settings. +The installation option :guilabel:`Install Translation files CDN` is enabled +by default. It installs :ref:`addon-weblate.cdn.files` to publish the original +Android XML files for the SDK. An existing installation of that add-on is reused. +Both CDNs publish publicly, including translations from private components. + +For an existing Kotlin SDK installation without Translation files CDN, use +:guilabel:`Install Translation files CDN` on the Kotlin SDK add-on configuration +page, or install it from the component's :guilabel:`Add-ons` tab. Return to the +Kotlin SDK configuration page afterwards to copy the updated Gradle snippet. + Copy the Gradle configuration from the add-on configuration page. It supplies -the server URL, CDN URL, project slug, and component path. +the server URL, both CDN URLs, project slug, and component path. Create a :ref:`project-scoped API token ` with permission to edit the component. Set it as ``WEBLATE_API_TOKEN`` in the build environment used to upload metadata. Keep the token out of source control and the application. @@ -68,10 +79,17 @@ slugs below with your component's values. Read the token from the environment: weblate { serverUrl = "https://hosted.weblate.org" - cdnUrl = "https://weblate-cdn.com/ADDON_ID" project = "my-project" component = "android" authToken.set(providers.environmentVariable("WEBLATE_API_TOKEN")) + + translationFiles { + cdnUrl = "https://weblate-cdn.com/TRANSLATION_FILES_ADDON_ID" + } + + kotlinSdk { + cdnUrl = "https://weblate-cdn.com/KOTLIN_SDK_ADDON_ID" + } } The token is needed only when uploading build metadata. The application diff --git a/docs/snippets/addons-autogenerated.rst b/docs/snippets/addons-autogenerated.rst index 16b802e9f8a9..8f658951a9df 100644 --- a/docs/snippets/addons-autogenerated.rst +++ b/docs/snippets/addons-autogenerated.rst @@ -323,8 +323,16 @@ names and their submitted IDs alongside translated text. Do not treat the uploaded resource mapping as confidential; enable publication only when this disclosure is intended. Registration and status API access remains permission-controlled. +Installation offers to install :ref:`addon-weblate.cdn.files` by default so the +SDK can also consume the original Android XML files. Existing Translation files +CDN installations are reused. For existing Kotlin SDK installations, use +:guilabel:`Install Translation files CDN` on the configuration page, then return +to copy the updated Gradle configuration. Original files are also published +publicly, including those from private components. + Copy the Gradle plugin configuration from the add-on configuration page. It -includes the Weblate server URL, public CDN URL, project slug, and component +includes the Weblate server URL, separate ``translationFiles`` and ``kotlinSdk`` +public CDN URLs, project slug, and component path (with any category path URL-encoded). Replace the placeholder with a project-scoped API token whose permissions allow editing the component. Project administrators can create one in the project's :guilabel:`API access` tab; see diff --git a/weblate/addons/base.py b/weblate/addons/base.py index 3ade33ad198c..6a93487462bd 100644 --- a/weblate/addons/base.py +++ b/weblate/addons/base.py @@ -48,6 +48,7 @@ from django.forms import Form from django.forms.boundfield import BoundField + from django.http import HttpRequest from django.urls.resolvers import URLPattern from django_stubs_ext import StrOrPromise @@ -220,7 +221,9 @@ def get_doc_anchor(cls) -> str: def has_settings(cls) -> bool: return cls.settings_form is not None - def get_integration_instructions(self, user: User) -> str: + def get_integration_instructions( + self, user: User, request: HttpRequest | None = None + ) -> str: """Render integration instructions independently of the settings form.""" return "" diff --git a/weblate/addons/cdn.py b/weblate/addons/cdn.py index f4d3b32e892f..93b2a693bbd2 100644 --- a/weblate/addons/cdn.py +++ b/weblate/addons/cdn.py @@ -36,6 +36,8 @@ CDN_MANIFEST_FILENAME = "manifest.json" if TYPE_CHECKING: + from django.http import HttpRequest + from weblate.addons.models import Addon from weblate.auth.models import User from weblate.trans.models import Category, Component, Project, Translation @@ -195,7 +197,9 @@ class CDNJSAddon(CDNBaseAddon): settings_form = CDNJSForm - def get_integration_instructions(self, user: User) -> str: + def get_integration_instructions( + self, user: User, request: HttpRequest | None = None + ) -> str: if not self.instance.pk: return "" return render_to_string( @@ -324,7 +328,9 @@ class CDNFilesAddon(CDNBaseAddon): ) needs_component = True - def get_integration_instructions(self, user: User) -> str: + def get_integration_instructions( + self, user: User, request: HttpRequest | None = None + ) -> str: if not self.instance.pk: return "" return render_to_string( diff --git a/weblate/addons/fedora_messaging.py b/weblate/addons/fedora_messaging.py index 28594cae8957..188e2006ef39 100644 --- a/weblate/addons/fedora_messaging.py +++ b/weblate/addons/fedora_messaging.py @@ -55,6 +55,7 @@ from typing import Any from django.forms.boundfield import BoundField + from django.http import HttpRequest from django_stubs_ext import StrOrPromise from weblate.addons.models import AddonActivityLog @@ -292,7 +293,9 @@ class FedoraMessagingAddon(ChangeBaseAddon): multiple = False version_added = "5.15" - def get_integration_instructions(self, user: User) -> str: + def get_integration_instructions( + self, user: User, request: HttpRequest | None = None + ) -> str: return render_to_string( "addons/fedora_messaging_integration.html", {"addon": self, "user": user} ) diff --git a/weblate/addons/views.py b/weblate/addons/views.py index dddaa4da851c..b1775d857f07 100644 --- a/weblate/addons/views.py +++ b/weblate/addons/views.py @@ -365,7 +365,7 @@ def post(self, request: AuthenticatedHttpRequest, **kwargs): "form": form, "object": self.path_object, "integration_instructions": integration_addon.get_integration_instructions( - request.user + request.user, request=request ), }, ) @@ -448,7 +448,9 @@ def get_context_data(self, **kwargs): result = super().get_context_data(**kwargs) if self.object.is_valid: result["integration_instructions"] = ( - self.object.addon.get_integration_instructions(self.request.user) + self.object.addon.get_integration_instructions( + self.request.user, request=self.request + ) ) execution_form = self.object.addon.get_execution_form() if execution_form is not None: diff --git a/weblate/addons/webhooks.py b/weblate/addons/webhooks.py index fe1cc8b3ce0f..c51a878f50d5 100644 --- a/weblate/addons/webhooks.py +++ b/weblate/addons/webhooks.py @@ -33,6 +33,8 @@ from collections.abc import Mapping from datetime import datetime + from django.http import HttpRequest + from weblate.addons.models import AddonActivityLog from weblate.auth.models import User from weblate.trans.models import Change @@ -178,7 +180,9 @@ class WebhookAddon(JSONWebhookBaseAddon): settings_form = WebhooksAddonForm - def get_integration_instructions(self, user: User) -> str: + def get_integration_instructions( + self, user: User, request: HttpRequest | None = None + ) -> str: return render_to_string( "addons/webhook_integration.html", {"addon": self, "user": user} ) @@ -291,7 +295,9 @@ class SlackWebhookAddon(JSONWebhookBaseAddon): settings_form = BaseWebhooksAddonForm version_added = "5.12" - def get_integration_instructions(self, user: User) -> str: + def get_integration_instructions( + self, user: User, request: HttpRequest | None = None + ) -> str: return render_to_string( "addons/slack_integration.html", {"addon": self, "user": user} ) diff --git a/weblate/kotlin_sdk/addons.py b/weblate/kotlin_sdk/addons.py index a114f6476ab0..386763ad6b0b 100644 --- a/weblate/kotlin_sdk/addons.py +++ b/weblate/kotlin_sdk/addons.py @@ -11,19 +11,23 @@ from crispy_forms.helper import FormHelper from crispy_forms.layout import Field, Layout from django import forms +from django.core.exceptions import ValidationError from django.template.loader import render_to_string from django.utils.translation import gettext_lazy -from weblate.addons.cdn import CDNBaseAddon +from weblate.addons.cdn import CDNBaseAddon, CDNFilesAddon from weblate.addons.events import AddonEvent from weblate.addons.forms import BaseAddonForm from weblate.kotlin_sdk.api import get_api_urls from weblate.kotlin_sdk.publication import Publication +from weblate.utils.change_origin import get_acting_user from weblate.utils.site import get_site_url if TYPE_CHECKING: + from django.http import HttpRequest from django.urls.resolvers import URLPattern + from weblate.addons.models import Addon from weblate.auth.models import User from weblate.trans.models import Component, Translation @@ -51,10 +55,30 @@ class KotlinSDKForm(BaseAddonForm): ), ) + install_translation_files = forms.BooleanField( + label=gettext_lazy("Install Translation files CDN"), + required=False, + initial=True, + help_text=gettext_lazy( + "Publishes original translation files publicly, including files from private components." + ), + ) + def __init__(self, *args, **kwargs) -> None: # ruff: ignore[missing-type-args, missing-type-kwargs] super().__init__(*args, **kwargs) self.helper = FormHelper(self) self.helper.layout = Layout(Field("maximum_age"), Field("maximum_versions")) + if self._addon.instance.pk is None and not self._addon.documentation_build: + self.helper.layout.append(Field("install_translation_files")) + else: + del self.fields["install_translation_files"] + + def serialize_form(self) -> dict[str, int | bool]: + data = dict(super().serialize_form()) + install = data.pop("install_translation_files", False) + if self._addon.instance.pk is None and install: + data["_install_translation_files"] = True + return data class KotlinSDKAddon(CDNBaseAddon): @@ -76,23 +100,68 @@ class KotlinSDKAddon(CDNBaseAddon): AddonEvent.EVENT_DAILY, } - def get_integration_instructions(self, user: User) -> str: + def get_translation_files_addon(self) -> Addon | None: + from weblate.addons.models import Addon # ruff: ignore[import-outside-top-level] + + return Addon.objects.filter( + component=self.instance.component, name=CDNFilesAddon.name + ).first() + + def post_configure(self, run: bool = True) -> None: + if self.instance.configuration.pop("_install_translation_files", False): + component = self.instance.component + if component is not None and self.get_translation_files_addon() is None: + if not CDNFilesAddon.can_install(component=component): + raise ValidationError( + gettext_lazy( + "Translation files CDN cannot be installed on this component." + ) + ) + CDNFilesAddon.create( + component=component, acting_user=get_acting_user(), run=run + ) + component.drop_addons_cache() + self.instance.save(update_fields=["configuration"]) + super().post_configure(run=run) + + def get_integration_instructions( + self, user: User, request: HttpRequest | None = None + ) -> str: component = self.instance.component if not self.instance.pk or component is None: return "" component_path = "%2F".join(component.get_url_path()[1:]) + files_addon = self.get_translation_files_addon() snippet = ( "weblate {\n" f' serverUrl = "{get_site_url()}"\n' - f' cdnUrl = "{self.cdn_base_url}"\n' ' authToken = "INSERT_TOKEN_HERE"\n' f' project = "{component.project.slug}"\n' f' component = "{component_path}"\n' - "}" + ) + if files_addon is not None: + snippet += ( + "\n translationFiles {\n" + f' cdnUrl = "{CDNFilesAddon(files_addon).cdn_base_url}"\n' + " }\n" + ) + snippet += ( + f'\n kotlinSdk {{\n cdnUrl = "{self.cdn_base_url}"\n }}\n}}' ) return render_to_string( "addons/kotlin.html", - {"snippet": snippet, "project": component.project, "user": user}, + { + "snippet": snippet, + "project": component.project, + "user": user, + "component": component, + "files_addon_missing": files_addon is None, + "can_install_files": request is not None + and files_addon is None + and user.has_perm("component.edit", component) + and CDNFilesAddon.can_install(component=component), + }, + request=request, ) @classmethod diff --git a/weblate/kotlin_sdk/tests.py b/weblate/kotlin_sdk/tests.py index 43ad45e5b0f7..ecf4fc84a38d 100644 --- a/weblate/kotlin_sdk/tests.py +++ b/weblate/kotlin_sdk/tests.py @@ -17,13 +17,15 @@ from arsc_writer import Text from django.db import DatabaseError, connections, transaction -from django.test import SimpleTestCase, TransactionTestCase, override_settings +from django.test import Client, SimpleTestCase, TransactionTestCase, override_settings from django.urls import reverse from django.utils import timezone from jsonschema.exceptions import ValidationError as SchemaValidationError from rest_framework.test import APIClient, APIRequestFactory from translate.storage.base import ParseError as TranslateParseError +from weblate.addons.cdn import CDNFilesAddon +from weblate.addons.models import Addon from weblate.formats.ttkit import AndroidFormat from weblate.kotlin_sdk.addons import ( KotlinSDKAddon, @@ -34,8 +36,9 @@ from weblate.kotlin_sdk.publication import Publication from weblate.kotlin_sdk.retention import select_retired_builds from weblate.kotlin_sdk.translations import LocaleSnapshot, LocaleValues +from weblate.trans.actions import ActionEvents from weblate.trans.exceptions import FileParseError -from weblate.trans.models import Category, Component, Translation, Unit +from weblate.trans.models import Category, Change, Component, Translation, Unit from weblate.trans.tests.test_views import ReusableViewTestCase from weblate.trans.tests.utils import RepoTestMixin, get_test_actor from weblate.utils.change_origin import acting_user_context @@ -300,7 +303,6 @@ def worker() -> None: def test_lifecycle_changes_during_compilation(self) -> None: from arsc_writer import generate # ruff: ignore[import-outside-top-level] - from weblate.addons.models import Addon # ruff: ignore[import-outside-top-level] from weblate.kotlin_sdk.api import register_build # ruff: ignore[import-outside-top-level] from weblate.kotlin_sdk.tasks import publish_kotlin_sdk # ruff: ignore[import-outside-top-level] @@ -409,8 +411,6 @@ def worker(addon_id: int = addon_id) -> None: @tempdir_setting("LOCALIZE_CDN_PATH") def test_concurrent_capacity(self) -> None: - # ruff: ignore[import-outside-top-level] - from weblate.addons.models import Addon from weblate.kotlin_sdk.api import register_build # ruff: ignore[import-outside-top-level] component = self.create_android() @@ -758,6 +758,143 @@ def test_registration_api(self) -> None: client.post(url, metadata(), format="json").status_code, (401, 403) ) + @tempdir_setting("LOCALIZE_CDN_PATH") + def test_install_translation_files(self) -> None: + self.make_manager() + addon = KotlinSDKAddon(KotlinSDKAddon.create_object(component=self.component)) + form = KotlinSDKForm(self.user, addon) + self.assertTrue(form.fields["install_translation_files"].initial) + form = KotlinSDKForm( + self.user, + addon, + data={ + "maximum_age": 365, + "maximum_versions": 20, + "install_translation_files": True, + }, + ) + self.assertTrue(form.is_valid(), form.errors) + with patch("weblate.addons.tasks.postconfigure_addon") as configure: + instance = form.save() + self.assertEqual(configure.call_count, 2) + files = Addon.objects.get(component=self.component, name=CDNFilesAddon.name) + self.assertEqual( + Change.objects.get(addon=files, action=ActionEvents.ADDON_CREATE).user, + self.user, + ) + instance.refresh_from_db() + self.assertEqual( + instance.configuration, {"maximum_age": 365, "maximum_versions": 20} + ) + settings_form = KotlinSDKForm( + self.user, + instance.addon, + data={ + "maximum_age": 100, + "maximum_versions": 10, + "install_translation_files": True, + "_install_translation_files": True, + }, + ) + self.assertNotIn("install_translation_files", settings_form.fields) + self.assertTrue(settings_form.is_valid(), settings_form.errors) + self.assertEqual( + settings_form.serialize_form(), {"maximum_age": 100, "maximum_versions": 10} + ) + response = self.client.get(instance.get_absolute_url()) + self.assertContains(response, "translationFiles {") + self.assertContains( + response, f"cdnUrl = "{CDNFilesAddon(files).cdn_base_url}"" + ) + self.assertContains(response, "kotlinSdk {") + self.assertNotContains(response, 'name="install_translation_files"') + self.assertNotContains(response, 'value="weblate.cdn.files"') + + @tempdir_setting("LOCALIZE_CDN_PATH") + def test_install_reuses_translation_files(self) -> None: + files = CDNFilesAddon.create( + component=self.component, acting_user=get_test_actor(self), run=False + ) + KotlinSDKAddon.create( + component=self.component, + acting_user=get_test_actor(self), + run=False, + configuration={"_install_translation_files": True}, + ) + self.assertEqual( + list( + Addon.objects.filter(component=self.component, name=CDNFilesAddon.name) + ), + [files.instance], + ) + + @tempdir_setting("LOCALIZE_CDN_PATH") + def test_install_without_translation_files(self) -> None: + self.make_manager() + addon = KotlinSDKAddon(KotlinSDKAddon.create_object(component=self.component)) + form = KotlinSDKForm( + self.user, addon, data={"maximum_age": 365, "maximum_versions": 20} + ) + self.assertTrue(form.is_valid(), form.errors) + with patch("weblate.addons.tasks.postconfigure_addon"): + instance = form.save() + self.assertFalse( + Addon.objects.filter( + component=self.component, name=CDNFilesAddon.name + ).exists() + ) + response = self.client.get(instance.get_absolute_url()) + self.assertNotContains(response, "translationFiles {") + self.assertContains(response, "kotlinSdk {") + self.assertContains(response, 'value="weblate.cdn.files"') + url = reverse("addons", kwargs={"path": self.component.get_url_path()}) + self.assertContains(response, f'action="{url}"') + csrf_client = Client(enforce_csrf_checks=True) + csrf_client.force_login(self.user) + page = csrf_client.get(instance.get_absolute_url()) + self.assertContains(page, 'name="csrfmiddlewaretoken"') + self.assertEqual( + csrf_client.post(url, {"name": CDNFilesAddon.name}).status_code, 403 + ) + with patch("weblate.addons.tasks.postconfigure_addon"): + installed = csrf_client.post( + url, + { + "name": CDNFilesAddon.name, + "csrfmiddlewaretoken": str(page.context["csrf_token"]), + }, + ) + files = Addon.objects.get(component=self.component, name=CDNFilesAddon.name) + self.assertRedirects(installed, files.get_absolute_url()) + self.client.post(url, {"name": CDNFilesAddon.name}) + self.assertEqual( + Addon.objects.filter( + component=self.component, name=CDNFilesAddon.name + ).count(), + 1, + ) + response = self.client.get(instance.get_absolute_url()) + self.assertContains(response, "translationFiles {") + self.assertNotContains(response, 'value="weblate.cdn.files"') + self.client.logout() + self.assertEqual( + self.client.post(url, {"name": CDNFilesAddon.name}).status_code, 403 + ) + + @tempdir_setting("LOCALIZE_CDN_PATH") + def test_translation_files_install_button_eligibility(self) -> None: + self.make_manager() + addon = self.install() + with patch.object(CDNFilesAddon, "can_install", return_value=False): + response = self.client.get(addon.instance.get_absolute_url()) + self.assertNotContains(response, 'value="weblate.cdn.files"') + self.assertContains(response, "Install Translation files CDN to let the SDK") + request = APIRequestFactory().get("/") + instructions = addon.cdn.get_integration_instructions( + self.anotheruser, request=request + ) + self.assertNotIn('value="weblate.cdn.files"', instructions) + @tempdir_setting("LOCALIZE_CDN_PATH") def test_api_links(self) -> None: self.make_manager() @@ -818,6 +955,9 @@ def test_integration_in_nested_category(self) -> None: self.component.category = child self.component.save() addon = self.install() + files = CDNFilesAddon.create( + component=self.component, acting_user=get_test_actor(self), run=False + ) response = self.client.get(addon.instance.get_absolute_url()) self.assertContains( @@ -825,9 +965,11 @@ def test_integration_in_nested_category(self) -> None: f"component = "parent%2Fchild%2F{self.component.slug}"", ) + self.assertContains(response, "translationFiles {") + self.assertContains(response, f"cdnUrl = "{files.cdn_base_url}"") + @tempdir_setting("LOCALIZE_CDN_PATH") def test_registration_uses_locked_configuration(self) -> None: - from weblate.addons.models import Addon # ruff: ignore[import-outside-top-level] from weblate.kotlin_sdk.api import register_build # ruff: ignore[import-outside-top-level] addon = self.install() diff --git a/weblate/templates/addons/kotlin.html b/weblate/templates/addons/kotlin.html index 8f0723d8d6ac..2f28e1bc1e89 100644 --- a/weblate/templates/addons/kotlin.html +++ b/weblate/templates/addons/kotlin.html @@ -1,6 +1,18 @@ {% load i18n icons permissions %}

{% blocktranslate with url="https://github.com/WeblateOrg/kotlin-sdk/" %}See the Kotlin SDK repository for Gradle plugin and library setup.{% endblocktranslate %}

+{% if files_addon_missing %} +

{% translate "Install Translation files CDN to let the SDK download original XML translation files. These files are published publicly, including files from private components." %}

+ {% if can_install_files %} +
+ {% csrf_token %} + + +
+

{% translate "After installation, return to this page to copy the updated Gradle configuration." %}

+ {% endif %} +{% endif %} +

{% translate "Configure the Kotlin SDK Gradle plugin with these values:" %}

{{ snippet }}
From 2bbfe85059b18e5e914ec4a2d05bcd83c36978c7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Sat, 31 Oct 2026 17:41:03 +0000 Subject: [PATCH 2/2] docs: Documentation snippets update --- pyproject.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ccd9feabc0de..c19486405dac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2205,6 +2205,10 @@ sort_inline_tables = true sort_table_keys = true spaces_before_inline_comment = 2 +[tool.tomlsort.overrides."tool.pytest.ini_options.filterwarnings"] +# Pytest applies the last matching filter, so preserve the warning precedence. +inline_arrays = false + [tool.typos.default] extend-ignore-re = [ "(?Rm)^.*# codespell:ignore.*$", @@ -2265,7 +2269,3 @@ extend-exclude = [ "weblate/trans/tests/data", "weblate/utils/licensedata.py" ] - -[tool.tomlsort.overrides."tool.pytest.ini_options.filterwarnings"] -# Pytest applies the last matching filter, so preserve the warning precedence. -inline_arrays = false