From 2c9662aa9ad20d4d620bbceec688a8138e1b6c56 Mon Sep 17 00:00:00 2001 From: Ernest McCarter Date: Fri, 6 Mar 2026 11:08:50 -0800 Subject: [PATCH 1/4] feat: quality of life --- .github/workflows/ci.yml | 22 ++++++++++ .github/workflows/lint.yml | 26 ++++++++++++ .github/workflows/release.yml | 59 ++++++++++++++++++++++++++ .sampo/changesets/.gitkeep | 0 .sampo/config.toml | 13 ++++++ CONTRIBUTING.md | 20 +++++++-- guides/creating-a-package.md | 67 ++++++++++++++++++++++++++++++ guides/releasing.md | 50 ++++++++++++++++++++++ guides/trusted-publishing-setup.md | 46 ++++++++++++++++++++ scripts/setup.sh | 32 ++++++++++++++ 10 files changed, 332 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/release.yml create mode 100644 .sampo/changesets/.gitkeep create mode 100644 .sampo/config.toml create mode 100644 guides/creating-a-package.md create mode 100644 guides/releasing.md create mode 100644 guides/trusted-publishing-setup.md create mode 100755 scripts/setup.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e134776 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,22 @@ +name: CI - Run Tests + +on: + pull_request: + types: [opened, synchronize, reopened] + +env: + CI: true + +permissions: + contents: read + +jobs: + tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v6 + - name: Install dependencies + run: uv sync --all-packages + - name: Run tests + run: uv run pytest diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..72a1d0d --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,26 @@ +name: Lint + +on: + pull_request: + types: [opened, synchronize, reopened] + +env: + CI: true + +permissions: + contents: read + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v6 + - name: Install dependencies + run: uv sync --all-packages + - name: Ruff check + run: uv run ruff check . + - name: Ruff format check + run: uv run ruff format --check . + - name: Mypy + run: uv run mypy . diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..2743627 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,59 @@ +name: Release + +on: + push: + branches: [main] + +concurrency: ${{ github.workflow }}-${{ github.ref }} + +permissions: + contents: write + pull-requests: write + +jobs: + release: + name: Release + runs-on: ubuntu-latest + environment: + name: release + outputs: + published: ${{ steps.sampo.outputs.published }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: astral-sh/setup-uv@v6 + - run: uv sync --all-packages + - name: Create Release Pull Request or Tag + id: sampo + uses: bruits/sampo/crates/sampo-github-action@main + with: + command: auto + create-github-release: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + publish: + name: Publish to PyPI + needs: release + if: needs.release.outputs.published == 'true' + runs-on: ubuntu-latest + environment: + name: release + permissions: + id-token: write + contents: read + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v6 + - name: Build all packages + run: | + for pkg in packages/*/; do + pkg_name=$(basename "$pkg") + if grep -q '\[build-system\]' "$pkg/pyproject.toml" 2>/dev/null; then + echo "Building $pkg_name..." + uv build --package "$pkg_name" --out-dir dist/ + fi + done + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.sampo/changesets/.gitkeep b/.sampo/changesets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/.sampo/config.toml b/.sampo/config.toml new file mode 100644 index 0000000..f8f976f --- /dev/null +++ b/.sampo/config.toml @@ -0,0 +1,13 @@ +[git] +default_branch = "main" + +[github] +repository = "generaltranslation/gt-python" + +[changelog] +show_commit_hash = true +show_acknowledgments = true + +[packages] +ignore_unpublished = true +ignore = ["examples/*"] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6aed3c7..fa1ae3b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -107,15 +107,15 @@ curl -LsSf https://astral.sh/uv/install.sh | sh ### Installation -Clone the repository and install all dependencies: +Clone the repository and run the setup script: ```bash git clone https://github.com/generaltranslation/gt-python.git cd gt-python -uv sync --all-packages +./scripts/setup.sh ``` -This creates a virtual environment at `.venv/` and installs all workspace packages as editable installs. Changes to source code are immediately reflected — no rebuild needed. +This installs uv (if needed), all workspace packages, Rust/Cargo (if needed), and [Sampo](https://github.com/bruits/sampo) (the release tool). A virtual environment is created at `.venv/` with all workspace packages as editable installs — changes to source code are immediately reflected. ### Recommended Editor Extensions @@ -152,6 +152,20 @@ uv run mypy . uv build --package generaltranslation ``` +### Releasing + +This project uses [Sampo](https://github.com/bruits/sampo) for automated releases. When you make a change that should be released, add a changeset: + +```bash +sampo add +``` + +This prompts you to select affected packages and the bump type (patch/minor/major), then creates a changeset file in `.sampo/changesets/`. Commit this file with your PR. + +When your PR merges to `main`, a GitHub Action automatically creates a Release PR that bumps versions and updates changelogs. Merging that Release PR publishes the packages to PyPI. + +See `guides/releasing.md` for the full workflow. + ## Styleguides ### Code Style diff --git a/guides/creating-a-package.md b/guides/creating-a-package.md new file mode 100644 index 0000000..4983745 --- /dev/null +++ b/guides/creating-a-package.md @@ -0,0 +1,67 @@ +# Creating a New Package + +This guide covers adding a new Python package to the gt-python monorepo. + +## 1. Create the directory structure + +``` +packages/my-package/ + src/ + my_package/ + __init__.py + pyproject.toml + README.md + LICENSE.md +``` + +## 2. Set up `pyproject.toml` + +Use the `uv_build` backend and follow the existing conventions: + +```toml +[project] +name = "my-package" +version = "0.0.0" +description = "Description of your package" +readme = "README.md" +authors = [ + { name = "Your Name", email = "you@generaltranslation.com" } +] +requires-python = ">=3.10" +license = "FSL-1.1-ALv2" +license-files = ["LICENSE.md"] +dependencies = [] + +[build-system] +requires = ["uv_build>=0.10.8,<0.11.0"] +build-backend = "uv_build" +``` + +## 3. Add workspace source references + +If your package depends on other workspace packages, add `[tool.uv.sources]` entries: + +```toml +[tool.uv.sources] +generaltranslation = { workspace = true } +``` + +## 4. Register the package in the workspace + +The root `pyproject.toml` uses `packages/*/` as the workspace members glob, so new packages under `packages/` are automatically discovered. + +Run `uv sync --all-packages` to verify the new package is picked up. + +## 5. Register as a trusted publisher on PyPI + +Before the first release, you must register the package as a pending publisher on PyPI. See [trusted-publishing-setup.md](./trusted-publishing-setup.md) for step-by-step instructions. + +## 6. Build and test + +```bash +# Build the package +uv build --package my-package + +# Run tests +uv run pytest packages/my-package/ +``` diff --git a/guides/releasing.md b/guides/releasing.md new file mode 100644 index 0000000..cd388c6 --- /dev/null +++ b/guides/releasing.md @@ -0,0 +1,50 @@ +# Releasing Packages + +This project uses [Sampo](https://github.com/bruits/sampo) for automated releases. Sampo is a Changesets-inspired tool with native Python/PyPI support. + +## How it works + +### 1. Add a changeset + +When you make a change that should be released, run: + +```bash +sampo add +``` + +This prompts you to: +- Select the affected package(s) +- Choose the bump type (patch / minor / major) +- Write a summary of the change + +A changeset file is created in `.sampo/changesets/`. Commit this file with your PR. + +### 2. Merge to main + +When your PR merges to `main`, the release GitHub Action runs automatically. If there are pending changesets, Sampo creates (or updates) a **Release PR** that: +- Bumps package versions in `pyproject.toml` +- Updates `CHANGELOG.md` for each affected package +- Consumes the changeset files + +### 3. Merge the Release PR + +When the Release PR is merged, the action runs again. This time: +- Sampo creates git tags for each released package +- GitHub releases are created +- The **publish** job builds all packages and publishes them to PyPI using trusted publishing (OIDC) + +## First-time setup for a new package + +Before a new package can be published, it must be registered as a trusted publisher on PyPI. See [trusted-publishing-setup.md](./trusted-publishing-setup.md). + +## Manual release (emergency) + +If you need to publish manually: + +```bash +# Build a specific package +uv build --package --out-dir dist/ + +# Upload (requires PyPI API token) +uv publish dist/-*.tar.gz dist/-*.whl +``` diff --git a/guides/trusted-publishing-setup.md b/guides/trusted-publishing-setup.md new file mode 100644 index 0000000..85efd77 --- /dev/null +++ b/guides/trusted-publishing-setup.md @@ -0,0 +1,46 @@ +# Trusted Publishing Setup (PyPI OIDC) + +This is a one-time setup for each package. It allows GitHub Actions to publish to PyPI without API tokens. + +## Prerequisites + +- A PyPI account with owner/maintainer access +- The GitHub repository must have a `release` environment configured + +## GitHub Environment Setup + +1. Go to the repository **Settings > Environments** +2. Click **New environment** +3. Name it `release` +4. Optionally add protection rules (e.g., required reviewers) + +## Register a Trusted Publisher on PyPI + +For each package, register it as a pending publisher on [pypi.org](https://pypi.org): + +1. Go to https://pypi.org/manage/account/publishing/ +2. Under **"Add a new pending publisher"**, fill in: + - **PyPI project name**: the package name (e.g., `generaltranslation`) + - **Owner**: `generaltranslation` + - **Repository name**: `gt-python` + - **Workflow name**: `release.yml` + - **Environment name**: `release` +3. Click **Add** + +## Packages to register + +Repeat the above for each package: + +| PyPI project name | +|---| +| `generaltranslation` | +| `generaltranslation-icu-messageformat-parser` | +| `generaltranslation-intl-messageformat` | +| `generaltranslation-supported-locales` | +| `gt-i18n` | +| `gt-flask` | +| `gt-fastapi` | + +## Verification + +After setup, the next release will use OIDC (no tokens needed). You can verify by checking the publish job logs in GitHub Actions for "Trusted publisher authentication" messages. diff --git a/scripts/setup.sh b/scripts/setup.sh new file mode 100755 index 0000000..9d909f2 --- /dev/null +++ b/scripts/setup.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "Setting up gt-python development environment..." + +# Check for uv +if ! command -v uv &> /dev/null; then + echo "Installing uv..." + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "uv installed. You may need to restart your shell or source your profile." +fi + +# Install all workspace packages +echo "Installing workspace packages..." +uv sync --all-packages + +# Check for cargo (needed for sampo) +if ! command -v cargo &> /dev/null; then + echo "Installing Rust toolchain (needed for sampo)..." + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + source "$HOME/.cargo/env" +fi + +# Install sampo +if ! command -v sampo &> /dev/null; then + echo "Installing sampo..." + cargo install sampo +fi + +echo "Setup complete!" +echo " uv: $(uv --version)" +echo " sampo: $(sampo --version)" From 5147e91c998436a64c08751a3e67dafe65578984 Mon Sep 17 00:00:00 2001 From: Ernest McCarter Date: Fri, 6 Mar 2026 12:13:02 -0800 Subject: [PATCH 2/4] fix: lint --- CONTRIBUTING.md | 50 ++- Makefile | 37 ++ .../fastapi-eager/scripts/test_endpoints.py | 4 +- .../fastapi-lazy/scripts/test_endpoints.py | 4 +- .../flask-eager/scripts/test_endpoints.py | 4 +- examples/flask-lazy/scripts/test_endpoints.py | 4 +- .../_parser.py | 25 +- .../_printer.py | 4 +- .../tests/test_formatter.py | 318 +++++++++++++++--- .../src/generaltranslation/_gt.py | 37 +- .../formatting/_format_currency.py | 3 +- .../formatting/_format_cutoff.py | 14 +- .../formatting/_format_date_time.py | 6 +- .../formatting/_format_list.py | 2 +- .../formatting/_format_relative_time.py | 6 +- .../locales/_get_locale_emoji.py | 12 +- .../locales/_get_locale_name.py | 4 +- .../locales/_get_locale_properties.py | 24 +- .../locales/_requires_translation.py | 3 +- .../generaltranslation/static/_index_vars.py | 4 +- .../static/_traverse_icu.py | 12 +- .../translate/_enqueue_files.py | 16 +- .../translate/_process_file_moves.py | 5 +- .../translate/_query_file_data.py | 4 +- .../generaltranslation/translate/_request.py | 4 +- .../translate/_translate.py | 16 +- .../generaltranslation/translate/_types.py | 12 + .../translate/_upload_source_files.py | 41 ++- .../translate/_upload_translations.py | 36 +- .../tests/errors/test_errors.py | 34 +- .../locales/test_custom_locale_mapping.py | 1 + .../tests/locales/test_determine_locale.py | 1 + .../locales/test_get_locale_direction.py | 1 + .../tests/locales/test_get_locale_emoji.py | 1 + .../tests/locales/test_get_locale_name.py | 1 + .../locales/test_get_locale_properties.py | 1 + .../tests/locales/test_get_plural_form.py | 1 + .../locales/test_get_region_properties.py | 1 + .../tests/locales/test_is_same_dialect.py | 1 + .../tests/locales/test_is_same_language.py | 1 + .../tests/locales/test_is_superset_locale.py | 1 + .../tests/locales/test_is_valid_locale.py | 1 + .../locales/test_requires_translation.py | 1 + .../tests/locales/test_resolve_locale.py | 1 + .../tests/static/test_known_discrepancies.py | 6 +- .../tests/translate/test_batch.py | 15 +- .../tests/translate/test_endpoints.py | 111 +++++- .../tests/translate/test_headers.py | 4 + .../tests/translate/test_request.py | 43 ++- packages/gt-fastapi/src/gt_fastapi/_setup.py | 4 +- packages/gt-flask/src/gt_flask/_setup.py | 4 +- .../src/gt_i18n/i18n_manager/_i18n_manager.py | 12 +- .../src/gt_i18n/i18n_manager/_singleton.py | 4 +- .../i18n_manager/_translations_manager.py | 4 +- .../translation_functions/_interpolate.py | 8 +- packages/gt-i18n/tests/test_hash_message.py | 75 ++++- packages/gt-i18n/tests/test_interpolate.py | 4 +- 57 files changed, 781 insertions(+), 272 deletions(-) create mode 100644 Makefile diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fa1ae3b..f1fa5b2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -65,7 +65,7 @@ A good bug report shouldn't leave others needing to chase you up for more inform #### How Do I Submit a Good Bug Report? -> You must never report security related issues, vulnerabilities or bugs including sensitive information to the issue tracker, or elsewhere in public. Instead sensitive bugs must be sent by email to . +> You must never report security related issues, vulnerabilities or bugs including sensitive information to the issue tracker, or elsewhere in public. Instead sensitive bugs must be sent by email to . We use GitHub issues to track bugs and errors. If you run into an issue with the project: @@ -86,7 +86,7 @@ Enhancement suggestions are tracked as [GitHub issues](https://github.com/genera ### Your First Code Contribution 1. Fork the repository -2. Create a feature branch (`git checkout -b feature/my-feature`) +2. Create a feature/fix/refactor/etc. branch (`git checkout -b feature/my-feature`) 3. Follow the [Development Setup](#development-setup) instructions below 4. Make your changes 5. Run tests and linting to make sure everything passes @@ -97,13 +97,6 @@ Enhancement suggestions are tracked as [GitHub issues](https://github.com/genera ### Prerequisites - **Python 3.10+** -- **[uv](https://docs.astral.sh/uv/)** — package manager and workspace tool - -Install uv: - -```bash -curl -LsSf https://astral.sh/uv/install.sh | sh -``` ### Installation @@ -115,7 +108,14 @@ cd gt-python ./scripts/setup.sh ``` -This installs uv (if needed), all workspace packages, Rust/Cargo (if needed), and [Sampo](https://github.com/bruits/sampo) (the release tool). A virtual environment is created at `.venv/` with all workspace packages as editable installs — changes to source code are immediately reflected. +The setup script installs everything you need: + +- [uv](https://docs.astral.sh/uv/) — package manager and workspace tool +- All workspace packages (as editable installs in `.venv/`) +- [Rust/Cargo](https://rustup.rs/) — needed to install Sampo +- [Sampo](https://github.com/bruits/sampo) — release automation tool + +Changes to source code are immediately reflected — no rebuild needed. ### Recommended Editor Extensions @@ -130,26 +130,21 @@ If you're using VS Code, install the following extensions: ### Common Commands ```bash -# Install all workspace packages -uv sync --all-packages +make setup # Install uv, workspace packages, and sampo +make lint # Run ruff linter +make format # Auto-format with ruff +make format-check # Check formatting without changes +make typecheck # Run mypy type checking +make test # Run all tests +make check # Run all checks (lint + format + typecheck + test) +make build # Build all packages to dist/ +make clean # Remove build artifacts and caches +``` -# Run all tests -uv run pytest +To run tests for a specific package: -# Run tests for a specific package +```bash uv run pytest packages/generaltranslation/ - -# Lint -uv run ruff check . - -# Format -uv run ruff format . - -# Type check -uv run mypy . - -# Build a specific package -uv build --package generaltranslation ``` ### Releasing @@ -179,6 +174,7 @@ This project uses [Ruff](https://docs.astral.sh/ruff/) for linting and formattin ### Commit Messages +Please use [Semantic Commit Messages](https://gist.github.com/joshbuchea/6f47e86d2510bce28f8e7f42ae84c716). Use clear, descriptive commit messages that explain the "why" behind the change. ## Join The Project Team diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a258809 --- /dev/null +++ b/Makefile @@ -0,0 +1,37 @@ +.PHONY: setup lint format typecheck test check build clean + +setup: + ./scripts/setup.sh + +lint: + uv run ruff check . + +format: + uv run ruff format . + +format-check: + uv run ruff format --check . + +typecheck: + uv run mypy . + +test: + uv run pytest + +check: lint format-check typecheck test + +build: + @for pkg in packages/*/; do \ + pkg_name=$$(basename "$$pkg"); \ + if grep -q '\[build-system\]' "$$pkg/pyproject.toml" 2>/dev/null; then \ + echo "Building $$pkg_name..."; \ + uv build --package "$$pkg_name" --out-dir dist/; \ + fi; \ + done + +clean: + rm -rf dist/ + find . -type d -name __pycache__ -exec rm -rf {} + + find . -type d -name '*.egg-info' -exec rm -rf {} + + find . -type d -name .mypy_cache -exec rm -rf {} + + find . -type d -name .ruff_cache -exec rm -rf {} + diff --git a/examples/fastapi-eager/scripts/test_endpoints.py b/examples/fastapi-eager/scripts/test_endpoints.py index 9b5a8c4..acdc5c8 100644 --- a/examples/fastapi-eager/scripts/test_endpoints.py +++ b/examples/fastapi-eager/scripts/test_endpoints.py @@ -9,9 +9,7 @@ BASE_URL = "http://localhost:8000" -def test_endpoint( - path: str, locale: str, expected_substr: str, name: str -) -> None: +def test_endpoint(path: str, locale: str, expected_substr: str, name: str) -> None: headers = {"Accept-Language": locale} resp = httpx.get(f"{BASE_URL}{path}", headers=headers) body = resp.json() diff --git a/examples/fastapi-lazy/scripts/test_endpoints.py b/examples/fastapi-lazy/scripts/test_endpoints.py index e3f2365..638a887 100644 --- a/examples/fastapi-lazy/scripts/test_endpoints.py +++ b/examples/fastapi-lazy/scripts/test_endpoints.py @@ -9,9 +9,7 @@ BASE_URL = "http://localhost:8001" -def test_endpoint( - path: str, locale: str, expected_substr: str, name: str -) -> None: +def test_endpoint(path: str, locale: str, expected_substr: str, name: str) -> None: headers = {"Accept-Language": locale} resp = httpx.get(f"{BASE_URL}{path}", headers=headers) body = resp.json() diff --git a/examples/flask-eager/scripts/test_endpoints.py b/examples/flask-eager/scripts/test_endpoints.py index 7a50432..76ba471 100644 --- a/examples/flask-eager/scripts/test_endpoints.py +++ b/examples/flask-eager/scripts/test_endpoints.py @@ -9,9 +9,7 @@ BASE_URL = "http://localhost:5050" -def test_endpoint( - path: str, locale: str, expected_substr: str, name: str -) -> None: +def test_endpoint(path: str, locale: str, expected_substr: str, name: str) -> None: headers = {"Accept-Language": locale} resp = httpx.get(f"{BASE_URL}{path}", headers=headers) body = resp.json() diff --git a/examples/flask-lazy/scripts/test_endpoints.py b/examples/flask-lazy/scripts/test_endpoints.py index bca2e5f..6542316 100644 --- a/examples/flask-lazy/scripts/test_endpoints.py +++ b/examples/flask-lazy/scripts/test_endpoints.py @@ -9,9 +9,7 @@ BASE_URL = "http://localhost:5051" -def test_endpoint( - path: str, locale: str, expected_substr: str, name: str -) -> None: +def test_endpoint(path: str, locale: str, expected_substr: str, name: str) -> None: headers = {"Accept-Language": locale} resp = httpx.get(f"{BASE_URL}{path}", headers=headers) body = resp.json() diff --git a/packages/generaltranslation-icu-messageformat-parser/src/generaltranslation_icu_messageformat_parser/_parser.py b/packages/generaltranslation-icu-messageformat-parser/src/generaltranslation_icu_messageformat_parser/_parser.py index 8da8e00..30a6486 100644 --- a/packages/generaltranslation-icu-messageformat-parser/src/generaltranslation_icu_messageformat_parser/_parser.py +++ b/packages/generaltranslation-icu-messageformat-parser/src/generaltranslation_icu_messageformat_parser/_parser.py @@ -233,9 +233,7 @@ def _parse_text( msg = context["msg"] length = context["length"] start = context["i"] - is_hash_special = ( - parent and parent["type"] in self.options["subnumeric_types"] - ) + is_hash_special = parent and parent["type"] in self.options["subnumeric_types"] is_tag_special = self.options["allow_tags"] allow_arg_spaces = self.options["allow_format_spaces"] @@ -323,9 +321,7 @@ def _parse_placeholder(self, context: dict, parent: dict | None) -> dict: msg = context["msg"] length = context["length"] preserve_ws = self.options["preserve_whitespace"] - is_hash_special = ( - parent and parent["type"] in self.options["subnumeric_types"] - ) + is_hash_special = parent and parent["type"] in self.options["subnumeric_types"] start_idx = context["i"] char = msg[start_idx] if start_idx < length else None @@ -575,7 +571,10 @@ def _parse_offset(self, context: dict) -> int: length = context["length"] start = context["i"] - if start >= length or msg[start : start + len(constants.OFFSET)] != constants.OFFSET: + if ( + start >= length + or msg[start : start + len(constants.OFFSET)] != constants.OFFSET + ): return 0 _append_token(context, "offset", constants.OFFSET) @@ -607,7 +606,11 @@ def _parse_submessages(self, context: dict, parent: dict) -> dict | None: while context["i"] < length and msg[context["i"]] != constants.CHAR_CLOSE: # Save position before consuming space so we can rewind if we hit } pre_space_pos = context["i"] - ws_before_selector = _skip_space(context, ret=preserve_ws) if preserve_ws else _skip_space(context) + ws_before_selector = ( + _skip_space(context, ret=preserve_ws) + if preserve_ws + else _skip_space(context) + ) if context["i"] >= length or msg[context["i"]] == constants.CHAR_CLOSE: # Rewind: this trailing space belongs to the outer placeholder's before_close @@ -620,7 +623,11 @@ def _parse_submessages(self, context: dict, parent: dict) -> dict | None: raise _expected("sub-message selector", context) _append_token(context, "selector", selector) - ws_after_selector = _skip_space(context, ret=preserve_ws) if preserve_ws else _skip_space(context) + ws_after_selector = ( + _skip_space(context, ret=preserve_ws) + if preserve_ws + else _skip_space(context) + ) submessage = self._parse_submessage(context, parent) diff --git a/packages/generaltranslation-icu-messageformat-parser/src/generaltranslation_icu_messageformat_parser/_printer.py b/packages/generaltranslation-icu-messageformat-parser/src/generaltranslation_icu_messageformat_parser/_printer.py index e8154f8..1155e3f 100644 --- a/packages/generaltranslation-icu-messageformat-parser/src/generaltranslation_icu_messageformat_parser/_printer.py +++ b/packages/generaltranslation-icu-messageformat-parser/src/generaltranslation_icu_messageformat_parser/_printer.py @@ -137,7 +137,9 @@ def _print_node(node: dict) -> str: result += f"offset:{offset} " options = node["options"] - options_ws = options.get("_ws", {}) if isinstance(options.get("_ws"), dict) else {} + options_ws = ( + options.get("_ws", {}) if isinstance(options.get("_ws"), dict) else {} + ) child_in_plural = node_type in ("plural", "selectordinal") diff --git a/packages/generaltranslation-intl-messageformat/tests/test_formatter.py b/packages/generaltranslation-intl-messageformat/tests/test_formatter.py index a104db5..58e056d 100644 --- a/packages/generaltranslation-intl-messageformat/tests/test_formatter.py +++ b/packages/generaltranslation-intl-messageformat/tests/test_formatter.py @@ -60,27 +60,117 @@ def test_simple_variables(pattern, locale, variables, desc): # Plural - English # --------------------------------------------------------------------------- PLURAL_EN_CASES = [ - ("{count, plural, one {# item} other {# items}}", "en", {"count": 0}, "en plural 0"), - ("{count, plural, one {# item} other {# items}}", "en", {"count": 1}, "en plural 1"), - ("{count, plural, one {# item} other {# items}}", "en", {"count": 2}, "en plural 2"), - ("{count, plural, one {# item} other {# items}}", "en", {"count": 5}, "en plural 5"), - ("{count, plural, one {# item} other {# items}}", "en", {"count": 10}, "en plural 10"), - ("{count, plural, one {# item} other {# items}}", "en", {"count": 100}, "en plural 100"), - ("{count, plural, one {# item} other {# items}}", "en", {"count": 1000}, "en plural 1000"), + ( + "{count, plural, one {# item} other {# items}}", + "en", + {"count": 0}, + "en plural 0", + ), + ( + "{count, plural, one {# item} other {# items}}", + "en", + {"count": 1}, + "en plural 1", + ), + ( + "{count, plural, one {# item} other {# items}}", + "en", + {"count": 2}, + "en plural 2", + ), + ( + "{count, plural, one {# item} other {# items}}", + "en", + {"count": 5}, + "en plural 5", + ), + ( + "{count, plural, one {# item} other {# items}}", + "en", + {"count": 10}, + "en plural 10", + ), + ( + "{count, plural, one {# item} other {# items}}", + "en", + {"count": 100}, + "en plural 100", + ), + ( + "{count, plural, one {# item} other {# items}}", + "en", + {"count": 1000}, + "en plural 1000", + ), # Exact matches - ("{n, plural, =0 {zero} =1 {one exactly} one {# one} other {# other}}", "en", {"n": 0}, "en exact 0"), - ("{n, plural, =0 {zero} =1 {one exactly} one {# one} other {# other}}", "en", {"n": 1}, "en exact 1"), - ("{n, plural, =0 {zero} =1 {one exactly} one {# one} other {# other}}", "en", {"n": 2}, "en exact 2"), - ("{n, plural, =0 {zero} =1 {one exactly} one {# one} other {# other}}", "en", {"n": 5}, "en exact 5"), + ( + "{n, plural, =0 {zero} =1 {one exactly} one {# one} other {# other}}", + "en", + {"n": 0}, + "en exact 0", + ), + ( + "{n, plural, =0 {zero} =1 {one exactly} one {# one} other {# other}}", + "en", + {"n": 1}, + "en exact 1", + ), + ( + "{n, plural, =0 {zero} =1 {one exactly} one {# one} other {# other}}", + "en", + {"n": 2}, + "en exact 2", + ), + ( + "{n, plural, =0 {zero} =1 {one exactly} one {# one} other {# other}}", + "en", + {"n": 5}, + "en exact 5", + ), # Multiple exact - ("{n, plural, =0 {none} =1 {single} =2 {double} =3 {triple} other {# many}}", "en", {"n": 0}, "en multi exact 0"), - ("{n, plural, =0 {none} =1 {single} =2 {double} =3 {triple} other {# many}}", "en", {"n": 1}, "en multi exact 1"), - ("{n, plural, =0 {none} =1 {single} =2 {double} =3 {triple} other {# many}}", "en", {"n": 2}, "en multi exact 2"), - ("{n, plural, =0 {none} =1 {single} =2 {double} =3 {triple} other {# many}}", "en", {"n": 3}, "en multi exact 3"), - ("{n, plural, =0 {none} =1 {single} =2 {double} =3 {triple} other {# many}}", "en", {"n": 7}, "en multi exact 7"), + ( + "{n, plural, =0 {none} =1 {single} =2 {double} =3 {triple} other {# many}}", + "en", + {"n": 0}, + "en multi exact 0", + ), + ( + "{n, plural, =0 {none} =1 {single} =2 {double} =3 {triple} other {# many}}", + "en", + {"n": 1}, + "en multi exact 1", + ), + ( + "{n, plural, =0 {none} =1 {single} =2 {double} =3 {triple} other {# many}}", + "en", + {"n": 2}, + "en multi exact 2", + ), + ( + "{n, plural, =0 {none} =1 {single} =2 {double} =3 {triple} other {# many}}", + "en", + {"n": 3}, + "en multi exact 3", + ), + ( + "{n, plural, =0 {none} =1 {single} =2 {double} =3 {triple} other {# many}}", + "en", + {"n": 7}, + "en multi exact 7", + ), # Hash in text - ("{count, plural, one {There is # cat} other {There are # cats}}", "en", {"count": 1}, "en hash text 1"), - ("{count, plural, one {There is # cat} other {There are # cats}}", "en", {"count": 5}, "en hash text 5"), + ( + "{count, plural, one {There is # cat} other {There are # cats}}", + "en", + {"count": 1}, + "en hash text 1", + ), + ( + "{count, plural, one {There is # cat} other {There are # cats}}", + "en", + {"count": 5}, + "en hash text 5", + ), # Only other ("{count, plural, other {# things}}", "en", {"count": 1}, "en other only 1"), ("{count, plural, other {# things}}", "en", {"count": 42}, "en other only 42"), @@ -118,7 +208,12 @@ def test_plural_german(pattern, locale, variables, desc): # Plural - French (0 and 1 are "one") # --------------------------------------------------------------------------- PLURAL_FR_CASES = [ - ("{n, plural, one {# élément} other {# éléments}}", "fr", {"n": v}, f"fr plural {v}") + ( + "{n, plural, one {# élément} other {# éléments}}", + "fr", + {"n": v}, + f"fr plural {v}", + ) for v in [0, 1, 2, 5, 10, 100] ] @@ -172,7 +267,9 @@ def test_plural_japanese(pattern, locale, variables, desc): # --------------------------------------------------------------------------- # Plural - Russian (one, few, many, other) # --------------------------------------------------------------------------- -RUSSIAN_PATTERN = "{n, plural, one {# книга} few {# книги} many {# книг} other {# книг}}" +RUSSIAN_PATTERN = ( + "{n, plural, one {# книга} few {# книги} many {# книг} other {# книг}}" +) PLURAL_RU_CASES = [ (RUSSIAN_PATTERN, "ru", {"n": v}, f"ru plural {v}") for v in [0, 1, 2, 3, 4, 5, 10, 11, 12, 14, 20, 21, 22, 25, 100, 101, 102] @@ -191,7 +288,9 @@ def test_plural_russian(pattern, locale, variables, desc): # --------------------------------------------------------------------------- # Plural - Polish (one, few, many, other) # --------------------------------------------------------------------------- -POLISH_PATTERN = "{n, plural, one {# plik} few {# pliki} many {# plików} other {# plików}}" +POLISH_PATTERN = ( + "{n, plural, one {# plik} few {# pliki} many {# plików} other {# plików}}" +) PLURAL_PL_CASES = [ (POLISH_PATTERN, "pl", {"n": v}, f"pl plural {v}") for v in [0, 1, 2, 3, 4, 5, 10, 12, 14, 21, 22, 23, 25, 100, 102] @@ -232,7 +331,33 @@ def test_plural_offset(pattern, locale, variables, desc): ORDINAL_PATTERN = "{n, selectordinal, one {#st} two {#nd} few {#rd} other {#th}}" SELECTORDINAL_CASES = [ (ORDINAL_PATTERN, "en", {"n": v}, f"en ordinal {v}") - for v in [1, 2, 3, 4, 5, 10, 11, 12, 13, 14, 21, 22, 23, 24, 31, 32, 33, 42, 100, 101, 102, 103, 111, 112, 113] + for v in [ + 1, + 2, + 3, + 4, + 5, + 10, + 11, + 12, + 13, + 14, + 21, + 22, + 23, + 24, + 31, + 32, + 33, + 42, + 100, + 101, + 102, + 103, + 111, + 112, + 113, + ] ] @@ -249,18 +374,78 @@ def test_selectordinal_english(pattern, locale, variables, desc): # Select # --------------------------------------------------------------------------- SELECT_CASES = [ - ("{g, select, male {He} female {She} other {They}}", "en", {"g": "male"}, "select male"), - ("{g, select, male {He} female {She} other {They}}", "en", {"g": "female"}, "select female"), - ("{g, select, male {He} female {She} other {They}}", "en", {"g": "other"}, "select other explicit"), - ("{g, select, male {He} female {She} other {They}}", "en", {"g": "unknown"}, "select fallback"), - ("{g, select, male {He} female {She} other {They}}", "en", {"g": ""}, "select empty"), - ("{type, select, cat {meow} dog {woof} bird {tweet} other {???}}", "en", {"type": "cat"}, "select cat"), - ("{type, select, cat {meow} dog {woof} bird {tweet} other {???}}", "en", {"type": "dog"}, "select dog"), - ("{type, select, cat {meow} dog {woof} bird {tweet} other {???}}", "en", {"type": "bird"}, "select bird"), - ("{type, select, cat {meow} dog {woof} bird {tweet} other {???}}", "en", {"type": "fish"}, "select fish fallback"), - ("{status, select, active {Active} inactive {Inactive} pending {Pending} other {Unknown}}", "en", {"status": "active"}, "status active"), - ("{status, select, active {Active} inactive {Inactive} pending {Pending} other {Unknown}}", "en", {"status": "pending"}, "status pending"), - ("{status, select, active {Active} inactive {Inactive} pending {Pending} other {Unknown}}", "en", {"status": "deleted"}, "status unknown"), + ( + "{g, select, male {He} female {She} other {They}}", + "en", + {"g": "male"}, + "select male", + ), + ( + "{g, select, male {He} female {She} other {They}}", + "en", + {"g": "female"}, + "select female", + ), + ( + "{g, select, male {He} female {She} other {They}}", + "en", + {"g": "other"}, + "select other explicit", + ), + ( + "{g, select, male {He} female {She} other {They}}", + "en", + {"g": "unknown"}, + "select fallback", + ), + ( + "{g, select, male {He} female {She} other {They}}", + "en", + {"g": ""}, + "select empty", + ), + ( + "{type, select, cat {meow} dog {woof} bird {tweet} other {???}}", + "en", + {"type": "cat"}, + "select cat", + ), + ( + "{type, select, cat {meow} dog {woof} bird {tweet} other {???}}", + "en", + {"type": "dog"}, + "select dog", + ), + ( + "{type, select, cat {meow} dog {woof} bird {tweet} other {???}}", + "en", + {"type": "bird"}, + "select bird", + ), + ( + "{type, select, cat {meow} dog {woof} bird {tweet} other {???}}", + "en", + {"type": "fish"}, + "select fish fallback", + ), + ( + "{status, select, active {Active} inactive {Inactive} pending {Pending} other {Unknown}}", + "en", + {"status": "active"}, + "status active", + ), + ( + "{status, select, active {Active} inactive {Inactive} pending {Pending} other {Unknown}}", + "en", + {"status": "pending"}, + "status pending", + ), + ( + "{status, select, active {Active} inactive {Inactive} pending {Pending} other {Unknown}}", + "en", + {"status": "deleted"}, + "status unknown", + ), ] @@ -279,55 +464,81 @@ def test_select(pattern, locale, variables, desc): NESTED_CASES = [ ( "You have {count, plural, one {# new message} other {# new messages}}.", - "en", {"count": 1}, "nested plural 1" + "en", + {"count": 1}, + "nested plural 1", ), ( "You have {count, plural, one {# new message} other {# new messages}}.", - "en", {"count": 5}, "nested plural 5" + "en", + {"count": 5}, + "nested plural 5", ), ( "{name} has {count, plural, one {# cat} other {# cats}}", - "en", {"name": "Alice", "count": 1}, "multi var plural 1" + "en", + {"name": "Alice", "count": 1}, + "multi var plural 1", ), ( "{name} has {count, plural, one {# cat} other {# cats}}", - "en", {"name": "Bob", "count": 3}, "multi var plural 3" + "en", + {"name": "Bob", "count": 3}, + "multi var plural 3", ), ( "{a, select, x {{b, plural, one {deep #} other {deeper #}}} other {fallback}}", - "en", {"a": "x", "b": 1}, "nested select-plural 1" + "en", + {"a": "x", "b": 1}, + "nested select-plural 1", ), ( "{a, select, x {{b, plural, one {deep #} other {deeper #}}} other {fallback}}", - "en", {"a": "x", "b": 5}, "nested select-plural 5" + "en", + {"a": "x", "b": 5}, + "nested select-plural 5", ), ( "{a, select, x {{b, plural, one {deep #} other {deeper #}}} other {fallback}}", - "en", {"a": "y", "b": 5}, "nested select fallback" + "en", + {"a": "y", "b": 5}, + "nested select fallback", ), ( "Hello {name}, you have {count, plural, =0 {no messages} one {# message} other {# messages}} from {sender}.", - "en", {"name": "Alice", "count": 0, "sender": "Bob"}, "complex nested 0" + "en", + {"name": "Alice", "count": 0, "sender": "Bob"}, + "complex nested 0", ), ( "Hello {name}, you have {count, plural, =0 {no messages} one {# message} other {# messages}} from {sender}.", - "en", {"name": "Alice", "count": 1, "sender": "Bob"}, "complex nested 1" + "en", + {"name": "Alice", "count": 1, "sender": "Bob"}, + "complex nested 1", ), ( "Hello {name}, you have {count, plural, =0 {no messages} one {# message} other {# messages}} from {sender}.", - "en", {"name": "Alice", "count": 42, "sender": "Bob"}, "complex nested 42" + "en", + {"name": "Alice", "count": 42, "sender": "Bob"}, + "complex nested 42", ), ( "{gender, select, male {He has {count, plural, one {# item} other {# items}}} female {She has {count, plural, one {# item} other {# items}}} other {They have {count, plural, one {# item} other {# items}}}}", - "en", {"gender": "male", "count": 1}, "gender+plural male 1" + "en", + {"gender": "male", "count": 1}, + "gender+plural male 1", ), ( "{gender, select, male {He has {count, plural, one {# item} other {# items}}} female {She has {count, plural, one {# item} other {# items}}} other {They have {count, plural, one {# item} other {# items}}}}", - "en", {"gender": "female", "count": 5}, "gender+plural female 5" + "en", + {"gender": "female", "count": 5}, + "gender+plural female 5", ), ( "{gender, select, male {He has {count, plural, one {# item} other {# items}}} female {She has {count, plural, one {# item} other {# items}}} other {They have {count, plural, one {# item} other {# items}}}}", - "en", {"gender": "unknown", "count": 0}, "gender+plural other 0" + "en", + {"gender": "unknown", "count": 0}, + "gender+plural other 0", ), ] @@ -366,7 +577,12 @@ def test_escaped(pattern, locale, variables, desc): GT_CASES = [ ("{_gt_, select, other {Hello World}}", "en", {"_gt_": "x"}, "gt simple"), ("{_gt_1, select, other {value1}}", "en", {"_gt_1": "x"}, "gt indexed"), - ("Je joue avec mon ami {_gt_1} au {_gt_2}", "en", {"_gt_1": "Brian", "_gt_2": "park"}, "gt condensed"), + ( + "Je joue avec mon ami {_gt_1} au {_gt_2}", + "en", + {"_gt_1": "Brian", "_gt_2": "park"}, + "gt condensed", + ), ] @@ -398,9 +614,9 @@ def test_invalid_locale_falls_back_to_english(self): assert result == "1 item" def test_select_missing_value_uses_other(self): - result = IntlMessageFormat( - "{x, select, a {A} other {default}}", "en" - ).format({"x": "missing"}) + result = IntlMessageFormat("{x, select, a {A} other {default}}", "en").format( + {"x": "missing"} + ) assert result == "default" def test_format_none_values(self): diff --git a/packages/generaltranslation/src/generaltranslation/_gt.py b/packages/generaltranslation/src/generaltranslation/_gt.py index 0bb031f..ddc2203 100644 --- a/packages/generaltranslation/src/generaltranslation/_gt.py +++ b/packages/generaltranslation/src/generaltranslation/_gt.py @@ -314,7 +314,11 @@ async def query_file_data( result["translatedFiles"] = [ { **item, - **({"locale": self.resolve_alias_locale(item["locale"])} if item.get("locale") else {}), + **( + {"locale": self.resolve_alias_locale(item["locale"])} + if item.get("locale") + else {} + ), } for item in result["translatedFiles"] ] @@ -322,8 +326,19 @@ async def query_file_data( result["sourceFiles"] = [ { **item, - **({"sourceLocale": self.resolve_alias_locale(item["sourceLocale"])} if item.get("sourceLocale") else {}), - "locales": [self.resolve_alias_locale(loc) for loc in item.get("locales", [])], + **( + { + "sourceLocale": self.resolve_alias_locale( + item["sourceLocale"] + ) + } + if item.get("sourceLocale") + else {} + ), + "locales": [ + self.resolve_alias_locale(loc) + for loc in item.get("locales", []) + ], } for item in result["sourceFiles"] ] @@ -343,7 +358,11 @@ async def query_source_file( result["translations"] = [ { **item, - **({"locale": self.resolve_alias_locale(item["locale"])} if item.get("locale") else {}), + **( + {"locale": self.resolve_alias_locale(item["locale"])} + if item.get("locale") + else {} + ), } for item in result["translations"] ] @@ -407,7 +426,11 @@ async def download_file_batch( files = [ { **f, - **({"locale": self.resolve_alias_locale(f["locale"])} if f.get("locale") else {}), + **( + {"locale": self.resolve_alias_locale(f["locale"])} + if f.get("locale") + else {} + ), } for f in result["data"] ] @@ -559,7 +582,9 @@ def format_cutoff( """Format a string with cutoff behaviour.""" opts = dict(options or {}) opts.update(kwargs) - return format_cutoff(value, locales=locales or self._rendering_locales, options=opts) + return format_cutoff( + value, locales=locales or self._rendering_locales, options=opts + ) def format_message( self, diff --git a/packages/generaltranslation/src/generaltranslation/formatting/_format_currency.py b/packages/generaltranslation/src/generaltranslation/formatting/_format_currency.py index 20b9982..bc54366 100644 --- a/packages/generaltranslation/src/generaltranslation/formatting/_format_currency.py +++ b/packages/generaltranslation/src/generaltranslation/formatting/_format_currency.py @@ -12,7 +12,7 @@ # Map JS currencyDisplay values to Babel format_type _DISPLAY_MAP = { - "symbol": None, # Babel default + "symbol": None, # Babel default "narrowSymbol": None, # Babel doesn't distinguish; use default "name": "name", } @@ -64,6 +64,7 @@ def format_currency( ) # Get the currency symbol to replace it with the code from babel.numbers import get_currency_symbol + symbol = get_currency_symbol(currency.upper(), locale=locale) return formatted.replace(symbol, currency.upper()) diff --git a/packages/generaltranslation/src/generaltranslation/formatting/_format_cutoff.py b/packages/generaltranslation/src/generaltranslation/formatting/_format_cutoff.py index 28411c9..647c607 100644 --- a/packages/generaltranslation/src/generaltranslation/formatting/_format_cutoff.py +++ b/packages/generaltranslation/src/generaltranslation/formatting/_format_cutoff.py @@ -15,8 +15,8 @@ TERMINATOR_MAP: dict[str, dict[str, dict[str, str | None]]] = { "ellipsis": { "fr": { - "terminator": "\u2026", # … - "separator": "\u202F", # narrow no-break space + "terminator": "\u2026", # … + "separator": "\u202f", # narrow no-break space }, "zh": { "terminator": "\u2026\u2026", # …… @@ -27,7 +27,7 @@ "separator": None, }, "_default": { - "terminator": "\u2026", # … + "terminator": "\u2026", # … "separator": None, }, }, @@ -83,10 +83,14 @@ def __init__( style_map = TERMINATOR_MAP[style] preset = style_map.get(lang, style_map["_default"]) - terminator = options.get("terminator", preset.get("terminator") if preset else None) + terminator = options.get( + "terminator", preset.get("terminator") if preset else None + ) separator: str | None = None if terminator is not None: - separator = options.get("separator", preset.get("separator") if preset else None) + separator = options.get( + "separator", preset.get("separator") if preset else None + ) # Calculate addition length self._addition_length = (len(terminator) if terminator else 0) + ( diff --git a/packages/generaltranslation/src/generaltranslation/formatting/_format_date_time.py b/packages/generaltranslation/src/generaltranslation/formatting/_format_date_time.py index ab857fc..2368923 100644 --- a/packages/generaltranslation/src/generaltranslation/formatting/_format_date_time.py +++ b/packages/generaltranslation/src/generaltranslation/formatting/_format_date_time.py @@ -46,7 +46,11 @@ def format_date_time( time_style = options.get("time_style") if date_style and time_style: - return format_datetime(value, format=date_style, locale=locale) + " " + format_time(value, format=time_style, locale=locale) + return ( + format_datetime(value, format=date_style, locale=locale) + + " " + + format_time(value, format=time_style, locale=locale) + ) if date_style: return format_date(value, format=date_style, locale=locale) diff --git a/packages/generaltranslation/src/generaltranslation/formatting/_format_list.py b/packages/generaltranslation/src/generaltranslation/formatting/_format_list.py index f484cc5..e1f71da 100644 --- a/packages/generaltranslation/src/generaltranslation/formatting/_format_list.py +++ b/packages/generaltranslation/src/generaltranslation/formatting/_format_list.py @@ -109,7 +109,7 @@ def format_list_to_parts( # There's a separator before this element parts.append(remaining[:idx]) parts.append(val) - remaining = remaining[idx + len(ph):] + remaining = remaining[idx + len(ph) :] # Any trailing text after the last placeholder if remaining: diff --git a/packages/generaltranslation/src/generaltranslation/formatting/_format_relative_time.py b/packages/generaltranslation/src/generaltranslation/formatting/_format_relative_time.py index 280a3bc..89bfb3a 100644 --- a/packages/generaltranslation/src/generaltranslation/formatting/_format_relative_time.py +++ b/packages/generaltranslation/src/generaltranslation/formatting/_format_relative_time.py @@ -23,11 +23,11 @@ "days": "days", "week": "weeks", "weeks": "weeks", - "month": "days", # approximate: 30 days per month + "month": "days", # approximate: 30 days per month "months": "days", - "year": "days", # approximate: 365 days per year + "year": "days", # approximate: 365 days per year "years": "days", - "quarter": "days", # approximate: 91 days per quarter + "quarter": "days", # approximate: 91 days per quarter "quarters": "days", } diff --git a/packages/generaltranslation/src/generaltranslation/locales/_get_locale_emoji.py b/packages/generaltranslation/src/generaltranslation/locales/_get_locale_emoji.py index 5b41388..4d11c9e 100644 --- a/packages/generaltranslation/src/generaltranslation/locales/_get_locale_emoji.py +++ b/packages/generaltranslation/src/generaltranslation/locales/_get_locale_emoji.py @@ -30,12 +30,8 @@ "ku": EUROPE_AFRICA_GLOBE, "bo": ASIA_AUSTRALIA_GLOBE, "ug": ASIA_AUSTRALIA_GLOBE, - "gd": ( - "\U0001f3f4\U000e0067\U000e0062\U000e0073\U000e0063\U000e0074\U000e007f" - ), - "cy": ( - "\U0001f3f4\U000e0067\U000e0062\U000e0077\U000e006c\U000e0073\U000e007f" - ), + "gd": ("\U0001f3f4\U000e0067\U000e0062\U000e0073\U000e0063\U000e0074\U000e007f"), + "cy": ("\U0001f3f4\U000e0067\U000e0062\U000e0077\U000e006c\U000e0073\U000e007f"), "gv": "\U0001f1ee\U0001f1f2", "grc": "\U0001f3fa", } @@ -323,9 +319,7 @@ def get_locale_emoji( entry = custom_mapping[locale] if isinstance(entry, dict): canonical = entry["code"] - custom_emoji = get_custom_property( - custom_mapping, canonical, "emoji" - ) + custom_emoji = get_custom_property(custom_mapping, canonical, "emoji") if custom_emoji: return custom_emoji diff --git a/packages/generaltranslation/src/generaltranslation/locales/_get_locale_name.py b/packages/generaltranslation/src/generaltranslation/locales/_get_locale_name.py index 727e63a..f283bd5 100644 --- a/packages/generaltranslation/src/generaltranslation/locales/_get_locale_name.py +++ b/packages/generaltranslation/src/generaltranslation/locales/_get_locale_name.py @@ -62,9 +62,7 @@ def get_locale_name( entry = custom_mapping[locale] if isinstance(entry, dict): canonical = entry["code"] - custom_name = get_custom_property( - custom_mapping, canonical, "name" - ) + custom_name = get_custom_property(custom_mapping, canonical, "name") if custom_name: return custom_name locale = canonical diff --git a/packages/generaltranslation/src/generaltranslation/locales/_get_locale_properties.py b/packages/generaltranslation/src/generaltranslation/locales/_get_locale_properties.py index 2a2ce06..3ef09c0 100644 --- a/packages/generaltranslation/src/generaltranslation/locales/_get_locale_properties.py +++ b/packages/generaltranslation/src/generaltranslation/locales/_get_locale_properties.py @@ -81,9 +81,7 @@ def _build_component_name( if script: details.append(display_locale.scripts.get(script, script)) if territory: - details.append( - display_locale.territories.get(territory, territory) - ) + details.append(display_locale.territories.get(territory, territory)) if not details: return lang_name @@ -205,12 +203,8 @@ def get_locale_properties( # --- Display names in default_locale --- name = _get_compound_name(std_locale, display_locale, parsed) language_name = display_locale.languages.get(language_code, language_code) - region_name = ( - display_locale.territories.get(region_code, "") if region_code else "" - ) - script_name = ( - display_locale.scripts.get(script_code, "") if script_code else "" - ) + region_name = display_locale.territories.get(region_code, "") if region_code else "" + script_name = display_locale.scripts.get(script_code, "") if script_code else "" # Maximized name: always use component form maximized_name = _build_component_name( @@ -236,9 +230,7 @@ def get_locale_properties( native_locale = parsed native_name = _get_compound_name(std_locale, native_locale, parsed) - native_language_name = native_locale.languages.get( - language_code, language_code - ) + native_language_name = native_locale.languages.get(language_code, language_code) native_region_name = ( native_locale.territories.get(region_code, "") if region_code else "" ) @@ -255,9 +247,7 @@ def get_locale_properties( # Name with region code if parsed.territory: name_with_region_code = f"{language_name} ({parsed.territory})" - native_name_with_region_code = ( - f"{native_language_name} ({parsed.territory})" - ) + native_name_with_region_code = f"{native_language_name} ({parsed.territory})" else: name_with_region_code = language_name native_name_with_region_code = native_language_name @@ -291,9 +281,7 @@ def get_locale_properties( # Apply custom overrides if custom_mapping is not None: locale_variants = [original_locale, std_locale, language_code] - custom_props = _create_custom_locale_properties( - locale_variants, custom_mapping - ) + custom_props = _create_custom_locale_properties(locale_variants, custom_mapping) if custom_props: key_map = { "name": "name", diff --git a/packages/generaltranslation/src/generaltranslation/locales/_requires_translation.py b/packages/generaltranslation/src/generaltranslation/locales/_requires_translation.py index 5a203d1..9533368 100644 --- a/packages/generaltranslation/src/generaltranslation/locales/_requires_translation.py +++ b/packages/generaltranslation/src/generaltranslation/locales/_requires_translation.py @@ -69,8 +69,7 @@ def requires_translation( # Check if target language is represented in approved locales target_represented = any( - is_same_language(target_locale, approved) - for approved in approved_locales + is_same_language(target_locale, approved) for approved in approved_locales ) if not target_represented: return False diff --git a/packages/generaltranslation/src/generaltranslation/static/_index_vars.py b/packages/generaltranslation/src/generaltranslation/static/_index_vars.py index e1eed38..1c492d4 100644 --- a/packages/generaltranslation/src/generaltranslation/static/_index_vars.py +++ b/packages/generaltranslation/src/generaltranslation/static/_index_vars.py @@ -4,7 +4,9 @@ from generaltranslation.static._traverse_icu import traverse_icu, is_gt_unindexed_select -def _find_other_span(icu_string: str, node_start: int, node_end: int) -> tuple[int, int]: +def _find_other_span( + icu_string: str, node_start: int, node_end: int +) -> tuple[int, int]: """Find the ``{content}`` span of the ``other`` option within a select node. Returns ``(brace_open, brace_close_exclusive)`` — the positions of the diff --git a/packages/generaltranslation/src/generaltranslation/static/_traverse_icu.py b/packages/generaltranslation/src/generaltranslation/static/_traverse_icu.py index 7302e50..6391b21 100644 --- a/packages/generaltranslation/src/generaltranslation/static/_traverse_icu.py +++ b/packages/generaltranslation/src/generaltranslation/static/_traverse_icu.py @@ -27,11 +27,13 @@ def traverse_icu( Returns: The (possibly mutated) AST. """ - parser = Parser({ - "include_indices": include_indices, - "require_other": False, - "preserve_whitespace": preserve_whitespace, - }) + parser = Parser( + { + "include_indices": include_indices, + "require_other": False, + "preserve_whitespace": preserve_whitespace, + } + ) ast = parser.parse(icu_string) def handle_children(children: list) -> None: diff --git a/packages/generaltranslation/src/generaltranslation/translate/_enqueue_files.py b/packages/generaltranslation/src/generaltranslation/translate/_enqueue_files.py index 3358946..71bc62e 100644 --- a/packages/generaltranslation/src/generaltranslation/translate/_enqueue_files.py +++ b/packages/generaltranslation/src/generaltranslation/translate/_enqueue_files.py @@ -26,11 +26,19 @@ async def _process_batch(batch: list[dict[str, Any]]) -> list[Any]: } for f in batch ], - "targetLocales": options.get("target_locales", options.get("targetLocales", [])), - "sourceLocale": options.get("source_locale", options.get("sourceLocale", "")), + "targetLocales": options.get( + "target_locales", options.get("targetLocales", []) + ), + "sourceLocale": options.get( + "source_locale", options.get("sourceLocale", "") + ), "publish": options.get("publish"), - "requireApproval": options.get("require_approval", options.get("requireApproval")), - "modelProvider": options.get("model_provider", options.get("modelProvider")), + "requireApproval": options.get( + "require_approval", options.get("requireApproval") + ), + "modelProvider": options.get( + "model_provider", options.get("modelProvider") + ), "force": options.get("force"), } body = {k: v for k, v in body.items() if v is not None} diff --git a/packages/generaltranslation/src/generaltranslation/translate/_process_file_moves.py b/packages/generaltranslation/src/generaltranslation/translate/_process_file_moves.py index c6f9516..325cc19 100644 --- a/packages/generaltranslation/src/generaltranslation/translate/_process_file_moves.py +++ b/packages/generaltranslation/src/generaltranslation/translate/_process_file_moves.py @@ -24,7 +24,10 @@ async def _process_batch(batch: list[dict[str, Any]]) -> list[Any]: result = await api_request( config, "/v2/project/files/moves", - body={"branchId": options.get("branch_id", options.get("branchId")), "moves": batch}, + body={ + "branchId": options.get("branch_id", options.get("branchId")), + "moves": batch, + }, timeout=options.get("timeout"), ) return result.get("results", []) diff --git a/packages/generaltranslation/src/generaltranslation/translate/_query_file_data.py b/packages/generaltranslation/src/generaltranslation/translate/_query_file_data.py index ca851c0..5981ebe 100644 --- a/packages/generaltranslation/src/generaltranslation/translate/_query_file_data.py +++ b/packages/generaltranslation/src/generaltranslation/translate/_query_file_data.py @@ -25,7 +25,9 @@ async def query_file_data( for item in source_files ] if data.get("translated_files") or data.get("translatedFiles"): - translated_files = data.get("translated_files") or data.get("translatedFiles") or [] + translated_files = ( + data.get("translated_files") or data.get("translatedFiles") or [] + ) body["translatedFiles"] = [ { "fileId": item.get("file_id", item.get("fileId", "")), diff --git a/packages/generaltranslation/src/generaltranslation/translate/_request.py b/packages/generaltranslation/src/generaltranslation/translate/_request.py index e0c2518..7bb0d7b 100644 --- a/packages/generaltranslation/src/generaltranslation/translate/_request.py +++ b/packages/generaltranslation/src/generaltranslation/translate/_request.py @@ -96,9 +96,7 @@ async def api_request( if attempt < max_retries: await asyncio.sleep(_get_retry_delay(retry_policy, attempt)) continue - raise Exception( - translation_request_failed_error(str(exc)) - ) from exc + raise Exception(translation_request_failed_error(str(exc))) from exc # Retry on 5XX server errors if response.status_code >= 500 and attempt < max_retries: diff --git a/packages/generaltranslation/src/generaltranslation/translate/_translate.py b/packages/generaltranslation/src/generaltranslation/translate/_translate.py index a6cbf0f..5a8fe66 100644 --- a/packages/generaltranslation/src/generaltranslation/translate/_translate.py +++ b/packages/generaltranslation/src/generaltranslation/translate/_translate.py @@ -60,7 +60,9 @@ async def translate_many( else: entry_hash = hash_source( source, - data_format=metadata.get("dataFormat", metadata.get("data_format", "STRING")), + data_format=metadata.get( + "dataFormat", metadata.get("data_format", "STRING") + ), context=metadata.get("context"), id=metadata.get("id"), max_chars=metadata.get("maxChars", metadata.get("max_chars")), @@ -77,8 +79,12 @@ async def translate_many( # Build request body using camelCase keys to match JS API body = { "requests": requests_object, - "targetLocale": global_metadata.get("target_locale", global_metadata.get("targetLocale", "")), - "sourceLocale": global_metadata.get("source_locale", global_metadata.get("sourceLocale", "")), + "targetLocale": global_metadata.get( + "target_locale", global_metadata.get("targetLocale", "") + ), + "sourceLocale": global_metadata.get( + "source_locale", global_metadata.get("sourceLocale", "") + ), "metadata": global_metadata, } @@ -98,7 +104,9 @@ async def translate_many( if hash_order is not None: return [ - response.get(h, {"success": False, "error": "No translation returned", "code": 500}) + response.get( + h, {"success": False, "error": "No translation returned", "code": 500} + ) for h in hash_order ] diff --git a/packages/generaltranslation/src/generaltranslation/translate/_types.py b/packages/generaltranslation/src/generaltranslation/translate/_types.py index a413e43..277cee2 100644 --- a/packages/generaltranslation/src/generaltranslation/translate/_types.py +++ b/packages/generaltranslation/src/generaltranslation/translate/_types.py @@ -7,6 +7,7 @@ # --- Config --- + class TranslationRequestConfig(TypedDict, total=False): project_id: str base_url: str @@ -79,6 +80,7 @@ class TranslationError(TypedDict): # --- Batch --- + class BatchList(TypedDict): data: list[Any] count: int @@ -87,6 +89,7 @@ class BatchList(TypedDict): # --- Enqueue --- + class EnqueueOptions(TypedDict, total=False): source_locale: str target_locales: list[str] @@ -105,6 +108,7 @@ class EnqueueFilesResult(TypedDict, total=False): # --- Setup --- + class SetupProjectOptions(TypedDict, total=False): force: bool locales: list[str] @@ -132,6 +136,7 @@ class JobStatusEntry(TypedDict, total=False): # --- Branch --- + class BranchQuery(TypedDict): branch_names: list[str] @@ -157,6 +162,7 @@ class CreateBranchResult(TypedDict): # --- File Data --- + class FileDataQuery(TypedDict, total=False): source_files: list[dict[str, str]] translated_files: list[dict[str, str]] @@ -184,6 +190,7 @@ class CheckFileTranslationsOptions(TypedDict, total=False): # --- Download --- + class DownloadFileBatchOptions(TypedDict, total=False): timeout: int @@ -207,6 +214,7 @@ class DownloadFileBatchResult(TypedDict): # --- Upload --- + class UploadFilesOptions(TypedDict, total=False): source_locale: str model_provider: str @@ -221,6 +229,7 @@ class UploadFilesResponse(TypedDict, total=False): # --- Orphaned Files --- + class OrphanedFile(TypedDict): file_id: str version_id: str @@ -233,6 +242,7 @@ class GetOrphanedFilesResult(TypedDict): # --- Process Moves --- + class MoveMapping(TypedDict): old_file_id: str new_file_id: str @@ -260,6 +270,7 @@ class ProcessMovesOptions(TypedDict, total=False): # --- Submit User Edit Diffs --- + class SubmitUserEditDiff(TypedDict): file_name: str locale: str @@ -276,6 +287,7 @@ class SubmitUserEditDiffsPayload(TypedDict): # --- Project Data --- + class ProjectData(TypedDict): id: str name: str diff --git a/packages/generaltranslation/src/generaltranslation/translate/_upload_source_files.py b/packages/generaltranslation/src/generaltranslation/translate/_upload_source_files.py index 3172abf..7a84866 100644 --- a/packages/generaltranslation/src/generaltranslation/translate/_upload_source_files.py +++ b/packages/generaltranslation/src/generaltranslation/translate/_upload_source_files.py @@ -24,21 +24,42 @@ async def _process_batch(batch: list[dict[str, Any]]) -> list[Any]: "content": base64.b64encode( item["source"]["content"].encode() ).decode(), - "fileName": item["source"].get("file_name", item["source"].get("fileName", "")), - "fileFormat": item["source"].get("file_format", item["source"].get("fileFormat", "")), + "fileName": item["source"].get( + "file_name", item["source"].get("fileName", "") + ), + "fileFormat": item["source"].get( + "file_format", item["source"].get("fileFormat", "") + ), "locale": item["source"].get("locale", ""), - "dataFormat": item["source"].get("data_format", item["source"].get("dataFormat")), - "formatMetadata": item["source"].get("format_metadata", item["source"].get("formatMetadata")), - "fileId": item["source"].get("file_id", item["source"].get("fileId")), - "versionId": item["source"].get("version_id", item["source"].get("versionId")), - "branchId": item["source"].get("branch_id", item["source"].get("branchId")), - "incomingBranchId": item["source"].get("incoming_branch_id", item["source"].get("incomingBranchId")), - "checkedOutBranchId": item["source"].get("checked_out_branch_id", item["source"].get("checkedOutBranchId")), + "dataFormat": item["source"].get( + "data_format", item["source"].get("dataFormat") + ), + "formatMetadata": item["source"].get( + "format_metadata", item["source"].get("formatMetadata") + ), + "fileId": item["source"].get( + "file_id", item["source"].get("fileId") + ), + "versionId": item["source"].get( + "version_id", item["source"].get("versionId") + ), + "branchId": item["source"].get( + "branch_id", item["source"].get("branchId") + ), + "incomingBranchId": item["source"].get( + "incoming_branch_id", item["source"].get("incomingBranchId") + ), + "checkedOutBranchId": item["source"].get( + "checked_out_branch_id", + item["source"].get("checkedOutBranchId"), + ), } } for item in batch ], - "sourceLocale": options.get("source_locale", options.get("sourceLocale", "")), + "sourceLocale": options.get( + "source_locale", options.get("sourceLocale", "") + ), } result = await api_request( config, diff --git a/packages/generaltranslation/src/generaltranslation/translate/_upload_translations.py b/packages/generaltranslation/src/generaltranslation/translate/_upload_translations.py index 8077ebb..41fbd05 100644 --- a/packages/generaltranslation/src/generaltranslation/translate/_upload_translations.py +++ b/packages/generaltranslation/src/generaltranslation/translate/_upload_translations.py @@ -24,20 +24,32 @@ async def _process_batch(batch: list[dict[str, Any]]) -> list[Any]: "content": base64.b64encode( item["source"]["content"].encode() ).decode(), - "fileName": item["source"].get("file_name", item["source"].get("fileName", "")), - "fileFormat": item["source"].get("file_format", item["source"].get("fileFormat", "")), + "fileName": item["source"].get( + "file_name", item["source"].get("fileName", "") + ), + "fileFormat": item["source"].get( + "file_format", item["source"].get("fileFormat", "") + ), "locale": item["source"].get("locale", ""), - "dataFormat": item["source"].get("data_format", item["source"].get("dataFormat")), - "formatMetadata": item["source"].get("format_metadata", item["source"].get("formatMetadata")), - "fileId": item["source"].get("file_id", item["source"].get("fileId")), - "versionId": item["source"].get("version_id", item["source"].get("versionId")), - "branchId": item["source"].get("branch_id", item["source"].get("branchId")), + "dataFormat": item["source"].get( + "data_format", item["source"].get("dataFormat") + ), + "formatMetadata": item["source"].get( + "format_metadata", item["source"].get("formatMetadata") + ), + "fileId": item["source"].get( + "file_id", item["source"].get("fileId") + ), + "versionId": item["source"].get( + "version_id", item["source"].get("versionId") + ), + "branchId": item["source"].get( + "branch_id", item["source"].get("branchId") + ), }, "translations": [ { - "content": base64.b64encode( - t["content"].encode() - ).decode(), + "content": base64.b64encode(t["content"].encode()).decode(), "fileName": t.get("file_name", t.get("fileName", "")), "fileFormat": t.get("file_format", t.get("fileFormat", "")), "locale": t.get("locale", ""), @@ -51,7 +63,9 @@ async def _process_batch(batch: list[dict[str, Any]]) -> list[Any]: } for item in batch ], - "sourceLocale": options.get("source_locale", options.get("sourceLocale", "")), + "sourceLocale": options.get( + "source_locale", options.get("sourceLocale", "") + ), } result = await api_request( config, diff --git a/packages/generaltranslation/tests/errors/test_errors.py b/packages/generaltranslation/tests/errors/test_errors.py index d1636fc..b2a081b 100644 --- a/packages/generaltranslation/tests/errors/test_errors.py +++ b/packages/generaltranslation/tests/errors/test_errors.py @@ -23,7 +23,9 @@ class TestApiError: """Tests for the ApiError exception class.""" def test_is_exception(self): - err = ApiError("something went wrong", code=500, message="Internal Server Error") + err = ApiError( + "something went wrong", code=500, message="Internal Server Error" + ) assert isinstance(err, Exception) def test_attributes(self): @@ -61,23 +63,38 @@ def test_translation_request_failed_error(self): def test_api_error_message(self): result = api_error_message(500, "Internal Server Error", "unexpected crash") - assert result == "GT Error: API returned error status. Status: 500, Status Text: Internal Server Error, Error: unexpected crash" + assert ( + result + == "GT Error: API returned error status. Status: 500, Status Text: Internal Server Error, Error: unexpected crash" + ) def test_no_target_locale_error(self): result = no_target_locale_error("translate") - assert result == "GT Error: Cannot call `translate` without a specified locale. Either pass a locale to the `translate` function or specify a targetLocale in the GT constructor." + assert ( + result + == "GT Error: Cannot call `translate` without a specified locale. Either pass a locale to the `translate` function or specify a targetLocale in the GT constructor." + ) def test_no_source_locale_error(self): result = no_source_locale_error("translate") - assert result == "GT Error: Cannot call `translate` without a specified locale. Either pass a locale to the `translate` function or specify a sourceLocale in the GT constructor." + assert ( + result + == "GT Error: Cannot call `translate` without a specified locale. Either pass a locale to the `translate` function or specify a sourceLocale in the GT constructor." + ) def test_no_project_id_error(self): result = no_project_id_error("translate") - assert result == "GT Error: Cannot call `translate` without a specified project ID. Either pass a project ID to the `translate` function or specify a projectId in the GT constructor." + assert ( + result + == "GT Error: Cannot call `translate` without a specified project ID. Either pass a project ID to the `translate` function or specify a projectId in the GT constructor." + ) def test_no_api_key_error(self): result = no_api_key_error("translate") - assert result == "GT Error: Cannot call `translate` without a specified API key. Either pass an API key to the `translate` function or specify an apiKey in the GT constructor." + assert ( + result + == "GT Error: Cannot call `translate` without a specified API key. Either pass an API key to the `translate` function or specify an apiKey in the GT constructor." + ) def test_invalid_locale_error(self): result = invalid_locale_error("xx-YY") @@ -93,4 +110,7 @@ def test_invalid_locales_error_single(self): def test_create_invalid_cutoff_style_error(self): result = create_invalid_cutoff_style_error("bad-style") - assert result == "generaltranslation Formatting Error: Invalid cutoff style: bad-style." + assert ( + result + == "generaltranslation Formatting Error: Invalid cutoff style: bad-style." + ) diff --git a/packages/generaltranslation/tests/locales/test_custom_locale_mapping.py b/packages/generaltranslation/tests/locales/test_custom_locale_mapping.py index 806b7dc..429b4ac 100644 --- a/packages/generaltranslation/tests/locales/test_custom_locale_mapping.py +++ b/packages/generaltranslation/tests/locales/test_custom_locale_mapping.py @@ -1,4 +1,5 @@ """Tests for _custom_locale_mapping.py.""" + import json from pathlib import Path diff --git a/packages/generaltranslation/tests/locales/test_determine_locale.py b/packages/generaltranslation/tests/locales/test_determine_locale.py index 931652f..0b7e77f 100644 --- a/packages/generaltranslation/tests/locales/test_determine_locale.py +++ b/packages/generaltranslation/tests/locales/test_determine_locale.py @@ -1,4 +1,5 @@ """Tests for _determine_locale.py.""" + import json from pathlib import Path diff --git a/packages/generaltranslation/tests/locales/test_get_locale_direction.py b/packages/generaltranslation/tests/locales/test_get_locale_direction.py index a167eee..e876db9 100644 --- a/packages/generaltranslation/tests/locales/test_get_locale_direction.py +++ b/packages/generaltranslation/tests/locales/test_get_locale_direction.py @@ -1,4 +1,5 @@ """Tests for _get_locale_direction.py.""" + import json from pathlib import Path diff --git a/packages/generaltranslation/tests/locales/test_get_locale_emoji.py b/packages/generaltranslation/tests/locales/test_get_locale_emoji.py index d656b79..c236bee 100644 --- a/packages/generaltranslation/tests/locales/test_get_locale_emoji.py +++ b/packages/generaltranslation/tests/locales/test_get_locale_emoji.py @@ -1,4 +1,5 @@ """Tests for _get_locale_emoji.py.""" + import json from pathlib import Path diff --git a/packages/generaltranslation/tests/locales/test_get_locale_name.py b/packages/generaltranslation/tests/locales/test_get_locale_name.py index 77a97a2..818d49a 100644 --- a/packages/generaltranslation/tests/locales/test_get_locale_name.py +++ b/packages/generaltranslation/tests/locales/test_get_locale_name.py @@ -1,4 +1,5 @@ """Tests for _get_locale_name.py.""" + import json from pathlib import Path diff --git a/packages/generaltranslation/tests/locales/test_get_locale_properties.py b/packages/generaltranslation/tests/locales/test_get_locale_properties.py index 5aa0ceb..35c77a7 100644 --- a/packages/generaltranslation/tests/locales/test_get_locale_properties.py +++ b/packages/generaltranslation/tests/locales/test_get_locale_properties.py @@ -1,4 +1,5 @@ """Tests for _get_locale_properties.py.""" + import json from pathlib import Path diff --git a/packages/generaltranslation/tests/locales/test_get_plural_form.py b/packages/generaltranslation/tests/locales/test_get_plural_form.py index ebbcdb3..33d3ba7 100644 --- a/packages/generaltranslation/tests/locales/test_get_plural_form.py +++ b/packages/generaltranslation/tests/locales/test_get_plural_form.py @@ -1,4 +1,5 @@ """Tests for _get_plural_form.py.""" + import json from pathlib import Path diff --git a/packages/generaltranslation/tests/locales/test_get_region_properties.py b/packages/generaltranslation/tests/locales/test_get_region_properties.py index c7a2c9b..2b555de 100644 --- a/packages/generaltranslation/tests/locales/test_get_region_properties.py +++ b/packages/generaltranslation/tests/locales/test_get_region_properties.py @@ -1,4 +1,5 @@ """Tests for _get_region_properties.py.""" + import json from pathlib import Path diff --git a/packages/generaltranslation/tests/locales/test_is_same_dialect.py b/packages/generaltranslation/tests/locales/test_is_same_dialect.py index b592775..a62d26d 100644 --- a/packages/generaltranslation/tests/locales/test_is_same_dialect.py +++ b/packages/generaltranslation/tests/locales/test_is_same_dialect.py @@ -1,4 +1,5 @@ """Tests for _is_same_dialect.py.""" + import json from pathlib import Path diff --git a/packages/generaltranslation/tests/locales/test_is_same_language.py b/packages/generaltranslation/tests/locales/test_is_same_language.py index 5c7c2c0..a68030c 100644 --- a/packages/generaltranslation/tests/locales/test_is_same_language.py +++ b/packages/generaltranslation/tests/locales/test_is_same_language.py @@ -1,4 +1,5 @@ """Tests for _is_same_language.py.""" + import json from pathlib import Path diff --git a/packages/generaltranslation/tests/locales/test_is_superset_locale.py b/packages/generaltranslation/tests/locales/test_is_superset_locale.py index 8c74f13..7f216de 100644 --- a/packages/generaltranslation/tests/locales/test_is_superset_locale.py +++ b/packages/generaltranslation/tests/locales/test_is_superset_locale.py @@ -1,4 +1,5 @@ """Tests for _is_superset_locale.py.""" + import json from pathlib import Path diff --git a/packages/generaltranslation/tests/locales/test_is_valid_locale.py b/packages/generaltranslation/tests/locales/test_is_valid_locale.py index 5effd09..eef6601 100644 --- a/packages/generaltranslation/tests/locales/test_is_valid_locale.py +++ b/packages/generaltranslation/tests/locales/test_is_valid_locale.py @@ -1,4 +1,5 @@ """Tests for _is_valid_locale.py.""" + import json from pathlib import Path diff --git a/packages/generaltranslation/tests/locales/test_requires_translation.py b/packages/generaltranslation/tests/locales/test_requires_translation.py index 8594230..e9a5723 100644 --- a/packages/generaltranslation/tests/locales/test_requires_translation.py +++ b/packages/generaltranslation/tests/locales/test_requires_translation.py @@ -1,4 +1,5 @@ """Tests for _requires_translation.py.""" + import json from pathlib import Path diff --git a/packages/generaltranslation/tests/locales/test_resolve_locale.py b/packages/generaltranslation/tests/locales/test_resolve_locale.py index 9525ac4..c5ac98c 100644 --- a/packages/generaltranslation/tests/locales/test_resolve_locale.py +++ b/packages/generaltranslation/tests/locales/test_resolve_locale.py @@ -1,4 +1,5 @@ """Tests for _resolve_locale.py.""" + import json from pathlib import Path diff --git a/packages/generaltranslation/tests/static/test_known_discrepancies.py b/packages/generaltranslation/tests/static/test_known_discrepancies.py index 92a40e2..7db3da4 100644 --- a/packages/generaltranslation/tests/static/test_known_discrepancies.py +++ b/packages/generaltranslation/tests/static/test_known_discrepancies.py @@ -36,9 +36,7 @@ def test_condense_escaped_hash_in_non_condensed_plural(): "{_gt_1, select, other {x}} " "{n, plural, offset:1 one {# item, not '#'} other {# items}}" ) - js_expected = ( - "{_gt_1} {n,plural,offset:1 one{# item, not '#'} other{# items}}" - ) + js_expected = "{_gt_1} {n,plural,offset:1 one{# item, not '#'} other{# items}}" assert condense_vars(icu) == js_expected @@ -66,7 +64,7 @@ def test_parser_escape_angle_brackets(): icu = ( "{_gt_1, select, other {val}} " "{mode, select, " - "json {'{\"key\": \"val\"}'} " + 'json {\'{"key": "val"}\'} ' "xml {''} " "other {plain}}" ) diff --git a/packages/generaltranslation/tests/translate/test_batch.py b/packages/generaltranslation/tests/translate/test_batch.py index 8ffcaf0..44fcbe0 100644 --- a/packages/generaltranslation/tests/translate/test_batch.py +++ b/packages/generaltranslation/tests/translate/test_batch.py @@ -1,46 +1,59 @@ import pytest from generaltranslation.translate._batch import create_batches, process_batches + def test_create_batches_basic(): items = list(range(5)) result = create_batches(items, 2) assert result == [[0, 1], [2, 3], [4]] + def test_create_batches_empty(): assert create_batches([], 10) == [] + def test_create_batches_exact(): items = list(range(4)) result = create_batches(items, 2) assert result == [[0, 1], [2, 3]] + def test_create_batches_single(): items = [1, 2, 3] result = create_batches(items, 100) assert result == [[1, 2, 3]] + @pytest.mark.asyncio async def test_process_batches_empty(): async def processor(batch): return batch + result = await process_batches([], processor) assert result == {"data": [], "count": 0, "batch_count": 0} + @pytest.mark.asyncio async def test_process_batches_basic(): async def processor(batch): return [x * 2 for x in batch] + result = await process_batches([1, 2, 3, 4, 5], processor, batch_size=2) assert sorted(result["data"]) == [2, 4, 6, 8, 10] assert result["count"] == 5 assert result["batch_count"] == 3 + @pytest.mark.asyncio async def test_process_batches_sequential(): order = [] + async def processor(batch): order.extend(batch) return batch - result = await process_batches([1, 2, 3, 4], processor, batch_size=2, parallel=False) + + result = await process_batches( + [1, 2, 3, 4], processor, batch_size=2, parallel=False + ) assert order == [1, 2, 3, 4] assert result["count"] == 4 diff --git a/packages/generaltranslation/tests/translate/test_endpoints.py b/packages/generaltranslation/tests/translate/test_endpoints.py index 344a8b1..b202df0 100644 --- a/packages/generaltranslation/tests/translate/test_endpoints.py +++ b/packages/generaltranslation/tests/translate/test_endpoints.py @@ -1,49 +1,76 @@ import pytest from unittest.mock import AsyncMock, patch + @pytest.mark.asyncio async def test_check_job_status(): - with patch("generaltranslation.translate._check_job_status.api_request", new_callable=AsyncMock) as mock: + with patch( + "generaltranslation.translate._check_job_status.api_request", + new_callable=AsyncMock, + ) as mock: mock.return_value = [{"jobId": "j1", "status": "completed"}] from generaltranslation.translate._check_job_status import check_job_status + result = await check_job_status(["j1"], {"project_id": "p"}) assert result[0]["status"] == "completed" mock.assert_called_once() call_args = mock.call_args assert call_args[0][1] == "/v2/project/jobs/info" + @pytest.mark.asyncio async def test_setup_project(): - with patch("generaltranslation.translate._setup_project.api_request", new_callable=AsyncMock) as mock: + with patch( + "generaltranslation.translate._setup_project.api_request", + new_callable=AsyncMock, + ) as mock: mock.return_value = {"setupJobId": "s1", "status": "queued"} from generaltranslation.translate._setup_project import setup_project + files = [{"branch_id": "b1", "file_id": "f1", "version_id": "v1"}] result = await setup_project(files, {"project_id": "p"}) assert result["status"] == "queued" call_body = mock.call_args[1]["body"] assert call_body["files"][0]["branchId"] == "b1" + @pytest.mark.asyncio async def test_query_branch_data(): - with patch("generaltranslation.translate._query_branch_data.api_request", new_callable=AsyncMock) as mock: + with patch( + "generaltranslation.translate._query_branch_data.api_request", + new_callable=AsyncMock, + ) as mock: mock.return_value = {"branches": [{"id": "b1", "name": "main"}]} from generaltranslation.translate._query_branch_data import query_branch_data + result = await query_branch_data({"branchNames": ["main"]}, {"project_id": "p"}) assert result["branches"][0]["name"] == "main" + @pytest.mark.asyncio async def test_create_branch(): - with patch("generaltranslation.translate._create_branch.api_request", new_callable=AsyncMock) as mock: + with patch( + "generaltranslation.translate._create_branch.api_request", + new_callable=AsyncMock, + ) as mock: mock.return_value = {"branch": {"id": "b1", "name": "feature"}} from generaltranslation.translate._create_branch import create_branch - result = await create_branch({"branchName": "feature", "defaultBranch": False}, {"project_id": "p"}) + + result = await create_branch( + {"branchName": "feature", "defaultBranch": False}, {"project_id": "p"} + ) assert result["branch"]["name"] == "feature" + @pytest.mark.asyncio async def test_query_source_file(): - with patch("generaltranslation.translate._query_source_file.api_request", new_callable=AsyncMock) as mock: + with patch( + "generaltranslation.translate._query_source_file.api_request", + new_callable=AsyncMock, + ) as mock: mock.return_value = {"sourceFile": {"fileId": "f1"}, "translations": []} from generaltranslation.translate._query_source_file import query_source_file + result = await query_source_file( {"file_id": "f1", "branch_id": "b1", "version_id": "v1"}, {}, @@ -53,49 +80,82 @@ async def test_query_source_file(): call_args = mock.call_args assert "/v2/project/translations/files/status/f1" in call_args[0][1] + @pytest.mark.asyncio async def test_get_project_data(): - with patch("generaltranslation.translate._get_project_data.api_request", new_callable=AsyncMock) as mock: + with patch( + "generaltranslation.translate._get_project_data.api_request", + new_callable=AsyncMock, + ) as mock: mock.return_value = {"id": "p1", "name": "My Project", "defaultLocale": "en"} from generaltranslation.translate._get_project_data import get_project_data + result = await get_project_data("p1", {}, {"project_id": "p"}) assert result["name"] == "My Project" call_args = mock.call_args assert "p1" in call_args[0][1] + @pytest.mark.asyncio async def test_submit_user_edit_diffs(): - with patch("generaltranslation.translate._submit_user_edit_diffs.api_request", new_callable=AsyncMock) as mock: + with patch( + "generaltranslation.translate._submit_user_edit_diffs.api_request", + new_callable=AsyncMock, + ) as mock: mock.return_value = {} - from generaltranslation.translate._submit_user_edit_diffs import submit_user_edit_diffs - result = await submit_user_edit_diffs({"diffs": [{"locale": "es"}]}, {"project_id": "p"}) + from generaltranslation.translate._submit_user_edit_diffs import ( + submit_user_edit_diffs, + ) + + result = await submit_user_edit_diffs( + {"diffs": [{"locale": "es"}]}, {"project_id": "p"} + ) assert result["success"] is True + @pytest.mark.asyncio async def test_process_file_moves_empty(): from generaltranslation.translate._process_file_moves import process_file_moves + result = await process_file_moves([], {}, {"project_id": "p"}) assert result["summary"]["total"] == 0 assert result["results"] == [] + @pytest.mark.asyncio async def test_get_orphaned_files_empty(): - with patch("generaltranslation.translate._get_orphaned_files.api_request", new_callable=AsyncMock) as mock: + with patch( + "generaltranslation.translate._get_orphaned_files.api_request", + new_callable=AsyncMock, + ) as mock: mock.return_value = {"orphanedFiles": []} from generaltranslation.translate._get_orphaned_files import get_orphaned_files + result = await get_orphaned_files("b1", [], {}, {"project_id": "p"}) assert result["orphanedFiles"] == [] # --- Fix 1: enqueue_files should not send None values in body --- + @pytest.mark.asyncio async def test_enqueue_files_omits_none_values(): """Body dict passed to api_request must not contain keys whose value is None.""" - with patch("generaltranslation.translate._enqueue_files.api_request", new_callable=AsyncMock) as mock: + with patch( + "generaltranslation.translate._enqueue_files.api_request", + new_callable=AsyncMock, + ) as mock: mock.return_value = {"jobData": {"j1": "queued"}} from generaltranslation.translate._enqueue_files import enqueue_files - files = [{"branch_id": "b1", "file_id": "f1", "version_id": "v1", "file_name": "a.txt"}] + + files = [ + { + "branch_id": "b1", + "file_id": "f1", + "version_id": "v1", + "file_name": "a.txt", + } + ] options = { "target_locales": ["es"], "source_locale": "en", @@ -105,32 +165,47 @@ async def test_enqueue_files_omits_none_values(): body = mock.call_args[1]["body"] # None-valued keys must be absent for key in ("publish", "requireApproval", "modelProvider", "force"): - assert key not in body, f"key '{key}' should not be in body when value is None" + assert key not in body, ( + f"key '{key}' should not be in body when value is None" + ) # --- Fix 4: URL encoding should match JS encodeURIComponent --- + @pytest.mark.asyncio async def test_query_source_file_preserves_special_chars(): """Characters ! ' ( ) * should NOT be percent-encoded (matching encodeURIComponent).""" - with patch("generaltranslation.translate._query_source_file.api_request", new_callable=AsyncMock) as mock: + with patch( + "generaltranslation.translate._query_source_file.api_request", + new_callable=AsyncMock, + ) as mock: mock.return_value = {"sourceFile": {}} from generaltranslation.translate._query_source_file import query_source_file + await query_source_file( {"file_id": "file!'()*"}, {}, {"project_id": "p"}, ) endpoint = mock.call_args[0][1] - assert "file!'()*" in endpoint, f"Special chars were percent-encoded in: {endpoint}" + assert "file!'()*" in endpoint, ( + f"Special chars were percent-encoded in: {endpoint}" + ) @pytest.mark.asyncio async def test_get_project_data_preserves_special_chars(): """Characters ! ' ( ) * should NOT be percent-encoded (matching encodeURIComponent).""" - with patch("generaltranslation.translate._get_project_data.api_request", new_callable=AsyncMock) as mock: + with patch( + "generaltranslation.translate._get_project_data.api_request", + new_callable=AsyncMock, + ) as mock: mock.return_value = {"id": "p!'()*"} from generaltranslation.translate._get_project_data import get_project_data + await get_project_data("p!'()*", {}, {"project_id": "p"}) endpoint = mock.call_args[0][1] - assert "p!'()*" in endpoint, f"Special chars were percent-encoded in: {endpoint}" + assert "p!'()*" in endpoint, ( + f"Special chars were percent-encoded in: {endpoint}" + ) diff --git a/packages/generaltranslation/tests/translate/test_headers.py b/packages/generaltranslation/tests/translate/test_headers.py index c3a9730..0199c8f 100644 --- a/packages/generaltranslation/tests/translate/test_headers.py +++ b/packages/generaltranslation/tests/translate/test_headers.py @@ -1,6 +1,7 @@ from generaltranslation.translate._headers import generate_request_headers from generaltranslation._settings import API_VERSION + def test_basic_headers(): config = {"project_id": "proj-123", "api_key": "my-key"} headers = generate_request_headers(config) @@ -9,11 +10,13 @@ def test_basic_headers(): assert headers["x-gt-api-key"] == "my-key" assert headers["gt-api-version"] == API_VERSION + def test_exclude_content_type(): config = {"project_id": "proj-123"} headers = generate_request_headers(config, exclude_content_type=True) assert "Content-Type" not in headers + def test_internal_api_key(): config = {"project_id": "proj-123", "api_key": "gtx-internal-abc"} headers = generate_request_headers(config) @@ -21,6 +24,7 @@ def test_internal_api_key(): assert headers["x-gt-internal-api-key"] == "gtx-internal-abc" assert "x-gt-api-key" not in headers + def test_no_api_key(): config = {"project_id": "proj-123"} headers = generate_request_headers(config) diff --git a/packages/generaltranslation/tests/translate/test_request.py b/packages/generaltranslation/tests/translate/test_request.py index 80955a5..e9ed1db 100644 --- a/packages/generaltranslation/tests/translate/test_request.py +++ b/packages/generaltranslation/tests/translate/test_request.py @@ -5,9 +5,15 @@ from generaltranslation.translate._request import api_request from generaltranslation.errors import ApiError + @pytest.fixture def config(): - return {"project_id": "proj-123", "api_key": "test-key", "base_url": "https://test.api.com"} + return { + "project_id": "proj-123", + "api_key": "test-key", + "base_url": "https://test.api.com", + } + @pytest.mark.asyncio async def test_successful_post(config): @@ -15,7 +21,9 @@ async def test_successful_post(config): mock_response.status_code = 200 mock_response.json.return_value = {"result": "ok"} - with patch("generaltranslation.translate._request.httpx.AsyncClient") as mock_client_cls: + with patch( + "generaltranslation.translate._request.httpx.AsyncClient" + ) as mock_client_cls: mock_client = AsyncMock() mock_client.post.return_value = mock_response mock_client.__aenter__ = AsyncMock(return_value=mock_client) @@ -25,13 +33,16 @@ async def test_successful_post(config): result = await api_request(config, "/v2/test", body={"key": "value"}) assert result == {"result": "ok"} + @pytest.mark.asyncio async def test_successful_get(config): mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = {"data": [1, 2, 3]} - with patch("generaltranslation.translate._request.httpx.AsyncClient") as mock_client_cls: + with patch( + "generaltranslation.translate._request.httpx.AsyncClient" + ) as mock_client_cls: mock_client = AsyncMock() mock_client.get.return_value = mock_response mock_client.__aenter__ = AsyncMock(return_value=mock_client) @@ -41,6 +52,7 @@ async def test_successful_get(config): result = await api_request(config, "/v2/test", method="GET") assert result == {"data": [1, 2, 3]} + @pytest.mark.asyncio async def test_4xx_raises_api_error(config): mock_response = MagicMock() @@ -48,7 +60,9 @@ async def test_4xx_raises_api_error(config): mock_response.reason_phrase = "Unauthorized" mock_response.text = json.dumps({"error": "Invalid API key"}) - with patch("generaltranslation.translate._request.httpx.AsyncClient") as mock_client_cls: + with patch( + "generaltranslation.translate._request.httpx.AsyncClient" + ) as mock_client_cls: mock_client = AsyncMock() mock_client.post.return_value = mock_response mock_client.__aenter__ = AsyncMock(return_value=mock_client) @@ -59,9 +73,12 @@ async def test_4xx_raises_api_error(config): await api_request(config, "/v2/test", body={}) assert exc_info.value.code == 401 + @pytest.mark.asyncio async def test_timeout_raises_error(config): - with patch("generaltranslation.translate._request.httpx.AsyncClient") as mock_client_cls: + with patch( + "generaltranslation.translate._request.httpx.AsyncClient" + ) as mock_client_cls: mock_client = AsyncMock() mock_client.post.side_effect = httpx.TimeoutException("timed out") mock_client.__aenter__ = AsyncMock(return_value=mock_client) @@ -71,6 +88,7 @@ async def test_timeout_raises_error(config): with pytest.raises(Exception, match="timed out"): await api_request(config, "/v2/test", body={}, retry_policy="none") + @pytest.mark.asyncio async def test_no_retry_on_none_policy(config): call_count = 0 @@ -79,12 +97,16 @@ async def test_no_retry_on_none_policy(config): mock_response.reason_phrase = "Internal Server Error" mock_response.text = "Server error" - with patch("generaltranslation.translate._request.httpx.AsyncClient") as mock_client_cls: + with patch( + "generaltranslation.translate._request.httpx.AsyncClient" + ) as mock_client_cls: mock_client = AsyncMock() + async def counting_post(*args, **kwargs): nonlocal call_count call_count += 1 return mock_response + mock_client.post = counting_post mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock(return_value=False) @@ -108,14 +130,19 @@ async def test_client_reused_across_retries(config): mock_response_200.status_code = 200 mock_response_200.json.return_value = {"ok": True} - with patch("generaltranslation.translate._request.httpx.AsyncClient") as mock_client_cls: + with patch( + "generaltranslation.translate._request.httpx.AsyncClient" + ) as mock_client_cls: mock_client = AsyncMock() mock_client.post = AsyncMock(side_effect=[mock_response_500, mock_response_200]) mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock(return_value=False) mock_client_cls.return_value = mock_client - with patch("generaltranslation.translate._request.asyncio.sleep", new_callable=AsyncMock): + with patch( + "generaltranslation.translate._request.asyncio.sleep", + new_callable=AsyncMock, + ): result = await api_request(config, "/v2/test", body={}) assert result == {"ok": True} diff --git a/packages/gt-fastapi/src/gt_fastapi/_setup.py b/packages/gt-fastapi/src/gt_fastapi/_setup.py index 9ee6d12..5830bdc 100644 --- a/packages/gt-fastapi/src/gt_fastapi/_setup.py +++ b/packages/gt-fastapi/src/gt_fastapi/_setup.py @@ -10,9 +10,7 @@ from gt_i18n import I18nManager, set_i18n_manager, t # noqa: F401 -def _detect_from_accept_language( - request: Any, manager: I18nManager -) -> str: +def _detect_from_accept_language(request: Any, manager: I18nManager) -> str: """Parse Accept-Language header and resolve against configured locales.""" accept = request.headers.get("accept-language", "") if not accept: diff --git a/packages/gt-flask/src/gt_flask/_setup.py b/packages/gt-flask/src/gt_flask/_setup.py index 981caa2..b28d0ba 100644 --- a/packages/gt-flask/src/gt_flask/_setup.py +++ b/packages/gt-flask/src/gt_flask/_setup.py @@ -10,9 +10,7 @@ from gt_i18n import I18nManager, set_i18n_manager, t # noqa: F401 -def _detect_from_accept_language( - request: Any, manager: I18nManager -) -> str: +def _detect_from_accept_language(request: Any, manager: I18nManager) -> str: """Parse Accept-Language header and resolve against configured locales.""" accept = request.headers.get("Accept-Language", "") if not accept: diff --git a/packages/gt-i18n/src/gt_i18n/i18n_manager/_i18n_manager.py b/packages/gt-i18n/src/gt_i18n/i18n_manager/_i18n_manager.py index fefdf09..3267c74 100644 --- a/packages/gt-i18n/src/gt_i18n/i18n_manager/_i18n_manager.py +++ b/packages/gt-i18n/src/gt_i18n/i18n_manager/_i18n_manager.py @@ -56,9 +56,7 @@ def __init__( create_remote_translation_loader, ) - loader = create_remote_translation_loader( - project_id, cache_url or "" - ) + loader = create_remote_translation_loader(project_id, cache_url or "") else: loader = lambda locale: {} # noqa: E731 @@ -91,16 +89,12 @@ def requires_translation(self, locale: str | None = None) -> bool: approved_locales=self._locales or None, ) - async def get_translations( - self, locale: str | None = None - ) -> dict[str, str]: + async def get_translations(self, locale: str | None = None) -> dict[str, str]: """Get translations for a locale (async, loads if needed).""" target = locale or self.get_locale() return await self._translations.get_translations(target) - def get_translations_sync( - self, locale: str | None = None - ) -> dict[str, str]: + def get_translations_sync(self, locale: str | None = None) -> dict[str, str]: """Get cached translations (sync, returns empty if not loaded).""" target = locale or self.get_locale() return self._translations.get_translations_sync(target) diff --git a/packages/gt-i18n/src/gt_i18n/i18n_manager/_singleton.py b/packages/gt-i18n/src/gt_i18n/i18n_manager/_singleton.py index 02b2aad..277eda6 100644 --- a/packages/gt-i18n/src/gt_i18n/i18n_manager/_singleton.py +++ b/packages/gt-i18n/src/gt_i18n/i18n_manager/_singleton.py @@ -14,9 +14,7 @@ def get_i18n_manager() -> I18nManager: RuntimeError: If ``set_i18n_manager`` has not been called yet. """ if _manager is None: - raise RuntimeError( - "I18nManager not initialized. Call initialize_gt() first." - ) + raise RuntimeError("I18nManager not initialized. Call initialize_gt() first.") return _manager diff --git a/packages/gt-i18n/src/gt_i18n/i18n_manager/_translations_manager.py b/packages/gt-i18n/src/gt_i18n/i18n_manager/_translations_manager.py index cedd1f8..e7cbeab 100644 --- a/packages/gt-i18n/src/gt_i18n/i18n_manager/_translations_manager.py +++ b/packages/gt-i18n/src/gt_i18n/i18n_manager/_translations_manager.py @@ -72,6 +72,4 @@ def get_translations_sync(self, locale: str) -> dict[str, str]: async def load_all(self, locales: list[str]) -> None: """Eagerly fetch translations for all *locales*.""" - await asyncio.gather( - *(self.get_translations(loc) for loc in locales) - ) + await asyncio.gather(*(self.get_translations(loc) for loc in locales)) diff --git a/packages/gt-i18n/src/gt_i18n/translation_functions/_interpolate.py b/packages/gt-i18n/src/gt_i18n/translation_functions/_interpolate.py index a45cec6..6205026 100644 --- a/packages/gt-i18n/src/gt_i18n/translation_functions/_interpolate.py +++ b/packages/gt-i18n/src/gt_i18n/translation_functions/_interpolate.py @@ -71,9 +71,7 @@ def interpolate_message( # Apply cutoff formatting if max_chars is not None: - result = format_cutoff( - result, locale, {"max_chars": int(max_chars)} - ) + result = format_cutoff(result, locale, {"max_chars": int(max_chars)}) return result @@ -89,8 +87,6 @@ def interpolate_message( # No fallback — return the raw message with cutoff applied if max_chars is not None: - return format_cutoff( - message, locale, {"max_chars": int(max_chars)} - ) + return format_cutoff(message, locale, {"max_chars": int(max_chars)}) return message diff --git a/packages/gt-i18n/tests/test_hash_message.py b/packages/gt-i18n/tests/test_hash_message.py index 3d27e40..a06de64 100644 --- a/packages/gt-i18n/tests/test_hash_message.py +++ b/packages/gt-i18n/tests/test_hash_message.py @@ -87,67 +87,112 @@ def _load(self, fixtures): def test_plain(self): c = self.cases["plain"] - assert hash_message(c["message"], **_js_options_to_kwargs(c["options"])) == c["hash"] + assert ( + hash_message(c["message"], **_js_options_to_kwargs(c["options"])) + == c["hash"] + ) def test_with_variable(self): c = self.cases["with_variable"] - assert hash_message(c["message"], **_js_options_to_kwargs(c["options"])) == c["hash"] + assert ( + hash_message(c["message"], **_js_options_to_kwargs(c["options"])) + == c["hash"] + ) def test_with_context_button(self): c = self.cases["with_context_button"] - assert hash_message(c["message"], **_js_options_to_kwargs(c["options"])) == c["hash"] + assert ( + hash_message(c["message"], **_js_options_to_kwargs(c["options"])) + == c["hash"] + ) def test_with_context_menu(self): c = self.cases["with_context_menu"] - assert hash_message(c["message"], **_js_options_to_kwargs(c["options"])) == c["hash"] + assert ( + hash_message(c["message"], **_js_options_to_kwargs(c["options"])) + == c["hash"] + ) def test_no_context(self): c = self.cases["no_context"] - assert hash_message(c["message"], **_js_options_to_kwargs(c["options"])) == c["hash"] + assert ( + hash_message(c["message"], **_js_options_to_kwargs(c["options"])) + == c["hash"] + ) def test_with_id(self): c = self.cases["with_id"] - assert hash_message(c["message"], **_js_options_to_kwargs(c["options"])) == c["hash"] + assert ( + hash_message(c["message"], **_js_options_to_kwargs(c["options"])) + == c["hash"] + ) def test_with_max_chars(self): c = self.cases["with_max_chars"] - assert hash_message(c["message"], **_js_options_to_kwargs(c["options"])) == c["hash"] + assert ( + hash_message(c["message"], **_js_options_to_kwargs(c["options"])) + == c["hash"] + ) def test_negative_max_chars_same_as_positive(self): """maxChars(-10) should produce the same hash as maxChars(10).""" c = self.cases["with_negative_max_chars"] - assert hash_message(c["message"], **_js_options_to_kwargs(c["options"])) == c["hash"] + assert ( + hash_message(c["message"], **_js_options_to_kwargs(c["options"])) + == c["hash"] + ) # Also verify it equals the positive case pos = self.cases["with_max_chars"] assert c["hash"] == pos["hash"] def test_empty(self): c = self.cases["empty"] - assert hash_message(c["message"], **_js_options_to_kwargs(c["options"])) == c["hash"] + assert ( + hash_message(c["message"], **_js_options_to_kwargs(c["options"])) + == c["hash"] + ) def test_plural(self): c = self.cases["plural"] - assert hash_message(c["message"], **_js_options_to_kwargs(c["options"])) == c["hash"] + assert ( + hash_message(c["message"], **_js_options_to_kwargs(c["options"])) + == c["hash"] + ) def test_plural_sentence(self): c = self.cases["plural_sentence"] - assert hash_message(c["message"], **_js_options_to_kwargs(c["options"])) == c["hash"] + assert ( + hash_message(c["message"], **_js_options_to_kwargs(c["options"])) + == c["hash"] + ) def test_with_gt_var(self): c = self.cases["with_gt_var"] - assert hash_message(c["message"], **_js_options_to_kwargs(c["options"])) == c["hash"] + assert ( + hash_message(c["message"], **_js_options_to_kwargs(c["options"])) + == c["hash"] + ) def test_with_gt_var_name(self): c = self.cases["with_gt_var_name"] - assert hash_message(c["message"], **_js_options_to_kwargs(c["options"])) == c["hash"] + assert ( + hash_message(c["message"], **_js_options_to_kwargs(c["options"])) + == c["hash"] + ) def test_context_and_id(self): c = self.cases["context_and_id"] - assert hash_message(c["message"], **_js_options_to_kwargs(c["options"])) == c["hash"] + assert ( + hash_message(c["message"], **_js_options_to_kwargs(c["options"])) + == c["hash"] + ) def test_context_and_max_chars(self): c = self.cases["context_and_max_chars"] - assert hash_message(c["message"], **_js_options_to_kwargs(c["options"])) == c["hash"] + assert ( + hash_message(c["message"], **_js_options_to_kwargs(c["options"])) + == c["hash"] + ) # ---- Behavioral tests ---- diff --git a/packages/gt-i18n/tests/test_interpolate.py b/packages/gt-i18n/tests/test_interpolate.py index 85fd2e0..b105177 100644 --- a/packages/gt-i18n/tests/test_interpolate.py +++ b/packages/gt-i18n/tests/test_interpolate.py @@ -176,9 +176,7 @@ def test_no_fallback_returns_raw_message(): def test_cutoff_applied_after_interpolation(): """$max_chars truncates after interpolation.""" - result = interpolate_message( - "Hello, {name}!", {"name": "Alice", "$max_chars": 8} - ) + result = interpolate_message("Hello, {name}!", {"name": "Alice", "$max_chars": 8}) assert len(result) <= 8 From 52edc49d38f9198fe186917b64f9201e77095414 Mon Sep 17 00:00:00 2001 From: Ernest McCarter Date: Fri, 6 Mar 2026 12:14:49 -0800 Subject: [PATCH 3/4] fix: lint --- .github/workflows/{ci.yml => test.yml} | 2 +- Makefile | 5 +- examples/fastapi-eager/tests/test_app.py | 1 - examples/fastapi-lazy/tests/test_app.py | 1 - examples/flask-eager/tests/test_app.py | 1 - examples/flask-lazy/tests/test_app.py | 1 - .../tests/test_parser.py | 1 - .../_formatter.py | 2 - .../tests/test_supported_locales.py | 1 - .../src/generaltranslation/__init__.py | 4 +- .../src/generaltranslation/_gt.py | 152 +++++++++++------- .../src/generaltranslation/_id/_hash.py | 13 +- .../generaltranslation/formatting/__init__.py | 6 +- .../formatting/_format_currency.py | 1 - .../formatting/_format_cutoff.py | 1 - .../formatting/_format_num.py | 2 +- .../src/generaltranslation/static/__init__.py | 6 +- .../static/_condense_vars.py | 2 +- .../generaltranslation/static/_decode_vars.py | 2 +- .../static/_extract_vars.py | 2 +- .../generaltranslation/static/_index_vars.py | 2 +- .../static/_traverse_icu.py | 2 +- .../generaltranslation/translate/_batch.py | 3 +- .../translate/_check_job_status.py | 4 +- .../generaltranslation/translate/_request.py | 4 +- .../translate/_setup_project.py | 4 +- .../translate/_translate.py | 4 +- .../generaltranslation/translate/_types.py | 5 +- .../generaltranslation/tests/_id/test_hash.py | 1 - .../tests/errors/test_errors.py | 3 +- .../tests/formatting/test_format_cutoff.py | 2 +- .../tests/static/test_condense_vars.py | 1 - .../tests/static/test_declare_var.py | 1 - .../tests/static/test_decode_vars.py | 1 - .../tests/static/test_extract_vars.py | 1 - .../tests/static/test_index_vars.py | 1 - .../tests/static/test_known_discrepancies.py | 1 - .../tests/static/test_sanitize_var.py | 1 - packages/generaltranslation/tests/test_gt.py | 5 +- .../tests/translate/test_endpoints.py | 3 +- .../tests/translate/test_headers.py | 2 +- .../tests/translate/test_request.py | 7 +- .../gt-fastapi/src/gt_fastapi/__init__.py | 3 +- packages/gt-fastapi/src/gt_fastapi/_setup.py | 4 +- .../tests/test_fastapi_integration.py | 2 - packages/gt-flask/src/gt_flask/__init__.py | 3 +- packages/gt-flask/src/gt_flask/_setup.py | 4 +- .../gt-flask/tests/test_flask_integration.py | 2 - packages/gt-i18n/src/gt_i18n/__init__.py | 1 + .../gt_i18n/i18n_manager/_remote_loader.py | 3 +- .../i18n_manager/_translations_manager.py | 2 +- packages/gt-i18n/tests/test_hash_message.py | 1 - packages/gt-i18n/tests/test_i18n_manager.py | 2 - packages/gt-i18n/tests/test_interpolate.py | 2 - packages/gt-i18n/tests/test_t.py | 2 - .../tests/test_translations_manager.py | 1 - 56 files changed, 149 insertions(+), 147 deletions(-) rename .github/workflows/{ci.yml => test.yml} (94%) diff --git a/.github/workflows/ci.yml b/.github/workflows/test.yml similarity index 94% rename from .github/workflows/ci.yml rename to .github/workflows/test.yml index e134776..36b26b9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/test.yml @@ -1,4 +1,4 @@ -name: CI - Run Tests +name: Test on: pull_request: diff --git a/Makefile b/Makefile index a258809..2a17729 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: setup lint format typecheck test check build clean +.PHONY: setup lint lint-fix format typecheck test check build clean setup: ./scripts/setup.sh @@ -6,6 +6,9 @@ setup: lint: uv run ruff check . +lint-fix: + uv run ruff check --fix . + format: uv run ruff format . diff --git a/examples/fastapi-eager/tests/test_app.py b/examples/fastapi-eager/tests/test_app.py index 311b802..c42b4bb 100644 --- a/examples/fastapi-eager/tests/test_app.py +++ b/examples/fastapi-eager/tests/test_app.py @@ -6,7 +6,6 @@ from fastapi import FastAPI from fastapi.testclient import TestClient - from gt_fastapi import initialize_gt, t TRANSLATIONS = { diff --git a/examples/fastapi-lazy/tests/test_app.py b/examples/fastapi-lazy/tests/test_app.py index a4f6b2e..143c3aa 100644 --- a/examples/fastapi-lazy/tests/test_app.py +++ b/examples/fastapi-lazy/tests/test_app.py @@ -6,7 +6,6 @@ from fastapi import Depends, FastAPI, Request from fastapi.testclient import TestClient - from gt_fastapi import initialize_gt, t TRANSLATIONS = { diff --git a/examples/flask-eager/tests/test_app.py b/examples/flask-eager/tests/test_app.py index 5fbbb75..ca24a5e 100644 --- a/examples/flask-eager/tests/test_app.py +++ b/examples/flask-eager/tests/test_app.py @@ -5,7 +5,6 @@ """ from flask import Flask - from gt_flask import initialize_gt, t TRANSLATIONS = { diff --git a/examples/flask-lazy/tests/test_app.py b/examples/flask-lazy/tests/test_app.py index f0ef6e0..2b38192 100644 --- a/examples/flask-lazy/tests/test_app.py +++ b/examples/flask-lazy/tests/test_app.py @@ -7,7 +7,6 @@ import asyncio from flask import Flask - from gt_flask import initialize_gt, t TRANSLATIONS = { diff --git a/packages/generaltranslation-icu-messageformat-parser/tests/test_parser.py b/packages/generaltranslation-icu-messageformat-parser/tests/test_parser.py index dc8982d..f6dae39 100644 --- a/packages/generaltranslation-icu-messageformat-parser/tests/test_parser.py +++ b/packages/generaltranslation-icu-messageformat-parser/tests/test_parser.py @@ -8,7 +8,6 @@ from generaltranslation_icu_messageformat_parser import Parser, print_ast from pyicumessageformat import Parser as RefParser - # --------------------------------------------------------------------------- # Exhaustive test input matrix # --------------------------------------------------------------------------- diff --git a/packages/generaltranslation-intl-messageformat/src/generaltranslation_intl_messageformat/_formatter.py b/packages/generaltranslation-intl-messageformat/src/generaltranslation_intl_messageformat/_formatter.py index 6ce8d86..a81890f 100644 --- a/packages/generaltranslation-intl-messageformat/src/generaltranslation_intl_messageformat/_formatter.py +++ b/packages/generaltranslation-intl-messageformat/src/generaltranslation_intl_messageformat/_formatter.py @@ -9,10 +9,8 @@ from babel import Locale from babel.core import UnknownLocaleError from babel.numbers import format_decimal - from generaltranslation_icu_messageformat_parser import Parser - _parser = Parser() diff --git a/packages/generaltranslation-supported-locales/tests/test_supported_locales.py b/packages/generaltranslation-supported-locales/tests/test_supported_locales.py index 2bf924b..fd116e3 100644 --- a/packages/generaltranslation-supported-locales/tests/test_supported_locales.py +++ b/packages/generaltranslation-supported-locales/tests/test_supported_locales.py @@ -4,7 +4,6 @@ from pathlib import Path import pytest - from generaltranslation_supported_locales import ( get_supported_locale, list_supported_locales, diff --git a/packages/generaltranslation/src/generaltranslation/__init__.py b/packages/generaltranslation/src/generaltranslation/__init__.py index 12481a4..73fec24 100644 --- a/packages/generaltranslation/src/generaltranslation/__init__.py +++ b/packages/generaltranslation/src/generaltranslation/__init__.py @@ -1,6 +1,7 @@ """Core Python language toolkit for General Translation.""" from generaltranslation._gt import GT +from generaltranslation._id import hash_source, hash_string, hash_template from generaltranslation._settings import ( API_VERSION, DEFAULT_BASE_URL, @@ -10,7 +11,6 @@ LIBRARY_DEFAULT_LOCALE, ) from generaltranslation.errors import ApiError -from generaltranslation._id import hash_source, hash_string, hash_template from generaltranslation.formatting import ( CutoffFormat, format_currency, @@ -49,9 +49,9 @@ VAR_IDENTIFIER, VAR_NAME_IDENTIFIER, condense_vars, - decode_vars, declare_static, declare_var, + decode_vars, extract_vars, index_vars, sanitize_var, diff --git a/packages/generaltranslation/src/generaltranslation/_gt.py b/packages/generaltranslation/src/generaltranslation/_gt.py index ddc2203..7f2788e 100644 --- a/packages/generaltranslation/src/generaltranslation/_gt.py +++ b/packages/generaltranslation/src/generaltranslation/_gt.py @@ -3,9 +3,9 @@ from __future__ import annotations import os -from typing import Any, Optional +from typing import Any -from generaltranslation._settings import DEFAULT_BASE_URL, LIBRARY_DEFAULT_LOCALE +from generaltranslation._settings import LIBRARY_DEFAULT_LOCALE from generaltranslation.errors import ( invalid_locale_error, invalid_locales_error, @@ -44,19 +44,47 @@ ) from generaltranslation.translate import ( check_job_status as _check_job_status, +) +from generaltranslation.translate import ( create_branch as _create_branch, +) +from generaltranslation.translate import ( download_file_batch as _download_file_batch, +) +from generaltranslation.translate import ( enqueue_files as _enqueue_files, +) +from generaltranslation.translate import ( get_orphaned_files as _get_orphaned_files, +) +from generaltranslation.translate import ( get_project_data as _get_project_data, +) +from generaltranslation.translate import ( process_file_moves as _process_file_moves, +) +from generaltranslation.translate import ( query_branch_data as _query_branch_data, +) +from generaltranslation.translate import ( query_file_data as _query_file_data, +) +from generaltranslation.translate import ( query_source_file as _query_source_file, +) +from generaltranslation.translate import ( setup_project as _setup_project, +) +from generaltranslation.translate import ( submit_user_edit_diffs as _submit_user_edit_diffs, +) +from generaltranslation.translate import ( translate_many as _translate_many, +) +from generaltranslation.translate import ( upload_source_files as _upload_source_files, +) +from generaltranslation.translate import ( upload_translations as _upload_translations, ) @@ -70,31 +98,31 @@ class GT: def __init__( self, *, - api_key: Optional[str] = None, - dev_api_key: Optional[str] = None, - project_id: Optional[str] = None, - base_url: Optional[str] = None, - source_locale: Optional[str] = None, - target_locale: Optional[str] = None, - locales: Optional[list[str]] = None, - custom_mapping: Optional[CustomMapping] = None, + api_key: str | None = None, + dev_api_key: str | None = None, + project_id: str | None = None, + base_url: str | None = None, + source_locale: str | None = None, + target_locale: str | None = None, + locales: list[str] | None = None, + custom_mapping: CustomMapping | None = None, ) -> None: # Read environment variables first - self.api_key: Optional[str] = api_key or os.environ.get("GT_API_KEY") or None - self.dev_api_key: Optional[str] = ( + self.api_key: str | None = api_key or os.environ.get("GT_API_KEY") or None + self.dev_api_key: str | None = ( dev_api_key or os.environ.get("GT_DEV_API_KEY") or None ) - self.project_id: Optional[str] = ( + self.project_id: str | None = ( project_id or os.environ.get("GT_PROJECT_ID") or None ) - self.base_url: Optional[str] = None - self.source_locale: Optional[str] = None - self.target_locale: Optional[str] = None - self.locales: Optional[list[str]] = None - self.custom_mapping: Optional[CustomMapping] = None - self.reverse_custom_mapping: Optional[dict[str, str]] = None - self.custom_region_mapping: Optional[CustomRegionMapping] = None + self.base_url: str | None = None + self.source_locale: str | None = None + self.target_locale: str | None = None + self.locales: list[str] | None = None + self.custom_mapping: CustomMapping | None = None + self.reverse_custom_mapping: dict[str, str] | None = None + self.custom_region_mapping: CustomRegionMapping | None = None self._rendering_locales: list[str] = [] self.set_config( @@ -111,14 +139,14 @@ def __init__( def set_config( self, *, - api_key: Optional[str] = None, - dev_api_key: Optional[str] = None, - project_id: Optional[str] = None, - base_url: Optional[str] = None, - source_locale: Optional[str] = None, - target_locale: Optional[str] = None, - locales: Optional[list[str]] = None, - custom_mapping: Optional[CustomMapping] = None, + api_key: str | None = None, + dev_api_key: str | None = None, + project_id: str | None = None, + base_url: str | None = None, + source_locale: str | None = None, + target_locale: str | None = None, + locales: list[str] | None = None, + custom_mapping: CustomMapping | None = None, ) -> None: """Update instance configuration.""" if api_key: @@ -204,7 +232,7 @@ async def create_branch(self, query: dict[str, Any]) -> dict[str, Any]: async def process_file_moves( self, moves: list[dict[str, Any]], - options: Optional[dict[str, Any]] = None, + options: dict[str, Any] | None = None, ) -> dict[str, Any]: """Process file moves by cloning source files and translations.""" self._validate_auth("process_file_moves") @@ -216,7 +244,7 @@ async def get_orphaned_files( self, branch_id: str, file_ids: list[str], - options: Optional[dict[str, Any]] = None, + options: dict[str, Any] | None = None, ) -> dict[str, Any]: """Get orphaned files for a branch.""" self._validate_auth("get_orphaned_files") @@ -229,7 +257,7 @@ async def get_orphaned_files( async def setup_project( self, files: list[dict[str, Any]], - options: Optional[dict[str, Any]] = None, + options: dict[str, Any] | None = None, ) -> dict[str, Any]: """Enqueue files for project setup.""" self._validate_auth("setup_project") @@ -243,7 +271,7 @@ async def setup_project( async def check_job_status( self, job_ids: list[str], - timeout_ms: Optional[int] = None, + timeout_ms: int | None = None, ) -> list[dict[str, Any]]: """Check job statuses.""" self._validate_auth("check_job_status") @@ -293,7 +321,7 @@ async def submit_user_edit_diffs(self, payload: dict[str, Any]) -> None: async def query_file_data( self, data: dict[str, Any], - options: Optional[dict[str, Any]] = None, + options: dict[str, Any] | None = None, ) -> dict[str, Any]: """Query data about one or more source or translation files.""" self._validate_auth("query_file_data") @@ -347,7 +375,7 @@ async def query_file_data( async def query_source_file( self, data: dict[str, Any], - options: Optional[dict[str, Any]] = None, + options: dict[str, Any] | None = None, ) -> dict[str, Any]: """Get source file and translation information.""" self._validate_auth("query_source_file") @@ -376,7 +404,7 @@ async def query_source_file( async def get_project_data( self, project_id: str, - options: Optional[dict[str, Any]] = None, + options: dict[str, Any] | None = None, ) -> dict[str, Any]: """Get project data for a given project ID.""" self._validate_auth("get_project_data") @@ -394,7 +422,7 @@ async def get_project_data( async def download_file( self, file: dict[str, Any], - options: Optional[dict[str, Any]] = None, + options: dict[str, Any] | None = None, ) -> str: """Download a single file.""" self._validate_auth("download_file") @@ -412,7 +440,7 @@ async def download_file( async def download_file_batch( self, requests: list[dict[str, Any]], - options: Optional[dict[str, Any]] = None, + options: dict[str, Any] | None = None, ) -> dict[str, Any]: """Download multiple files in a batch.""" self._validate_auth("download_file_batch") @@ -440,7 +468,7 @@ async def translate( self, source: Any, options: str | dict[str, Any], - timeout: Optional[int] = None, + timeout: int | None = None, ) -> dict[str, Any]: """Translate a single source string.""" if isinstance(options, str): @@ -475,7 +503,7 @@ async def translate_many( self, sources: list[Any] | dict[str, Any], options: str | dict[str, Any], - timeout: Optional[int] = None, + timeout: int | None = None, ) -> list[dict[str, Any]] | dict[str, dict[str, Any]]: """Translate multiple source strings.""" if isinstance(options, str): @@ -575,8 +603,8 @@ def format_cutoff( self, value: str, *, - locales: Optional[str | list[str]] = None, - options: Optional[dict[str, Any]] = None, + locales: str | list[str] | None = None, + options: dict[str, Any] | None = None, **kwargs: Any, ) -> str: """Format a string with cutoff behaviour.""" @@ -590,8 +618,8 @@ def format_message( self, message: str, *, - locales: Optional[str | list[str]] = None, - variables: Optional[dict[str, Any]] = None, + locales: str | list[str] | None = None, + variables: dict[str, Any] | None = None, ) -> str: """Format a message with variables.""" return format_message( @@ -632,21 +660,21 @@ def format_relative_time(self, value: float | int, unit: str, **kwargs: Any) -> # -------------- Locale Properties -------------- # - def get_locale_name(self, locale: Optional[str] = None) -> str: + def get_locale_name(self, locale: str | None = None) -> str: """Get the display name of a locale code.""" locale = locale or self.target_locale if not locale: raise ValueError(no_target_locale_error("get_locale_name")) return get_locale_name(locale, self.source_locale, self.custom_mapping) - def get_locale_emoji(self, locale: Optional[str] = None) -> str: + def get_locale_emoji(self, locale: str | None = None) -> str: """Get emoji for a locale.""" locale = locale or self.target_locale if not locale: raise ValueError(no_target_locale_error("get_locale_emoji")) return get_locale_emoji(locale, self.custom_mapping) - def get_locale_properties(self, locale: Optional[str] = None) -> Any: + def get_locale_properties(self, locale: str | None = None) -> Any: """Get detailed locale properties.""" locale = locale or self.target_locale if not locale: @@ -655,8 +683,8 @@ def get_locale_properties(self, locale: Optional[str] = None) -> Any: def get_region_properties( self, - region: Optional[str] = None, - custom_mapping: Optional[CustomRegionMapping] = None, + region: str | None = None, + custom_mapping: CustomRegionMapping | None = None, ) -> dict[str, str]: """Get region properties.""" if region is None: @@ -683,10 +711,10 @@ def get_region_properties( def requires_translation( self, - source_locale: Optional[str] = None, - target_locale: Optional[str] = None, - approved_locales: Optional[list[str]] = None, - custom_mapping: Optional[CustomMapping] = None, + source_locale: str | None = None, + target_locale: str | None = None, + approved_locales: list[str] | None = None, + custom_mapping: CustomMapping | None = None, ) -> bool: """Check if translation is required.""" source_locale = source_locale or self.source_locale @@ -706,9 +734,9 @@ def requires_translation( def determine_locale( self, locales: str | list[str], - approved_locales: Optional[list[str]] = None, - custom_mapping: Optional[CustomMapping] = None, - ) -> Optional[str]: + approved_locales: list[str] | None = None, + custom_mapping: CustomMapping | None = None, + ) -> str | None: """Determine the best matching locale.""" if approved_locales is None: approved_locales = self.locales or [] @@ -716,7 +744,7 @@ def determine_locale( custom_mapping = self.custom_mapping return determine_locale(locales, approved_locales, custom_mapping) - def get_locale_direction(self, locale: Optional[str] = None) -> str: + def get_locale_direction(self, locale: str | None = None) -> str: """Get text direction for a locale.""" locale = locale or self.target_locale if not locale: @@ -725,8 +753,8 @@ def get_locale_direction(self, locale: Optional[str] = None) -> str: def is_valid_locale( self, - locale: Optional[str] = None, - custom_mapping: Optional[CustomMapping] = None, + locale: str | None = None, + custom_mapping: CustomMapping | None = None, ) -> bool: """Check if a locale code is valid.""" locale = locale or self.target_locale @@ -738,8 +766,8 @@ def is_valid_locale( def resolve_canonical_locale( self, - locale: Optional[str] = None, - custom_mapping: Optional[CustomMapping] = None, + locale: str | None = None, + custom_mapping: CustomMapping | None = None, ) -> str: """Resolve the canonical locale for a given locale.""" locale = locale or self.target_locale @@ -752,7 +780,7 @@ def resolve_canonical_locale( def resolve_alias_locale( self, locale: str, - custom_mapping: Optional[CustomMapping] = None, + custom_mapping: CustomMapping | None = None, ) -> str: """Resolve the alias locale for a given locale.""" if custom_mapping is None: @@ -761,7 +789,7 @@ def resolve_alias_locale( raise ValueError(no_target_locale_error("resolve_alias_locale")) return resolve_alias_locale(locale, custom_mapping) - def standardize_locale(self, locale: Optional[str] = None) -> str: + def standardize_locale(self, locale: str | None = None) -> str: """Standardise a BCP 47 locale code.""" locale = locale or self.target_locale if not locale: diff --git a/packages/generaltranslation/src/generaltranslation/_id/_hash.py b/packages/generaltranslation/src/generaltranslation/_id/_hash.py index 1d3a797..086d23d 100644 --- a/packages/generaltranslation/src/generaltranslation/_id/_hash.py +++ b/packages/generaltranslation/src/generaltranslation/_id/_hash.py @@ -1,6 +1,7 @@ import hashlib import json -from typing import Any, Callable, Optional +from collections.abc import Callable +from typing import Any def hash_string(s: str) -> str: @@ -11,11 +12,11 @@ def hash_string(s: str) -> str: def hash_source( source: Any, *, - context: Optional[str] = None, - id: Optional[str] = None, - max_chars: Optional[int] = None, + context: str | None = None, + id: str | None = None, + max_chars: int | None = None, data_format: str = "STRING", - hash_function: Optional[Callable[[str], str]] = None, + hash_function: Callable[[str], str] | None = None, ) -> str: """Hash source content with metadata. Only ICU/STRING path (no JSX).""" if hash_function is None: @@ -38,7 +39,7 @@ def hash_source( def hash_template( template: dict[str, str], - hash_function: Optional[Callable[[str], str]] = None, + hash_function: Callable[[str], str] | None = None, ) -> str: """Hash a template dict.""" if hash_function is None: diff --git a/packages/generaltranslation/src/generaltranslation/formatting/__init__.py b/packages/generaltranslation/src/generaltranslation/formatting/__init__.py index 41bbc9d..d2fca63 100644 --- a/packages/generaltranslation/src/generaltranslation/formatting/__init__.py +++ b/packages/generaltranslation/src/generaltranslation/formatting/__init__.py @@ -1,12 +1,12 @@ """Number, currency, datetime, and list formatting.""" -from generaltranslation.formatting._format_num import format_num from generaltranslation.formatting._format_currency import format_currency +from generaltranslation.formatting._format_cutoff import CutoffFormat, format_cutoff from generaltranslation.formatting._format_date_time import format_date_time from generaltranslation.formatting._format_list import format_list, format_list_to_parts -from generaltranslation.formatting._format_relative_time import format_relative_time from generaltranslation.formatting._format_message import format_message -from generaltranslation.formatting._format_cutoff import format_cutoff, CutoffFormat +from generaltranslation.formatting._format_num import format_num +from generaltranslation.formatting._format_relative_time import format_relative_time __all__ = [ "format_num", diff --git a/packages/generaltranslation/src/generaltranslation/formatting/_format_currency.py b/packages/generaltranslation/src/generaltranslation/formatting/_format_currency.py index bc54366..bf9d50f 100644 --- a/packages/generaltranslation/src/generaltranslation/formatting/_format_currency.py +++ b/packages/generaltranslation/src/generaltranslation/formatting/_format_currency.py @@ -9,7 +9,6 @@ from generaltranslation.formatting._helpers import _resolve_babel_locale - # Map JS currencyDisplay values to Babel format_type _DISPLAY_MAP = { "symbol": None, # Babel default diff --git a/packages/generaltranslation/src/generaltranslation/formatting/_format_cutoff.py b/packages/generaltranslation/src/generaltranslation/formatting/_format_cutoff.py index 647c607..5c920ad 100644 --- a/packages/generaltranslation/src/generaltranslation/formatting/_format_cutoff.py +++ b/packages/generaltranslation/src/generaltranslation/formatting/_format_cutoff.py @@ -7,7 +7,6 @@ from generaltranslation.formatting._helpers import _get_language_code - # Default style when maxChars is set DEFAULT_CUTOFF_FORMAT_STYLE = "ellipsis" diff --git a/packages/generaltranslation/src/generaltranslation/formatting/_format_num.py b/packages/generaltranslation/src/generaltranslation/formatting/_format_num.py index c0d09ba..ee2ed10 100644 --- a/packages/generaltranslation/src/generaltranslation/formatting/_format_num.py +++ b/packages/generaltranslation/src/generaltranslation/formatting/_format_num.py @@ -5,7 +5,7 @@ from __future__ import annotations -from babel.numbers import format_decimal, format_percent, format_compact_decimal +from babel.numbers import format_compact_decimal, format_decimal, format_percent from generaltranslation.formatting._helpers import _resolve_babel_locale diff --git a/packages/generaltranslation/src/generaltranslation/static/__init__.py b/packages/generaltranslation/src/generaltranslation/static/__init__.py index 465e08d..414093f 100644 --- a/packages/generaltranslation/src/generaltranslation/static/__init__.py +++ b/packages/generaltranslation/src/generaltranslation/static/__init__.py @@ -1,11 +1,11 @@ +from generaltranslation.static._condense_vars import condense_vars from generaltranslation.static._constants import VAR_IDENTIFIER, VAR_NAME_IDENTIFIER -from generaltranslation.static._sanitize_var import sanitize_var -from generaltranslation.static._declare_var import declare_var from generaltranslation.static._declare_static import declare_static +from generaltranslation.static._declare_var import declare_var from generaltranslation.static._decode_vars import decode_vars from generaltranslation.static._extract_vars import extract_vars from generaltranslation.static._index_vars import index_vars -from generaltranslation.static._condense_vars import condense_vars +from generaltranslation.static._sanitize_var import sanitize_var __all__ = [ "VAR_IDENTIFIER", diff --git a/packages/generaltranslation/src/generaltranslation/static/_condense_vars.py b/packages/generaltranslation/src/generaltranslation/static/_condense_vars.py index 6463c34..38e7b6c 100644 --- a/packages/generaltranslation/src/generaltranslation/static/_condense_vars.py +++ b/packages/generaltranslation/src/generaltranslation/static/_condense_vars.py @@ -3,7 +3,7 @@ from generaltranslation_icu_messageformat_parser import print_ast from generaltranslation.static._constants import VAR_IDENTIFIER -from generaltranslation.static._traverse_icu import traverse_icu, is_gt_indexed_select +from generaltranslation.static._traverse_icu import is_gt_indexed_select, traverse_icu def condense_vars(icu_string: str) -> str: diff --git a/packages/generaltranslation/src/generaltranslation/static/_decode_vars.py b/packages/generaltranslation/src/generaltranslation/static/_decode_vars.py index d750a05..8bf8676 100644 --- a/packages/generaltranslation/src/generaltranslation/static/_decode_vars.py +++ b/packages/generaltranslation/src/generaltranslation/static/_decode_vars.py @@ -1,7 +1,7 @@ from __future__ import annotations from generaltranslation.static._constants import VAR_IDENTIFIER -from generaltranslation.static._traverse_icu import traverse_icu, is_gt_unindexed_select +from generaltranslation.static._traverse_icu import is_gt_unindexed_select, traverse_icu def decode_vars(icu_string: str) -> str: diff --git a/packages/generaltranslation/src/generaltranslation/static/_extract_vars.py b/packages/generaltranslation/src/generaltranslation/static/_extract_vars.py index 3b9d49b..3aad556 100644 --- a/packages/generaltranslation/src/generaltranslation/static/_extract_vars.py +++ b/packages/generaltranslation/src/generaltranslation/static/_extract_vars.py @@ -1,7 +1,7 @@ from __future__ import annotations from generaltranslation.static._constants import VAR_IDENTIFIER -from generaltranslation.static._traverse_icu import traverse_icu, is_gt_unindexed_select +from generaltranslation.static._traverse_icu import is_gt_unindexed_select, traverse_icu def extract_vars(icu_string: str) -> dict[str, str]: diff --git a/packages/generaltranslation/src/generaltranslation/static/_index_vars.py b/packages/generaltranslation/src/generaltranslation/static/_index_vars.py index 1c492d4..d04f2e7 100644 --- a/packages/generaltranslation/src/generaltranslation/static/_index_vars.py +++ b/packages/generaltranslation/src/generaltranslation/static/_index_vars.py @@ -1,7 +1,7 @@ from __future__ import annotations from generaltranslation.static._constants import VAR_IDENTIFIER -from generaltranslation.static._traverse_icu import traverse_icu, is_gt_unindexed_select +from generaltranslation.static._traverse_icu import is_gt_unindexed_select, traverse_icu def _find_other_span( diff --git a/packages/generaltranslation/src/generaltranslation/static/_traverse_icu.py b/packages/generaltranslation/src/generaltranslation/static/_traverse_icu.py index 6391b21..6f2230c 100644 --- a/packages/generaltranslation/src/generaltranslation/static/_traverse_icu.py +++ b/packages/generaltranslation/src/generaltranslation/static/_traverse_icu.py @@ -1,7 +1,7 @@ from __future__ import annotations import re -from typing import Callable +from collections.abc import Callable from generaltranslation_icu_messageformat_parser import Parser diff --git a/packages/generaltranslation/src/generaltranslation/translate/_batch.py b/packages/generaltranslation/src/generaltranslation/translate/_batch.py index 458869b..b8f97fe 100644 --- a/packages/generaltranslation/src/generaltranslation/translate/_batch.py +++ b/packages/generaltranslation/src/generaltranslation/translate/_batch.py @@ -3,7 +3,8 @@ from __future__ import annotations import asyncio -from typing import Any, Awaitable, Callable, TypeVar +from collections.abc import Awaitable, Callable +from typing import Any, TypeVar T = TypeVar("T") U = TypeVar("U") diff --git a/packages/generaltranslation/src/generaltranslation/translate/_check_job_status.py b/packages/generaltranslation/src/generaltranslation/translate/_check_job_status.py index 40c9f2c..fa0c2dc 100644 --- a/packages/generaltranslation/src/generaltranslation/translate/_check_job_status.py +++ b/packages/generaltranslation/src/generaltranslation/translate/_check_job_status.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Any, Optional +from typing import Any from generaltranslation.translate._request import api_request @@ -10,7 +10,7 @@ async def check_job_status( job_ids: list[str], config: dict[str, Any], - timeout_ms: Optional[int] = None, + timeout_ms: int | None = None, ) -> list[dict[str, Any]]: """Query job statuses for a project.""" return await api_request( diff --git a/packages/generaltranslation/src/generaltranslation/translate/_request.py b/packages/generaltranslation/src/generaltranslation/translate/_request.py index 7bb0d7b..8662f25 100644 --- a/packages/generaltranslation/src/generaltranslation/translate/_request.py +++ b/packages/generaltranslation/src/generaltranslation/translate/_request.py @@ -4,7 +4,7 @@ import asyncio import json -from typing import Any, Literal, Optional +from typing import Any, Literal import httpx @@ -37,7 +37,7 @@ async def api_request( endpoint: str, *, body: Any = None, - timeout: Optional[int] = None, + timeout: int | None = None, method: str = "POST", retry_policy: RetryPolicy = "exponential", ) -> Any: diff --git a/packages/generaltranslation/src/generaltranslation/translate/_setup_project.py b/packages/generaltranslation/src/generaltranslation/translate/_setup_project.py index 2de857c..b5723bf 100644 --- a/packages/generaltranslation/src/generaltranslation/translate/_setup_project.py +++ b/packages/generaltranslation/src/generaltranslation/translate/_setup_project.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Any, Optional +from typing import Any from generaltranslation.translate._request import api_request @@ -10,7 +10,7 @@ async def setup_project( files: list[dict[str, Any]], config: dict[str, Any], - options: Optional[dict[str, Any]] = None, + options: dict[str, Any] | None = None, ) -> dict[str, Any]: """Enqueue files for project setup.""" options = options or {} diff --git a/packages/generaltranslation/src/generaltranslation/translate/_translate.py b/packages/generaltranslation/src/generaltranslation/translate/_translate.py index 5a8fe66..2a40f8c 100644 --- a/packages/generaltranslation/src/generaltranslation/translate/_translate.py +++ b/packages/generaltranslation/src/generaltranslation/translate/_translate.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Any, Optional +from typing import Any from generaltranslation._id import hash_source from generaltranslation._settings import DEFAULT_RUNTIME_API_URL @@ -13,7 +13,7 @@ async def translate_many( requests: list[Any] | dict[str, Any], global_metadata: dict[str, Any], config: dict[str, Any], - timeout: Optional[int] = None, + timeout: int | None = None, ) -> list[dict[str, Any]] | dict[str, dict[str, Any]]: """Translate multiple entries in a single API request. diff --git a/packages/generaltranslation/src/generaltranslation/translate/_types.py b/packages/generaltranslation/src/generaltranslation/translate/_types.py index 277cee2..f0cafef 100644 --- a/packages/generaltranslation/src/generaltranslation/translate/_types.py +++ b/packages/generaltranslation/src/generaltranslation/translate/_types.py @@ -2,8 +2,7 @@ from __future__ import annotations -from typing import Any, Literal, Optional, TypedDict - +from typing import Any, Literal, TypedDict # --- Config --- @@ -148,7 +147,7 @@ class BranchInfo(TypedDict): class BranchDataResult(TypedDict, total=False): branches: list[BranchInfo] - default_branch: Optional[BranchInfo] + default_branch: BranchInfo | None class CreateBranchQuery(TypedDict): diff --git a/packages/generaltranslation/tests/_id/test_hash.py b/packages/generaltranslation/tests/_id/test_hash.py index e858af4..49120d0 100644 --- a/packages/generaltranslation/tests/_id/test_hash.py +++ b/packages/generaltranslation/tests/_id/test_hash.py @@ -2,7 +2,6 @@ import pathlib import pytest - from generaltranslation._id import hash_source, hash_string, hash_template FIXTURES = json.loads( diff --git a/packages/generaltranslation/tests/errors/test_errors.py b/packages/generaltranslation/tests/errors/test_errors.py index b2a081b..bb245d2 100644 --- a/packages/generaltranslation/tests/errors/test_errors.py +++ b/packages/generaltranslation/tests/errors/test_errors.py @@ -1,11 +1,10 @@ """Tests for the errors module.""" import pytest - from generaltranslation.errors import ( - ApiError, GT_ERROR_PREFIX, INVALID_AUTH_ERROR, + ApiError, api_error_message, create_invalid_cutoff_style_error, invalid_locale_error, diff --git a/packages/generaltranslation/tests/formatting/test_format_cutoff.py b/packages/generaltranslation/tests/formatting/test_format_cutoff.py index 47e3646..18b8d18 100644 --- a/packages/generaltranslation/tests/formatting/test_format_cutoff.py +++ b/packages/generaltranslation/tests/formatting/test_format_cutoff.py @@ -4,7 +4,7 @@ from pathlib import Path import pytest -from generaltranslation.formatting import format_cutoff, CutoffFormat +from generaltranslation.formatting import CutoffFormat, format_cutoff FIXTURES = json.loads( (Path(__file__).parent / "fixtures" / "formatting_fixtures.json").read_text() diff --git a/packages/generaltranslation/tests/static/test_condense_vars.py b/packages/generaltranslation/tests/static/test_condense_vars.py index e09502d..ec8bc28 100644 --- a/packages/generaltranslation/tests/static/test_condense_vars.py +++ b/packages/generaltranslation/tests/static/test_condense_vars.py @@ -2,7 +2,6 @@ from pathlib import Path import pytest - from generaltranslation.static import condense_vars FIXTURES = json.loads( diff --git a/packages/generaltranslation/tests/static/test_declare_var.py b/packages/generaltranslation/tests/static/test_declare_var.py index d5fed33..8ce772c 100644 --- a/packages/generaltranslation/tests/static/test_declare_var.py +++ b/packages/generaltranslation/tests/static/test_declare_var.py @@ -2,7 +2,6 @@ from pathlib import Path import pytest - from generaltranslation.static import declare_var FIXTURES = json.loads( diff --git a/packages/generaltranslation/tests/static/test_decode_vars.py b/packages/generaltranslation/tests/static/test_decode_vars.py index 774201d..f8bbd6d 100644 --- a/packages/generaltranslation/tests/static/test_decode_vars.py +++ b/packages/generaltranslation/tests/static/test_decode_vars.py @@ -2,7 +2,6 @@ from pathlib import Path import pytest - from generaltranslation.static import decode_vars FIXTURES = json.loads( diff --git a/packages/generaltranslation/tests/static/test_extract_vars.py b/packages/generaltranslation/tests/static/test_extract_vars.py index 03e9055..365cbe0 100644 --- a/packages/generaltranslation/tests/static/test_extract_vars.py +++ b/packages/generaltranslation/tests/static/test_extract_vars.py @@ -2,7 +2,6 @@ from pathlib import Path import pytest - from generaltranslation.static import extract_vars FIXTURES = json.loads( diff --git a/packages/generaltranslation/tests/static/test_index_vars.py b/packages/generaltranslation/tests/static/test_index_vars.py index 42a0716..0bf7456 100644 --- a/packages/generaltranslation/tests/static/test_index_vars.py +++ b/packages/generaltranslation/tests/static/test_index_vars.py @@ -2,7 +2,6 @@ from pathlib import Path import pytest - from generaltranslation.static import index_vars FIXTURES = json.loads( diff --git a/packages/generaltranslation/tests/static/test_known_discrepancies.py b/packages/generaltranslation/tests/static/test_known_discrepancies.py index 7db3da4..fa4b7d1 100644 --- a/packages/generaltranslation/tests/static/test_known_discrepancies.py +++ b/packages/generaltranslation/tests/static/test_known_discrepancies.py @@ -7,7 +7,6 @@ from generaltranslation.static import condense_vars - # --------------------------------------------------------------------------- # print_ast re-escapes {} in literal text # JS printAST calls printEscapedMessage() which wraps regions containing {} diff --git a/packages/generaltranslation/tests/static/test_sanitize_var.py b/packages/generaltranslation/tests/static/test_sanitize_var.py index 06ea7ea..5e150df 100644 --- a/packages/generaltranslation/tests/static/test_sanitize_var.py +++ b/packages/generaltranslation/tests/static/test_sanitize_var.py @@ -2,7 +2,6 @@ from pathlib import Path import pytest - from generaltranslation.static import sanitize_var FIXTURES = json.loads( diff --git a/packages/generaltranslation/tests/test_gt.py b/packages/generaltranslation/tests/test_gt.py index 4c4cdcb..34199de 100644 --- a/packages/generaltranslation/tests/test_gt.py +++ b/packages/generaltranslation/tests/test_gt.py @@ -1,11 +1,8 @@ """Tests for the GT class.""" -import os import pytest -from unittest.mock import AsyncMock, patch - from generaltranslation._gt import GT -from generaltranslation._settings import DEFAULT_BASE_URL, LIBRARY_DEFAULT_LOCALE +from generaltranslation._settings import LIBRARY_DEFAULT_LOCALE class TestGTConstructor: diff --git a/packages/generaltranslation/tests/translate/test_endpoints.py b/packages/generaltranslation/tests/translate/test_endpoints.py index b202df0..4c7bb40 100644 --- a/packages/generaltranslation/tests/translate/test_endpoints.py +++ b/packages/generaltranslation/tests/translate/test_endpoints.py @@ -1,6 +1,7 @@ -import pytest from unittest.mock import AsyncMock, patch +import pytest + @pytest.mark.asyncio async def test_check_job_status(): diff --git a/packages/generaltranslation/tests/translate/test_headers.py b/packages/generaltranslation/tests/translate/test_headers.py index 0199c8f..b631420 100644 --- a/packages/generaltranslation/tests/translate/test_headers.py +++ b/packages/generaltranslation/tests/translate/test_headers.py @@ -1,5 +1,5 @@ -from generaltranslation.translate._headers import generate_request_headers from generaltranslation._settings import API_VERSION +from generaltranslation.translate._headers import generate_request_headers def test_basic_headers(): diff --git a/packages/generaltranslation/tests/translate/test_request.py b/packages/generaltranslation/tests/translate/test_request.py index e9ed1db..ed60d3a 100644 --- a/packages/generaltranslation/tests/translate/test_request.py +++ b/packages/generaltranslation/tests/translate/test_request.py @@ -1,9 +1,10 @@ import json -import pytest -from unittest.mock import AsyncMock, patch, MagicMock +from unittest.mock import AsyncMock, MagicMock, patch + import httpx -from generaltranslation.translate._request import api_request +import pytest from generaltranslation.errors import ApiError +from generaltranslation.translate._request import api_request @pytest.fixture diff --git a/packages/gt-fastapi/src/gt_fastapi/__init__.py b/packages/gt-fastapi/src/gt_fastapi/__init__.py index 42ca887..49168dd 100644 --- a/packages/gt-fastapi/src/gt_fastapi/__init__.py +++ b/packages/gt-fastapi/src/gt_fastapi/__init__.py @@ -1,6 +1,7 @@ """FastAPI integration for General Translation.""" -from gt_fastapi._setup import initialize_gt from gt_i18n import declare_static, declare_var, decode_vars, t +from gt_fastapi._setup import initialize_gt + __all__ = ["initialize_gt", "t", "declare_var", "declare_static", "decode_vars"] diff --git a/packages/gt-fastapi/src/gt_fastapi/_setup.py b/packages/gt-fastapi/src/gt_fastapi/_setup.py index 5830bdc..3f0fcf6 100644 --- a/packages/gt-fastapi/src/gt_fastapi/_setup.py +++ b/packages/gt-fastapi/src/gt_fastapi/_setup.py @@ -2,11 +2,11 @@ from __future__ import annotations +from collections.abc import AsyncGenerator, Callable from contextlib import asynccontextmanager -from typing import Any, AsyncGenerator, Callable +from typing import Any from generaltranslation.locales import determine_locale - from gt_i18n import I18nManager, set_i18n_manager, t # noqa: F401 diff --git a/packages/gt-fastapi/tests/test_fastapi_integration.py b/packages/gt-fastapi/tests/test_fastapi_integration.py index ed5e2ed..1e8c4f9 100644 --- a/packages/gt-fastapi/tests/test_fastapi_integration.py +++ b/packages/gt-fastapi/tests/test_fastapi_integration.py @@ -1,10 +1,8 @@ """FastAPI integration tests.""" import pytest - from fastapi import FastAPI from fastapi.testclient import TestClient - from gt_fastapi import initialize_gt, t from gt_i18n.translation_functions._hash_message import hash_message diff --git a/packages/gt-flask/src/gt_flask/__init__.py b/packages/gt-flask/src/gt_flask/__init__.py index 1b4064b..0d0b6a9 100644 --- a/packages/gt-flask/src/gt_flask/__init__.py +++ b/packages/gt-flask/src/gt_flask/__init__.py @@ -1,6 +1,7 @@ """Flask integration for General Translation.""" -from gt_flask._setup import initialize_gt from gt_i18n import t +from gt_flask._setup import initialize_gt + __all__ = ["initialize_gt", "t"] diff --git a/packages/gt-flask/src/gt_flask/_setup.py b/packages/gt-flask/src/gt_flask/_setup.py index b28d0ba..5268f58 100644 --- a/packages/gt-flask/src/gt_flask/_setup.py +++ b/packages/gt-flask/src/gt_flask/_setup.py @@ -3,10 +3,10 @@ from __future__ import annotations import asyncio -from typing import Any, Callable +from collections.abc import Callable +from typing import Any from generaltranslation.locales import determine_locale - from gt_i18n import I18nManager, set_i18n_manager, t # noqa: F401 diff --git a/packages/gt-flask/tests/test_flask_integration.py b/packages/gt-flask/tests/test_flask_integration.py index 387dab1..b75524a 100644 --- a/packages/gt-flask/tests/test_flask_integration.py +++ b/packages/gt-flask/tests/test_flask_integration.py @@ -1,9 +1,7 @@ """Flask integration tests.""" import pytest - from flask import Flask - from gt_flask import initialize_gt, t from gt_i18n.translation_functions._hash_message import hash_message diff --git a/packages/gt-i18n/src/gt_i18n/__init__.py b/packages/gt-i18n/src/gt_i18n/__init__.py index 2c7c9ef..0715702 100644 --- a/packages/gt-i18n/src/gt_i18n/__init__.py +++ b/packages/gt-i18n/src/gt_i18n/__init__.py @@ -1,6 +1,7 @@ """Python i18n library for General Translation.""" from generaltranslation.static import declare_static, declare_var, decode_vars + from gt_i18n.i18n_manager import ( ContextVarStorageAdapter, I18nManager, diff --git a/packages/gt-i18n/src/gt_i18n/i18n_manager/_remote_loader.py b/packages/gt-i18n/src/gt_i18n/i18n_manager/_remote_loader.py index 4e3bd7c..f8264a0 100644 --- a/packages/gt-i18n/src/gt_i18n/i18n_manager/_remote_loader.py +++ b/packages/gt-i18n/src/gt_i18n/i18n_manager/_remote_loader.py @@ -2,10 +2,9 @@ from __future__ import annotations -from typing import Callable, Awaitable +from collections.abc import Awaitable, Callable import httpx - from generaltranslation._settings import DEFAULT_CACHE_URL TranslationsLoader = Callable[[str], dict[str, str] | Awaitable[dict[str, str]]] diff --git a/packages/gt-i18n/src/gt_i18n/i18n_manager/_translations_manager.py b/packages/gt-i18n/src/gt_i18n/i18n_manager/_translations_manager.py index e7cbeab..7b687cd 100644 --- a/packages/gt-i18n/src/gt_i18n/i18n_manager/_translations_manager.py +++ b/packages/gt-i18n/src/gt_i18n/i18n_manager/_translations_manager.py @@ -5,7 +5,7 @@ import asyncio import inspect import time -from typing import Awaitable, Callable +from collections.abc import Awaitable, Callable TranslationsLoader = Callable[[str], dict[str, str] | Awaitable[dict[str, str]]] diff --git a/packages/gt-i18n/tests/test_hash_message.py b/packages/gt-i18n/tests/test_hash_message.py index a06de64..d64fc57 100644 --- a/packages/gt-i18n/tests/test_hash_message.py +++ b/packages/gt-i18n/tests/test_hash_message.py @@ -8,7 +8,6 @@ from pathlib import Path import pytest - from generaltranslation.static._index_vars import index_vars from gt_i18n.translation_functions._hash_message import hash_message diff --git a/packages/gt-i18n/tests/test_i18n_manager.py b/packages/gt-i18n/tests/test_i18n_manager.py index d2f033b..5260b80 100644 --- a/packages/gt-i18n/tests/test_i18n_manager.py +++ b/packages/gt-i18n/tests/test_i18n_manager.py @@ -2,8 +2,6 @@ import asyncio -import pytest - from gt_i18n import I18nManager diff --git a/packages/gt-i18n/tests/test_interpolate.py b/packages/gt-i18n/tests/test_interpolate.py index b105177..cd81302 100644 --- a/packages/gt-i18n/tests/test_interpolate.py +++ b/packages/gt-i18n/tests/test_interpolate.py @@ -7,10 +7,8 @@ """ from generaltranslation.static._declare_var import declare_var - from gt_i18n.translation_functions._interpolate import interpolate_message - # --- Basic interpolation --- diff --git a/packages/gt-i18n/tests/test_t.py b/packages/gt-i18n/tests/test_t.py index 4eccbf5..41f752e 100644 --- a/packages/gt-i18n/tests/test_t.py +++ b/packages/gt-i18n/tests/test_t.py @@ -1,9 +1,7 @@ """Tests for the t() function with mock translations.""" import pytest - from gt_i18n import I18nManager, set_i18n_manager, t -from gt_i18n.i18n_manager._singleton import _manager from gt_i18n.translation_functions._hash_message import hash_message diff --git a/packages/gt-i18n/tests/test_translations_manager.py b/packages/gt-i18n/tests/test_translations_manager.py index 0c2db08..0c17844 100644 --- a/packages/gt-i18n/tests/test_translations_manager.py +++ b/packages/gt-i18n/tests/test_translations_manager.py @@ -3,7 +3,6 @@ import asyncio import pytest - from gt_i18n.i18n_manager._translations_manager import TranslationsManager From 52aa0d39f5aa1f01238a107dab4821c5246e5a88 Mon Sep 17 00:00:00 2001 From: Ernest McCarter Date: Fri, 6 Mar 2026 12:48:56 -0800 Subject: [PATCH 4/4] fix: lint --- CONTRIBUTING.md | 5 + .../_constants.py | 2 +- .../_parser.py | 106 +++++-------- .../_printer.py | 12 +- .../tests/test_parser.py | 76 +++++---- .../tests/test_formatter.py | 94 +++++------ .../tests/test_supported_locales.py | 16 +- .../src/generaltranslation/_gt.py | 146 ++++-------------- .../generaltranslation/errors/_messages.py | 20 ++- .../formatting/_format_cutoff.py | 23 ++- .../formatting/_format_list.py | 4 +- .../formatting/_format_relative_time.py | 4 +- .../locales/_get_locale_emoji.py | 4 +- .../locales/_get_locale_name.py | 2 +- .../locales/_get_locale_properties.py | 32 ++-- .../locales/_get_plural_form.py | 4 +- .../locales/_get_region_properties.py | 2 +- .../locales/_requires_translation.py | 4 +- .../locales/utils/_minimize.py | 3 +- .../static/_declare_static.py | 2 +- .../generaltranslation/static/_index_vars.py | 4 +- .../static/_traverse_icu.py | 13 +- .../translate/_enqueue_files.py | 20 +-- .../translate/_get_orphaned_files.py | 4 +- .../generaltranslation/translate/_headers.py | 4 +- .../translate/_query_file_data.py | 4 +- .../generaltranslation/translate/_request.py | 4 +- .../translate/_translate.py | 18 +-- .../translate/_upload_source_files.py | 36 ++--- .../translate/_upload_translations.py | 36 ++--- .../generaltranslation/tests/_id/test_hash.py | 16 +- .../tests/errors/test_errors.py | 63 ++++---- .../tests/formatting/test_format_currency.py | 7 +- .../tests/formatting/test_format_cutoff.py | 11 +- .../tests/formatting/test_format_date_time.py | 7 +- .../tests/formatting/test_format_list.py | 7 +- .../formatting/test_format_list_to_parts.py | 7 +- .../tests/formatting/test_format_message.py | 7 +- .../tests/formatting/test_format_num.py | 7 +- .../formatting/test_format_relative_time.py | 7 +- .../locales/test_custom_locale_mapping.py | 9 +- .../tests/locales/test_determine_locale.py | 7 +- .../locales/test_get_locale_direction.py | 7 +- .../tests/locales/test_get_locale_emoji.py | 7 +- .../tests/locales/test_get_locale_name.py | 7 +- .../locales/test_get_locale_properties.py | 15 +- .../tests/locales/test_get_plural_form.py | 7 +- .../locales/test_get_region_properties.py | 11 +- .../tests/locales/test_is_same_dialect.py | 7 +- .../tests/locales/test_is_same_language.py | 7 +- .../tests/locales/test_is_superset_locale.py | 7 +- .../tests/locales/test_is_valid_locale.py | 13 +- .../locales/test_requires_translation.py | 7 +- .../tests/locales/test_resolve_locale.py | 9 +- .../tests/static/test_condense_vars.py | 7 +- .../tests/static/test_declare_var.py | 11 +- .../tests/static/test_decode_vars.py | 7 +- .../tests/static/test_extract_vars.py | 7 +- .../tests/static/test_index_vars.py | 7 +- .../tests/static/test_known_discrepancies.py | 30 +--- .../tests/static/test_sanitize_var.py | 7 +- packages/generaltranslation/tests/test_gt.py | 98 ++++++------ .../tests/translate/test_batch.py | 24 ++- .../tests/translate/test_endpoints.py | 44 ++---- .../tests/translate/test_headers.py | 8 +- .../tests/translate/test_request.py | 40 ++--- .../tests/test_fastapi_integration.py | 23 +-- .../gt-flask/tests/test_flask_integration.py | 28 ++-- .../src/gt_i18n/i18n_manager/_i18n_manager.py | 4 +- .../translation_functions/_interpolate.py | 6 +- .../src/gt_i18n/translation_functions/_msg.py | 8 +- .../src/gt_i18n/translation_functions/_t.py | 4 +- .../gt-i18n/tests/test_extract_variables.py | 10 +- packages/gt-i18n/tests/test_fallbacks.py | 18 +-- packages/gt-i18n/tests/test_hash_message.py | 136 ++++++---------- packages/gt-i18n/tests/test_i18n_manager.py | 32 ++-- packages/gt-i18n/tests/test_interpolate.py | 53 +++---- packages/gt-i18n/tests/test_msg.py | 41 ++--- .../gt-i18n/tests/test_storage_adapter.py | 17 +- packages/gt-i18n/tests/test_t.py | 19 ++- packages/gt-i18n/tests/test_t_fallback.py | 10 +- .../tests/test_translations_manager.py | 26 ++-- pyproject.toml | 10 +- 83 files changed, 688 insertions(+), 1010 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f1fa5b2..e4e971c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -124,6 +124,7 @@ If you're using VS Code, install the following extensions: | Extension | ID | Purpose | | --------- | -- | ------- | | Python | `ms-python.python` | IntelliSense, debugging, virtualenv support (includes Pylance) | +| Mypy Type Checker | `ms-python.mypy-type-checker` | Inline mypy type errors (matches project config) | | Ruff | `charliermarsh.ruff` | Linting and formatting (matches project config) | | Even Better TOML | `tamasfe.even-better-toml` | Syntax highlighting for `pyproject.toml` | @@ -175,8 +176,12 @@ This project uses [Ruff](https://docs.astral.sh/ruff/) for linting and formattin ### Commit Messages Please use [Semantic Commit Messages](https://gist.github.com/joshbuchea/6f47e86d2510bce28f8e7f42ae84c716). +For the scope, you can also mention the package names. Use clear, descriptive commit messages that explain the "why" behind the change. +These commit conventions are generally most important on commits to main. +All branch commits get squash-merged. + ## Join The Project Team Interested in joining the team? Reach out on [Discord](https://discord.gg/W99K6fchSu) or email us at [hello@generaltranslation.com](mailto:hello@generaltranslation.com). diff --git a/packages/generaltranslation-icu-messageformat-parser/src/generaltranslation_icu_messageformat_parser/_constants.py b/packages/generaltranslation-icu-messageformat-parser/src/generaltranslation_icu_messageformat_parser/_constants.py index 7fb4dd7..38fb356 100644 --- a/packages/generaltranslation-icu-messageformat-parser/src/generaltranslation_icu_messageformat_parser/_constants.py +++ b/packages/generaltranslation-icu-messageformat-parser/src/generaltranslation_icu_messageformat_parser/_constants.py @@ -35,4 +35,4 @@ 0xFEFF, ] -CLOSE_TAG = {} +CLOSE_TAG: dict[str, str] = {} diff --git a/packages/generaltranslation-icu-messageformat-parser/src/generaltranslation_icu_messageformat_parser/_parser.py b/packages/generaltranslation-icu-messageformat-parser/src/generaltranslation_icu_messageformat_parser/_parser.py index 30a6486..8fa6b53 100644 --- a/packages/generaltranslation-icu-messageformat-parser/src/generaltranslation_icu_messageformat_parser/_parser.py +++ b/packages/generaltranslation-icu-messageformat-parser/src/generaltranslation_icu_messageformat_parser/_parser.py @@ -6,6 +6,8 @@ from __future__ import annotations +from typing import cast + from . import _constants as constants SEP_OR_CLOSE = f"{constants.CHAR_SEP} or {constants.CHAR_CLOSE}" @@ -34,11 +36,7 @@ def _is_space(char: str) -> bool: if not char: return False code = ord(char) - return ( - code in constants.SPACE_CHARS - or (0x09 <= code <= 0x0D) - or (0x2000 <= code <= 0x200D) - ) + return code in constants.SPACE_CHARS or (0x09 <= code <= 0x0D) or (0x2000 <= code <= 0x200D) def _skip_space(context: dict, ret: bool = False) -> str: @@ -63,22 +61,24 @@ def _recursion(context: dict) -> SyntaxError: return SyntaxError(f"Too much recursion at position {context['i']}") -def _unexpected(char, index=None) -> SyntaxError: +def _unexpected(char: str | dict[str, object], index: int | None = None) -> SyntaxError: if isinstance(char, dict): - index = char["i"] - c = char["msg"][index] if index < char["length"] else "" - return _unexpected(c, index) + idx = cast(int, char["i"]) + msg = cast(str, char["msg"]) + length = cast(int, char["length"]) + c = msg[idx] if idx < length else "" + return _unexpected(c, idx) return SyntaxError(f'Unexpected "{char}" at position {index}') -def _expected(char, found, index=None) -> SyntaxError: +def _expected(char: str, found: str | dict[str, object] | None, index: int | None = None) -> SyntaxError: if isinstance(found, dict): - index = found["i"] - f = found["msg"][index] if index < found["length"] else "" - return _expected(char, f, index) - return SyntaxError( - f'Expected {char} at position {index} but found "{found if found else ""}"' - ) + idx = cast(int, found["i"]) + msg = cast(str, found["msg"]) + length = cast(int, found["length"]) + f = msg[idx] if idx < length else "" + return _expected(char, f, idx) + return SyntaxError(f'Expected {char} at position {index} but found "{found if found else ""}"') class Parser: @@ -189,9 +189,7 @@ def _parse_ast(self, context: dict, parent: dict | None) -> list: return out - def _can_read_tag( - self, context: dict, parent: dict | None, require_closing: bool = False - ) -> bool: + def _can_read_tag(self, context: dict, parent: dict | None, require_closing: bool = False) -> bool: msg = context["msg"] length = context["length"] current = context["i"] @@ -217,9 +215,9 @@ def _can_read_tag( return False if self.options["tag_prefix"]: - prefix = self.options["tag_prefix"] + prefix = cast(str, self.options["tag_prefix"]) return prefix == msg[current : current + len(prefix)] - elif _is_alpha(char): + elif char is not None and _is_alpha(char): return True return False @@ -233,7 +231,7 @@ def _parse_text( msg = context["msg"] length = context["length"] start = context["i"] - is_hash_special = parent and parent["type"] in self.options["subnumeric_types"] + is_hash_special = parent and parent["type"] in self.options["subnumeric_types"] # type: ignore[operator] is_tag_special = self.options["allow_tags"] allow_arg_spaces = self.options["allow_format_spaces"] @@ -247,11 +245,7 @@ def _parse_text( if ( char in constants.VAR_CHARS or (is_hash_special and char == constants.CHAR_HASH) - or ( - is_tag_special - and char == constants.CHAR_TAG_OPEN - and self._can_read_tag(context, parent) - ) + or (is_tag_special and char == constants.CHAR_TAG_OPEN and self._can_read_tag(context, parent)) or (is_arg_style and not allow_arg_spaces and is_sp) ): break @@ -281,10 +275,7 @@ def _parse_text( nxt = msg[context["i"]] if nxt == constants.CHAR_ESCAPE: context["i"] += 1 - if ( - context["i"] < length - and msg[context["i"]] == constants.CHAR_ESCAPE - ): + if context["i"] < length and msg[context["i"]] == constants.CHAR_ESCAPE: text += nxt else: break @@ -320,14 +311,15 @@ def _token_indices(self, token: dict, start: int, end: int) -> dict: def _parse_placeholder(self, context: dict, parent: dict | None) -> dict: msg = context["msg"] length = context["length"] - preserve_ws = self.options["preserve_whitespace"] - is_hash_special = parent and parent["type"] in self.options["subnumeric_types"] + preserve_ws = bool(self.options["preserve_whitespace"]) + is_hash_special = parent and parent["type"] in self.options["subnumeric_types"] # type: ignore[operator] start_idx = context["i"] char = msg[start_idx] if start_idx < length else None if is_hash_special and char == constants.CHAR_HASH: _append_token(context, "hash", char) context["i"] += 1 + assert parent is not None return self._token_indices( {"type": "number", "name": parent["name"], "hash": True}, start_idx, @@ -393,7 +385,7 @@ def _parse_placeholder(self, context: dict, parent: dict | None) -> dict: char = msg[context["i"]] if context["i"] < length else None if char == constants.CHAR_CLOSE: _append_token(context, "syntax", char) - if ttype in self.options["submessage_types"]: + if ttype in self.options["submessage_types"]: # type: ignore[operator] raise _expected(f"{ttype} sub-messages", context) context["i"] += 1 if preserve_ws and ws: @@ -410,13 +402,13 @@ def _parse_placeholder(self, context: dict, parent: dict | None) -> dict: if preserve_ws: ws["after_style_sep"] = ws_after_style_sep - if ttype in self.options["subnumeric_types"]: + if ttype in self.options["subnumeric_types"]: # type: ignore[operator] offset = self._parse_offset(context) token["offset"] = offset if offset else 0 if offset: _skip_space(context) - if ttype in self.options["submessage_types"]: + if ttype in self.options["submessage_types"]: # type: ignore[operator] messages = self._parse_submessages(context, token) if not messages: raise _expected(f"{ttype} sub-messages", context) @@ -430,11 +422,7 @@ def _parse_placeholder(self, context: dict, parent: dict | None) -> dict: end = context["i"] spaces = _skip_space(context, True) - if ( - self.options["loose_submessages"] - and context["i"] < length - and msg[context["i"]] == constants.CHAR_OPEN - ): + if self.options["loose_submessages"] and context["i"] < length and msg[context["i"]] == constants.CHAR_OPEN: context["i"] = start messages = self._parse_submessages(context, token) if not messages: @@ -495,19 +483,14 @@ def _parse_tag(self, context: dict, parent: dict | None) -> dict | None: _skip_space(context) i = context["i"] - if ( - i < length - and msg[i : i + len(constants.TAG_CLOSING)] == constants.TAG_CLOSING - ): + if i < length and msg[i : i + len(constants.TAG_CLOSING)] == constants.TAG_CLOSING: _append_token(context, "syntax", constants.TAG_CLOSING) context["i"] += len(constants.TAG_CLOSING) return self._token_indices(token, start_idx, context["i"]) char = msg[i] if i < length else None if char != constants.CHAR_TAG_END: - raise _expected( - constants.CHAR_TAG_END + " or " + constants.TAG_CLOSING, context - ) + raise _expected(constants.CHAR_TAG_END + " or " + constants.TAG_CLOSING, context) _append_token(context, "syntax", char) context["i"] += 1 @@ -517,10 +500,7 @@ def _parse_tag(self, context: dict, parent: dict | None) -> dict | None: token["contents"] = children end = context["i"] - if ( - end < length - and msg[end : end + len(constants.TAG_END)] != constants.TAG_END - ): + if end < length and msg[end : end + len(constants.TAG_END)] != constants.TAG_END: raise _expected(constants.TAG_END, context) _append_token(context, "syntax", constants.TAG_END) @@ -571,10 +551,7 @@ def _parse_offset(self, context: dict) -> int: length = context["length"] start = context["i"] - if ( - start >= length - or msg[start : start + len(constants.OFFSET)] != constants.OFFSET - ): + if start >= length or msg[start : start + len(constants.OFFSET)] != constants.OFFSET: return 0 _append_token(context, "offset", constants.OFFSET) @@ -583,8 +560,7 @@ def _parse_offset(self, context: dict) -> int: start = context["i"] while context["i"] < length and ( - _is_digit(msg[context["i"]]) - or (context["i"] == start and msg[context["i"]] == "-") + _is_digit(msg[context["i"]]) or (context["i"] == start and msg[context["i"]] == "-") ): context["i"] += 1 @@ -598,7 +574,7 @@ def _parse_offset(self, context: dict) -> int: def _parse_submessages(self, context: dict, parent: dict) -> dict | None: msg = context["msg"] length = context["length"] - preserve_ws = self.options["preserve_whitespace"] + preserve_ws = bool(self.options["preserve_whitespace"]) options: dict = {} context["depth"] += 1 @@ -606,11 +582,7 @@ def _parse_submessages(self, context: dict, parent: dict) -> dict | None: while context["i"] < length and msg[context["i"]] != constants.CHAR_CLOSE: # Save position before consuming space so we can rewind if we hit } pre_space_pos = context["i"] - ws_before_selector = ( - _skip_space(context, ret=preserve_ws) - if preserve_ws - else _skip_space(context) - ) + ws_before_selector = _skip_space(context, ret=preserve_ws) if preserve_ws else _skip_space(context) if context["i"] >= length or msg[context["i"]] == constants.CHAR_CLOSE: # Rewind: this trailing space belongs to the outer placeholder's before_close @@ -623,11 +595,7 @@ def _parse_submessages(self, context: dict, parent: dict) -> dict | None: raise _expected("sub-message selector", context) _append_token(context, "selector", selector) - ws_after_selector = ( - _skip_space(context, ret=preserve_ws) - if preserve_ws - else _skip_space(context) - ) + ws_after_selector = _skip_space(context, ret=preserve_ws) if preserve_ws else _skip_space(context) submessage = self._parse_submessage(context, parent) diff --git a/packages/generaltranslation-icu-messageformat-parser/src/generaltranslation_icu_messageformat_parser/_printer.py b/packages/generaltranslation-icu-messageformat-parser/src/generaltranslation_icu_messageformat_parser/_printer.py index 1155e3f..4e5b49e 100644 --- a/packages/generaltranslation-icu-messageformat-parser/src/generaltranslation_icu_messageformat_parser/_printer.py +++ b/packages/generaltranslation-icu-messageformat-parser/src/generaltranslation_icu_messageformat_parser/_printer.py @@ -35,9 +35,7 @@ def _escape_message(message: str) -> str: return _BRACE_RE.sub(r"'\1'", message, count=1) -def _print_literal( - value: str, is_in_plural: bool, is_first: bool, is_last: bool -) -> str: +def _print_literal(value: str, is_in_plural: bool, is_first: bool, is_last: bool) -> str: """Re-escape a literal text node for safe ICU round-tripping. Port of JS ``printLiteralElement``. @@ -66,9 +64,7 @@ def _print_nodes(nodes: list, is_in_plural: bool = False) -> str: parts: list[str] = [] for i, node in enumerate(nodes): if isinstance(node, str): - parts.append( - _print_literal(node, is_in_plural, i == 0, i == len(nodes) - 1) - ) + parts.append(_print_literal(node, is_in_plural, i == 0, i == len(nodes) - 1)) elif isinstance(node, dict): parts.append(_print_node(node)) return "".join(parts) @@ -137,9 +133,7 @@ def _print_node(node: dict) -> str: result += f"offset:{offset} " options = node["options"] - options_ws = ( - options.get("_ws", {}) if isinstance(options.get("_ws"), dict) else {} - ) + options_ws = options.get("_ws", {}) if isinstance(options.get("_ws"), dict) else {} child_in_plural = node_type in ("plural", "selectordinal") diff --git a/packages/generaltranslation-icu-messageformat-parser/tests/test_parser.py b/packages/generaltranslation-icu-messageformat-parser/tests/test_parser.py index f6dae39..056bf53 100644 --- a/packages/generaltranslation-icu-messageformat-parser/tests/test_parser.py +++ b/packages/generaltranslation-icu-messageformat-parser/tests/test_parser.py @@ -4,9 +4,11 @@ Tests are generated programmatically from a large matrix of ICU patterns. """ +from typing import Any + import pytest from generaltranslation_icu_messageformat_parser import Parser, print_ast -from pyicumessageformat import Parser as RefParser +from pyicumessageformat import Parser as RefParser # type: ignore[import-untyped] # --------------------------------------------------------------------------- # Exhaustive test input matrix @@ -77,7 +79,7 @@ "{name} has {count, plural, one {# cat} other {# cats}}", "{a, select, x {{b, plural, one {deep #} other {deeper #}}} other {fallback}}", "Hello {name}, you have {count, plural, =0 {no messages} one {# message} other {# messages}} from {sender}.", - "{gender, select, male {He has {count, plural, one {# item} other {# items}}} female {She has {count, plural, one {# item} other {# items}}} other {They have {count, plural, one {# item} other {# items}}}}", + "{gender, select, male {He has {count, plural, one {# item} other {# items}}} female {She has {count, plural, one {# item} other {# items}}} other {They have {count, plural, one {# item} other {# items}}}}", # noqa: E501 "Outer {a, select, x {inner {b, select, y {deep} other {deeper}}} other {fallback}}", ] @@ -112,7 +114,7 @@ "{_gt_, select, other {}}", "Welcome {_gt_, select, other {user}}! You have {count, plural, one {# message} other {# messages}}.", "{_gt_1, select, other {toys}} at the {_gt_2, select, other {park}}", - "{count, plural, =0 {No {_gt_, select, other {messages}}} =1 {One {_gt_, select, other {message}}} other {# {_gt_, select, other {messages}}}}", + "{count, plural, =0 {No {_gt_, select, other {messages}}} =1 {One {_gt_, select, other {message}}} other {# {_gt_, select, other {messages}}}}", # noqa: E501 "{_gt_, select, other {content with spaces}}", "{_gt_, select, other {café naïve résumé}}", "{_gt_, select, one {book} other {books}}", @@ -145,7 +147,7 @@ ) -def _strip_ws(node): +def _strip_ws(node: Any) -> Any: """Remove _ws keys recursively for comparison with reference parser.""" if isinstance(node, dict): return {k: _strip_ws(v) for k, v in node.items() if k != "_ws"} @@ -158,7 +160,7 @@ def _strip_ws(node): # Shot-for-shot AST comparison with pyicumessageformat # --------------------------------------------------------------------------- @pytest.mark.parametrize("msg", ALL_PARSE_CASES) -def test_parse_matches_reference(msg): +def test_parse_matches_reference(msg: str) -> None: """Our parser produces the same AST as pyicumessageformat.""" our_ast = Parser().parse(msg) ref_ast = RefParser().parse(msg) @@ -166,7 +168,7 @@ def test_parse_matches_reference(msg): @pytest.mark.parametrize("msg", ALL_PARSE_CASES) -def test_parse_with_indices_matches_reference(msg): +def test_parse_with_indices_matches_reference(msg: str) -> None: """include_indices produces same start/end as pyicumessageformat.""" our_ast = Parser({"include_indices": True}).parse(msg) ref_ast = RefParser({"include_indices": True}).parse(msg) @@ -177,7 +179,7 @@ def test_parse_with_indices_matches_reference(msg): # Escaped content: AST comparison (parser unescapes, so compare ASTs) # --------------------------------------------------------------------------- @pytest.mark.parametrize("msg", ESCAPED_CASES) -def test_escaped_parse_matches_reference(msg): +def test_escaped_parse_matches_reference(msg: str) -> None: """Escaped content produces same AST as reference.""" our_ast = Parser().parse(msg) ref_ast = RefParser().parse(msg) @@ -201,13 +203,11 @@ def test_escaped_parse_matches_reference(msg): @pytest.mark.parametrize("msg", ROUNDTRIP_CASES) -def test_roundtrip(msg): +def test_roundtrip(msg: str) -> None: """parse(preserve_whitespace=True) → print_ast() equals original input.""" ast = Parser({"preserve_whitespace": True}).parse(msg) reconstructed = print_ast(ast) - assert reconstructed == msg, ( - f"Round-trip failed:\n original: {msg!r}\n reconstructed: {reconstructed!r}" - ) + assert reconstructed == msg, f"Round-trip failed:\n original: {msg!r}\n reconstructed: {reconstructed!r}" # --------------------------------------------------------------------------- @@ -221,21 +221,19 @@ def test_roundtrip(msg): # print_ast with AST modifications # --------------------------------------------------------------------------- class TestPrintAstModifications: - def test_change_variable_name(self): + def test_change_variable_name(self) -> None: ast = Parser({"preserve_whitespace": True}).parse("Hello, {name}!") ast[1]["name"] = "world" assert print_ast(ast) == "Hello, {world}!" - def test_change_select_branch_content(self): - ast = Parser({"preserve_whitespace": True}).parse( - "{gender, select, male {He} female {She} other {They}}" - ) + def test_change_select_branch_content(self) -> None: + ast = Parser({"preserve_whitespace": True}).parse("{gender, select, male {He} female {She} other {They}}") ast[0]["options"]["male"] = ["Him"] result = print_ast(ast) assert "Him" in result assert "She" in result - def test_condense_indexed_select(self): + def test_condense_indexed_select(self) -> None: """Simulates condenseVars: replace {_gt_1, select, other {X}} with {_gt_1}.""" ast = Parser({"preserve_whitespace": True, "include_indices": True}).parse( "Hello {_gt_1, select, other {World}} end" @@ -243,7 +241,7 @@ def test_condense_indexed_select(self): ast[1] = {"name": "_gt_1"} assert print_ast(ast) == "Hello {_gt_1} end" - def test_condense_multiple_indexed_selects(self): + def test_condense_multiple_indexed_selects(self) -> None: ast = Parser({"preserve_whitespace": True}).parse( "I play with {_gt_1, select, other {toys}} at the {_gt_2, select, other {park}}" ) @@ -251,26 +249,22 @@ def test_condense_multiple_indexed_selects(self): ast[3] = {"name": "_gt_2"} assert print_ast(ast) == "I play with {_gt_1} at the {_gt_2}" - def test_add_new_branch(self): - ast = Parser({"preserve_whitespace": True}).parse( - "{x, select, a {A} other {default}}" - ) + def test_add_new_branch(self) -> None: + ast = Parser({"preserve_whitespace": True}).parse("{x, select, a {A} other {default}}") ast[0]["options"]["b"] = ["B"] result = print_ast(ast) assert "b{B}" in result or "b {B}" in result - def test_modify_plural_branch(self): - ast = Parser({"preserve_whitespace": True}).parse( - "{count, plural, one {# item} other {# items}}" - ) + def test_modify_plural_branch(self) -> None: + ast = Parser({"preserve_whitespace": True}).parse("{count, plural, one {# item} other {# items}}") ast[0]["options"]["one"] = ["exactly one"] result = print_ast(ast) assert "exactly one" in result - def test_empty_ast(self): + def test_empty_ast(self) -> None: assert print_ast([]) == "" - def test_plain_text_ast(self): + def test_plain_text_ast(self) -> None: assert print_ast(["Hello world"]) == "Hello world" @@ -278,24 +272,24 @@ def test_plain_text_ast(self): # Token list tests # --------------------------------------------------------------------------- class TestTokens: - def test_tokens_populated(self): - tokens = [] + def test_tokens_populated(self) -> None: + tokens: list[Any] = [] Parser().parse("{name}", tokens) assert len(tokens) > 0 types = {t["type"] for t in tokens} assert "name" in types assert "syntax" in types - def test_tokens_for_plural(self): - tokens = [] + def test_tokens_for_plural(self) -> None: + tokens: list[Any] = [] Parser().parse("{count, plural, one {# item} other {# items}}", tokens) types = [t["type"] for t in tokens] assert "type" in types # "plural" assert "selector" in types # "one", "other" assert "hash" in types # "#" - def test_tokens_for_select(self): - tokens = [] + def test_tokens_for_select(self) -> None: + tokens: list[Any] = [] Parser().parse("{gender, select, male {He} other {They}}", tokens) selectors = [t["text"] for t in tokens if t["type"] == "selector"] assert "male" in selectors @@ -306,22 +300,22 @@ def test_tokens_for_select(self): # Error handling # --------------------------------------------------------------------------- class TestErrors: - def test_invalid_input_type(self): + def test_invalid_input_type(self) -> None: with pytest.raises(TypeError): - Parser().parse(123) + Parser().parse(123) # type: ignore[arg-type] - def test_unmatched_close_brace(self): + def test_unmatched_close_brace(self) -> None: with pytest.raises(SyntaxError): Parser().parse("Hello } world") - def test_missing_placeholder_name(self): + def test_missing_placeholder_name(self) -> None: with pytest.raises(SyntaxError): Parser().parse("{}") - def test_missing_submessages(self): + def test_missing_submessages(self) -> None: with pytest.raises(SyntaxError): Parser().parse("{x, select}") - def test_tokens_invalid_type(self): + def test_tokens_invalid_type(self) -> None: with pytest.raises(TypeError): - Parser().parse("hello", "not a list") + Parser().parse("hello", "not a list") # type: ignore[arg-type] diff --git a/packages/generaltranslation-intl-messageformat/tests/test_formatter.py b/packages/generaltranslation-intl-messageformat/tests/test_formatter.py index 58e056d..3422b4c 100644 --- a/packages/generaltranslation-intl-messageformat/tests/test_formatter.py +++ b/packages/generaltranslation-intl-messageformat/tests/test_formatter.py @@ -4,6 +4,8 @@ Tests cover all ICU MessageFormat features across multiple locales. """ +from typing import Any + import pytest from generaltranslation_intl_messageformat import IntlMessageFormat from icu4py.messageformat import MessageFormat as RefMessageFormat @@ -12,7 +14,7 @@ # --------------------------------------------------------------------------- # Helper: compare our output against icu4py reference # --------------------------------------------------------------------------- -def _assert_matches_ref(pattern, locale, variables, desc): +def _assert_matches_ref(pattern: str, locale: str, variables: dict[str, Any], desc: str) -> None: our_result = IntlMessageFormat(pattern, locale).format(variables) ref_result = RefMessageFormat(pattern, locale).format(variables) assert our_result == ref_result, ( @@ -52,7 +54,7 @@ def _assert_matches_ref(pattern, locale, variables, desc): SIMPLE_VAR_CASES, ids=[c[3] for c in SIMPLE_VAR_CASES], ) -def test_simple_variables(pattern, locale, variables, desc): +def test_simple_variables(pattern: str, locale: str, variables: dict[str, Any], desc: str) -> None: _assert_matches_ref(pattern, locale, variables, desc) @@ -182,7 +184,7 @@ def test_simple_variables(pattern, locale, variables, desc): PLURAL_EN_CASES, ids=[c[3] for c in PLURAL_EN_CASES], ) -def test_plural_english(pattern, locale, variables, desc): +def test_plural_english(pattern: str, locale: str, variables: dict[str, Any], desc: str) -> None: _assert_matches_ref(pattern, locale, variables, desc) @@ -190,8 +192,7 @@ def test_plural_english(pattern, locale, variables, desc): # Plural - German # --------------------------------------------------------------------------- PLURAL_DE_CASES = [ - ("{n, plural, one {# Artikel} other {# Artikel}}", "de", {"n": v}, f"de plural {v}") - for v in [0, 1, 2, 5, 10, 100] + ("{n, plural, one {# Artikel} other {# Artikel}}", "de", {"n": v}, f"de plural {v}") for v in [0, 1, 2, 5, 10, 100] ] @@ -200,7 +201,7 @@ def test_plural_english(pattern, locale, variables, desc): PLURAL_DE_CASES, ids=[c[3] for c in PLURAL_DE_CASES], ) -def test_plural_german(pattern, locale, variables, desc): +def test_plural_german(pattern: str, locale: str, variables: dict[str, Any], desc: str) -> None: _assert_matches_ref(pattern, locale, variables, desc) @@ -223,7 +224,7 @@ def test_plural_german(pattern, locale, variables, desc): PLURAL_FR_CASES, ids=[c[3] for c in PLURAL_FR_CASES], ) -def test_plural_french(pattern, locale, variables, desc): +def test_plural_french(pattern: str, locale: str, variables: dict[str, Any], desc: str) -> None: _assert_matches_ref(pattern, locale, variables, desc) @@ -242,17 +243,14 @@ def test_plural_french(pattern, locale, variables, desc): PLURAL_AR_CASES, ids=[c[3] for c in PLURAL_AR_CASES], ) -def test_plural_arabic(pattern, locale, variables, desc): +def test_plural_arabic(pattern: str, locale: str, variables: dict[str, Any], desc: str) -> None: _assert_matches_ref(pattern, locale, variables, desc) # --------------------------------------------------------------------------- # Plural - Japanese (no plural distinction, always "other") # --------------------------------------------------------------------------- -PLURAL_JA_CASES = [ - ("{n, plural, other {#個}}", "ja", {"n": v}, f"ja plural {v}") - for v in [0, 1, 2, 5, 100] -] +PLURAL_JA_CASES = [("{n, plural, other {#個}}", "ja", {"n": v}, f"ja plural {v}") for v in [0, 1, 2, 5, 100]] @pytest.mark.parametrize( @@ -260,16 +258,14 @@ def test_plural_arabic(pattern, locale, variables, desc): PLURAL_JA_CASES, ids=[c[3] for c in PLURAL_JA_CASES], ) -def test_plural_japanese(pattern, locale, variables, desc): +def test_plural_japanese(pattern: str, locale: str, variables: dict[str, Any], desc: str) -> None: _assert_matches_ref(pattern, locale, variables, desc) # --------------------------------------------------------------------------- # Plural - Russian (one, few, many, other) # --------------------------------------------------------------------------- -RUSSIAN_PATTERN = ( - "{n, plural, one {# книга} few {# книги} many {# книг} other {# книг}}" -) +RUSSIAN_PATTERN = "{n, plural, one {# книга} few {# книги} many {# книг} other {# книг}}" PLURAL_RU_CASES = [ (RUSSIAN_PATTERN, "ru", {"n": v}, f"ru plural {v}") for v in [0, 1, 2, 3, 4, 5, 10, 11, 12, 14, 20, 21, 22, 25, 100, 101, 102] @@ -281,19 +277,16 @@ def test_plural_japanese(pattern, locale, variables, desc): PLURAL_RU_CASES, ids=[c[3] for c in PLURAL_RU_CASES], ) -def test_plural_russian(pattern, locale, variables, desc): +def test_plural_russian(pattern: str, locale: str, variables: dict[str, Any], desc: str) -> None: _assert_matches_ref(pattern, locale, variables, desc) # --------------------------------------------------------------------------- # Plural - Polish (one, few, many, other) # --------------------------------------------------------------------------- -POLISH_PATTERN = ( - "{n, plural, one {# plik} few {# pliki} many {# plików} other {# plików}}" -) +POLISH_PATTERN = "{n, plural, one {# plik} few {# pliki} many {# plików} other {# plików}}" PLURAL_PL_CASES = [ - (POLISH_PATTERN, "pl", {"n": v}, f"pl plural {v}") - for v in [0, 1, 2, 3, 4, 5, 10, 12, 14, 21, 22, 23, 25, 100, 102] + (POLISH_PATTERN, "pl", {"n": v}, f"pl plural {v}") for v in [0, 1, 2, 3, 4, 5, 10, 12, 14, 21, 22, 23, 25, 100, 102] ] @@ -302,17 +295,18 @@ def test_plural_russian(pattern, locale, variables, desc): PLURAL_PL_CASES, ids=[c[3] for c in PLURAL_PL_CASES], ) -def test_plural_polish(pattern, locale, variables, desc): +def test_plural_polish(pattern: str, locale: str, variables: dict[str, Any], desc: str) -> None: _assert_matches_ref(pattern, locale, variables, desc) # --------------------------------------------------------------------------- # Plural with offset # --------------------------------------------------------------------------- -OFFSET_PATTERN = "{guests, plural, offset:1 =0 {nobody} =1 {{host}} one {{host} and # other} other {{host} and # others}}" +OFFSET_PATTERN = ( + "{guests, plural, offset:1 =0 {nobody} =1 {{host}} one {{host} and # other} other {{host} and # others}}" # noqa: E501 +) PLURAL_OFFSET_CASES = [ - (OFFSET_PATTERN, "en", {"guests": v, "host": "Alice"}, f"offset guests={v}") - for v in [0, 1, 2, 3, 5, 10] + (OFFSET_PATTERN, "en", {"guests": v, "host": "Alice"}, f"offset guests={v}") for v in [0, 1, 2, 3, 5, 10] ] @@ -321,7 +315,7 @@ def test_plural_polish(pattern, locale, variables, desc): PLURAL_OFFSET_CASES, ids=[c[3] for c in PLURAL_OFFSET_CASES], ) -def test_plural_offset(pattern, locale, variables, desc): +def test_plural_offset(pattern: str, locale: str, variables: dict[str, Any], desc: str) -> None: _assert_matches_ref(pattern, locale, variables, desc) @@ -366,7 +360,7 @@ def test_plural_offset(pattern, locale, variables, desc): SELECTORDINAL_CASES, ids=[c[3] for c in SELECTORDINAL_CASES], ) -def test_selectordinal_english(pattern, locale, variables, desc): +def test_selectordinal_english(pattern: str, locale: str, variables: dict[str, Any], desc: str) -> None: _assert_matches_ref(pattern, locale, variables, desc) @@ -454,7 +448,7 @@ def test_selectordinal_english(pattern, locale, variables, desc): SELECT_CASES, ids=[c[3] for c in SELECT_CASES], ) -def test_select(pattern, locale, variables, desc): +def test_select(pattern: str, locale: str, variables: dict[str, Any], desc: str) -> None: _assert_matches_ref(pattern, locale, variables, desc) @@ -523,19 +517,19 @@ def test_select(pattern, locale, variables, desc): "complex nested 42", ), ( - "{gender, select, male {He has {count, plural, one {# item} other {# items}}} female {She has {count, plural, one {# item} other {# items}}} other {They have {count, plural, one {# item} other {# items}}}}", + "{gender, select, male {He has {count, plural, one {# item} other {# items}}} female {She has {count, plural, one {# item} other {# items}}} other {They have {count, plural, one {# item} other {# items}}}}", # noqa: E501 "en", {"gender": "male", "count": 1}, "gender+plural male 1", ), ( - "{gender, select, male {He has {count, plural, one {# item} other {# items}}} female {She has {count, plural, one {# item} other {# items}}} other {They have {count, plural, one {# item} other {# items}}}}", + "{gender, select, male {He has {count, plural, one {# item} other {# items}}} female {She has {count, plural, one {# item} other {# items}}} other {They have {count, plural, one {# item} other {# items}}}}", # noqa: E501 "en", {"gender": "female", "count": 5}, "gender+plural female 5", ), ( - "{gender, select, male {He has {count, plural, one {# item} other {# items}}} female {She has {count, plural, one {# item} other {# items}}} other {They have {count, plural, one {# item} other {# items}}}}", + "{gender, select, male {He has {count, plural, one {# item} other {# items}}} female {She has {count, plural, one {# item} other {# items}}} other {They have {count, plural, one {# item} other {# items}}}}", # noqa: E501 "en", {"gender": "unknown", "count": 0}, "gender+plural other 0", @@ -548,7 +542,7 @@ def test_select(pattern, locale, variables, desc): NESTED_CASES, ids=[c[3] for c in NESTED_CASES], ) -def test_nested(pattern, locale, variables, desc): +def test_nested(pattern: str, locale: str, variables: dict[str, Any], desc: str) -> None: _assert_matches_ref(pattern, locale, variables, desc) @@ -567,7 +561,7 @@ def test_nested(pattern, locale, variables, desc): ESCAPED_CASES, ids=[c[3] for c in ESCAPED_CASES], ) -def test_escaped(pattern, locale, variables, desc): +def test_escaped(pattern: str, locale: str, variables: dict[str, Any], desc: str) -> None: _assert_matches_ref(pattern, locale, variables, desc) @@ -591,7 +585,7 @@ def test_escaped(pattern, locale, variables, desc): GT_CASES, ids=[c[3] for c in GT_CASES], ) -def test_gt_patterns(pattern, locale, variables, desc): +def test_gt_patterns(pattern: str, locale: str, variables: dict[str, Any], desc: str) -> None: _assert_matches_ref(pattern, locale, variables, desc) @@ -599,52 +593,48 @@ def test_gt_patterns(pattern, locale, variables, desc): # Formatter-specific behavior tests (not compared against icu4py) # --------------------------------------------------------------------------- class TestFormatterBehavior: - def test_missing_variable_returns_empty(self): + def test_missing_variable_returns_empty(self) -> None: result = IntlMessageFormat("Hello, {name}!", "en").format({}) assert result == "Hello, !" - def test_missing_variable_in_text(self): + def test_missing_variable_in_text(self) -> None: result = IntlMessageFormat("{a} and {b}", "en").format({"a": "yes"}) assert result == "yes and " - def test_invalid_locale_falls_back_to_english(self): - result = IntlMessageFormat( - "{n, plural, one {# item} other {# items}}", "invalid-locale" - ).format({"n": 1}) + def test_invalid_locale_falls_back_to_english(self) -> None: + result = IntlMessageFormat("{n, plural, one {# item} other {# items}}", "invalid-locale").format({"n": 1}) assert result == "1 item" - def test_select_missing_value_uses_other(self): - result = IntlMessageFormat("{x, select, a {A} other {default}}", "en").format( - {"x": "missing"} - ) + def test_select_missing_value_uses_other(self) -> None: + result = IntlMessageFormat("{x, select, a {A} other {default}}", "en").format({"x": "missing"}) assert result == "default" - def test_format_none_values(self): + def test_format_none_values(self) -> None: result = IntlMessageFormat("Hello, {name}!", "en").format(None) assert result == "Hello, !" - def test_pattern_property(self): + def test_pattern_property(self) -> None: pattern = "{count, plural, one {# item} other {# items}}" mf = IntlMessageFormat(pattern, "en") assert mf.pattern == pattern - def test_locale_property(self): + def test_locale_property(self) -> None: mf = IntlMessageFormat("hello", "de") assert str(mf.locale) == "de" - def test_locale_property_with_region(self): + def test_locale_property_with_region(self) -> None: mf = IntlMessageFormat("hello", "en-US") assert mf.locale.language == "en" - def test_plain_text_no_formatting(self): + def test_plain_text_no_formatting(self) -> None: result = IntlMessageFormat("Hello world", "en").format({}) assert result == "Hello world" - def test_empty_pattern(self): + def test_empty_pattern(self) -> None: result = IntlMessageFormat("", "en").format({}) assert result == "" - def test_hash_outside_plural_not_special(self): + def test_hash_outside_plural_not_special(self) -> None: # Outside plural, # is literal text result = IntlMessageFormat("Price is #5", "en").format({}) assert result == "Price is #5" diff --git a/packages/generaltranslation-supported-locales/tests/test_supported_locales.py b/packages/generaltranslation-supported-locales/tests/test_supported_locales.py index fd116e3..81855d8 100644 --- a/packages/generaltranslation-supported-locales/tests/test_supported_locales.py +++ b/packages/generaltranslation-supported-locales/tests/test_supported_locales.py @@ -10,9 +10,7 @@ ) from generaltranslation_supported_locales._data import _SUPPORTED_LOCALES -FIXTURES = json.loads( - (Path(__file__).parent / "fixtures" / "supported_locales_fixtures.json").read_text() -) +FIXTURES = json.loads((Path(__file__).parent / "fixtures" / "supported_locales_fixtures.json").read_text()) @pytest.mark.parametrize( @@ -20,30 +18,30 @@ FIXTURES["get_supported_locale"], ids=[c["input"] or "" for c in FIXTURES["get_supported_locale"]], ) -def test_get_supported_locale(case): +def test_get_supported_locale(case: dict[str, str]) -> None: result = get_supported_locale(case["input"]) assert result == case["expected"] -def test_list_supported_locales(): +def test_list_supported_locales() -> None: result = list_supported_locales() assert result == FIXTURES["list_supported_locales"]["expected"] -def test_list_supported_locales_is_list(): +def test_list_supported_locales_is_list() -> None: assert isinstance(list_supported_locales(), list) -def test_list_supported_locales_is_sorted(): +def test_list_supported_locales_is_sorted() -> None: result = list_supported_locales() assert result == sorted(result) -def test_list_supported_locales_no_duplicates(): +def test_list_supported_locales_no_duplicates() -> None: result = list_supported_locales() assert len(result) == len(set(result)) -def test_list_supported_locales_count_matches_data(): +def test_list_supported_locales_count_matches_data() -> None: expected_count = sum(len(v) for v in _SUPPORTED_LOCALES.values()) assert len(list_supported_locales()) == expected_count diff --git a/packages/generaltranslation/src/generaltranslation/_gt.py b/packages/generaltranslation/src/generaltranslation/_gt.py index 7f2788e..0361201 100644 --- a/packages/generaltranslation/src/generaltranslation/_gt.py +++ b/packages/generaltranslation/src/generaltranslation/_gt.py @@ -109,12 +109,8 @@ def __init__( ) -> None: # Read environment variables first self.api_key: str | None = api_key or os.environ.get("GT_API_KEY") or None - self.dev_api_key: str | None = ( - dev_api_key or os.environ.get("GT_DEV_API_KEY") or None - ) - self.project_id: str | None = ( - project_id or os.environ.get("GT_PROJECT_ID") or None - ) + self.dev_api_key: str | None = dev_api_key or os.environ.get("GT_DEV_API_KEY") or None + self.project_id: str | None = project_id or os.environ.get("GT_PROJECT_ID") or None self.base_url: str | None = None self.source_locale: str | None = None @@ -194,9 +190,7 @@ def set_config( if custom_mapping: self.custom_mapping = custom_mapping self.reverse_custom_mapping = { - v["code"]: k - for k, v in custom_mapping.items() - if v and isinstance(v, dict) and "code" in v + v["code"]: k for k, v in custom_mapping.items() if v and isinstance(v, dict) and "code" in v } # -------------- Private Methods -------------- # @@ -236,9 +230,7 @@ async def process_file_moves( ) -> dict[str, Any]: """Process file moves by cloning source files and translations.""" self._validate_auth("process_file_moves") - return await _process_file_moves( - moves, options or {}, self._get_translation_config() - ) + return await _process_file_moves(moves, options or {}, self._get_translation_config()) async def get_orphaned_files( self, @@ -248,9 +240,7 @@ async def get_orphaned_files( ) -> dict[str, Any]: """Get orphaned files for a branch.""" self._validate_auth("get_orphaned_files") - return await _get_orphaned_files( - branch_id, file_ids, options or {}, self._get_translation_config() - ) + return await _get_orphaned_files(branch_id, file_ids, options or {}, self._get_translation_config()) # -------------- Translation Methods -------------- # @@ -263,9 +253,7 @@ async def setup_project( self._validate_auth("setup_project") opts = dict(options) if options else {} if opts.get("locales"): - opts["locales"] = [ - self.resolve_canonical_locale(loc) for loc in opts["locales"] - ] + opts["locales"] = [self.resolve_canonical_locale(loc) for loc in opts["locales"]] return await _setup_project(files, self._get_translation_config(), opts) async def check_job_status( @@ -275,9 +263,7 @@ async def check_job_status( ) -> list[dict[str, Any]]: """Check job statuses.""" self._validate_auth("check_job_status") - return await _check_job_status( - job_ids, self._get_translation_config(), timeout_ms - ) + return await _check_job_status(job_ids, self._get_translation_config(), timeout_ms) async def enqueue_files( self, @@ -313,8 +299,7 @@ async def submit_user_edit_diffs(self, payload: dict[str, Any]) -> None: normalized = dict(payload) if normalized.get("diffs"): normalized["diffs"] = [ - {**d, "locale": self.resolve_canonical_locale(d.get("locale"))} - for d in normalized["diffs"] + {**d, "locale": self.resolve_canonical_locale(d.get("locale"))} for d in normalized["diffs"] ] await _submit_user_edit_diffs(normalized, self._get_translation_config()) @@ -329,24 +314,17 @@ async def query_file_data( tf_key = "translated_files" if "translated_files" in data else "translatedFiles" if data.get(tf_key): data[tf_key] = [ - {**item, "locale": self.resolve_canonical_locale(item.get("locale"))} - for item in data[tf_key] + {**item, "locale": self.resolve_canonical_locale(item.get("locale"))} for item in data[tf_key] ] - result = await _query_file_data( - data, options or {}, self._get_translation_config() - ) + result = await _query_file_data(data, options or {}, self._get_translation_config()) # Resolve alias locales in response if result.get("translatedFiles"): result["translatedFiles"] = [ { **item, - **( - {"locale": self.resolve_alias_locale(item["locale"])} - if item.get("locale") - else {} - ), + **({"locale": self.resolve_alias_locale(item["locale"])} if item.get("locale") else {}), } for item in result["translatedFiles"] ] @@ -355,18 +333,11 @@ async def query_file_data( { **item, **( - { - "sourceLocale": self.resolve_alias_locale( - item["sourceLocale"] - ) - } + {"sourceLocale": self.resolve_alias_locale(item["sourceLocale"])} if item.get("sourceLocale") else {} ), - "locales": [ - self.resolve_alias_locale(loc) - for loc in item.get("locales", []) - ], + "locales": [self.resolve_alias_locale(loc) for loc in item.get("locales", [])], } for item in result["sourceFiles"] ] @@ -379,18 +350,12 @@ async def query_source_file( ) -> dict[str, Any]: """Get source file and translation information.""" self._validate_auth("query_source_file") - result = await _query_source_file( - data, options or {}, self._get_translation_config() - ) + result = await _query_source_file(data, options or {}, self._get_translation_config()) if result.get("translations"): result["translations"] = [ { **item, - **( - {"locale": self.resolve_alias_locale(item["locale"])} - if item.get("locale") - else {} - ), + **({"locale": self.resolve_alias_locale(item["locale"])} if item.get("locale") else {}), } for item in result["translations"] ] @@ -408,13 +373,9 @@ async def get_project_data( ) -> dict[str, Any]: """Get project data for a given project ID.""" self._validate_auth("get_project_data") - result = await _get_project_data( - project_id, options or {}, self._get_translation_config() - ) + result = await _get_project_data(project_id, options or {}, self._get_translation_config()) if result.get("currentLocales"): - result["currentLocales"] = [ - self.resolve_alias_locale(loc) for loc in result["currentLocales"] - ] + result["currentLocales"] = [self.resolve_alias_locale(loc) for loc in result["currentLocales"]] if result.get("defaultLocale"): result["defaultLocale"] = self.resolve_alias_locale(result["defaultLocale"]) return result @@ -432,9 +393,7 @@ async def download_file( "locale": self.resolve_canonical_locale(file.get("locale")), "versionId": file.get("version_id", file.get("versionId")), } - result = await _download_file_batch( - [request], options or {}, self._get_translation_config() - ) + result = await _download_file_batch([request], options or {}, self._get_translation_config()) return result["data"][0]["data"] async def download_file_batch( @@ -444,21 +403,12 @@ async def download_file_batch( ) -> dict[str, Any]: """Download multiple files in a batch.""" self._validate_auth("download_file_batch") - mapped_requests = [ - {**r, "locale": self.resolve_canonical_locale(r.get("locale"))} - for r in requests - ] - result = await _download_file_batch( - mapped_requests, options or {}, self._get_translation_config() - ) + mapped_requests = [{**r, "locale": self.resolve_canonical_locale(r.get("locale"))} for r in requests] + result = await _download_file_batch(mapped_requests, options or {}, self._get_translation_config()) files = [ { **f, - **( - {"locale": self.resolve_alias_locale(f["locale"])} - if f.get("locale") - else {} - ), + **({"locale": self.resolve_alias_locale(f["locale"])} if f.get("locale") else {}), } for f in result["data"] ] @@ -475,20 +425,13 @@ async def translate( options = {"target_locale": options} self._validate_auth("translate") - target_locale = ( - options.get("target_locale") - or options.get("targetLocale") - or self.target_locale - ) + target_locale = options.get("target_locale") or options.get("targetLocale") or self.target_locale if not target_locale: raise ValueError(no_target_locale_error("translate")) target_locale = self.resolve_canonical_locale(target_locale) source_locale = self.resolve_canonical_locale( - options.get("source_locale") - or options.get("sourceLocale") - or self.source_locale - or LIBRARY_DEFAULT_LOCALE + options.get("source_locale") or options.get("sourceLocale") or self.source_locale or LIBRARY_DEFAULT_LOCALE ) results = await _translate_many( @@ -497,6 +440,7 @@ async def translate( self._get_translation_config(), timeout, ) + assert isinstance(results, list) return results[0] async def translate_many( @@ -510,20 +454,13 @@ async def translate_many( options = {"target_locale": options} self._validate_auth("translate_many") - target_locale = ( - options.get("target_locale") - or options.get("targetLocale") - or self.target_locale - ) + target_locale = options.get("target_locale") or options.get("targetLocale") or self.target_locale if not target_locale: raise ValueError(no_target_locale_error("translate_many")) target_locale = self.resolve_canonical_locale(target_locale) source_locale = self.resolve_canonical_locale( - options.get("source_locale") - or options.get("sourceLocale") - or self.source_locale - or LIBRARY_DEFAULT_LOCALE + options.get("source_locale") or options.get("sourceLocale") or self.source_locale or LIBRARY_DEFAULT_LOCALE ) return await _translate_many( @@ -542,10 +479,7 @@ async def upload_source_files( self._validate_auth("upload_source_files") merged = dict(options) merged["source_locale"] = self.resolve_canonical_locale( - options.get("source_locale") - or options.get("sourceLocale") - or self.source_locale - or LIBRARY_DEFAULT_LOCALE + options.get("source_locale") or options.get("sourceLocale") or self.source_locale or LIBRARY_DEFAULT_LOCALE ) mapped_files = [ { @@ -557,9 +491,7 @@ async def upload_source_files( } for f in files ] - result = await _upload_source_files( - mapped_files, merged, self._get_translation_config() - ) + result = await _upload_source_files(mapped_files, merged, self._get_translation_config()) return { "uploadedFiles": result["data"], "count": result["count"], @@ -582,15 +514,12 @@ async def upload_translations( { **f, "translations": [ - {**t, "locale": self.resolve_canonical_locale(t.get("locale"))} - for t in f.get("translations", []) + {**t, "locale": self.resolve_canonical_locale(t.get("locale"))} for t in f.get("translations", []) ], } for f in files ] - result = await _upload_translations( - mapped_files, merged, self._get_translation_config() - ) + result = await _upload_translations(mapped_files, merged, self._get_translation_config()) return { "uploadedFiles": result["data"], "count": result["count"], @@ -610,9 +539,7 @@ def format_cutoff( """Format a string with cutoff behaviour.""" opts = dict(options or {}) opts.update(kwargs) - return format_cutoff( - value, locales=locales or self._rendering_locales, options=opts - ) + return format_cutoff(value, locales=locales or self._rendering_locales, options=opts) def format_message( self, @@ -693,12 +620,7 @@ def get_region_properties( if self.custom_mapping and not self.custom_region_mapping: crm: CustomRegionMapping = {} for loc, lp in self.custom_mapping.items(): - if ( - lp - and isinstance(lp, dict) - and lp.get("regionCode") - and lp["regionCode"] not in crm - ): + if lp and isinstance(lp, dict) and lp.get("regionCode") and lp["regionCode"] not in crm: entry: dict[str, Any] = {"locale": loc} if lp.get("regionName"): entry["name"] = lp["regionName"] @@ -727,9 +649,7 @@ def requires_translation( raise ValueError(no_source_locale_error("requires_translation")) if not target_locale: raise ValueError(no_target_locale_error("requires_translation")) - return requires_translation( - source_locale, target_locale, approved_locales, custom_mapping - ) + return requires_translation(source_locale, target_locale, approved_locales, custom_mapping) def determine_locale( self, diff --git a/packages/generaltranslation/src/generaltranslation/errors/_messages.py b/packages/generaltranslation/src/generaltranslation/errors/_messages.py index 160b00e..75770a3 100644 --- a/packages/generaltranslation/src/generaltranslation/errors/_messages.py +++ b/packages/generaltranslation/src/generaltranslation/errors/_messages.py @@ -17,19 +17,31 @@ def api_error_message(status: int, status_text: str, error: str) -> str: def no_target_locale_error(fn_name: str) -> str: - return f"{GT_ERROR_PREFIX} Cannot call `{fn_name}` without a specified locale. Either pass a locale to the `{fn_name}` function or specify a targetLocale in the GT constructor." + return ( + f"{GT_ERROR_PREFIX} Cannot call `{fn_name}` without a specified locale." + f" Either pass a locale to the `{fn_name}` function or specify a targetLocale in the GT constructor." + ) def no_source_locale_error(fn_name: str) -> str: - return f"{GT_ERROR_PREFIX} Cannot call `{fn_name}` without a specified locale. Either pass a locale to the `{fn_name}` function or specify a sourceLocale in the GT constructor." + return ( + f"{GT_ERROR_PREFIX} Cannot call `{fn_name}` without a specified locale." + f" Either pass a locale to the `{fn_name}` function or specify a sourceLocale in the GT constructor." + ) def no_project_id_error(fn_name: str) -> str: - return f"{GT_ERROR_PREFIX} Cannot call `{fn_name}` without a specified project ID. Either pass a project ID to the `{fn_name}` function or specify a projectId in the GT constructor." + return ( + f"{GT_ERROR_PREFIX} Cannot call `{fn_name}` without a specified project ID." + f" Either pass a project ID to the `{fn_name}` function or specify a projectId in the GT constructor." + ) def no_api_key_error(fn_name: str) -> str: - return f"{GT_ERROR_PREFIX} Cannot call `{fn_name}` without a specified API key. Either pass an API key to the `{fn_name}` function or specify an apiKey in the GT constructor." + return ( + f"{GT_ERROR_PREFIX} Cannot call `{fn_name}` without a specified API key." + f" Either pass an API key to the `{fn_name}` function or specify an apiKey in the GT constructor." + ) def invalid_locale_error(locale: str) -> str: diff --git a/packages/generaltranslation/src/generaltranslation/formatting/_format_cutoff.py b/packages/generaltranslation/src/generaltranslation/formatting/_format_cutoff.py index 5c920ad..fac7f4e 100644 --- a/packages/generaltranslation/src/generaltranslation/formatting/_format_cutoff.py +++ b/packages/generaltranslation/src/generaltranslation/formatting/_format_cutoff.py @@ -5,6 +5,8 @@ from __future__ import annotations +from typing import cast + from generaltranslation.formatting._helpers import _get_language_code # Default style when maxChars is set @@ -69,8 +71,7 @@ def __init__( # Validate style if style_input not in TERMINATOR_MAP: raise ValueError( - f"Invalid cutoff format style: {style_input!r}. " - f"Must be one of: {', '.join(TERMINATOR_MAP.keys())}" + f"Invalid cutoff format style: {style_input!r}. Must be one of: {', '.join(TERMINATOR_MAP.keys())}" ) # Resolve terminator options @@ -82,19 +83,13 @@ def __init__( style_map = TERMINATOR_MAP[style] preset = style_map.get(lang, style_map["_default"]) - terminator = options.get( - "terminator", preset.get("terminator") if preset else None - ) + terminator = options.get("terminator", preset.get("terminator") if preset else None) separator: str | None = None if terminator is not None: - separator = options.get( - "separator", preset.get("separator") if preset else None - ) + separator = options.get("separator", preset.get("separator") if preset else None) # Calculate addition length - self._addition_length = (len(terminator) if terminator else 0) + ( - len(separator) if separator else 0 - ) + self._addition_length = (len(terminator) if terminator else 0) + (len(separator) if separator else 0) # If maxChars doesn't have enough space for terminator+separator, drop them if max_chars is not None and abs(max_chars) < self._addition_length: @@ -132,9 +127,9 @@ def format_to_parts(self, value: str) -> list[str]: - Negative max_chars: ``[terminator?, separator?, sliced_value]`` - No cutoff: ``[original_value]`` """ - max_chars = self._options["max_chars"] - terminator = self._options["terminator"] - separator = self._options["separator"] + max_chars = cast("int | None", self._options["max_chars"]) + terminator = cast("str | None", self._options["terminator"]) + separator = cast("str | None", self._options["separator"]) # Calculate adjusted cutoff if max_chars is None or abs(max_chars) >= len(value): diff --git a/packages/generaltranslation/src/generaltranslation/formatting/_format_list.py b/packages/generaltranslation/src/generaltranslation/formatting/_format_list.py index e1f71da..a09def5 100644 --- a/packages/generaltranslation/src/generaltranslation/formatting/_format_list.py +++ b/packages/generaltranslation/src/generaltranslation/formatting/_format_list.py @@ -57,7 +57,7 @@ def format_list( babel_style = _STYLE_MAP.get((list_type, list_style), "standard") str_items = [str(item) for item in value] - return babel_format_list(str_items, style=babel_style, locale=locale) + return babel_format_list(str_items, style=babel_style, locale=locale) # type: ignore[arg-type] def format_list_to_parts( @@ -97,7 +97,7 @@ def format_list_to_parts( # Use unique placeholders to identify element positions placeholder = "\x00" placeholders = [f"{placeholder}{i}{placeholder}" for i in range(len(value))] - formatted = babel_format_list(placeholders, style=babel_style, locale=locale) + formatted = babel_format_list(placeholders, style=babel_style, locale=locale) # type: ignore[arg-type] # Split by placeholders to extract separators parts: list[T | str] = [] diff --git a/packages/generaltranslation/src/generaltranslation/formatting/_format_relative_time.py b/packages/generaltranslation/src/generaltranslation/formatting/_format_relative_time.py index 89bfb3a..aff5c40 100644 --- a/packages/generaltranslation/src/generaltranslation/formatting/_format_relative_time.py +++ b/packages/generaltranslation/src/generaltranslation/formatting/_format_relative_time.py @@ -86,10 +86,10 @@ def format_relative_time( # (match JS literal behavior where "5 seconds" stays as seconds) return format_timedelta( delta, - granularity=_singular_unit(unit), + granularity=_singular_unit(unit), # type: ignore[arg-type] threshold=999, add_direction=True, - format=babel_format, + format=babel_format, # type: ignore[arg-type] locale=locale, ) diff --git a/packages/generaltranslation/src/generaltranslation/locales/_get_locale_emoji.py b/packages/generaltranslation/src/generaltranslation/locales/_get_locale_emoji.py index 4d11c9e..35babd0 100644 --- a/packages/generaltranslation/src/generaltranslation/locales/_get_locale_emoji.py +++ b/packages/generaltranslation/src/generaltranslation/locales/_get_locale_emoji.py @@ -7,6 +7,8 @@ from __future__ import annotations +from typing import cast + from babel import Locale from babel.core import get_global @@ -295,7 +297,7 @@ } # CLDR likely subtags for territory inference -_likely_subtags: dict[str, str] = get_global("likely_subtags") +_likely_subtags: dict[str, str] = cast(dict[str, str], get_global("likely_subtags")) def get_locale_emoji( diff --git a/packages/generaltranslation/src/generaltranslation/locales/_get_locale_name.py b/packages/generaltranslation/src/generaltranslation/locales/_get_locale_name.py index f283bd5..91402eb 100644 --- a/packages/generaltranslation/src/generaltranslation/locales/_get_locale_name.py +++ b/packages/generaltranslation/src/generaltranslation/locales/_get_locale_name.py @@ -13,7 +13,7 @@ def get_locale_name( locale: str, - default_locale: str = "en", + default_locale: str | None = "en", custom_mapping: CustomMapping | None = None, ) -> str: """Return the display name of *locale* rendered in *default_locale*. diff --git a/packages/generaltranslation/src/generaltranslation/locales/_get_locale_properties.py b/packages/generaltranslation/src/generaltranslation/locales/_get_locale_properties.py index 3ef09c0..ac995dd 100644 --- a/packages/generaltranslation/src/generaltranslation/locales/_get_locale_properties.py +++ b/packages/generaltranslation/src/generaltranslation/locales/_get_locale_properties.py @@ -12,7 +12,7 @@ from generaltranslation.locales._types import CustomMapping, LocaleProperties -_likely_subtags: dict[str, str] = get_global("likely_subtags") +_likely_subtags: dict[str, str] = dict(get_global("likely_subtags")) def _create_custom_locale_properties( @@ -105,7 +105,7 @@ def _build_component_name( def get_locale_properties( locale: str, - default_locale: str = "en", + default_locale: str | None = "en", custom_mapping: CustomMapping | None = None, ) -> LocaleProperties: """Return a :class:`LocaleProperties` for *locale*.""" @@ -120,9 +120,7 @@ def get_locale_properties( # Resolve canonical if needed canonical_code = locale - if custom_mapping is not None and should_use_canonical_locale( - locale, custom_mapping - ): + if custom_mapping is not None and should_use_canonical_locale(locale, custom_mapping): entry = custom_mapping[locale] if isinstance(entry, dict) and "code" in entry: canonical_code = entry["code"] @@ -207,14 +205,10 @@ def get_locale_properties( script_name = display_locale.scripts.get(script_code, "") if script_code else "" # Maximized name: always use component form - maximized_name = _build_component_name( - language_code, max_script, max_territory, display_locale - ) + maximized_name = _build_component_name(language_code, max_script, max_territory, display_locale) # Minimized name: use compound lookup - minimized_name = _get_compound_name( - minimized_code, display_locale, minimized_parsed - ) + minimized_name = _get_compound_name(minimized_code, display_locale, minimized_parsed) # --- Native display names --- # Include script in native locale for correct display (e.g. sr-Latn vs sr-Cyrl) @@ -231,18 +225,10 @@ def get_locale_properties( native_name = _get_compound_name(std_locale, native_locale, parsed) native_language_name = native_locale.languages.get(language_code, language_code) - native_region_name = ( - native_locale.territories.get(region_code, "") if region_code else "" - ) - native_script_name = ( - native_locale.scripts.get(script_code, "") if script_code else "" - ) - native_maximized_name = _build_component_name( - language_code, max_script, max_territory, native_locale - ) - native_minimized_name = _get_compound_name( - minimized_code, native_locale, minimized_parsed - ) + native_region_name = native_locale.territories.get(region_code, "") if region_code else "" + native_script_name = native_locale.scripts.get(script_code, "") if script_code else "" + native_maximized_name = _build_component_name(language_code, max_script, max_territory, native_locale) + native_minimized_name = _get_compound_name(minimized_code, native_locale, minimized_parsed) # Name with region code if parsed.territory: diff --git a/packages/generaltranslation/src/generaltranslation/locales/_get_plural_form.py b/packages/generaltranslation/src/generaltranslation/locales/_get_plural_form.py index 4e7f9ea..c28cc3c 100644 --- a/packages/generaltranslation/src/generaltranslation/locales/_get_plural_form.py +++ b/packages/generaltranslation/src/generaltranslation/locales/_get_plural_form.py @@ -7,6 +7,8 @@ from __future__ import annotations +from typing import cast + from babel import Locale from generaltranslation.locales._types import PLURAL_FORMS, PluralType @@ -78,7 +80,7 @@ def get_plural_form( cldr_category = _get_cldr_category(n, locales) # Try to find the best matching form - return _find_best_form(cldr_category, forms_set, forms) + return _find_best_form(cldr_category, cast("set[str]", forms_set), cast("list[str]", forms)) def _get_cldr_category(n: int | float, locales: list[str]) -> str: diff --git a/packages/generaltranslation/src/generaltranslation/locales/_get_region_properties.py b/packages/generaltranslation/src/generaltranslation/locales/_get_region_properties.py index 947e235..a8cca79 100644 --- a/packages/generaltranslation/src/generaltranslation/locales/_get_region_properties.py +++ b/packages/generaltranslation/src/generaltranslation/locales/_get_region_properties.py @@ -15,7 +15,7 @@ def get_region_properties( region: str, - default_locale: str = "en", + default_locale: str | None = "en", custom_mapping: CustomRegionMapping | None = None, ) -> dict[str, str]: """Return metadata for a region code. diff --git a/packages/generaltranslation/src/generaltranslation/locales/_requires_translation.py b/packages/generaltranslation/src/generaltranslation/locales/_requires_translation.py index 9533368..58ead41 100644 --- a/packages/generaltranslation/src/generaltranslation/locales/_requires_translation.py +++ b/packages/generaltranslation/src/generaltranslation/locales/_requires_translation.py @@ -68,9 +68,7 @@ def requires_translation( return False # Check if target language is represented in approved locales - target_represented = any( - is_same_language(target_locale, approved) for approved in approved_locales - ) + target_represented = any(is_same_language(target_locale, approved) for approved in approved_locales) if not target_represented: return False diff --git a/packages/generaltranslation/src/generaltranslation/locales/utils/_minimize.py b/packages/generaltranslation/src/generaltranslation/locales/utils/_minimize.py index 4b9354b..82ffd99 100644 --- a/packages/generaltranslation/src/generaltranslation/locales/utils/_minimize.py +++ b/packages/generaltranslation/src/generaltranslation/locales/utils/_minimize.py @@ -25,6 +25,7 @@ from __future__ import annotations import logging +from typing import cast from babel import Locale from babel.core import get_global @@ -34,7 +35,7 @@ # CLDR likely-subtags table: maps partial locale tags to their most # likely fully-qualified form. # e.g. "en" → "en_Latn_US", "zh_Hant" → "zh_Hant_TW" -_likely_subtags: dict[str, str] = get_global("likely_subtags") +_likely_subtags: dict[str, str] = cast(dict[str, str], get_global("likely_subtags")) def _parse_locale( diff --git a/packages/generaltranslation/src/generaltranslation/static/_declare_static.py b/packages/generaltranslation/src/generaltranslation/static/_declare_static.py index ef2f9b2..76a3da1 100644 --- a/packages/generaltranslation/src/generaltranslation/static/_declare_static.py +++ b/packages/generaltranslation/src/generaltranslation/static/_declare_static.py @@ -1,7 +1,7 @@ from __future__ import annotations -def declare_static(content): +def declare_static(content: object) -> object: """Mark *content* as statically analyzable. This is an identity function used as a marker for static analysis. diff --git a/packages/generaltranslation/src/generaltranslation/static/_index_vars.py b/packages/generaltranslation/src/generaltranslation/static/_index_vars.py index d04f2e7..20f8c55 100644 --- a/packages/generaltranslation/src/generaltranslation/static/_index_vars.py +++ b/packages/generaltranslation/src/generaltranslation/static/_index_vars.py @@ -4,9 +4,7 @@ from generaltranslation.static._traverse_icu import is_gt_unindexed_select, traverse_icu -def _find_other_span( - icu_string: str, node_start: int, node_end: int -) -> tuple[int, int]: +def _find_other_span(icu_string: str, node_start: int, node_end: int) -> tuple[int, int]: """Find the ``{content}`` span of the ``other`` option within a select node. Returns ``(brace_open, brace_close_exclusive)`` — the positions of the diff --git a/packages/generaltranslation/src/generaltranslation/static/_traverse_icu.py b/packages/generaltranslation/src/generaltranslation/static/_traverse_icu.py index 6f2230c..b34aa27 100644 --- a/packages/generaltranslation/src/generaltranslation/static/_traverse_icu.py +++ b/packages/generaltranslation/src/generaltranslation/static/_traverse_icu.py @@ -2,6 +2,7 @@ import re from collections.abc import Callable +from typing import Any from generaltranslation_icu_messageformat_parser import Parser @@ -40,7 +41,7 @@ def handle_children(children: list) -> None: for child in children: handle_child(child) - def handle_child(child) -> None: + def handle_child(child: str | dict[str, Any]) -> None: if isinstance(child, str): return @@ -72,10 +73,7 @@ def is_gt_indexed_select(node: dict) -> bool: node.get("type") == "select" and bool(_GT_INDEXED_RE.match(node.get("name", ""))) and "other" in node.get("options", {}) - and ( - len(node["options"]["other"]) == 0 - or isinstance(node["options"]["other"][0], str) - ) + and (len(node["options"]["other"]) == 0 or isinstance(node["options"]["other"][0], str)) ) @@ -85,8 +83,5 @@ def is_gt_unindexed_select(node: dict) -> bool: node.get("type") == "select" and node.get("name", "") == "_gt_" and "other" in node.get("options", {}) - and ( - len(node["options"]["other"]) == 0 - or isinstance(node["options"]["other"][0], str) - ) + and (len(node["options"]["other"]) == 0 or isinstance(node["options"]["other"][0], str)) ) diff --git a/packages/generaltranslation/src/generaltranslation/translate/_enqueue_files.py b/packages/generaltranslation/src/generaltranslation/translate/_enqueue_files.py index 71bc62e..0b36a6e 100644 --- a/packages/generaltranslation/src/generaltranslation/translate/_enqueue_files.py +++ b/packages/generaltranslation/src/generaltranslation/translate/_enqueue_files.py @@ -26,19 +26,11 @@ async def _process_batch(batch: list[dict[str, Any]]) -> list[Any]: } for f in batch ], - "targetLocales": options.get( - "target_locales", options.get("targetLocales", []) - ), - "sourceLocale": options.get( - "source_locale", options.get("sourceLocale", "") - ), + "targetLocales": options.get("target_locales", options.get("targetLocales", [])), + "sourceLocale": options.get("source_locale", options.get("sourceLocale", "")), "publish": options.get("publish"), - "requireApproval": options.get( - "require_approval", options.get("requireApproval") - ), - "modelProvider": options.get( - "model_provider", options.get("modelProvider") - ), + "requireApproval": options.get("require_approval", options.get("requireApproval")), + "modelProvider": options.get("model_provider", options.get("modelProvider")), "force": options.get("force"), } body = {k: v for k, v in body.items() if v is not None} @@ -56,5 +48,7 @@ async def _process_batch(batch: list[dict[str, Any]]) -> list[Any]: return { "jobData": jobs, "locales": target_locales, - "message": f"Successfully enqueued {result['count']} file translation jobs in {result['batch_count']} batch(es)", + "message": ( + f"Successfully enqueued {result['count']} file translation jobs in {result['batch_count']} batch(es)" + ), } diff --git a/packages/generaltranslation/src/generaltranslation/translate/_get_orphaned_files.py b/packages/generaltranslation/src/generaltranslation/translate/_get_orphaned_files.py index b5ee6ee..6b0faf1 100644 --- a/packages/generaltranslation/src/generaltranslation/translate/_get_orphaned_files.py +++ b/packages/generaltranslation/src/generaltranslation/translate/_get_orphaned_files.py @@ -43,9 +43,7 @@ async def _make_request(batch_file_ids: list[str]) -> dict[str, Any]: orphaned_map[orphan["fileId"]] = orphan for i in range(1, len(batch_results)): - batch_orphan_ids = { - f["fileId"] for f in batch_results[i].get("orphanedFiles", []) - } + batch_orphan_ids = {f["fileId"] for f in batch_results[i].get("orphanedFiles", [])} for file_id in list(orphaned_map.keys()): if file_id not in batch_orphan_ids: del orphaned_map[file_id] diff --git a/packages/generaltranslation/src/generaltranslation/translate/_headers.py b/packages/generaltranslation/src/generaltranslation/translate/_headers.py index f28fd6d..780b575 100644 --- a/packages/generaltranslation/src/generaltranslation/translate/_headers.py +++ b/packages/generaltranslation/src/generaltranslation/translate/_headers.py @@ -7,9 +7,7 @@ from generaltranslation._settings import API_VERSION -def generate_request_headers( - config: dict[str, Any], *, exclude_content_type: bool = False -) -> dict[str, str]: +def generate_request_headers(config: dict[str, Any], *, exclude_content_type: bool = False) -> dict[str, str]: headers: dict[str, str] = {} if not exclude_content_type: headers["Content-Type"] = "application/json" diff --git a/packages/generaltranslation/src/generaltranslation/translate/_query_file_data.py b/packages/generaltranslation/src/generaltranslation/translate/_query_file_data.py index 5981ebe..ca851c0 100644 --- a/packages/generaltranslation/src/generaltranslation/translate/_query_file_data.py +++ b/packages/generaltranslation/src/generaltranslation/translate/_query_file_data.py @@ -25,9 +25,7 @@ async def query_file_data( for item in source_files ] if data.get("translated_files") or data.get("translatedFiles"): - translated_files = ( - data.get("translated_files") or data.get("translatedFiles") or [] - ) + translated_files = data.get("translated_files") or data.get("translatedFiles") or [] body["translatedFiles"] = [ { "fileId": item.get("file_id", item.get("fileId", "")), diff --git a/packages/generaltranslation/src/generaltranslation/translate/_request.py b/packages/generaltranslation/src/generaltranslation/translate/_request.py index 8662f25..e4c1569 100644 --- a/packages/generaltranslation/src/generaltranslation/translate/_request.py +++ b/packages/generaltranslation/src/generaltranslation/translate/_request.py @@ -115,9 +115,7 @@ async def api_request( error_msg = text or "Unknown error" except Exception: pass - error_message = api_error_message( - response.status_code, response.reason_phrase or "", error_msg - ) + error_message = api_error_message(response.status_code, response.reason_phrase or "", error_msg) raise ApiError(error_message, response.status_code, error_msg) return response.json() diff --git a/packages/generaltranslation/src/generaltranslation/translate/_translate.py b/packages/generaltranslation/src/generaltranslation/translate/_translate.py index 2a40f8c..b4bd04a 100644 --- a/packages/generaltranslation/src/generaltranslation/translate/_translate.py +++ b/packages/generaltranslation/src/generaltranslation/translate/_translate.py @@ -42,6 +42,7 @@ async def translate_many( if is_array: entries: list[tuple[str | None, Any]] = [(None, r) for r in requests] else: + assert isinstance(requests, dict) entries = list(requests.items()) for key, request in entries: @@ -60,9 +61,7 @@ async def translate_many( else: entry_hash = hash_source( source, - data_format=metadata.get( - "dataFormat", metadata.get("data_format", "STRING") - ), + data_format=metadata.get("dataFormat", metadata.get("data_format", "STRING")), context=metadata.get("context"), id=metadata.get("id"), max_chars=metadata.get("maxChars", metadata.get("max_chars")), @@ -79,12 +78,8 @@ async def translate_many( # Build request body using camelCase keys to match JS API body = { "requests": requests_object, - "targetLocale": global_metadata.get( - "target_locale", global_metadata.get("targetLocale", "") - ), - "sourceLocale": global_metadata.get( - "source_locale", global_metadata.get("sourceLocale", "") - ), + "targetLocale": global_metadata.get("target_locale", global_metadata.get("targetLocale", "")), + "sourceLocale": global_metadata.get("source_locale", global_metadata.get("sourceLocale", "")), "metadata": global_metadata, } @@ -104,10 +99,7 @@ async def translate_many( if hash_order is not None: return [ - response.get( - h, {"success": False, "error": "No translation returned", "code": 500} - ) - for h in hash_order + response.get(h, {"success": False, "error": "No translation returned", "code": 500}) for h in hash_order ] return response diff --git a/packages/generaltranslation/src/generaltranslation/translate/_upload_source_files.py b/packages/generaltranslation/src/generaltranslation/translate/_upload_source_files.py index 7a84866..db51ed0 100644 --- a/packages/generaltranslation/src/generaltranslation/translate/_upload_source_files.py +++ b/packages/generaltranslation/src/generaltranslation/translate/_upload_source_files.py @@ -21,31 +21,15 @@ async def _process_batch(batch: list[dict[str, Any]]) -> list[Any]: "data": [ { "source": { - "content": base64.b64encode( - item["source"]["content"].encode() - ).decode(), - "fileName": item["source"].get( - "file_name", item["source"].get("fileName", "") - ), - "fileFormat": item["source"].get( - "file_format", item["source"].get("fileFormat", "") - ), + "content": base64.b64encode(item["source"]["content"].encode()).decode(), + "fileName": item["source"].get("file_name", item["source"].get("fileName", "")), + "fileFormat": item["source"].get("file_format", item["source"].get("fileFormat", "")), "locale": item["source"].get("locale", ""), - "dataFormat": item["source"].get( - "data_format", item["source"].get("dataFormat") - ), - "formatMetadata": item["source"].get( - "format_metadata", item["source"].get("formatMetadata") - ), - "fileId": item["source"].get( - "file_id", item["source"].get("fileId") - ), - "versionId": item["source"].get( - "version_id", item["source"].get("versionId") - ), - "branchId": item["source"].get( - "branch_id", item["source"].get("branchId") - ), + "dataFormat": item["source"].get("data_format", item["source"].get("dataFormat")), + "formatMetadata": item["source"].get("format_metadata", item["source"].get("formatMetadata")), + "fileId": item["source"].get("file_id", item["source"].get("fileId")), + "versionId": item["source"].get("version_id", item["source"].get("versionId")), + "branchId": item["source"].get("branch_id", item["source"].get("branchId")), "incomingBranchId": item["source"].get( "incoming_branch_id", item["source"].get("incomingBranchId") ), @@ -57,9 +41,7 @@ async def _process_batch(batch: list[dict[str, Any]]) -> list[Any]: } for item in batch ], - "sourceLocale": options.get( - "source_locale", options.get("sourceLocale", "") - ), + "sourceLocale": options.get("source_locale", options.get("sourceLocale", "")), } result = await api_request( config, diff --git a/packages/generaltranslation/src/generaltranslation/translate/_upload_translations.py b/packages/generaltranslation/src/generaltranslation/translate/_upload_translations.py index 41fbd05..c67a9ea 100644 --- a/packages/generaltranslation/src/generaltranslation/translate/_upload_translations.py +++ b/packages/generaltranslation/src/generaltranslation/translate/_upload_translations.py @@ -21,31 +21,15 @@ async def _process_batch(batch: list[dict[str, Any]]) -> list[Any]: "data": [ { "source": { - "content": base64.b64encode( - item["source"]["content"].encode() - ).decode(), - "fileName": item["source"].get( - "file_name", item["source"].get("fileName", "") - ), - "fileFormat": item["source"].get( - "file_format", item["source"].get("fileFormat", "") - ), + "content": base64.b64encode(item["source"]["content"].encode()).decode(), + "fileName": item["source"].get("file_name", item["source"].get("fileName", "")), + "fileFormat": item["source"].get("file_format", item["source"].get("fileFormat", "")), "locale": item["source"].get("locale", ""), - "dataFormat": item["source"].get( - "data_format", item["source"].get("dataFormat") - ), - "formatMetadata": item["source"].get( - "format_metadata", item["source"].get("formatMetadata") - ), - "fileId": item["source"].get( - "file_id", item["source"].get("fileId") - ), - "versionId": item["source"].get( - "version_id", item["source"].get("versionId") - ), - "branchId": item["source"].get( - "branch_id", item["source"].get("branchId") - ), + "dataFormat": item["source"].get("data_format", item["source"].get("dataFormat")), + "formatMetadata": item["source"].get("format_metadata", item["source"].get("formatMetadata")), + "fileId": item["source"].get("file_id", item["source"].get("fileId")), + "versionId": item["source"].get("version_id", item["source"].get("versionId")), + "branchId": item["source"].get("branch_id", item["source"].get("branchId")), }, "translations": [ { @@ -63,9 +47,7 @@ async def _process_batch(batch: list[dict[str, Any]]) -> list[Any]: } for item in batch ], - "sourceLocale": options.get( - "source_locale", options.get("sourceLocale", "") - ), + "sourceLocale": options.get("source_locale", options.get("sourceLocale", "")), } result = await api_request( config, diff --git a/packages/generaltranslation/tests/_id/test_hash.py b/packages/generaltranslation/tests/_id/test_hash.py index 49120d0..98fb569 100644 --- a/packages/generaltranslation/tests/_id/test_hash.py +++ b/packages/generaltranslation/tests/_id/test_hash.py @@ -1,21 +1,20 @@ import json import pathlib +from typing import Any import pytest from generaltranslation._id import hash_source, hash_string, hash_template -FIXTURES = json.loads( - (pathlib.Path(__file__).parent / "fixtures" / "id_fixtures.json").read_text() -) +FIXTURES = json.loads((pathlib.Path(__file__).parent / "fixtures" / "id_fixtures.json").read_text()) @pytest.mark.parametrize("case", FIXTURES["hash_string"]) -def test_hash_string(case): +def test_hash_string(case: dict[str, Any]) -> None: assert hash_string(case["input"]) == case["expected"] @pytest.mark.parametrize("case", FIXTURES["hash_source"]) -def test_hash_source(case): +def test_hash_source(case: dict[str, Any]) -> None: inp = case["input"] result = hash_source( inp["source"], @@ -28,15 +27,14 @@ def test_hash_source(case): @pytest.mark.parametrize("case", FIXTURES["hash_template"]) -def test_hash_template(case): +def test_hash_template(case: dict[str, Any]) -> None: assert hash_template(case["input"]) == case["expected"] -def test_hash_source_default_data_format_is_string(): +def test_hash_source_default_data_format_is_string() -> None: """Calling hash_source without data_format should use STRING (matching JS).""" expected = hash_source("hello", data_format="STRING") actual = hash_source("hello") assert actual == expected, ( - f"Default data_format should be STRING. " - f"Got hash {actual} (expected {expected} for STRING)" + f"Default data_format should be STRING. Got hash {actual} (expected {expected} for STRING)" ) diff --git a/packages/generaltranslation/tests/errors/test_errors.py b/packages/generaltranslation/tests/errors/test_errors.py index bb245d2..45efa49 100644 --- a/packages/generaltranslation/tests/errors/test_errors.py +++ b/packages/generaltranslation/tests/errors/test_errors.py @@ -21,22 +21,20 @@ class TestApiError: """Tests for the ApiError exception class.""" - def test_is_exception(self): - err = ApiError( - "something went wrong", code=500, message="Internal Server Error" - ) + def test_is_exception(self) -> None: + err = ApiError("something went wrong", code=500, message="Internal Server Error") assert isinstance(err, Exception) - def test_attributes(self): + def test_attributes(self) -> None: err = ApiError("something went wrong", code=404, message="Not Found") assert err.code == 404 assert err.message == "Not Found" - def test_str_matches_error_arg(self): + def test_str_matches_error_arg(self) -> None: err = ApiError("custom error text", code=400, message="Bad Request") assert str(err) == "custom error text" - def test_can_be_raised_and_caught(self): + def test_can_be_raised_and_caught(self) -> None: with pytest.raises(ApiError) as exc_info: raise ApiError("boom", code=503, message="Service Unavailable") assert exc_info.value.code == 503 @@ -46,70 +44,67 @@ def test_can_be_raised_and_caught(self): class TestErrorMessages: """Tests for all error message helpers and constants.""" - def test_gt_error_prefix(self): + def test_gt_error_prefix(self) -> None: assert GT_ERROR_PREFIX == "GT Error:" - def test_invalid_auth_error(self): + def test_invalid_auth_error(self) -> None: assert INVALID_AUTH_ERROR == "GT Error: Invalid authentication." - def test_translation_timeout_error(self): + def test_translation_timeout_error(self) -> None: result = translation_timeout_error(5000) assert result == "GT Error: Translation request timed out after 5000ms." - def test_translation_request_failed_error(self): + def test_translation_request_failed_error(self) -> None: result = translation_request_failed_error("Network failure") assert result == "GT Error: Translation request failed. Error: Network failure" - def test_api_error_message(self): + def test_api_error_message(self) -> None: result = api_error_message(500, "Internal Server Error", "unexpected crash") assert ( - result - == "GT Error: API returned error status. Status: 500, Status Text: Internal Server Error, Error: unexpected crash" + result == "GT Error: API returned error status." + " Status: 500, Status Text: Internal Server Error, Error: unexpected crash" ) - def test_no_target_locale_error(self): + def test_no_target_locale_error(self) -> None: result = no_target_locale_error("translate") assert ( - result - == "GT Error: Cannot call `translate` without a specified locale. Either pass a locale to the `translate` function or specify a targetLocale in the GT constructor." + result == "GT Error: Cannot call `translate` without a specified locale." + " Either pass a locale to the `translate` function or specify a targetLocale in the GT constructor." ) - def test_no_source_locale_error(self): + def test_no_source_locale_error(self) -> None: result = no_source_locale_error("translate") assert ( - result - == "GT Error: Cannot call `translate` without a specified locale. Either pass a locale to the `translate` function or specify a sourceLocale in the GT constructor." + result == "GT Error: Cannot call `translate` without a specified locale." + " Either pass a locale to the `translate` function or specify a sourceLocale in the GT constructor." ) - def test_no_project_id_error(self): + def test_no_project_id_error(self) -> None: result = no_project_id_error("translate") assert ( - result - == "GT Error: Cannot call `translate` without a specified project ID. Either pass a project ID to the `translate` function or specify a projectId in the GT constructor." + result == "GT Error: Cannot call `translate` without a specified project ID." + " Either pass a project ID to the `translate` function or specify a projectId in the GT constructor." ) - def test_no_api_key_error(self): + def test_no_api_key_error(self) -> None: result = no_api_key_error("translate") assert ( - result - == "GT Error: Cannot call `translate` without a specified API key. Either pass an API key to the `translate` function or specify an apiKey in the GT constructor." + result == "GT Error: Cannot call `translate` without a specified API key." + " Either pass an API key to the `translate` function or specify an apiKey in the GT constructor." ) - def test_invalid_locale_error(self): + def test_invalid_locale_error(self) -> None: result = invalid_locale_error("xx-YY") assert result == "GT Error: Invalid locale: xx-YY." - def test_invalid_locales_error(self): + def test_invalid_locales_error(self) -> None: result = invalid_locales_error(["xx", "yy", "zz"]) assert result == "GT Error: Invalid locales: xx, yy, zz." - def test_invalid_locales_error_single(self): + def test_invalid_locales_error_single(self) -> None: result = invalid_locales_error(["xx"]) assert result == "GT Error: Invalid locales: xx." - def test_create_invalid_cutoff_style_error(self): + def test_create_invalid_cutoff_style_error(self) -> None: result = create_invalid_cutoff_style_error("bad-style") - assert ( - result - == "generaltranslation Formatting Error: Invalid cutoff style: bad-style." - ) + assert result == "generaltranslation Formatting Error: Invalid cutoff style: bad-style." diff --git a/packages/generaltranslation/tests/formatting/test_format_currency.py b/packages/generaltranslation/tests/formatting/test_format_currency.py index 14ea45d..40d6816 100644 --- a/packages/generaltranslation/tests/formatting/test_format_currency.py +++ b/packages/generaltranslation/tests/formatting/test_format_currency.py @@ -2,17 +2,16 @@ import json from pathlib import Path +from typing import Any import pytest from generaltranslation.formatting import format_currency -FIXTURES = json.loads( - (Path(__file__).parent / "fixtures" / "formatting_fixtures.json").read_text() -) +FIXTURES = json.loads((Path(__file__).parent / "fixtures" / "formatting_fixtures.json").read_text()) @pytest.mark.parametrize("case", FIXTURES["format_currency"]) -def test_format_currency(case): +def test_format_currency(case: dict[str, Any]) -> None: result = format_currency( case["value"], currency=case.get("currency", "USD"), diff --git a/packages/generaltranslation/tests/formatting/test_format_cutoff.py b/packages/generaltranslation/tests/formatting/test_format_cutoff.py index 18b8d18..51dbf30 100644 --- a/packages/generaltranslation/tests/formatting/test_format_cutoff.py +++ b/packages/generaltranslation/tests/formatting/test_format_cutoff.py @@ -2,17 +2,16 @@ import json from pathlib import Path +from typing import Any import pytest from generaltranslation.formatting import CutoffFormat, format_cutoff -FIXTURES = json.loads( - (Path(__file__).parent / "fixtures" / "formatting_fixtures.json").read_text() -) +FIXTURES = json.loads((Path(__file__).parent / "fixtures" / "formatting_fixtures.json").read_text()) @pytest.mark.parametrize("case", FIXTURES["format_cutoff"]) -def test_format_cutoff(case): +def test_format_cutoff(case: dict[str, Any]) -> None: result = format_cutoff( case["value"], locales=case.get("locales"), @@ -24,7 +23,7 @@ def test_format_cutoff(case): ) -def test_cutoff_format_class(): +def test_cutoff_format_class() -> None: """Test CutoffFormat class directly.""" fmt = CutoffFormat("en", {"max_chars": 5}) assert fmt.format("Hello, world!") == "Hell\u2026" @@ -38,7 +37,7 @@ def test_cutoff_format_class(): assert opts["terminator"] == "\u2026" -def test_cutoff_format_invalid_style(): +def test_cutoff_format_invalid_style() -> None: """Test that invalid style raises ValueError.""" with pytest.raises(ValueError): CutoffFormat("en", {"max_chars": 5, "style": "invalid"}) diff --git a/packages/generaltranslation/tests/formatting/test_format_date_time.py b/packages/generaltranslation/tests/formatting/test_format_date_time.py index 555d5fe..381061f 100644 --- a/packages/generaltranslation/tests/formatting/test_format_date_time.py +++ b/packages/generaltranslation/tests/formatting/test_format_date_time.py @@ -2,13 +2,12 @@ import json from pathlib import Path +from typing import Any import pytest from generaltranslation.formatting import format_date_time -FIXTURES = json.loads( - (Path(__file__).parent / "fixtures" / "formatting_fixtures.json").read_text() -) +FIXTURES = json.loads((Path(__file__).parent / "fixtures" / "formatting_fixtures.json").read_text()) def _normalize_ws(s: str) -> str: @@ -21,7 +20,7 @@ def _normalize_ws(s: str) -> str: @pytest.mark.parametrize("case", FIXTURES["format_date_time"]) -def test_format_date_time(case): +def test_format_date_time(case: dict[str, Any]) -> None: result = format_date_time( case["value"], locales=case.get("locales"), diff --git a/packages/generaltranslation/tests/formatting/test_format_list.py b/packages/generaltranslation/tests/formatting/test_format_list.py index 6365119..296cc2c 100644 --- a/packages/generaltranslation/tests/formatting/test_format_list.py +++ b/packages/generaltranslation/tests/formatting/test_format_list.py @@ -2,17 +2,16 @@ import json from pathlib import Path +from typing import Any import pytest from generaltranslation.formatting import format_list -FIXTURES = json.loads( - (Path(__file__).parent / "fixtures" / "formatting_fixtures.json").read_text() -) +FIXTURES = json.loads((Path(__file__).parent / "fixtures" / "formatting_fixtures.json").read_text()) @pytest.mark.parametrize("case", FIXTURES["format_list"]) -def test_format_list(case): +def test_format_list(case: dict[str, Any]) -> None: result = format_list( case["value"], locales=case.get("locales"), diff --git a/packages/generaltranslation/tests/formatting/test_format_list_to_parts.py b/packages/generaltranslation/tests/formatting/test_format_list_to_parts.py index 754b880..ecae751 100644 --- a/packages/generaltranslation/tests/formatting/test_format_list_to_parts.py +++ b/packages/generaltranslation/tests/formatting/test_format_list_to_parts.py @@ -2,17 +2,16 @@ import json from pathlib import Path +from typing import Any import pytest from generaltranslation.formatting import format_list_to_parts -FIXTURES = json.loads( - (Path(__file__).parent / "fixtures" / "formatting_fixtures.json").read_text() -) +FIXTURES = json.loads((Path(__file__).parent / "fixtures" / "formatting_fixtures.json").read_text()) @pytest.mark.parametrize("case", FIXTURES["format_list_to_parts"]) -def test_format_list_to_parts(case): +def test_format_list_to_parts(case: dict[str, Any]) -> None: result = format_list_to_parts( case["value"], locales=case.get("locales"), diff --git a/packages/generaltranslation/tests/formatting/test_format_message.py b/packages/generaltranslation/tests/formatting/test_format_message.py index d4c6d3a..ae3b93c 100644 --- a/packages/generaltranslation/tests/formatting/test_format_message.py +++ b/packages/generaltranslation/tests/formatting/test_format_message.py @@ -2,17 +2,16 @@ import json from pathlib import Path +from typing import Any import pytest from generaltranslation.formatting import format_message -FIXTURES = json.loads( - (Path(__file__).parent / "fixtures" / "formatting_fixtures.json").read_text() -) +FIXTURES = json.loads((Path(__file__).parent / "fixtures" / "formatting_fixtures.json").read_text()) @pytest.mark.parametrize("case", FIXTURES["format_message"]) -def test_format_message(case): +def test_format_message(case: dict[str, Any]) -> None: result = format_message( case["message"], locales=case.get("locales"), diff --git a/packages/generaltranslation/tests/formatting/test_format_num.py b/packages/generaltranslation/tests/formatting/test_format_num.py index 7c27a7f..c23f7d2 100644 --- a/packages/generaltranslation/tests/formatting/test_format_num.py +++ b/packages/generaltranslation/tests/formatting/test_format_num.py @@ -2,17 +2,16 @@ import json from pathlib import Path +from typing import Any import pytest from generaltranslation.formatting import format_num -FIXTURES = json.loads( - (Path(__file__).parent / "fixtures" / "formatting_fixtures.json").read_text() -) +FIXTURES = json.loads((Path(__file__).parent / "fixtures" / "formatting_fixtures.json").read_text()) @pytest.mark.parametrize("case", FIXTURES["format_num"]) -def test_format_num(case): +def test_format_num(case: dict[str, Any]) -> None: result = format_num( case["value"], locales=case.get("locales"), diff --git a/packages/generaltranslation/tests/formatting/test_format_relative_time.py b/packages/generaltranslation/tests/formatting/test_format_relative_time.py index b0a8a39..833da3e 100644 --- a/packages/generaltranslation/tests/formatting/test_format_relative_time.py +++ b/packages/generaltranslation/tests/formatting/test_format_relative_time.py @@ -2,17 +2,16 @@ import json from pathlib import Path +from typing import Any import pytest from generaltranslation.formatting import format_relative_time -FIXTURES = json.loads( - (Path(__file__).parent / "fixtures" / "formatting_fixtures.json").read_text() -) +FIXTURES = json.loads((Path(__file__).parent / "fixtures" / "formatting_fixtures.json").read_text()) @pytest.mark.parametrize("case", FIXTURES["format_relative_time"]) -def test_format_relative_time(case): +def test_format_relative_time(case: dict[str, Any]) -> None: result = format_relative_time( case["value"], unit=case["unit"], diff --git a/packages/generaltranslation/tests/locales/test_custom_locale_mapping.py b/packages/generaltranslation/tests/locales/test_custom_locale_mapping.py index 429b4ac..5c9b001 100644 --- a/packages/generaltranslation/tests/locales/test_custom_locale_mapping.py +++ b/packages/generaltranslation/tests/locales/test_custom_locale_mapping.py @@ -2,18 +2,17 @@ import json from pathlib import Path +from typing import Any import pytest from generaltranslation.locales import get_custom_property, should_use_canonical_locale -FIXTURES = json.loads( - (Path(__file__).parent / "fixtures" / "locale_fixtures.json").read_text() -) +FIXTURES = json.loads((Path(__file__).parent / "fixtures" / "locale_fixtures.json").read_text()) MAPPING = FIXTURES["custom_mapping"]["mapping"] @pytest.mark.parametrize("case", FIXTURES["custom_mapping"]["get_custom_property"]) -def test_get_custom_property(case): +def test_get_custom_property(case: dict[str, Any]) -> None: result = get_custom_property(MAPPING, case["locale"], case["property"]) assert result == case["expected"] @@ -22,6 +21,6 @@ def test_get_custom_property(case): @pytest.mark.parametrize("case", _CANONICAL_CASES) -def test_should_use_canonical_locale(case): +def test_should_use_canonical_locale(case: dict[str, Any]) -> None: result = should_use_canonical_locale(case["locale"], MAPPING) assert result == case["expected"] diff --git a/packages/generaltranslation/tests/locales/test_determine_locale.py b/packages/generaltranslation/tests/locales/test_determine_locale.py index 0b7e77f..a04adbd 100644 --- a/packages/generaltranslation/tests/locales/test_determine_locale.py +++ b/packages/generaltranslation/tests/locales/test_determine_locale.py @@ -2,16 +2,15 @@ import json from pathlib import Path +from typing import Any import pytest from generaltranslation.locales import determine_locale -FIXTURES = json.loads( - (Path(__file__).parent / "fixtures" / "locale_fixtures.json").read_text() -) +FIXTURES = json.loads((Path(__file__).parent / "fixtures" / "locale_fixtures.json").read_text()) @pytest.mark.parametrize("case", FIXTURES["determine_locale"]) -def test_determine_locale(case): +def test_determine_locale(case: dict[str, Any]) -> None: result = determine_locale(case["locales"], case["approved"]) assert result == case["expected"] diff --git a/packages/generaltranslation/tests/locales/test_get_locale_direction.py b/packages/generaltranslation/tests/locales/test_get_locale_direction.py index e876db9..2f6ea99 100644 --- a/packages/generaltranslation/tests/locales/test_get_locale_direction.py +++ b/packages/generaltranslation/tests/locales/test_get_locale_direction.py @@ -2,16 +2,15 @@ import json from pathlib import Path +from typing import Any import pytest from generaltranslation.locales import get_locale_direction -FIXTURES = json.loads( - (Path(__file__).parent / "fixtures" / "locale_fixtures.json").read_text() -) +FIXTURES = json.loads((Path(__file__).parent / "fixtures" / "locale_fixtures.json").read_text()) @pytest.mark.parametrize("case", FIXTURES["get_locale_direction"]) -def test_get_locale_direction(case): +def test_get_locale_direction(case: dict[str, Any]) -> None: result = get_locale_direction(case["locale"]) assert result == case["expected"] diff --git a/packages/generaltranslation/tests/locales/test_get_locale_emoji.py b/packages/generaltranslation/tests/locales/test_get_locale_emoji.py index c236bee..3061276 100644 --- a/packages/generaltranslation/tests/locales/test_get_locale_emoji.py +++ b/packages/generaltranslation/tests/locales/test_get_locale_emoji.py @@ -2,16 +2,15 @@ import json from pathlib import Path +from typing import Any import pytest from generaltranslation.locales import get_locale_emoji -FIXTURES = json.loads( - (Path(__file__).parent / "fixtures" / "locale_fixtures.json").read_text() -) +FIXTURES = json.loads((Path(__file__).parent / "fixtures" / "locale_fixtures.json").read_text()) @pytest.mark.parametrize("case", FIXTURES["get_locale_emoji"]) -def test_get_locale_emoji(case): +def test_get_locale_emoji(case: dict[str, Any]) -> None: result = get_locale_emoji(case["locale"]) assert result == case["expected"] diff --git a/packages/generaltranslation/tests/locales/test_get_locale_name.py b/packages/generaltranslation/tests/locales/test_get_locale_name.py index 818d49a..f6a3c02 100644 --- a/packages/generaltranslation/tests/locales/test_get_locale_name.py +++ b/packages/generaltranslation/tests/locales/test_get_locale_name.py @@ -2,16 +2,15 @@ import json from pathlib import Path +from typing import Any import pytest from generaltranslation.locales import get_locale_name -FIXTURES = json.loads( - (Path(__file__).parent / "fixtures" / "locale_fixtures.json").read_text() -) +FIXTURES = json.loads((Path(__file__).parent / "fixtures" / "locale_fixtures.json").read_text()) @pytest.mark.parametrize("case", FIXTURES["get_locale_name"]) -def test_get_locale_name(case): +def test_get_locale_name(case: dict[str, Any]) -> None: result = get_locale_name(case["locale"], default_locale=case["default_locale"]) assert result == case["expected"] diff --git a/packages/generaltranslation/tests/locales/test_get_locale_properties.py b/packages/generaltranslation/tests/locales/test_get_locale_properties.py index 35c77a7..cf403ec 100644 --- a/packages/generaltranslation/tests/locales/test_get_locale_properties.py +++ b/packages/generaltranslation/tests/locales/test_get_locale_properties.py @@ -2,13 +2,12 @@ import json from pathlib import Path +from typing import Any import pytest from generaltranslation.locales import get_locale_properties -FIXTURES = json.loads( - (Path(__file__).parent / "fixtures" / "locale_fixtures.json").read_text() -) +FIXTURES = json.loads((Path(__file__).parent / "fixtures" / "locale_fixtures.json").read_text()) # Map camelCase fixture keys to snake_case dataclass fields KEY_MAP = { @@ -37,14 +36,10 @@ @pytest.mark.parametrize("case", FIXTURES["get_locale_properties"]) -def test_get_locale_properties(case): - result = get_locale_properties( - case["locale"], default_locale=case["default_locale"] - ) +def test_get_locale_properties(case: dict[str, Any]) -> None: + result = get_locale_properties(case["locale"], default_locale=case["default_locale"]) expected = case["expected"] for camel_key, snake_key in KEY_MAP.items(): if camel_key in expected: actual = getattr(result, snake_key) - assert actual == expected[camel_key], ( - f"{snake_key}: {actual!r} != {expected[camel_key]!r}" - ) + assert actual == expected[camel_key], f"{snake_key}: {actual!r} != {expected[camel_key]!r}" diff --git a/packages/generaltranslation/tests/locales/test_get_plural_form.py b/packages/generaltranslation/tests/locales/test_get_plural_form.py index 33d3ba7..efc42b6 100644 --- a/packages/generaltranslation/tests/locales/test_get_plural_form.py +++ b/packages/generaltranslation/tests/locales/test_get_plural_form.py @@ -2,17 +2,16 @@ import json from pathlib import Path +from typing import Any import pytest from generaltranslation.locales import get_plural_form -FIXTURES = json.loads( - (Path(__file__).parent / "fixtures" / "locale_fixtures.json").read_text() -) +FIXTURES = json.loads((Path(__file__).parent / "fixtures" / "locale_fixtures.json").read_text()) @pytest.mark.parametrize("case", FIXTURES["get_plural_form"]) -def test_get_plural_form(case): +def test_get_plural_form(case: dict[str, Any]) -> None: result = get_plural_form( case["n"], forms=case["forms"], diff --git a/packages/generaltranslation/tests/locales/test_get_region_properties.py b/packages/generaltranslation/tests/locales/test_get_region_properties.py index 2b555de..384cab1 100644 --- a/packages/generaltranslation/tests/locales/test_get_region_properties.py +++ b/packages/generaltranslation/tests/locales/test_get_region_properties.py @@ -2,20 +2,17 @@ import json from pathlib import Path +from typing import Any import pytest from generaltranslation.locales import get_region_properties -FIXTURES = json.loads( - (Path(__file__).parent / "fixtures" / "locale_fixtures.json").read_text() -) +FIXTURES = json.loads((Path(__file__).parent / "fixtures" / "locale_fixtures.json").read_text()) @pytest.mark.parametrize("case", FIXTURES["get_region_properties"]) -def test_get_region_properties(case): - result = get_region_properties( - case["region"], default_locale=case["default_locale"] - ) +def test_get_region_properties(case: dict[str, Any]) -> None: + result = get_region_properties(case["region"], default_locale=case["default_locale"]) expected = case["expected"] assert result["code"] == expected["code"] assert result["name"] == expected["name"] diff --git a/packages/generaltranslation/tests/locales/test_is_same_dialect.py b/packages/generaltranslation/tests/locales/test_is_same_dialect.py index a62d26d..a9a2a68 100644 --- a/packages/generaltranslation/tests/locales/test_is_same_dialect.py +++ b/packages/generaltranslation/tests/locales/test_is_same_dialect.py @@ -2,16 +2,15 @@ import json from pathlib import Path +from typing import Any import pytest from generaltranslation.locales import is_same_dialect -FIXTURES = json.loads( - (Path(__file__).parent / "fixtures" / "locale_fixtures.json").read_text() -) +FIXTURES = json.loads((Path(__file__).parent / "fixtures" / "locale_fixtures.json").read_text()) @pytest.mark.parametrize("case", FIXTURES["is_same_dialect"]) -def test_is_same_dialect(case): +def test_is_same_dialect(case: dict[str, Any]) -> None: result = is_same_dialect(*case["locales"]) assert result == case["expected"] diff --git a/packages/generaltranslation/tests/locales/test_is_same_language.py b/packages/generaltranslation/tests/locales/test_is_same_language.py index a68030c..4c6e337 100644 --- a/packages/generaltranslation/tests/locales/test_is_same_language.py +++ b/packages/generaltranslation/tests/locales/test_is_same_language.py @@ -2,16 +2,15 @@ import json from pathlib import Path +from typing import Any import pytest from generaltranslation.locales import is_same_language -FIXTURES = json.loads( - (Path(__file__).parent / "fixtures" / "locale_fixtures.json").read_text() -) +FIXTURES = json.loads((Path(__file__).parent / "fixtures" / "locale_fixtures.json").read_text()) @pytest.mark.parametrize("case", FIXTURES["is_same_language"]) -def test_is_same_language(case): +def test_is_same_language(case: dict[str, Any]) -> None: result = is_same_language(*case["locales"]) assert result == case["expected"] diff --git a/packages/generaltranslation/tests/locales/test_is_superset_locale.py b/packages/generaltranslation/tests/locales/test_is_superset_locale.py index 7f216de..60c53bc 100644 --- a/packages/generaltranslation/tests/locales/test_is_superset_locale.py +++ b/packages/generaltranslation/tests/locales/test_is_superset_locale.py @@ -2,16 +2,15 @@ import json from pathlib import Path +from typing import Any import pytest from generaltranslation.locales import is_superset_locale -FIXTURES = json.loads( - (Path(__file__).parent / "fixtures" / "locale_fixtures.json").read_text() -) +FIXTURES = json.loads((Path(__file__).parent / "fixtures" / "locale_fixtures.json").read_text()) @pytest.mark.parametrize("case", FIXTURES["is_superset_locale"]) -def test_is_superset_locale(case): +def test_is_superset_locale(case: dict[str, Any]) -> None: result = is_superset_locale(case["super_locale"], case["sub_locale"]) assert result == case["expected"] diff --git a/packages/generaltranslation/tests/locales/test_is_valid_locale.py b/packages/generaltranslation/tests/locales/test_is_valid_locale.py index eef6601..4373f87 100644 --- a/packages/generaltranslation/tests/locales/test_is_valid_locale.py +++ b/packages/generaltranslation/tests/locales/test_is_valid_locale.py @@ -2,32 +2,31 @@ import json from pathlib import Path +from typing import Any import pytest from generaltranslation.locales import is_valid_locale, standardize_locale -FIXTURES = json.loads( - (Path(__file__).parent / "fixtures" / "locale_fixtures.json").read_text() -) +FIXTURES = json.loads((Path(__file__).parent / "fixtures" / "locale_fixtures.json").read_text()) @pytest.mark.parametrize("locale", FIXTURES["is_valid_locale"]["valid"]) -def test_valid_locale(locale): +def test_valid_locale(locale: str) -> None: assert is_valid_locale(locale) is True @pytest.mark.parametrize("locale", FIXTURES["is_valid_locale"]["invalid"]) -def test_invalid_locale(locale): +def test_invalid_locale(locale: str) -> None: assert is_valid_locale(locale) is False @pytest.mark.parametrize("case", FIXTURES["is_valid_locale"]["custom_mapping_cases"]) -def test_custom_mapping(case): +def test_custom_mapping(case: dict[str, Any]) -> None: result = is_valid_locale(case["locale"], case["mapping"]) assert result == case["expected"] @pytest.mark.parametrize("case", FIXTURES["standardize_locale"]) -def test_standardize_locale(case): +def test_standardize_locale(case: dict[str, Any]) -> None: result = standardize_locale(case["input"]) assert result == case["expected"] diff --git a/packages/generaltranslation/tests/locales/test_requires_translation.py b/packages/generaltranslation/tests/locales/test_requires_translation.py index e9a5723..5c981f3 100644 --- a/packages/generaltranslation/tests/locales/test_requires_translation.py +++ b/packages/generaltranslation/tests/locales/test_requires_translation.py @@ -2,17 +2,16 @@ import json from pathlib import Path +from typing import Any import pytest from generaltranslation.locales import requires_translation -FIXTURES = json.loads( - (Path(__file__).parent / "fixtures" / "locale_fixtures.json").read_text() -) +FIXTURES = json.loads((Path(__file__).parent / "fixtures" / "locale_fixtures.json").read_text()) @pytest.mark.parametrize("case", FIXTURES["requires_translation"]) -def test_requires_translation(case): +def test_requires_translation(case: dict[str, Any]) -> None: result = requires_translation( case["source"], case["target"], diff --git a/packages/generaltranslation/tests/locales/test_resolve_locale.py b/packages/generaltranslation/tests/locales/test_resolve_locale.py index c5ac98c..7057c59 100644 --- a/packages/generaltranslation/tests/locales/test_resolve_locale.py +++ b/packages/generaltranslation/tests/locales/test_resolve_locale.py @@ -2,22 +2,21 @@ import json from pathlib import Path +from typing import Any import pytest from generaltranslation.locales import resolve_alias_locale, resolve_canonical_locale -FIXTURES = json.loads( - (Path(__file__).parent / "fixtures" / "locale_fixtures.json").read_text() -) +FIXTURES = json.loads((Path(__file__).parent / "fixtures" / "locale_fixtures.json").read_text()) @pytest.mark.parametrize("case", FIXTURES["resolve_canonical_locale"]) -def test_resolve_canonical_locale(case): +def test_resolve_canonical_locale(case: dict[str, Any]) -> None: result = resolve_canonical_locale(case["locale"], case["mapping"]) assert result == case["expected"] @pytest.mark.parametrize("case", FIXTURES["resolve_alias_locale"]) -def test_resolve_alias_locale(case): +def test_resolve_alias_locale(case: dict[str, Any]) -> None: result = resolve_alias_locale(case["locale"], case["mapping"]) assert result == case["expected"] diff --git a/packages/generaltranslation/tests/static/test_condense_vars.py b/packages/generaltranslation/tests/static/test_condense_vars.py index ec8bc28..16eb7f0 100644 --- a/packages/generaltranslation/tests/static/test_condense_vars.py +++ b/packages/generaltranslation/tests/static/test_condense_vars.py @@ -1,12 +1,11 @@ import json from pathlib import Path +from typing import Any import pytest from generaltranslation.static import condense_vars -FIXTURES = json.loads( - (Path(__file__).parent / "fixtures" / "static_fixtures.json").read_text() -) +FIXTURES = json.loads((Path(__file__).parent / "fixtures" / "static_fixtures.json").read_text()) @pytest.mark.parametrize( @@ -14,6 +13,6 @@ FIXTURES["condense_vars"], ids=[c["label"] for c in FIXTURES["condense_vars"]], ) -def test_condense_vars(case): +def test_condense_vars(case: dict[str, Any]) -> None: result = condense_vars(case["input"]) assert result == case["expected"] diff --git a/packages/generaltranslation/tests/static/test_declare_var.py b/packages/generaltranslation/tests/static/test_declare_var.py index 8ce772c..1b03234 100644 --- a/packages/generaltranslation/tests/static/test_declare_var.py +++ b/packages/generaltranslation/tests/static/test_declare_var.py @@ -1,22 +1,21 @@ import json from pathlib import Path +from typing import Any import pytest from generaltranslation.static import declare_var -FIXTURES = json.loads( - (Path(__file__).parent / "fixtures" / "static_fixtures.json").read_text() -) +FIXTURES = json.loads((Path(__file__).parent / "fixtures" / "static_fixtures.json").read_text()) -def _to_python_variable(v): +def _to_python_variable(v: Any) -> Any: """Convert JSON fixture variable to the Python type declareVar expects.""" if v is None: return None return v -def _to_python_options(opts): +def _to_python_options(opts: dict[str, Any] | None) -> dict[str, Any]: """Convert JS ``{$name: ...}`` to Python ``name=...`` kwarg.""" if not opts: return {} @@ -31,7 +30,7 @@ def _to_python_options(opts): FIXTURES["declare_var"], ids=[c["label"] for c in FIXTURES["declare_var"]], ) -def test_declare_var(case): +def test_declare_var(case: dict[str, Any]) -> None: variable = _to_python_variable(case.get("variable")) kwargs = _to_python_options(case.get("options")) result = declare_var(variable, **kwargs) diff --git a/packages/generaltranslation/tests/static/test_decode_vars.py b/packages/generaltranslation/tests/static/test_decode_vars.py index f8bbd6d..56efc27 100644 --- a/packages/generaltranslation/tests/static/test_decode_vars.py +++ b/packages/generaltranslation/tests/static/test_decode_vars.py @@ -1,12 +1,11 @@ import json from pathlib import Path +from typing import Any import pytest from generaltranslation.static import decode_vars -FIXTURES = json.loads( - (Path(__file__).parent / "fixtures" / "static_fixtures.json").read_text() -) +FIXTURES = json.loads((Path(__file__).parent / "fixtures" / "static_fixtures.json").read_text()) @pytest.mark.parametrize( @@ -14,6 +13,6 @@ FIXTURES["decode_vars"], ids=[c["label"] for c in FIXTURES["decode_vars"]], ) -def test_decode_vars(case): +def test_decode_vars(case: dict[str, Any]) -> None: result = decode_vars(case["input"]) assert result == case["expected"] diff --git a/packages/generaltranslation/tests/static/test_extract_vars.py b/packages/generaltranslation/tests/static/test_extract_vars.py index 365cbe0..ce5f966 100644 --- a/packages/generaltranslation/tests/static/test_extract_vars.py +++ b/packages/generaltranslation/tests/static/test_extract_vars.py @@ -1,12 +1,11 @@ import json from pathlib import Path +from typing import Any import pytest from generaltranslation.static import extract_vars -FIXTURES = json.loads( - (Path(__file__).parent / "fixtures" / "static_fixtures.json").read_text() -) +FIXTURES = json.loads((Path(__file__).parent / "fixtures" / "static_fixtures.json").read_text()) @pytest.mark.parametrize( @@ -14,6 +13,6 @@ FIXTURES["extract_vars"], ids=[c["label"] for c in FIXTURES["extract_vars"]], ) -def test_extract_vars(case): +def test_extract_vars(case: dict[str, Any]) -> None: result = extract_vars(case["input"]) assert result == case["expected"] diff --git a/packages/generaltranslation/tests/static/test_index_vars.py b/packages/generaltranslation/tests/static/test_index_vars.py index 0bf7456..01c6b5d 100644 --- a/packages/generaltranslation/tests/static/test_index_vars.py +++ b/packages/generaltranslation/tests/static/test_index_vars.py @@ -1,12 +1,11 @@ import json from pathlib import Path +from typing import Any import pytest from generaltranslation.static import index_vars -FIXTURES = json.loads( - (Path(__file__).parent / "fixtures" / "static_fixtures.json").read_text() -) +FIXTURES = json.loads((Path(__file__).parent / "fixtures" / "static_fixtures.json").read_text()) @pytest.mark.parametrize( @@ -14,6 +13,6 @@ FIXTURES["index_vars"], ids=[c["label"] for c in FIXTURES["index_vars"]], ) -def test_index_vars(case): +def test_index_vars(case: dict[str, Any]) -> None: result = index_vars(case["input"]) assert result == case["expected"] diff --git a/packages/generaltranslation/tests/static/test_known_discrepancies.py b/packages/generaltranslation/tests/static/test_known_discrepancies.py index fa4b7d1..33fbcf5 100644 --- a/packages/generaltranslation/tests/static/test_known_discrepancies.py +++ b/packages/generaltranslation/tests/static/test_known_discrepancies.py @@ -14,11 +14,8 @@ # --------------------------------------------------------------------------- -def test_condense_escaped_braces_in_non_condensed_select(): - icu = ( - "{_gt_1, select, other {x}} " - "{type, select, a {text with '{braces}'} other {plain}}" - ) +def test_condense_escaped_braces_in_non_condensed_select() -> None: + icu = "{_gt_1, select, other {x}} {type, select, a {text with '{braces}'} other {plain}}" js_expected = "{_gt_1} {type,select,a{text with '{braces}'} other{plain}}" assert condense_vars(icu) == js_expected @@ -30,11 +27,8 @@ def test_condense_escaped_braces_in_non_condensed_select(): # --------------------------------------------------------------------------- -def test_condense_escaped_hash_in_non_condensed_plural(): - icu = ( - "{_gt_1, select, other {x}} " - "{n, plural, offset:1 one {# item, not '#'} other {# items}}" - ) +def test_condense_escaped_hash_in_non_condensed_plural() -> None: + icu = "{_gt_1, select, other {x}} {n, plural, offset:1 one {# item, not '#'} other {# items}}" js_expected = "{_gt_1} {n,plural,offset:1 one{# item, not '#'} other{# items}}" assert condense_vars(icu) == js_expected @@ -46,7 +40,7 @@ def test_condense_escaped_hash_in_non_condensed_plural(): # --------------------------------------------------------------------------- -def test_condense_apostrophe_at_literal_boundary(): +def test_condense_apostrophe_at_literal_boundary() -> None: icu = "{_gt_1, select, other {x}} it''s {name}''s" js_expected = "{_gt_1} it's {name}''s" assert condense_vars(icu) == js_expected @@ -59,15 +53,7 @@ def test_condense_apostrophe_at_literal_boundary(): # --------------------------------------------------------------------------- -def test_parser_escape_angle_brackets(): - icu = ( - "{_gt_1, select, other {val}} " - "{mode, select, " - 'json {\'{"key": "val"}\'} ' - "xml {''} " - "other {plain}}" - ) - js_expected = ( - '{_gt_1} {mode,select,json{\'{"key": "val"}\'} xml{} other{plain}}' - ) +def test_parser_escape_angle_brackets() -> None: + icu = "{_gt_1, select, other {val}} {mode, select, json {'{\"key\": \"val\"}'} xml {''} other {plain}}" + js_expected = '{_gt_1} {mode,select,json{\'{"key": "val"}\'} xml{} other{plain}}' assert condense_vars(icu) == js_expected diff --git a/packages/generaltranslation/tests/static/test_sanitize_var.py b/packages/generaltranslation/tests/static/test_sanitize_var.py index 5e150df..250f57a 100644 --- a/packages/generaltranslation/tests/static/test_sanitize_var.py +++ b/packages/generaltranslation/tests/static/test_sanitize_var.py @@ -1,12 +1,11 @@ import json from pathlib import Path +from typing import Any import pytest from generaltranslation.static import sanitize_var -FIXTURES = json.loads( - (Path(__file__).parent / "fixtures" / "static_fixtures.json").read_text() -) +FIXTURES = json.loads((Path(__file__).parent / "fixtures" / "static_fixtures.json").read_text()) @pytest.mark.parametrize( @@ -14,6 +13,6 @@ FIXTURES["sanitize_var"], ids=[c["label"] for c in FIXTURES["sanitize_var"]], ) -def test_sanitize_var(case): +def test_sanitize_var(case: dict[str, Any]) -> None: result = sanitize_var(case["input"]) assert result == case["expected"] diff --git a/packages/generaltranslation/tests/test_gt.py b/packages/generaltranslation/tests/test_gt.py index 34199de..13afc46 100644 --- a/packages/generaltranslation/tests/test_gt.py +++ b/packages/generaltranslation/tests/test_gt.py @@ -6,7 +6,7 @@ class TestGTConstructor: - def test_defaults(self): + def test_defaults(self) -> None: gt = GT() assert gt.source_locale is None assert gt.target_locale is None @@ -16,7 +16,7 @@ def test_defaults(self): assert gt.locales is None assert LIBRARY_DEFAULT_LOCALE in gt._rendering_locales - def test_explicit_params(self): + def test_explicit_params(self) -> None: gt = GT( api_key="key-123", dev_api_key="dev-456", @@ -32,7 +32,7 @@ def test_explicit_params(self): assert gt.source_locale == "en-US" assert gt.target_locale == "es-ES" - def test_env_vars(self, monkeypatch): + def test_env_vars(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("GT_API_KEY", "env-key") monkeypatch.setenv("GT_DEV_API_KEY", "env-dev-key") monkeypatch.setenv("GT_PROJECT_ID", "env-proj") @@ -41,36 +41,36 @@ def test_env_vars(self, monkeypatch): assert gt.dev_api_key == "env-dev-key" assert gt.project_id == "env-proj" - def test_explicit_overrides_env(self, monkeypatch): + def test_explicit_overrides_env(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("GT_API_KEY", "env-key") gt = GT(api_key="explicit-key") assert gt.api_key == "explicit-key" - def test_invalid_source_locale_raises(self): + def test_invalid_source_locale_raises(self) -> None: with pytest.raises(ValueError, match="Invalid locale"): GT(source_locale="not-a-locale-xxxx") - def test_invalid_target_locale_raises(self): + def test_invalid_target_locale_raises(self) -> None: with pytest.raises(ValueError, match="Invalid locale"): GT(target_locale="not-a-locale-xxxx") - def test_invalid_locales_raises(self): + def test_invalid_locales_raises(self) -> None: with pytest.raises(ValueError, match="Invalid locales"): GT(locales=["en", "not-valid-xxxx"]) - def test_valid_locales(self): + def test_valid_locales(self) -> None: gt = GT(locales=["en", "es", "fr"]) assert gt.locales is not None assert len(gt.locales) == 3 - def test_rendering_locales(self): + def test_rendering_locales(self) -> None: gt = GT(source_locale="en-US", target_locale="fr-FR") assert "en-US" in gt._rendering_locales assert "fr-FR" in gt._rendering_locales assert LIBRARY_DEFAULT_LOCALE in gt._rendering_locales - def test_custom_mapping(self): - mapping = {"my-locale": {"code": "en-US", "name": "My English"}} + def test_custom_mapping(self) -> None: + mapping: dict[str, str | dict[str, str]] = {"my-locale": {"code": "en-US", "name": "My English"}} gt = GT(custom_mapping=mapping) assert gt.custom_mapping is not None assert gt.reverse_custom_mapping is not None @@ -78,56 +78,56 @@ def test_custom_mapping(self): class TestSetConfig: - def test_update_api_key(self): + def test_update_api_key(self) -> None: gt = GT() gt.set_config(api_key="new-key") assert gt.api_key == "new-key" - def test_update_source_locale(self): + def test_update_source_locale(self) -> None: gt = GT() gt.set_config(source_locale="fr") assert gt.source_locale == "fr" - def test_update_target_locale(self): + def test_update_target_locale(self) -> None: gt = GT() gt.set_config(target_locale="de") assert gt.target_locale == "de" class TestValidateAuth: - def test_no_auth_raises(self): + def test_no_auth_raises(self) -> None: gt = GT() with pytest.raises(ValueError, match="API key"): gt._validate_auth("test_fn") - def test_no_project_id_raises(self): + def test_no_project_id_raises(self) -> None: gt = GT(api_key="key-123") with pytest.raises(ValueError, match="project ID"): gt._validate_auth("test_fn") - def test_both_set_passes(self): + def test_both_set_passes(self) -> None: gt = GT(api_key="key-123", project_id="proj-456") gt._validate_auth("test_fn") # should not raise - def test_dev_api_key_counts(self): + def test_dev_api_key_counts(self) -> None: gt = GT(dev_api_key="dev-key", project_id="proj-456") gt._validate_auth("test_fn") # should not raise class TestTranslationConfig: - def test_config_with_api_key(self): + def test_config_with_api_key(self) -> None: gt = GT(api_key="key", project_id="proj", base_url="https://example.com") config = gt._get_translation_config() assert config["api_key"] == "key" assert config["project_id"] == "proj" assert config["base_url"] == "https://example.com" - def test_config_with_dev_key(self): + def test_config_with_dev_key(self) -> None: gt = GT(dev_api_key="dev-key", project_id="proj") config = gt._get_translation_config() assert config["api_key"] == "dev-key" - def test_config_prefers_api_key(self): + def test_config_prefers_api_key(self) -> None: gt = GT(api_key="main", dev_api_key="dev", project_id="proj") config = gt._get_translation_config() assert config["api_key"] == "main" @@ -136,78 +136,78 @@ def test_config_prefers_api_key(self): class TestLocaleMethodsDelegation: """Test that locale methods properly delegate to standalone functions.""" - def test_get_locale_name(self): + def test_get_locale_name(self) -> None: gt = GT(source_locale="en", target_locale="es") name = gt.get_locale_name("fr") assert isinstance(name, str) assert len(name) > 0 - def test_get_locale_name_default(self): + def test_get_locale_name_default(self) -> None: gt = GT(target_locale="fr") name = gt.get_locale_name() assert isinstance(name, str) - def test_get_locale_name_no_locale_raises(self): + def test_get_locale_name_no_locale_raises(self) -> None: gt = GT() with pytest.raises(ValueError): gt.get_locale_name() - def test_get_locale_emoji(self): + def test_get_locale_emoji(self) -> None: gt = GT(target_locale="en-US") emoji = gt.get_locale_emoji() assert isinstance(emoji, str) - def test_get_locale_direction(self): + def test_get_locale_direction(self) -> None: gt = GT(target_locale="ar") assert gt.get_locale_direction() == "rtl" gt2 = GT(target_locale="en") assert gt2.get_locale_direction() == "ltr" - def test_is_valid_locale(self): + def test_is_valid_locale(self) -> None: gt = GT(target_locale="en") assert gt.is_valid_locale() is True - def test_requires_translation(self): + def test_requires_translation(self) -> None: gt = GT(source_locale="en", target_locale="es") assert gt.requires_translation() is True - def test_requires_translation_same(self): + def test_requires_translation_same(self) -> None: gt = GT(source_locale="en", target_locale="en") assert gt.requires_translation() is False - def test_determine_locale(self): + def test_determine_locale(self) -> None: gt = GT(locales=["en", "fr", "es"]) result = gt.determine_locale("fr-FR") assert result == "fr" - def test_is_same_language(self): + def test_is_same_language(self) -> None: gt = GT() assert gt.is_same_language("en-US", "en-GB") is True - def test_is_same_dialect(self): + def test_is_same_dialect(self) -> None: gt = GT() assert gt.is_same_dialect("en-US", "en-GB") is False - def test_is_superset_locale(self): + def test_is_superset_locale(self) -> None: gt = GT() assert gt.is_superset_locale("en", "en-US") is True - def test_standardize_locale(self): + def test_standardize_locale(self) -> None: gt = GT(target_locale="en") result = gt.standardize_locale("EN-US") assert result == "en-US" - def test_resolve_canonical_locale(self): + def test_resolve_canonical_locale(self) -> None: gt = GT(target_locale="en") result = gt.resolve_canonical_locale("en") assert result == "en" - def test_resolve_alias_locale(self): + def test_resolve_alias_locale(self) -> None: gt = GT() result = gt.resolve_alias_locale("en") assert result == "en" - def test_get_locale_properties(self): + def test_get_locale_properties(self) -> None: gt = GT(target_locale="en-US") props = gt.get_locale_properties() assert props is not None @@ -216,18 +216,18 @@ def test_get_locale_properties(self): class TestFormattingDelegation: """Test formatting methods delegate correctly.""" - def test_format_num(self): + def test_format_num(self) -> None: gt = GT(source_locale="en-US") result = gt.format_num(1234.5) assert isinstance(result, str) assert "1" in result - def test_format_message(self): + def test_format_message(self) -> None: gt = GT(source_locale="en") result = gt.format_message("Hello {name}", variables={"name": "World"}) assert result == "Hello World" - def test_format_cutoff(self): + def test_format_cutoff(self) -> None: gt = GT(source_locale="en") result = gt.format_cutoff("Hello, world!", options={"max_chars": 5}) assert isinstance(result, str) @@ -237,55 +237,55 @@ class TestAPIMethodsAuth: """Test that API methods enforce auth.""" @pytest.mark.asyncio - async def test_translate_requires_auth(self): + async def test_translate_requires_auth(self) -> None: gt = GT() with pytest.raises(ValueError, match="API key"): await gt.translate("Hello", "es") @pytest.mark.asyncio - async def test_translate_requires_target(self): + async def test_translate_requires_target(self) -> None: gt = GT(api_key="key", project_id="proj") with pytest.raises(ValueError, match="locale"): await gt.translate("Hello", {}) @pytest.mark.asyncio - async def test_query_branch_data_requires_auth(self): + async def test_query_branch_data_requires_auth(self) -> None: gt = GT() with pytest.raises(ValueError): await gt.query_branch_data({"branchNames": ["main"]}) @pytest.mark.asyncio - async def test_setup_project_requires_auth(self): + async def test_setup_project_requires_auth(self) -> None: gt = GT() with pytest.raises(ValueError): await gt.setup_project([]) @pytest.mark.asyncio - async def test_check_job_status_requires_auth(self): + async def test_check_job_status_requires_auth(self) -> None: gt = GT() with pytest.raises(ValueError): await gt.check_job_status(["job-1"]) @pytest.mark.asyncio - async def test_enqueue_files_requires_auth(self): + async def test_enqueue_files_requires_auth(self) -> None: gt = GT() with pytest.raises(ValueError): await gt.enqueue_files([], {"target_locales": ["es"]}) @pytest.mark.asyncio - async def test_upload_source_files_requires_auth(self): + async def test_upload_source_files_requires_auth(self) -> None: gt = GT() with pytest.raises(ValueError): await gt.upload_source_files([], {"source_locale": "en"}) @pytest.mark.asyncio - async def test_download_file_requires_auth(self): + async def test_download_file_requires_auth(self) -> None: gt = GT() with pytest.raises(ValueError): await gt.download_file({"file_id": "f1"}) @pytest.mark.asyncio - async def test_get_project_data_requires_auth(self): + async def test_get_project_data_requires_auth(self) -> None: gt = GT() with pytest.raises(ValueError): await gt.get_project_data("proj-1") diff --git a/packages/generaltranslation/tests/translate/test_batch.py b/packages/generaltranslation/tests/translate/test_batch.py index 44fcbe0..4553f5d 100644 --- a/packages/generaltranslation/tests/translate/test_batch.py +++ b/packages/generaltranslation/tests/translate/test_batch.py @@ -2,31 +2,31 @@ from generaltranslation.translate._batch import create_batches, process_batches -def test_create_batches_basic(): +def test_create_batches_basic() -> None: items = list(range(5)) result = create_batches(items, 2) assert result == [[0, 1], [2, 3], [4]] -def test_create_batches_empty(): +def test_create_batches_empty() -> None: assert create_batches([], 10) == [] -def test_create_batches_exact(): +def test_create_batches_exact() -> None: items = list(range(4)) result = create_batches(items, 2) assert result == [[0, 1], [2, 3]] -def test_create_batches_single(): +def test_create_batches_single() -> None: items = [1, 2, 3] result = create_batches(items, 100) assert result == [[1, 2, 3]] @pytest.mark.asyncio -async def test_process_batches_empty(): - async def processor(batch): +async def test_process_batches_empty() -> None: + async def processor(batch: list[int]) -> list[int]: return batch result = await process_batches([], processor) @@ -34,8 +34,8 @@ async def processor(batch): @pytest.mark.asyncio -async def test_process_batches_basic(): - async def processor(batch): +async def test_process_batches_basic() -> None: + async def processor(batch: list[int]) -> list[int]: return [x * 2 for x in batch] result = await process_batches([1, 2, 3, 4, 5], processor, batch_size=2) @@ -45,15 +45,13 @@ async def processor(batch): @pytest.mark.asyncio -async def test_process_batches_sequential(): +async def test_process_batches_sequential() -> None: order = [] - async def processor(batch): + async def processor(batch: list[int]) -> list[int]: order.extend(batch) return batch - result = await process_batches( - [1, 2, 3, 4], processor, batch_size=2, parallel=False - ) + result = await process_batches([1, 2, 3, 4], processor, batch_size=2, parallel=False) assert order == [1, 2, 3, 4] assert result["count"] == 4 diff --git a/packages/generaltranslation/tests/translate/test_endpoints.py b/packages/generaltranslation/tests/translate/test_endpoints.py index 4c7bb40..f3baf07 100644 --- a/packages/generaltranslation/tests/translate/test_endpoints.py +++ b/packages/generaltranslation/tests/translate/test_endpoints.py @@ -4,7 +4,7 @@ @pytest.mark.asyncio -async def test_check_job_status(): +async def test_check_job_status() -> None: with patch( "generaltranslation.translate._check_job_status.api_request", new_callable=AsyncMock, @@ -20,7 +20,7 @@ async def test_check_job_status(): @pytest.mark.asyncio -async def test_setup_project(): +async def test_setup_project() -> None: with patch( "generaltranslation.translate._setup_project.api_request", new_callable=AsyncMock, @@ -36,7 +36,7 @@ async def test_setup_project(): @pytest.mark.asyncio -async def test_query_branch_data(): +async def test_query_branch_data() -> None: with patch( "generaltranslation.translate._query_branch_data.api_request", new_callable=AsyncMock, @@ -49,7 +49,7 @@ async def test_query_branch_data(): @pytest.mark.asyncio -async def test_create_branch(): +async def test_create_branch() -> None: with patch( "generaltranslation.translate._create_branch.api_request", new_callable=AsyncMock, @@ -57,14 +57,12 @@ async def test_create_branch(): mock.return_value = {"branch": {"id": "b1", "name": "feature"}} from generaltranslation.translate._create_branch import create_branch - result = await create_branch( - {"branchName": "feature", "defaultBranch": False}, {"project_id": "p"} - ) + result = await create_branch({"branchName": "feature", "defaultBranch": False}, {"project_id": "p"}) assert result["branch"]["name"] == "feature" @pytest.mark.asyncio -async def test_query_source_file(): +async def test_query_source_file() -> None: with patch( "generaltranslation.translate._query_source_file.api_request", new_callable=AsyncMock, @@ -83,7 +81,7 @@ async def test_query_source_file(): @pytest.mark.asyncio -async def test_get_project_data(): +async def test_get_project_data() -> None: with patch( "generaltranslation.translate._get_project_data.api_request", new_callable=AsyncMock, @@ -98,7 +96,7 @@ async def test_get_project_data(): @pytest.mark.asyncio -async def test_submit_user_edit_diffs(): +async def test_submit_user_edit_diffs() -> None: with patch( "generaltranslation.translate._submit_user_edit_diffs.api_request", new_callable=AsyncMock, @@ -108,14 +106,12 @@ async def test_submit_user_edit_diffs(): submit_user_edit_diffs, ) - result = await submit_user_edit_diffs( - {"diffs": [{"locale": "es"}]}, {"project_id": "p"} - ) + result = await submit_user_edit_diffs({"diffs": [{"locale": "es"}]}, {"project_id": "p"}) assert result["success"] is True @pytest.mark.asyncio -async def test_process_file_moves_empty(): +async def test_process_file_moves_empty() -> None: from generaltranslation.translate._process_file_moves import process_file_moves result = await process_file_moves([], {}, {"project_id": "p"}) @@ -124,7 +120,7 @@ async def test_process_file_moves_empty(): @pytest.mark.asyncio -async def test_get_orphaned_files_empty(): +async def test_get_orphaned_files_empty() -> None: with patch( "generaltranslation.translate._get_orphaned_files.api_request", new_callable=AsyncMock, @@ -140,7 +136,7 @@ async def test_get_orphaned_files_empty(): @pytest.mark.asyncio -async def test_enqueue_files_omits_none_values(): +async def test_enqueue_files_omits_none_values() -> None: """Body dict passed to api_request must not contain keys whose value is None.""" with patch( "generaltranslation.translate._enqueue_files.api_request", @@ -166,16 +162,14 @@ async def test_enqueue_files_omits_none_values(): body = mock.call_args[1]["body"] # None-valued keys must be absent for key in ("publish", "requireApproval", "modelProvider", "force"): - assert key not in body, ( - f"key '{key}' should not be in body when value is None" - ) + assert key not in body, f"key '{key}' should not be in body when value is None" # --- Fix 4: URL encoding should match JS encodeURIComponent --- @pytest.mark.asyncio -async def test_query_source_file_preserves_special_chars(): +async def test_query_source_file_preserves_special_chars() -> None: """Characters ! ' ( ) * should NOT be percent-encoded (matching encodeURIComponent).""" with patch( "generaltranslation.translate._query_source_file.api_request", @@ -190,13 +184,11 @@ async def test_query_source_file_preserves_special_chars(): {"project_id": "p"}, ) endpoint = mock.call_args[0][1] - assert "file!'()*" in endpoint, ( - f"Special chars were percent-encoded in: {endpoint}" - ) + assert "file!'()*" in endpoint, f"Special chars were percent-encoded in: {endpoint}" @pytest.mark.asyncio -async def test_get_project_data_preserves_special_chars(): +async def test_get_project_data_preserves_special_chars() -> None: """Characters ! ' ( ) * should NOT be percent-encoded (matching encodeURIComponent).""" with patch( "generaltranslation.translate._get_project_data.api_request", @@ -207,6 +199,4 @@ async def test_get_project_data_preserves_special_chars(): await get_project_data("p!'()*", {}, {"project_id": "p"}) endpoint = mock.call_args[0][1] - assert "p!'()*" in endpoint, ( - f"Special chars were percent-encoded in: {endpoint}" - ) + assert "p!'()*" in endpoint, f"Special chars were percent-encoded in: {endpoint}" diff --git a/packages/generaltranslation/tests/translate/test_headers.py b/packages/generaltranslation/tests/translate/test_headers.py index b631420..9104d9d 100644 --- a/packages/generaltranslation/tests/translate/test_headers.py +++ b/packages/generaltranslation/tests/translate/test_headers.py @@ -2,7 +2,7 @@ from generaltranslation.translate._headers import generate_request_headers -def test_basic_headers(): +def test_basic_headers() -> None: config = {"project_id": "proj-123", "api_key": "my-key"} headers = generate_request_headers(config) assert headers["Content-Type"] == "application/json" @@ -11,13 +11,13 @@ def test_basic_headers(): assert headers["gt-api-version"] == API_VERSION -def test_exclude_content_type(): +def test_exclude_content_type() -> None: config = {"project_id": "proj-123"} headers = generate_request_headers(config, exclude_content_type=True) assert "Content-Type" not in headers -def test_internal_api_key(): +def test_internal_api_key() -> None: config = {"project_id": "proj-123", "api_key": "gtx-internal-abc"} headers = generate_request_headers(config) assert "x-gt-internal-api-key" in headers @@ -25,7 +25,7 @@ def test_internal_api_key(): assert "x-gt-api-key" not in headers -def test_no_api_key(): +def test_no_api_key() -> None: config = {"project_id": "proj-123"} headers = generate_request_headers(config) assert "x-gt-api-key" not in headers diff --git a/packages/generaltranslation/tests/translate/test_request.py b/packages/generaltranslation/tests/translate/test_request.py index ed60d3a..0f3338d 100644 --- a/packages/generaltranslation/tests/translate/test_request.py +++ b/packages/generaltranslation/tests/translate/test_request.py @@ -8,7 +8,7 @@ @pytest.fixture -def config(): +def config() -> dict[str, str]: return { "project_id": "proj-123", "api_key": "test-key", @@ -17,14 +17,12 @@ def config(): @pytest.mark.asyncio -async def test_successful_post(config): +async def test_successful_post(config: dict[str, str]) -> None: mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = {"result": "ok"} - with patch( - "generaltranslation.translate._request.httpx.AsyncClient" - ) as mock_client_cls: + with patch("generaltranslation.translate._request.httpx.AsyncClient") as mock_client_cls: mock_client = AsyncMock() mock_client.post.return_value = mock_response mock_client.__aenter__ = AsyncMock(return_value=mock_client) @@ -36,14 +34,12 @@ async def test_successful_post(config): @pytest.mark.asyncio -async def test_successful_get(config): +async def test_successful_get(config: dict[str, str]) -> None: mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = {"data": [1, 2, 3]} - with patch( - "generaltranslation.translate._request.httpx.AsyncClient" - ) as mock_client_cls: + with patch("generaltranslation.translate._request.httpx.AsyncClient") as mock_client_cls: mock_client = AsyncMock() mock_client.get.return_value = mock_response mock_client.__aenter__ = AsyncMock(return_value=mock_client) @@ -55,15 +51,13 @@ async def test_successful_get(config): @pytest.mark.asyncio -async def test_4xx_raises_api_error(config): +async def test_4xx_raises_api_error(config: dict[str, str]) -> None: mock_response = MagicMock() mock_response.status_code = 401 mock_response.reason_phrase = "Unauthorized" mock_response.text = json.dumps({"error": "Invalid API key"}) - with patch( - "generaltranslation.translate._request.httpx.AsyncClient" - ) as mock_client_cls: + with patch("generaltranslation.translate._request.httpx.AsyncClient") as mock_client_cls: mock_client = AsyncMock() mock_client.post.return_value = mock_response mock_client.__aenter__ = AsyncMock(return_value=mock_client) @@ -76,10 +70,8 @@ async def test_4xx_raises_api_error(config): @pytest.mark.asyncio -async def test_timeout_raises_error(config): - with patch( - "generaltranslation.translate._request.httpx.AsyncClient" - ) as mock_client_cls: +async def test_timeout_raises_error(config: dict[str, str]) -> None: + with patch("generaltranslation.translate._request.httpx.AsyncClient") as mock_client_cls: mock_client = AsyncMock() mock_client.post.side_effect = httpx.TimeoutException("timed out") mock_client.__aenter__ = AsyncMock(return_value=mock_client) @@ -91,19 +83,17 @@ async def test_timeout_raises_error(config): @pytest.mark.asyncio -async def test_no_retry_on_none_policy(config): +async def test_no_retry_on_none_policy(config: dict[str, str]) -> None: call_count = 0 mock_response = MagicMock() mock_response.status_code = 500 mock_response.reason_phrase = "Internal Server Error" mock_response.text = "Server error" - with patch( - "generaltranslation.translate._request.httpx.AsyncClient" - ) as mock_client_cls: + with patch("generaltranslation.translate._request.httpx.AsyncClient") as mock_client_cls: mock_client = AsyncMock() - async def counting_post(*args, **kwargs): + async def counting_post(*args: object, **kwargs: object) -> object: nonlocal call_count call_count += 1 return mock_response @@ -119,7 +109,7 @@ async def counting_post(*args, **kwargs): @pytest.mark.asyncio -async def test_client_reused_across_retries(config): +async def test_client_reused_across_retries(config: dict[str, str]) -> None: """AsyncClient should be instantiated once even when retries occur.""" # First call returns 500 (triggers retry), second returns 200 mock_response_500 = MagicMock() @@ -131,9 +121,7 @@ async def test_client_reused_across_retries(config): mock_response_200.status_code = 200 mock_response_200.json.return_value = {"ok": True} - with patch( - "generaltranslation.translate._request.httpx.AsyncClient" - ) as mock_client_cls: + with patch("generaltranslation.translate._request.httpx.AsyncClient") as mock_client_cls: mock_client = AsyncMock() mock_client.post = AsyncMock(side_effect=[mock_response_500, mock_response_200]) mock_client.__aenter__ = AsyncMock(return_value=mock_client) diff --git a/packages/gt-fastapi/tests/test_fastapi_integration.py b/packages/gt-fastapi/tests/test_fastapi_integration.py index 1e8c4f9..c44a1cb 100644 --- a/packages/gt-fastapi/tests/test_fastapi_integration.py +++ b/packages/gt-fastapi/tests/test_fastapi_integration.py @@ -1,5 +1,8 @@ """FastAPI integration tests.""" +from collections.abc import Generator +from typing import Any + import pytest from fastapi import FastAPI from fastapi.testclient import TestClient @@ -8,7 +11,7 @@ @pytest.fixture(autouse=True) -def _reset_singleton(): +def _reset_singleton() -> Generator[None, None, None]: import gt_i18n.i18n_manager._singleton as mod old = mod._manager @@ -16,7 +19,7 @@ def _reset_singleton(): mod._manager = old -def test_fastapi_t_source_locale(): +def test_fastapi_t_source_locale() -> None: app = FastAPI() initialize_gt( app, @@ -26,7 +29,7 @@ def test_fastapi_t_source_locale(): ) @app.get("/hello") - def hello(): + def hello() -> dict[str, Any]: return {"message": t("Hello, world!")} with TestClient(app) as client: @@ -34,10 +37,10 @@ def hello(): assert resp.json()["message"] == "Hello, world!" -def test_fastapi_t_with_accept_language(): +def test_fastapi_t_with_accept_language() -> None: h = hash_message("Hello, world!") - def loader(locale): + def loader(locale: str) -> dict[str, str]: if locale == "es": return {h: "Hola, mundo!"} return {} @@ -51,7 +54,7 @@ def loader(locale): ) @app.get("/hello") - def hello(): + def hello() -> dict[str, Any]: return {"message": t("Hello, world!")} with TestClient(app) as client: @@ -62,7 +65,7 @@ def hello(): assert resp.json()["message"] == "Hello, world!" -def test_fastapi_custom_get_locale(): +def test_fastapi_custom_get_locale() -> None: h = hash_message("Hello!") app = FastAPI() @@ -75,7 +78,7 @@ def test_fastapi_custom_get_locale(): ) @app.get("/hello") - def hello(): + def hello() -> dict[str, Any]: return {"message": t("Hello!")} with TestClient(app) as client: @@ -83,7 +86,7 @@ def hello(): assert resp.json()["message"] == "Bonjour!" -def test_fastapi_variable_interpolation(): +def test_fastapi_variable_interpolation() -> None: h = hash_message("Hello, {name}!") app = FastAPI() @@ -95,7 +98,7 @@ def test_fastapi_variable_interpolation(): ) @app.get("/hello") - def hello(): + def hello() -> dict[str, Any]: return {"message": t("Hello, {name}!", name="Carlos")} with TestClient(app) as client: diff --git a/packages/gt-flask/tests/test_flask_integration.py b/packages/gt-flask/tests/test_flask_integration.py index b75524a..5170e65 100644 --- a/packages/gt-flask/tests/test_flask_integration.py +++ b/packages/gt-flask/tests/test_flask_integration.py @@ -1,5 +1,8 @@ """Flask integration tests.""" +from collections.abc import Generator +from typing import Any + import pytest from flask import Flask from gt_flask import initialize_gt, t @@ -7,7 +10,7 @@ @pytest.fixture(autouse=True) -def _reset_singleton(): +def _reset_singleton() -> Generator[None, None, None]: import gt_i18n.i18n_manager._singleton as mod old = mod._manager @@ -15,7 +18,7 @@ def _reset_singleton(): mod._manager = old -def test_flask_t_source_locale(): +def test_flask_t_source_locale() -> None: app = Flask(__name__) initialize_gt( app, @@ -25,18 +28,19 @@ def test_flask_t_source_locale(): ) @app.route("/hello") - def hello(): + def hello() -> dict[str, Any]: return {"message": t("Hello, world!")} with app.test_client() as client: resp = client.get("/hello", headers={"Accept-Language": "en"}) + assert resp.json is not None assert resp.json["message"] == "Hello, world!" -def test_flask_t_with_accept_language(): +def test_flask_t_with_accept_language() -> None: h = hash_message("Hello, world!") - def loader(locale): + def loader(locale: str) -> dict[str, str]: if locale == "es": return {h: "Hola, mundo!"} return {} @@ -50,18 +54,20 @@ def loader(locale): ) @app.route("/hello") - def hello(): + def hello() -> dict[str, Any]: return {"message": t("Hello, world!")} with app.test_client() as client: resp = client.get("/hello", headers={"Accept-Language": "es"}) + assert resp.json is not None assert resp.json["message"] == "Hola, mundo!" resp = client.get("/hello", headers={"Accept-Language": "en"}) + assert resp.json is not None assert resp.json["message"] == "Hello, world!" -def test_flask_custom_get_locale(): +def test_flask_custom_get_locale() -> None: h = hash_message("Hello!") app = Flask(__name__) @@ -74,15 +80,16 @@ def test_flask_custom_get_locale(): ) @app.route("/hello") - def hello(): + def hello() -> dict[str, Any]: return {"message": t("Hello!")} with app.test_client() as client: resp = client.get("/hello") + assert resp.json is not None assert resp.json["message"] == "Bonjour!" -def test_flask_variable_interpolation(): +def test_flask_variable_interpolation() -> None: h = hash_message("Hello, {name}!") app = Flask(__name__) @@ -94,9 +101,10 @@ def test_flask_variable_interpolation(): ) @app.route("/hello") - def hello(): + def hello() -> dict[str, Any]: return {"message": t("Hello, {name}!", name="Carlos")} with app.test_client() as client: resp = client.get("/hello", headers={"Accept-Language": "es"}) + assert resp.json is not None assert resp.json["message"] == "Hola, Carlos!" diff --git a/packages/gt-i18n/src/gt_i18n/i18n_manager/_i18n_manager.py b/packages/gt-i18n/src/gt_i18n/i18n_manager/_i18n_manager.py index 3267c74..d9c6ced 100644 --- a/packages/gt-i18n/src/gt_i18n/i18n_manager/_i18n_manager.py +++ b/packages/gt-i18n/src/gt_i18n/i18n_manager/_i18n_manager.py @@ -60,9 +60,7 @@ def __init__( else: loader = lambda locale: {} # noqa: E731 - self._translations = TranslationsManager( - loader, cache_expiry_time=cache_expiry_time - ) + self._translations = TranslationsManager(loader, cache_expiry_time=cache_expiry_time) @property def default_locale(self) -> str: diff --git a/packages/gt-i18n/src/gt_i18n/translation_functions/_interpolate.py b/packages/gt-i18n/src/gt_i18n/translation_functions/_interpolate.py index 6205026..61eb7c9 100644 --- a/packages/gt-i18n/src/gt_i18n/translation_functions/_interpolate.py +++ b/packages/gt-i18n/src/gt_i18n/translation_functions/_interpolate.py @@ -5,6 +5,8 @@ from __future__ import annotations +from typing import cast + from generaltranslation.formatting._format_cutoff import format_cutoff from generaltranslation.formatting._format_message import format_message from generaltranslation.static._condense_vars import condense_vars @@ -71,7 +73,7 @@ def interpolate_message( # Apply cutoff formatting if max_chars is not None: - result = format_cutoff(result, locale, {"max_chars": int(max_chars)}) + result = format_cutoff(result, locale, {"max_chars": int(cast(int, max_chars))}) return result @@ -87,6 +89,6 @@ def interpolate_message( # No fallback — return the raw message with cutoff applied if max_chars is not None: - return format_cutoff(message, locale, {"max_chars": int(max_chars)}) + return format_cutoff(message, locale, {"max_chars": int(cast(int, max_chars))}) return message diff --git a/packages/gt-i18n/src/gt_i18n/translation_functions/_msg.py b/packages/gt-i18n/src/gt_i18n/translation_functions/_msg.py index da9e8aa..2247f94 100644 --- a/packages/gt-i18n/src/gt_i18n/translation_functions/_msg.py +++ b/packages/gt-i18n/src/gt_i18n/translation_functions/_msg.py @@ -25,9 +25,7 @@ def msg(message: str, **kwargs: object) -> str: # Interpolate the message try: - interpolated = format_message( - message, None, {**variables, VAR_IDENTIFIER: "other"} - ) + interpolated = format_message(message, None, {**variables, VAR_IDENTIFIER: "other"}) except Exception: return message @@ -40,8 +38,6 @@ def msg(message: str, **kwargs: object) -> str: ) encoded_options = {**kwargs, "$_source": message, "$_hash": h} - options_encoding = base64.b64encode( - json.dumps(encoded_options, separators=(",", ":")).encode() - ).decode() + options_encoding = base64.b64encode(json.dumps(encoded_options, separators=(",", ":")).encode()).decode() return f"{interpolated}:{options_encoding}" diff --git a/packages/gt-i18n/src/gt_i18n/translation_functions/_t.py b/packages/gt-i18n/src/gt_i18n/translation_functions/_t.py index 2f4f57a..f5f6bca 100644 --- a/packages/gt-i18n/src/gt_i18n/translation_functions/_t.py +++ b/packages/gt-i18n/src/gt_i18n/translation_functions/_t.py @@ -37,9 +37,7 @@ def t(message: str, **kwargs: object) -> str: ) translated = translations.get(h) if translated: - return interpolate_message( - translated, {**kwargs, "$_fallback": message}, locale - ) + return interpolate_message(translated, {**kwargs, "$_fallback": message}, locale) # No translation found — use source return interpolate_message(message, kwargs, locale) diff --git a/packages/gt-i18n/tests/test_extract_variables.py b/packages/gt-i18n/tests/test_extract_variables.py index 95ddb93..a556cff 100644 --- a/packages/gt-i18n/tests/test_extract_variables.py +++ b/packages/gt-i18n/tests/test_extract_variables.py @@ -3,22 +3,22 @@ from gt_i18n.translation_functions._extract_variables import extract_variables -def test_filters_dollar_keys(): - opts = {"name": "Alice", "$context": "greeting", "$id": "hello"} +def test_filters_dollar_keys() -> None: + opts: dict[str, object] = dict({"name": "Alice", "$context": "greeting", "$id": "hello"}) result = extract_variables(opts) assert result == {"name": "Alice"} -def test_keeps_all_non_dollar(): +def test_keeps_all_non_dollar() -> None: opts = {"name": "Alice", "count": 5, "item": "apples"} result = extract_variables(opts) assert result == opts -def test_empty_dict(): +def test_empty_dict() -> None: assert extract_variables({}) == {} -def test_all_dollar_keys(): +def test_all_dollar_keys() -> None: opts = {"$context": "x", "$id": "y", "$max_chars": 10} assert extract_variables(opts) == {} diff --git a/packages/gt-i18n/tests/test_fallbacks.py b/packages/gt-i18n/tests/test_fallbacks.py index 2790405..22ae72e 100644 --- a/packages/gt-i18n/tests/test_fallbacks.py +++ b/packages/gt-i18n/tests/test_fallbacks.py @@ -5,38 +5,36 @@ from gt_i18n import m_fallback, t_fallback -FIXTURES = json.loads( - (Path(__file__).parent / "fixtures" / "js_msg_parity.json").read_text() -) +FIXTURES = json.loads((Path(__file__).parent / "fixtures" / "js_msg_parity.json").read_text()) class TestMFallback: - def test_encoded_message(self): + def test_encoded_message(self) -> None: f = FIXTURES["mFallback"]["encoded"] assert m_fallback(f["input"]) == f["result"] - def test_plain_text(self): + def test_plain_text(self) -> None: f = FIXTURES["mFallback"]["plain"] assert m_fallback(f["input"]) == f["result"] - def test_no_opts_message(self): + def test_no_opts_message(self) -> None: f = FIXTURES["mFallback"]["no_opts"] assert m_fallback(f["input"]) == f["result"] - def test_null_input(self): + def test_null_input(self) -> None: f = FIXTURES["mFallback"]["null_input"] assert m_fallback(f["input"]) == f["result"] class TestTFallback: - def test_plain(self): + def test_plain(self) -> None: f = FIXTURES["gtFallback"]["plain"] assert t_fallback(f["input"][0]) == f["result"] - def test_with_vars(self): + def test_with_vars(self) -> None: f = FIXTURES["gtFallback"]["with_vars"] assert t_fallback(f["input"][0], **f["input"][1]) == f["result"] - def test_declare_var(self): + def test_declare_var(self) -> None: f = FIXTURES["gtFallback"]["declare_var"] assert t_fallback(f["input"][0]) == f["result"] diff --git a/packages/gt-i18n/tests/test_hash_message.py b/packages/gt-i18n/tests/test_hash_message.py index d64fc57..09159f1 100644 --- a/packages/gt-i18n/tests/test_hash_message.py +++ b/packages/gt-i18n/tests/test_hash_message.py @@ -6,6 +6,7 @@ import json from pathlib import Path +from typing import Any import pytest from generaltranslation.static._index_vars import index_vars @@ -15,7 +16,7 @@ @pytest.fixture(scope="module") -def fixtures(): +def fixtures() -> dict[str, Any]: return json.loads(FIXTURES_PATH.read_text()) @@ -26,38 +27,38 @@ class TestIndexVarsParity: """Verify index_vars produces identical output to JS indexVars.""" @pytest.fixture(autouse=True) - def _load(self, fixtures): + def _load(self, fixtures: dict[str, Any]) -> None: self.cases = fixtures["indexVars"] - def test_plain_no_vars(self): + def test_plain_no_vars(self) -> None: c = self.cases["plain_no_vars"] assert index_vars(c["input"]) == c["output"] - def test_simple_variable(self): + def test_simple_variable(self) -> None: c = self.cases["simple_variable"] assert index_vars(c["input"]) == c["output"] - def test_single_gt(self): + def test_single_gt(self) -> None: c = self.cases["single_gt"] assert index_vars(c["input"]) == c["output"] - def test_two_gt(self): + def test_two_gt(self) -> None: c = self.cases["two_gt"] assert index_vars(c["input"]) == c["output"] - def test_gt_with_var_name(self): + def test_gt_with_var_name(self) -> None: c = self.cases["gt_with_var_name"] assert index_vars(c["input"]) == c["output"] - def test_nested_in_plural(self): + def test_nested_in_plural(self) -> None: c = self.cases["nested_in_plural"] assert index_vars(c["input"]) == c["output"] - def test_empty_string(self): + def test_empty_string(self) -> None: c = self.cases["empty_string"] assert index_vars(c["input"]) == c["output"] - def test_no_placeholders(self): + def test_no_placeholders(self) -> None: c = self.cases["no_placeholders"] assert index_vars(c["input"]) == c["output"] @@ -81,135 +82,90 @@ class TestHashMessageParity: """Verify hash_message produces identical hashes to JS hashMessage.""" @pytest.fixture(autouse=True) - def _load(self, fixtures): + def _load(self, fixtures: dict[str, Any]) -> None: self.cases = fixtures["hashMessage"] - def test_plain(self): + def test_plain(self) -> None: c = self.cases["plain"] - assert ( - hash_message(c["message"], **_js_options_to_kwargs(c["options"])) - == c["hash"] - ) + assert hash_message(c["message"], **_js_options_to_kwargs(c["options"])) == c["hash"] - def test_with_variable(self): + def test_with_variable(self) -> None: c = self.cases["with_variable"] - assert ( - hash_message(c["message"], **_js_options_to_kwargs(c["options"])) - == c["hash"] - ) + assert hash_message(c["message"], **_js_options_to_kwargs(c["options"])) == c["hash"] - def test_with_context_button(self): + def test_with_context_button(self) -> None: c = self.cases["with_context_button"] - assert ( - hash_message(c["message"], **_js_options_to_kwargs(c["options"])) - == c["hash"] - ) + assert hash_message(c["message"], **_js_options_to_kwargs(c["options"])) == c["hash"] - def test_with_context_menu(self): + def test_with_context_menu(self) -> None: c = self.cases["with_context_menu"] - assert ( - hash_message(c["message"], **_js_options_to_kwargs(c["options"])) - == c["hash"] - ) + assert hash_message(c["message"], **_js_options_to_kwargs(c["options"])) == c["hash"] - def test_no_context(self): + def test_no_context(self) -> None: c = self.cases["no_context"] - assert ( - hash_message(c["message"], **_js_options_to_kwargs(c["options"])) - == c["hash"] - ) + assert hash_message(c["message"], **_js_options_to_kwargs(c["options"])) == c["hash"] - def test_with_id(self): + def test_with_id(self) -> None: c = self.cases["with_id"] - assert ( - hash_message(c["message"], **_js_options_to_kwargs(c["options"])) - == c["hash"] - ) + assert hash_message(c["message"], **_js_options_to_kwargs(c["options"])) == c["hash"] - def test_with_max_chars(self): + def test_with_max_chars(self) -> None: c = self.cases["with_max_chars"] - assert ( - hash_message(c["message"], **_js_options_to_kwargs(c["options"])) - == c["hash"] - ) + assert hash_message(c["message"], **_js_options_to_kwargs(c["options"])) == c["hash"] - def test_negative_max_chars_same_as_positive(self): + def test_negative_max_chars_same_as_positive(self) -> None: """maxChars(-10) should produce the same hash as maxChars(10).""" c = self.cases["with_negative_max_chars"] - assert ( - hash_message(c["message"], **_js_options_to_kwargs(c["options"])) - == c["hash"] - ) + assert hash_message(c["message"], **_js_options_to_kwargs(c["options"])) == c["hash"] # Also verify it equals the positive case pos = self.cases["with_max_chars"] assert c["hash"] == pos["hash"] - def test_empty(self): + def test_empty(self) -> None: c = self.cases["empty"] - assert ( - hash_message(c["message"], **_js_options_to_kwargs(c["options"])) - == c["hash"] - ) + assert hash_message(c["message"], **_js_options_to_kwargs(c["options"])) == c["hash"] - def test_plural(self): + def test_plural(self) -> None: c = self.cases["plural"] - assert ( - hash_message(c["message"], **_js_options_to_kwargs(c["options"])) - == c["hash"] - ) + assert hash_message(c["message"], **_js_options_to_kwargs(c["options"])) == c["hash"] - def test_plural_sentence(self): + def test_plural_sentence(self) -> None: c = self.cases["plural_sentence"] - assert ( - hash_message(c["message"], **_js_options_to_kwargs(c["options"])) - == c["hash"] - ) + assert hash_message(c["message"], **_js_options_to_kwargs(c["options"])) == c["hash"] - def test_with_gt_var(self): + def test_with_gt_var(self) -> None: c = self.cases["with_gt_var"] - assert ( - hash_message(c["message"], **_js_options_to_kwargs(c["options"])) - == c["hash"] - ) + assert hash_message(c["message"], **_js_options_to_kwargs(c["options"])) == c["hash"] - def test_with_gt_var_name(self): + def test_with_gt_var_name(self) -> None: c = self.cases["with_gt_var_name"] - assert ( - hash_message(c["message"], **_js_options_to_kwargs(c["options"])) - == c["hash"] - ) + assert hash_message(c["message"], **_js_options_to_kwargs(c["options"])) == c["hash"] - def test_context_and_id(self): + def test_context_and_id(self) -> None: c = self.cases["context_and_id"] - assert ( - hash_message(c["message"], **_js_options_to_kwargs(c["options"])) - == c["hash"] - ) + assert hash_message(c["message"], **_js_options_to_kwargs(c["options"])) == c["hash"] - def test_context_and_max_chars(self): + def test_context_and_max_chars(self) -> None: c = self.cases["context_and_max_chars"] - assert ( - hash_message(c["message"], **_js_options_to_kwargs(c["options"])) - == c["hash"] - ) + assert hash_message(c["message"], **_js_options_to_kwargs(c["options"])) == c["hash"] # ---- Behavioral tests ---- -def test_same_message_same_hash(): +def test_same_message_same_hash() -> None: h1 = hash_message("Hello, world!") h2 = hash_message("Hello, world!") assert h1 == h2 -def test_different_messages_different_hash(): +def test_different_messages_different_hash() -> None: h1 = hash_message("Hello") h2 = hash_message("Goodbye") assert h1 != h2 -def test_context_changes_hash(): +def test_context_changes_hash() -> None: h1 = hash_message("Save", context="button") h2 = hash_message("Save", context="menu") h3 = hash_message("Save") @@ -217,7 +173,7 @@ def test_context_changes_hash(): assert h1 != h3 -def test_with_declare_var(): +def test_with_declare_var() -> None: from generaltranslation.static._declare_var import declare_var msg = f"Hello, {declare_var('Alice', name='user')}!" diff --git a/packages/gt-i18n/tests/test_i18n_manager.py b/packages/gt-i18n/tests/test_i18n_manager.py index 5260b80..ccb7bba 100644 --- a/packages/gt-i18n/tests/test_i18n_manager.py +++ b/packages/gt-i18n/tests/test_i18n_manager.py @@ -5,23 +5,23 @@ from gt_i18n import I18nManager -def test_default_locale(): +def test_default_locale() -> None: mgr = I18nManager(default_locale="en") assert mgr.default_locale == "en" -def test_get_set_locale(): +def test_get_set_locale() -> None: mgr = I18nManager(default_locale="en") mgr.set_locale("fr") assert mgr.get_locale() == "fr" -def test_get_locale_default(): +def test_get_locale_default() -> None: import threading - result = [None] + result: list[str | None] = [None] - def check(): + def check() -> None: # Run in a new thread to get a clean ContextVar mgr = I18nManager(default_locale="en") result[0] = mgr.get_locale() @@ -32,43 +32,39 @@ def check(): assert result[0] == "en" -def test_requires_translation_same_locale(): +def test_requires_translation_same_locale() -> None: mgr = I18nManager(default_locale="en", locales=["en", "es"]) assert not mgr.requires_translation("en") -def test_requires_translation_different_locale(): +def test_requires_translation_different_locale() -> None: mgr = I18nManager(default_locale="en", locales=["en", "es"]) assert mgr.requires_translation("es") -def test_get_locales(): +def test_get_locales() -> None: mgr = I18nManager(default_locale="en", locales=["en", "es", "fr"]) assert mgr.get_locales() == ["en", "es", "fr"] -def test_custom_loader(): +def test_custom_loader() -> None: translations = {"hash1": "Translated!"} - def loader(locale): + def loader(locale: str) -> dict[str, str]: return translations if locale == "es" else {} - mgr = I18nManager( - default_locale="en", locales=["en", "es"], load_translations=loader - ) + mgr = I18nManager(default_locale="en", locales=["en", "es"], load_translations=loader) asyncio.run(mgr.get_translations("es")) assert mgr.get_translations_sync("es") == translations -def test_load_all(): +def test_load_all() -> None: loaded = set() - def loader(locale): + def loader(locale: str) -> dict[str, str]: loaded.add(locale) return {} - mgr = I18nManager( - default_locale="en", locales=["es", "fr"], load_translations=loader - ) + mgr = I18nManager(default_locale="en", locales=["es", "fr"], load_translations=loader) asyncio.run(mgr.load_all_translations()) assert loaded == {"es", "fr"} diff --git a/packages/gt-i18n/tests/test_interpolate.py b/packages/gt-i18n/tests/test_interpolate.py index cd81302..3005bee 100644 --- a/packages/gt-i18n/tests/test_interpolate.py +++ b/packages/gt-i18n/tests/test_interpolate.py @@ -12,24 +12,22 @@ # --- Basic interpolation --- -def test_simple_variable(): +def test_simple_variable() -> None: result = interpolate_message("Hello, {name}!", {"name": "Alice"}) assert result == "Hello, Alice!" -def test_multiple_variables(): - result = interpolate_message( - "{greeting}, {name}!", {"greeting": "Hi", "name": "Bob"} - ) +def test_multiple_variables() -> None: + result = interpolate_message("{greeting}, {name}!", {"greeting": "Hi", "name": "Bob"}) assert result == "Hi, Bob!" -def test_no_variables(): +def test_no_variables() -> None: result = interpolate_message("Hello, world!", {}) assert result == "Hello, world!" -def test_max_chars_cutoff(): +def test_max_chars_cutoff() -> None: result = interpolate_message( "This is a very long message that should be cut off", {"$max_chars": 10}, @@ -40,14 +38,14 @@ def test_max_chars_cutoff(): # --- declare_var (source message, no translation) --- -def test_declare_var_source_only(): +def test_declare_var_source_only() -> None: """declare_var values should be interpolated in the source message.""" msg = f"Hello, {declare_var('Alice', name='user')}!" result = interpolate_message(msg, {}) assert result == "Hello, Alice!" -def test_multiple_declare_vars(): +def test_multiple_declare_vars() -> None: """Multiple declare_var values should all be interpolated.""" msg = ( f"{declare_var('Bob', name='user')} bought " @@ -58,7 +56,7 @@ def test_multiple_declare_vars(): assert result == "Bob bought 5 of apples" -def test_declare_var_with_user_variables(): +def test_declare_var_with_user_variables() -> None: """declare_var + regular ICU variables should both work.""" msg = f"Hello, {declare_var('Alice', name='user')}! You have {{count}} items." result = interpolate_message(msg, {"count": "3"}) @@ -66,7 +64,7 @@ def test_declare_var_with_user_variables(): assert "3" in result -def test_empty_declare_var(): +def test_empty_declare_var() -> None: """Empty declare_var value should produce empty string.""" msg = f"Hello, {declare_var('', name='user')}!" result = interpolate_message(msg, {}) @@ -76,7 +74,7 @@ def test_empty_declare_var(): # --- declare_var (translated message with $_fallback) --- -def test_translated_with_declare_var(): +def test_translated_with_declare_var() -> None: """Translated strings reference _gt_ vars by index. JS behavior: extractVars is called on source/$_fallback, not on @@ -89,7 +87,7 @@ def test_translated_with_declare_var(): assert result == "Bienvenido de nuevo, Alice!" -def test_translated_multiple_declare_vars(): +def test_translated_multiple_declare_vars() -> None: """Multiple _gt_ vars extracted from source, used in translation.""" source = ( f"{declare_var('Bob', name='user')} bought " @@ -101,12 +99,9 @@ def test_translated_multiple_declare_vars(): assert result == "Bob compró 5 de oranges" -def test_translated_reordered_vars(): +def test_translated_reordered_vars() -> None: """Translation can reorder _gt_ variables.""" - source = ( - f"{declare_var('Alice', name='user')} bought " - f"{declare_var('apples', name='item')}" - ) + source = f"{declare_var('Alice', name='user')} bought {declare_var('apples', name='item')}" # Translation reorders: item before user translated = "{_gt_2} fueron comprados por {_gt_1}" result = interpolate_message(translated, {"$_fallback": source}) @@ -116,7 +111,7 @@ def test_translated_reordered_vars(): # --- Fallback behavior (JS parity) --- -def test_fallback_retry_on_format_error(): +def test_fallback_retry_on_format_error() -> None: """When formatting the translated string fails, retry with the source. JS behavior: catch block recursively calls interpolateMessage(source, @@ -130,7 +125,7 @@ def test_fallback_retry_on_format_error(): assert result == "Hello, world!" -def test_fallback_retry_preserves_user_vars(): +def test_fallback_retry_preserves_user_vars() -> None: """Fallback retry should preserve user variables. JS behavior: on retry, the same options (minus $_fallback) are passed, @@ -138,13 +133,11 @@ def test_fallback_retry_preserves_user_vars(): """ bad_translation = "{broken, plural, }" source = "Hello, {name}!" - result = interpolate_message( - bad_translation, {"$_fallback": source, "name": "Alice"} - ) + result = interpolate_message(bad_translation, {"$_fallback": source, "name": "Alice"}) assert result == "Hello, Alice!" -def test_fallback_retry_preserves_declare_vars(): +def test_fallback_retry_preserves_declare_vars() -> None: """Fallback retry should also handle declare_var in the source.""" bad_translation = "{broken, plural, }" source = f"Hello, {declare_var('Alice', name='user')}!" @@ -152,7 +145,7 @@ def test_fallback_retry_preserves_declare_vars(): assert result == "Hello, Alice!" -def test_no_fallback_returns_raw_with_cutoff(): +def test_no_fallback_returns_raw_with_cutoff() -> None: """When there's no fallback and formatting fails, return raw message with cutoff. JS behavior: final catch returns formatCutoff(encodedMsg, {maxChars}). @@ -162,7 +155,7 @@ def test_no_fallback_returns_raw_with_cutoff(): assert len(result) <= 5 -def test_no_fallback_returns_raw_message(): +def test_no_fallback_returns_raw_message() -> None: """When there's no fallback and formatting fails, return raw message.""" bad_msg = "{broken, plural, }" result = interpolate_message(bad_msg, {}) @@ -172,17 +165,15 @@ def test_no_fallback_returns_raw_message(): # --- Cutoff behavior --- -def test_cutoff_applied_after_interpolation(): +def test_cutoff_applied_after_interpolation() -> None: """$max_chars truncates after interpolation.""" result = interpolate_message("Hello, {name}!", {"name": "Alice", "$max_chars": 8}) assert len(result) <= 8 -def test_cutoff_on_fallback_retry(): +def test_cutoff_on_fallback_retry() -> None: """$max_chars should still apply when falling back to source.""" bad_translation = "{broken, plural, }" source = "This is a long fallback message" - result = interpolate_message( - bad_translation, {"$_fallback": source, "$max_chars": 10} - ) + result = interpolate_message(bad_translation, {"$_fallback": source, "$max_chars": 10}) assert len(result) <= 10 diff --git a/packages/gt-i18n/tests/test_msg.py b/packages/gt-i18n/tests/test_msg.py index 471d588..961c42e 100644 --- a/packages/gt-i18n/tests/test_msg.py +++ b/packages/gt-i18n/tests/test_msg.py @@ -5,27 +5,26 @@ from gt_i18n import decode_msg, decode_options, msg -FIXTURES = json.loads( - (Path(__file__).parent / "fixtures" / "js_msg_parity.json").read_text() -) +FIXTURES = json.loads((Path(__file__).parent / "fixtures" / "js_msg_parity.json").read_text()) class TestMsg: - def test_no_options(self): + def test_no_options(self) -> None: f = FIXTURES["msg"]["no_options"] assert msg(f["args"][0]) == f["result"] - def test_with_vars(self): + def test_with_vars(self) -> None: f = FIXTURES["msg"]["with_vars"] result = msg(f["args"][0], **f["args"][1]) assert decode_msg(result) == decode_msg(f["result"]) py_opts = decode_options(result) + assert py_opts is not None js_opts = FIXTURES["decodeOptions"]["encoded"]["result"] assert py_opts["$_hash"] == js_opts["$_hash"] assert py_opts["$_source"] == js_opts["$_source"] assert py_opts["name"] == js_opts["name"] - def test_with_context(self): + def test_with_context(self) -> None: f = FIXTURES["msg"]["with_context"] result = msg(f["args"][0], **f["args"][1]) assert ":" in result @@ -33,9 +32,11 @@ def test_with_context(self): opts = decode_options(result) assert opts is not None assert opts["$context"] == "button" - assert opts["$_hash"] == decode_options(f["result"])["$_hash"] + f_opts = decode_options(f["result"]) + assert f_opts is not None + assert opts["$_hash"] == f_opts["$_hash"] - def test_with_id(self): + def test_with_id(self) -> None: f = FIXTURES["msg"]["with_id"] result = msg(f["args"][0], **f["args"][1]) assert ":" in result @@ -43,9 +44,11 @@ def test_with_id(self): opts = decode_options(result) assert opts is not None assert opts["$id"] == "greeting" - assert opts["$_hash"] == decode_options(f["result"])["$_hash"] + f_opts = decode_options(f["result"]) + assert f_opts is not None + assert opts["$_hash"] == f_opts["$_hash"] - def test_with_id_and_var(self): + def test_with_id_and_var(self) -> None: f = FIXTURES["msg"]["with_id_and_var"] result = msg(f["args"][0], **f["args"][1]) assert decode_msg(result) == "Hi, Bob!" @@ -53,29 +56,31 @@ def test_with_id_and_var(self): assert opts is not None assert opts["name"] == "Bob" assert opts["$id"] == "greet" - assert opts["$_hash"] == decode_options(f["result"])["$_hash"] + f_opts = decode_options(f["result"]) + assert f_opts is not None + assert opts["$_hash"] == f_opts["$_hash"] class TestDecodeMsg: - def test_encoded(self): + def test_encoded(self) -> None: f = FIXTURES["decodeMsg"]["encoded"] assert decode_msg(f["input"]) == f["result"] - def test_plain_text(self): + def test_plain_text(self) -> None: f = FIXTURES["decodeMsg"]["plain"] assert decode_msg(f["input"]) == f["result"] - def test_no_opts(self): + def test_no_opts(self) -> None: f = FIXTURES["decodeMsg"]["no_opts"] assert decode_msg(f["input"]) == f["result"] - def test_with_colon(self): + def test_with_colon(self) -> None: f = FIXTURES["decodeMsg"]["with_colon"] assert decode_msg(f["input"]) == f["result"] class TestDecodeOptions: - def test_encoded(self): + def test_encoded(self) -> None: f = FIXTURES["decodeOptions"]["encoded"] result = decode_options(f["input"]) assert result is not None @@ -83,10 +88,10 @@ def test_encoded(self): assert result["$_source"] == f["result"]["$_source"] assert result["name"] == f["result"]["name"] - def test_plain_returns_none(self): + def test_plain_returns_none(self) -> None: f = FIXTURES["decodeOptions"]["plain"] assert decode_options(f["input"]) == f["result"] - def test_no_opts_returns_none(self): + def test_no_opts_returns_none(self) -> None: f = FIXTURES["decodeOptions"]["no_opts"] assert decode_options(f["input"]) == f["result"] diff --git a/packages/gt-i18n/tests/test_storage_adapter.py b/packages/gt-i18n/tests/test_storage_adapter.py index 882ea34..0696f93 100644 --- a/packages/gt-i18n/tests/test_storage_adapter.py +++ b/packages/gt-i18n/tests/test_storage_adapter.py @@ -5,18 +5,17 @@ from gt_i18n.i18n_manager._context_var_adapter import ContextVarStorageAdapter -def test_set_get_locale(): +def test_set_get_locale() -> None: adapter = ContextVarStorageAdapter() adapter.set_item("locale", "fr") assert adapter.get_item("locale") == "fr" -def test_default_none(): - adapter = ContextVarStorageAdapter() +def test_default_none() -> None: # In a fresh context, value might be from previous test — use a thread - result = [None] + result: list[str | None] = [None] - def check(): + def check() -> None: a = ContextVarStorageAdapter() result[0] = a.get_item("locale") @@ -26,19 +25,19 @@ def check(): assert result[0] is None -def test_non_locale_key(): +def test_non_locale_key() -> None: adapter = ContextVarStorageAdapter() adapter.set_item("other", "value") assert adapter.get_item("other") is None -def test_thread_isolation(): +def test_thread_isolation() -> None: adapter = ContextVarStorageAdapter() adapter.set_item("locale", "en") - other_thread_value = [None] + other_thread_value: list[str | None] = [None] - def check(): + def check() -> None: a = ContextVarStorageAdapter() other_thread_value[0] = a.get_item("locale") diff --git a/packages/gt-i18n/tests/test_t.py b/packages/gt-i18n/tests/test_t.py index 41f752e..28d61ab 100644 --- a/packages/gt-i18n/tests/test_t.py +++ b/packages/gt-i18n/tests/test_t.py @@ -1,12 +1,15 @@ """Tests for the t() function with mock translations.""" +from collections.abc import Generator +from typing import Any + import pytest from gt_i18n import I18nManager, set_i18n_manager, t from gt_i18n.translation_functions._hash_message import hash_message @pytest.fixture(autouse=True) -def _reset_singleton(): +def _reset_singleton() -> Generator[None, None, None]: """Reset singleton after each test.""" import gt_i18n.i18n_manager._singleton as mod @@ -15,11 +18,11 @@ def _reset_singleton(): mod._manager = old -def _make_manager(translations_by_locale=None, **kwargs): +def _make_manager(translations_by_locale: dict[str, dict[str, str]] | None = None, **kwargs: Any) -> I18nManager: """Create a manager with a custom loader.""" translations_by_locale = translations_by_locale or {} - def loader(locale): + def loader(locale: str) -> dict[str, str]: return translations_by_locale.get(locale, {}) manager = I18nManager(load_translations=loader, **kwargs) @@ -27,7 +30,7 @@ def loader(locale): return manager -def test_source_locale_no_translation(): +def test_source_locale_no_translation() -> None: """Source locale returns interpolated source.""" _make_manager(default_locale="en", locales=["en", "es"]) manager = _make_manager(default_locale="en", locales=["en", "es"]) @@ -36,7 +39,7 @@ def test_source_locale_no_translation(): assert result == "Hello, Alice!" -def test_target_locale_with_translation(): +def test_target_locale_with_translation() -> None: """Target locale with matching translation.""" h = hash_message("Hello, world!") translations = {"es": {h: "Hola, mundo!"}} @@ -55,7 +58,7 @@ def test_target_locale_with_translation(): assert result == "Hola, mundo!" -def test_target_locale_missing_translation(): +def test_target_locale_missing_translation() -> None: """Target locale without translation falls back to source.""" manager = _make_manager( translations_by_locale={"es": {}}, @@ -67,7 +70,7 @@ def test_target_locale_missing_translation(): assert result == "Hello, world!" -def test_variable_interpolation_in_translation(): +def test_variable_interpolation_in_translation() -> None: """Variables are interpolated in the translated string.""" h = hash_message("Hello, {name}!") translations = {"es": {h: "Hola, {name}!"}} @@ -85,7 +88,7 @@ def test_variable_interpolation_in_translation(): assert result == "Hola, Carlos!" -def test_t_raises_without_init(): +def test_t_raises_without_init() -> None: """t() raises RuntimeError if manager not initialized.""" import gt_i18n.i18n_manager._singleton as mod diff --git a/packages/gt-i18n/tests/test_t_fallback.py b/packages/gt-i18n/tests/test_t_fallback.py index a102ecf..183be07 100644 --- a/packages/gt-i18n/tests/test_t_fallback.py +++ b/packages/gt-i18n/tests/test_t_fallback.py @@ -3,22 +3,22 @@ from gt_i18n import t_fallback -def test_simple_interpolation(): +def test_simple_interpolation() -> None: result = t_fallback("Hello, {name}!", name="World") assert result == "Hello, World!" -def test_no_variables(): +def test_no_variables() -> None: result = t_fallback("Hello, world!") assert result == "Hello, world!" -def test_multiple_variables(): +def test_multiple_variables() -> None: result = t_fallback("{a} and {b}", a="X", b="Y") assert result == "X and Y" -def test_with_declared_variables(): +def test_with_declared_variables() -> None: from generaltranslation.static._declare_var import declare_var msg = f"Price: {declare_var('$99', name='price')}" @@ -26,7 +26,7 @@ def test_with_declared_variables(): assert "$99" in result -def test_bad_icu_returns_source(): +def test_bad_icu_returns_source() -> None: """Invalid ICU syntax should return the source message.""" result = t_fallback("{bad, plural, }") # Should return source (or something reasonable), not crash diff --git a/packages/gt-i18n/tests/test_translations_manager.py b/packages/gt-i18n/tests/test_translations_manager.py index 0c17844..36dff79 100644 --- a/packages/gt-i18n/tests/test_translations_manager.py +++ b/packages/gt-i18n/tests/test_translations_manager.py @@ -7,12 +7,12 @@ @pytest.fixture -def sample_translations(): +def sample_translations() -> dict[str, str]: return {"greeting_hash": "Hola, mundo!"} -def test_custom_loader(sample_translations): - def loader(locale): +def test_custom_loader(sample_translations: dict[str, str]) -> None: + def loader(locale: str) -> dict[str, str]: if locale == "es": return sample_translations return {} @@ -22,10 +22,10 @@ def loader(locale): assert result == sample_translations -def test_cache_hit(sample_translations): +def test_cache_hit(sample_translations: dict[str, str]) -> None: call_count = [0] - def loader(locale): + def loader(locale: str) -> dict[str, str]: call_count[0] += 1 return sample_translations @@ -35,8 +35,8 @@ def loader(locale): assert call_count[0] == 1 -def test_sync_returns_cached(sample_translations): - def loader(locale): +def test_sync_returns_cached(sample_translations: dict[str, str]) -> None: + def loader(locale: str) -> dict[str, str]: return sample_translations mgr = TranslationsManager(loader) @@ -47,10 +47,10 @@ def loader(locale): assert mgr.get_translations_sync("es") == sample_translations -def test_load_all(): +def test_load_all() -> None: loaded = [] - def loader(locale): + def loader(locale: str) -> dict[str, str]: loaded.append(locale) return {f"{locale}_hash": f"{locale}_value"} @@ -60,8 +60,8 @@ def loader(locale): assert mgr.get_translations_sync("es") == {"es_hash": "es_value"} -def test_loader_error_returns_empty(): - def loader(locale): +def test_loader_error_returns_empty() -> None: + def loader(locale: str) -> dict[str, str]: raise RuntimeError("fail") mgr = TranslationsManager(loader) @@ -69,8 +69,8 @@ def loader(locale): assert result == {} -def test_async_loader(): - async def loader(locale): +def test_async_loader() -> None: + async def loader(locale: str) -> dict[str, str]: return {"async_hash": "async_value"} mgr = TranslationsManager(loader) diff --git a/pyproject.toml b/pyproject.toml index 8c59058..9a6456c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,16 +23,20 @@ dev = [ [tool.ruff] target-version = "py310" -line-length = 88 +line-length = 120 [tool.ruff.lint] select = ["E", "F", "I", "N", "W", "UP"] +[tool.ruff.lint.per-file-ignores] +"**/tests/**" = ["E501"] + [tool.mypy] python_version = "3.10" -strict = true -warn_return_any = true +warn_return_any = false warn_unused_configs = true +disallow_untyped_defs = true +exclude = ["examples/"] [tool.pytest.ini_options] testpaths = ["packages", "examples"]