From 71cd2c3f76484e597bb4764add74ff5ba145e5b6 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Mon, 29 Dec 2025 15:14:45 +0200 Subject: [PATCH 01/10] Use uv, pyproject.toml files, and markdown --- .flake8 | 2 +- .github/workflows/ci.yml | 43 +- README.md | 38 ++ README.rst | 47 --- dev-requirements.txt | 7 - fluent.runtime/README.md | 14 + fluent.runtime/README.rst | 19 - fluent.runtime/pyproject.toml | 36 ++ fluent.runtime/setup.cfg | 5 - fluent.runtime/setup.py | 42 -- fluent.runtime/tox.ini | 22 - fluent.syntax/README.md | 17 + fluent.syntax/README.rst | 22 - fluent.syntax/pyproject.toml | 29 ++ fluent.syntax/setup.cfg | 5 - fluent.syntax/setup.py | 33 -- fluent.syntax/tox.ini | 12 - pyproject.toml | 23 +- uv.lock | 747 ++++++++++++++++++++++++++++++++++ 19 files changed, 922 insertions(+), 241 deletions(-) create mode 100644 README.md delete mode 100644 README.rst delete mode 100644 dev-requirements.txt create mode 100644 fluent.runtime/README.md delete mode 100644 fluent.runtime/README.rst create mode 100644 fluent.runtime/pyproject.toml delete mode 100644 fluent.runtime/setup.cfg delete mode 100755 fluent.runtime/setup.py delete mode 100644 fluent.runtime/tox.ini create mode 100644 fluent.syntax/README.md delete mode 100644 fluent.syntax/README.rst create mode 100644 fluent.syntax/pyproject.toml delete mode 100644 fluent.syntax/setup.cfg delete mode 100644 fluent.syntax/setup.py delete mode 100644 fluent.syntax/tox.ini create mode 100644 uv.lock diff --git a/.flake8 b/.flake8 index cefce7ad..688f475f 100644 --- a/.flake8 +++ b/.flake8 @@ -1,4 +1,4 @@ [flake8] -exclude=.tox +exclude=.venv extend-ignore = E203 max-line-length=120 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 841eae82..cb466d26 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,23 +5,20 @@ on: branches: - main pull_request: + workflow_dispatch: jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v6 + - uses: astral-sh/setup-uv@v7 with: - cache: pip - cache-dependency-path: | - dev-requirements.txt - fluent.syntax/setup.py - fluent.runtime/setup.py - - run: python -m pip install -r dev-requirements.txt - - run: python -m pip install ./fluent.syntax ./fluent.runtime - - run: python -m flake8 - - run: python -m mypy fluent.syntax/fluent fluent.runtime/fluent + python-version: 3.8 + - uses: actions/setup-python@v6 + - run: uv sync --dev --all-packages + - run: uv run python -m flake8 + - run: uv run mypy fluent.syntax/fluent fluent.runtime/fluent test: runs-on: ${{ matrix.os }} strategy: @@ -29,17 +26,13 @@ jobs: os: [ubuntu-22.04, windows-2022] python-version: [3.7, 3.8, 3.9, "3.10", 3.11, 3.12, pypy3.9, pypy3.10] steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v6 + - uses: astral-sh/setup-uv@v7 with: python-version: ${{ matrix.python-version }} - cache: pip - cache-dependency-path: | - fluent.syntax/setup.py - fluent.runtime/setup.py - - run: python -m pip install ./fluent.syntax ./fluent.runtime - - run: python -m unittest discover -s fluent.syntax - - run: python -m unittest discover -s fluent.runtime + - run: uv sync --dev --all-packages + - run: uv run python -m unittest discover -s fluent.syntax + - run: uv run python -m unittest discover -s fluent.runtime # Test compatibility with the oldest Python version we claim to support, # and for fluent.runtime's compatibility with a range of fluent.syntax versions. @@ -53,10 +46,10 @@ jobs: - fluent.syntax==0.18.1 six - fluent.syntax==0.17.0 six steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v6 + - uses: astral-sh/setup-uv@v7 with: python-version: 3.6 - - run: python -m pip install ${{ matrix.fluent-syntax }} - - run: python -m pip install ./fluent.runtime - - run: python -m unittest discover -s fluent.runtime + - run: uv pip install ${{ matrix.fluent-syntax }} + - run: uv pip install ./fluent.runtime + - run: uv run python -m unittest discover -s fluent.runtime diff --git a/README.md b/README.md new file mode 100644 index 00000000..a7d3843c --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +# Project Fluent + +This is a collection of Python packages to use the +[Fluent localization system](http://projectfluent.org/): + +## `fluent.syntax` + +The [syntax package](./fluent.syntax) includes the +parser, serializer, and traversal utilities like `Visitor` and `Transformer`. +You’re looking for this package if you work on tooling for Fluent in Python. + +## `fluent.runtime` + +The [runtime package](./fluent.runtime) includes the library required to +use Fluent to localize your Python application. +It comes with a `Localization` class to use, based on an implementation of `FluentBundle`. +It uses the tooling parser above to read Fluent files. + +## `fluent.pygments` + +A [plugin for pygments](./fluent.pygments) to add syntax highlighting to Sphinx. + +## Discuss + +We’d love to hear your thoughts on Project Fluent! Whether you’re a +localizer looking for a better way to express yourself in your language, +or a developer trying to make your app localizable and multilingual, or +a hacker looking for a project to contribute to, please do get in touch +on the mailing list and the IRC channel. + +- Mozilla Discourse: https://discourse.mozilla.org/c/fluent +- Matrix channel: [#fluent:mozilla.org](https://chat.mozilla.org/#/room/#fluent:mozilla.org) + +## Get Involved + +python-fluent is open-source, licensed under the Apache License, Version +2.0. We encourage everyone to take a look at our code and we’ll listen +to your feedback. diff --git a/README.rst b/README.rst deleted file mode 100644 index 5d2fdb94..00000000 --- a/README.rst +++ /dev/null @@ -1,47 +0,0 @@ -Project Fluent -============== - -This is a collection of Python packages to use the `Fluent localization -system `__. - -python-fluent consists of these packages: - -``fluent.syntax`` ------------------ - -The `syntax package `_ includes the parser, serializer, and traversal -utilities like Visitor and Transformer. You’re looking for this package -if you work on tooling for Fluent in Python. - -``fluent.runtime`` ------------------- - -The `runtime package `__ includes the library required to use Fluent to localize -your Python application. It comes with a ``Localization`` class to use, -based on an implementation of ``FluentBundle``. It uses the tooling parser above -to read Fluent files. - -``fluent.pygments`` -------------------- - -A `plugin for pygments `_ to add syntax highlighting to Sphinx. - -Discuss -------- - -We’d love to hear your thoughts on Project Fluent! Whether you’re a -localizer looking for a better way to express yourself in your language, -or a developer trying to make your app localizable and multilingual, or -a hacker looking for a project to contribute to, please do get in touch -on the mailing list and the IRC channel. - -- Mozilla Discourse: https://discourse.mozilla.org/c/fluent -- Matrix channel: - `#fluent:mozilla.org `__ - -Get Involved ------------- - -python-fluent is open-source, licensed under the Apache License, Version -2.0. We encourage everyone to take a look at our code and we’ll listen -to your feedback. diff --git a/dev-requirements.txt b/dev-requirements.txt deleted file mode 100644 index 6369451d..00000000 --- a/dev-requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -black ~= 24.0 -flake8 ~= 7.0 -isort ~= 5.0 -mypy ~= 1.0 -tox ~= 4.0 -types-babel -types-pytz diff --git a/fluent.runtime/README.md b/fluent.runtime/README.md new file mode 100644 index 00000000..a5aef97d --- /dev/null +++ b/fluent.runtime/README.md @@ -0,0 +1,14 @@ +# fluent.runtime + +Use [Fluent](https://projectfluent.org/) to localize your Python application. +It comes with a `Localization` class to use, based on an implementation of `FluentBundle`. +It uses the parser from `fluent.syntax` to read Fluent files. + +```python +from datetime import date +l10n = DemoLocalization("today-is = Today is { $today }") +val = l10n.format_value("today-is", { "today": date.today() }) +val # 'Today is Jun 16, 2018' +``` + +Find the full documentation at https://projectfluent.org/python-fluent/fluent.runtime/. diff --git a/fluent.runtime/README.rst b/fluent.runtime/README.rst deleted file mode 100644 index e583da0e..00000000 --- a/fluent.runtime/README.rst +++ /dev/null @@ -1,19 +0,0 @@ -fluent.runtime |fluent.runtime| -=============================== - -Use `Fluent`_ to localize your Python application. It comes with a ``Localization`` -class to use, based on an implementation of ``FluentBundle``. It uses the parser from -``fluent.syntax`` to read Fluent files. - -.. code-block:: python - - >>> from datetime import date - >>> l10n = DemoLocalization("today-is = Today is { $today }") - >>> val = l10n.format_value("today-is", {"today": date.today() }) - >>> val - 'Today is Jun 16, 2018' - -Find the full documentation on https://projectfluent.org/python-fluent/fluent.runtime/. - -.. _fluent: https://projectfluent.org/ -.. |fluent.runtime| image:: https://github.com/projectfluent/python-fluent/workflows/fluent.runtime/badge.svg diff --git a/fluent.runtime/pyproject.toml b/fluent.runtime/pyproject.toml new file mode 100644 index 00000000..84d23cfa --- /dev/null +++ b/fluent.runtime/pyproject.toml @@ -0,0 +1,36 @@ +[project] +name = "fluent.runtime" +version = "0.4.0" +requires-python = ">= 3.6" +dependencies = [ + "fluent.syntax>=0.17,<0.20", + "attrs", + "babel", + "pytz", + "typing-extensions>=3.7,<5", +] +license = { text = "Apache-2.0" } +description = "Localization library for expressive translations." +keywords = ["fluent", "localization", "l10n"] +authors = [ + { name = "Mozilla", email = "l10n-drivers@mozilla.org" }, + { name = "Eemeli Aro", email = "eemeli@mozilla.com" }, + { name = "Luke Plant", email = "L.Plant.98@cantab.net" }, +] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3 :: Only", +] + +[project.urls] +repository = "https://github.com/projectfluent/python-fluent" + +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" diff --git a/fluent.runtime/setup.cfg b/fluent.runtime/setup.cfg deleted file mode 100644 index b721e30e..00000000 --- a/fluent.runtime/setup.cfg +++ /dev/null @@ -1,5 +0,0 @@ -[metadata] -version=0.4.0 - -[bdist_wheel] -universal=1 diff --git a/fluent.runtime/setup.py b/fluent.runtime/setup.py deleted file mode 100755 index 8a32f929..00000000 --- a/fluent.runtime/setup.py +++ /dev/null @@ -1,42 +0,0 @@ -from os import path - -from setuptools import setup - -this_directory = path.abspath(path.dirname(__file__)) -with open(path.join(this_directory, "README.rst"), "rb") as f: - long_description = f.read().decode("utf-8") - - -setup( - name="fluent.runtime", - description="Localization library for expressive translations.", - long_description=long_description, - long_description_content_type="text/x-rst", - author="Luke Plant", - author_email="L.Plant.98@cantab.net", - license="APL 2", - url="https://github.com/projectfluent/python-fluent", - keywords=["fluent", "localization", "l10n"], - classifiers=[ - "Development Status :: 3 - Alpha", - "Intended Audience :: Developers", - "License :: OSI Approved :: Apache Software License", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3 :: Only", - ], - packages=["fluent.runtime"], - package_data={"fluent.runtime": ["py.typed"]}, - # These should also be duplicated in tox.ini and /.github/workflows/fluent.runtime.yml - python_requires=">=3.6", - install_requires=[ - "fluent.syntax>=0.17,<0.20", - "attrs", - "babel", - "pytz", - "typing-extensions>=3.7,<5", - ], - test_suite="tests", -) diff --git a/fluent.runtime/tox.ini b/fluent.runtime/tox.ini deleted file mode 100644 index c532b65f..00000000 --- a/fluent.runtime/tox.ini +++ /dev/null @@ -1,22 +0,0 @@ -# This config is for local testing. -# It should be correspond to .github/workflows/fluent.runtime.yml -[tox] -envlist = {py36,py37,py38,py39,pypy3}-syntax, py3-syntax0.17, latest -skipsdist=True - -[testenv] -setenv = - PYTHONPATH = {toxinidir} -deps = - syntax0.17: fluent.syntax==0.17 - attrs==19.1.0 - babel==2.7.0 - pytz==2019.2 - typing-extensions~=3.7 - syntax: . -commands = python -m unittest - -[testenv:latest] -basepython = python3 -deps = - . diff --git a/fluent.syntax/README.md b/fluent.syntax/README.md new file mode 100644 index 00000000..a71c47a8 --- /dev/null +++ b/fluent.syntax/README.md @@ -0,0 +1,17 @@ +# fluent.syntax + +Read, write, and transform [Fluent](https://projectfluent.org/) files. + +This package includes the parser, serializer, and traversal +utilities like Visitor and Transformer. You’re looking for this package +if you work on tooling for Fluent in Python. + +```python +from fluent.syntax import parse, ast, serialize +resource = parse("a-key = String to localize") +resource.body[0].value.elements[0].value = "Localized string" +serialize(resource) +# 'a-key = Localized string\n' +``` + +Find the full documentation at https://projectfluent.org/python-fluent/fluent.syntax/. diff --git a/fluent.syntax/README.rst b/fluent.syntax/README.rst deleted file mode 100644 index c847f5e6..00000000 --- a/fluent.syntax/README.rst +++ /dev/null @@ -1,22 +0,0 @@ -``fluent.syntax`` |fluent.syntax| ---------------------------------- - -Read, write, and transform `Fluent`_ files. - -This package includes the parser, serializer, and traversal -utilities like Visitor and Transformer. You’re looking for this package -if you work on tooling for Fluent in Python. - -.. code-block:: python - - >>> from fluent.syntax import parse, ast, serialize - >>> resource = parse("a-key = String to localize") - >>> resource.body[0].value.elements[0].value = "Localized string" - >>> serialize(resource) - 'a-key = Localized string\n' - - -Find the full documentation on https://projectfluent.org/python-fluent/fluent.syntax/. - -.. _fluent: https://projectfluent.org/ -.. |fluent.syntax| image:: https://github.com/projectfluent/python-fluent/workflows/fluent.syntax/badge.svg diff --git a/fluent.syntax/pyproject.toml b/fluent.syntax/pyproject.toml new file mode 100644 index 00000000..291da817 --- /dev/null +++ b/fluent.syntax/pyproject.toml @@ -0,0 +1,29 @@ +[project] +name = "fluent.syntax" +version = "0.19.0" +requires-python = ">= 3.6" +dependencies = ["typing-extensions>=3.7,<5"] +license = { text = "Apache-2.0" } +description = "Localization library for expressive translations." +keywords = ["fluent", "localization", "l10n"] +authors = [ + { name = "Mozilla", email = "l10n-drivers@mozilla.org" }, + { name = "Eemeli Aro", email = "eemeli@mozilla.com" }, +] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3 :: Only", +] + +[project.urls] +repository = "https://github.com/projectfluent/python-fluent" + +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" diff --git a/fluent.syntax/setup.cfg b/fluent.syntax/setup.cfg deleted file mode 100644 index 2ee441e6..00000000 --- a/fluent.syntax/setup.cfg +++ /dev/null @@ -1,5 +0,0 @@ -[metadata] -version=0.19.0 - -[bdist_wheel] -universal=1 diff --git a/fluent.syntax/setup.py b/fluent.syntax/setup.py deleted file mode 100644 index 08899e2e..00000000 --- a/fluent.syntax/setup.py +++ /dev/null @@ -1,33 +0,0 @@ -from os import path - -from setuptools import setup - -this_directory = path.abspath(path.dirname(__file__)) -with open(path.join(this_directory, "README.rst"), "rb") as f: - long_description = f.read().decode("utf-8") - -setup( - name="fluent.syntax", - description="Localization library for expressive translations.", - long_description=long_description, - long_description_content_type="text/x-rst", - author="Mozilla", - author_email="l10n-drivers@mozilla.org", - license="APL 2", - url="https://github.com/projectfluent/python-fluent", - keywords=["fluent", "localization", "l10n"], - classifiers=[ - "Development Status :: 3 - Alpha", - "Intended Audience :: Developers", - "License :: OSI Approved :: Apache Software License", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3 :: Only", - ], - packages=["fluent.syntax"], - package_data={"fluent.syntax": ["py.typed"]}, - install_requires=["typing-extensions>=3.7,<5"], - test_suite="tests.syntax", -) diff --git a/fluent.syntax/tox.ini b/fluent.syntax/tox.ini deleted file mode 100644 index a64d39b6..00000000 --- a/fluent.syntax/tox.ini +++ /dev/null @@ -1,12 +0,0 @@ -# This config is for local testing. -# It should be correspond to .github/workflows/fluent.syntax.yml -[tox] -envlist = py36, py37, py38, py39, pypy3 -skipsdist=True - -[testenv] -setenv = - PYTHONPATH = {toxinidir} -deps = - typing-extensions~=3.7 -commands = python -m unittest diff --git a/pyproject.toml b/pyproject.toml index 7156c53b..567f3ca0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,27 @@ +[project] +name = "python-fluent" +version = "0" +requires-python = ">= 3.8.1" +dependencies = [ + "fluent.syntax", + "black ~= 24.0", + "flake8 ~= 7.0", + "isort ~= 5.0", + "mypy ~= 1.0", + "types-babel", + "types-pytz", +] + [tool.isort] line_length = 120 -skip_glob = ".tox" [tool.mypy] +explicit_package_bases = true +mypy_path = ['./fluent.syntax'] strict = true + +[tool.uv.sources] +fluent-syntax = { workspace = true, editable = true } + +[tool.uv.workspace] +members = ["fluent.runtime", "fluent.syntax"] diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..777227e3 --- /dev/null +++ b/uv.lock @@ -0,0 +1,747 @@ +version = 1 +revision = 3 +requires-python = ">=3.8.1" +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", + "python_full_version < '3.9'", +] + +[manifest] +members = [ + "fluent-runtime", + "fluent-syntax", + "python-fluent", +] + +[[package]] +name = "attrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "babel" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytz", marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, +] + +[[package]] +name = "black" +version = "24.8.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "mypy-extensions", marker = "python_full_version < '3.9'" }, + { name = "packaging", marker = "python_full_version < '3.9'" }, + { name = "pathspec", marker = "python_full_version < '3.9'" }, + { name = "platformdirs", version = "4.3.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "tomli", marker = "python_full_version < '3.9'" }, + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/b0/46fb0d4e00372f4a86a6f8efa3cb193c9f64863615e39010b1477e010578/black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f", size = 644810, upload-time = "2024-08-02T17:43:18.405Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/6e/74e29edf1fba3887ed7066930a87f698ffdcd52c5dbc263eabb06061672d/black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6", size = 1632092, upload-time = "2024-08-02T17:47:26.911Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/575cb6c3faee690b05c9d11ee2e8dba8fbd6d6c134496e644c1feb1b47da/black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb", size = 1457529, upload-time = "2024-08-02T17:47:29.109Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/d34099e95c437b53d01c4aa37cf93944b233066eb034ccf7897fa4e5f286/black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42", size = 1757443, upload-time = "2024-08-02T17:46:20.306Z" }, + { url = "https://files.pythonhosted.org/packages/87/a0/6d2e4175ef364b8c4b64f8441ba041ed65c63ea1db2720d61494ac711c15/black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a", size = 1418012, upload-time = "2024-08-02T17:47:20.33Z" }, + { url = "https://files.pythonhosted.org/packages/08/a6/0a3aa89de9c283556146dc6dbda20cd63a9c94160a6fbdebaf0918e4a3e1/black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1", size = 1615080, upload-time = "2024-08-02T17:48:05.467Z" }, + { url = "https://files.pythonhosted.org/packages/db/94/b803d810e14588bb297e565821a947c108390a079e21dbdcb9ab6956cd7a/black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af", size = 1438143, upload-time = "2024-08-02T17:47:30.247Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b5/f485e1bbe31f768e2e5210f52ea3f432256201289fd1a3c0afda693776b0/black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4", size = 1738774, upload-time = "2024-08-02T17:46:17.837Z" }, + { url = "https://files.pythonhosted.org/packages/a8/69/a000fc3736f89d1bdc7f4a879f8aaf516fb03613bb51a0154070383d95d9/black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af", size = 1427503, upload-time = "2024-08-02T17:46:22.654Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a8/05fb14195cfef32b7c8d4585a44b7499c2a4b205e1662c427b941ed87054/black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368", size = 1646132, upload-time = "2024-08-02T17:49:52.843Z" }, + { url = "https://files.pythonhosted.org/packages/41/77/8d9ce42673e5cb9988f6df73c1c5c1d4e9e788053cccd7f5fb14ef100982/black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed", size = 1448665, upload-time = "2024-08-02T17:47:54.479Z" }, + { url = "https://files.pythonhosted.org/packages/cc/94/eff1ddad2ce1d3cc26c162b3693043c6b6b575f538f602f26fe846dfdc75/black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018", size = 1762458, upload-time = "2024-08-02T17:46:19.384Z" }, + { url = "https://files.pythonhosted.org/packages/28/ea/18b8d86a9ca19a6942e4e16759b2fa5fc02bbc0eb33c1b866fcd387640ab/black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2", size = 1436109, upload-time = "2024-08-02T17:46:52.97Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d4/ae03761ddecc1a37d7e743b89cccbcf3317479ff4b88cfd8818079f890d0/black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd", size = 1617322, upload-time = "2024-08-02T17:51:20.203Z" }, + { url = "https://files.pythonhosted.org/packages/14/4b/4dfe67eed7f9b1ddca2ec8e4418ea74f0d1dc84d36ea874d618ffa1af7d4/black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2", size = 1442108, upload-time = "2024-08-02T17:50:40.824Z" }, + { url = "https://files.pythonhosted.org/packages/97/14/95b3f91f857034686cae0e73006b8391d76a8142d339b42970eaaf0416ea/black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e", size = 1745786, upload-time = "2024-08-02T17:46:02.939Z" }, + { url = "https://files.pythonhosted.org/packages/95/54/68b8883c8aa258a6dde958cd5bdfada8382bec47c5162f4a01e66d839af1/black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920", size = 1426754, upload-time = "2024-08-02T17:46:38.603Z" }, + { url = "https://files.pythonhosted.org/packages/13/b2/b3f24fdbb46f0e7ef6238e131f13572ee8279b70f237f221dd168a9dba1a/black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c", size = 1631706, upload-time = "2024-08-02T17:49:57.606Z" }, + { url = "https://files.pythonhosted.org/packages/d9/35/31010981e4a05202a84a3116423970fd1a59d2eda4ac0b3570fbb7029ddc/black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e", size = 1457429, upload-time = "2024-08-02T17:49:12.764Z" }, + { url = "https://files.pythonhosted.org/packages/27/25/3f706b4f044dd569a20a4835c3b733dedea38d83d2ee0beb8178a6d44945/black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47", size = 1756488, upload-time = "2024-08-02T17:46:08.067Z" }, + { url = "https://files.pythonhosted.org/packages/63/72/79375cd8277cbf1c5670914e6bd4c1b15dea2c8f8e906dc21c448d0535f0/black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb", size = 1417721, upload-time = "2024-08-02T17:46:42.637Z" }, + { url = "https://files.pythonhosted.org/packages/27/1e/83fa8a787180e1632c3d831f7e58994d7aaf23a0961320d21e84f922f919/black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed", size = 206504, upload-time = "2024-08-02T17:43:15.747Z" }, +] + +[[package]] +name = "black" +version = "24.10.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "mypy-extensions", marker = "python_full_version >= '3.9'" }, + { name = "packaging", marker = "python_full_version >= '3.9'" }, + { name = "pathspec", marker = "python_full_version >= '3.9'" }, + { name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "platformdirs", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "tomli", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/0d/cc2fb42b8c50d80143221515dd7e4766995bd07c56c9a3ed30baf080b6dc/black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875", size = 645813, upload-time = "2024-10-07T19:20:50.361Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/f3/465c0eb5cddf7dbbfe1fecd9b875d1dcf51b88923cd2c1d7e9ab95c6336b/black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812", size = 1623211, upload-time = "2024-10-07T19:26:12.43Z" }, + { url = "https://files.pythonhosted.org/packages/df/57/b6d2da7d200773fdfcc224ffb87052cf283cec4d7102fab450b4a05996d8/black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea", size = 1457139, upload-time = "2024-10-07T19:25:06.453Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c5/9023b7673904a5188f9be81f5e129fff69f51f5515655fbd1d5a4e80a47b/black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f", size = 1753774, upload-time = "2024-10-07T19:23:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/e1/32/df7f18bd0e724e0d9748829765455d6643ec847b3f87e77456fc99d0edab/black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e", size = 1414209, upload-time = "2024-10-07T19:24:42.54Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cc/7496bb63a9b06a954d3d0ac9fe7a73f3bf1cd92d7a58877c27f4ad1e9d41/black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad", size = 1607468, upload-time = "2024-10-07T19:26:14.966Z" }, + { url = "https://files.pythonhosted.org/packages/2b/e3/69a738fb5ba18b5422f50b4f143544c664d7da40f09c13969b2fd52900e0/black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50", size = 1437270, upload-time = "2024-10-07T19:25:24.291Z" }, + { url = "https://files.pythonhosted.org/packages/c9/9b/2db8045b45844665c720dcfe292fdaf2e49825810c0103e1191515fc101a/black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392", size = 1737061, upload-time = "2024-10-07T19:23:52.18Z" }, + { url = "https://files.pythonhosted.org/packages/a3/95/17d4a09a5be5f8c65aa4a361444d95edc45def0de887810f508d3f65db7a/black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175", size = 1423293, upload-time = "2024-10-07T19:24:41.7Z" }, + { url = "https://files.pythonhosted.org/packages/90/04/bf74c71f592bcd761610bbf67e23e6a3cff824780761f536512437f1e655/black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3", size = 1644256, upload-time = "2024-10-07T19:27:53.355Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ea/a77bab4cf1887f4b2e0bce5516ea0b3ff7d04ba96af21d65024629afedb6/black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65", size = 1448534, upload-time = "2024-10-07T19:26:44.953Z" }, + { url = "https://files.pythonhosted.org/packages/4e/3e/443ef8bc1fbda78e61f79157f303893f3fddf19ca3c8989b163eb3469a12/black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f", size = 1761892, upload-time = "2024-10-07T19:24:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/52/93/eac95ff229049a6901bc84fec6908a5124b8a0b7c26ea766b3b8a5debd22/black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8", size = 1434796, upload-time = "2024-10-07T19:25:06.239Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a0/a993f58d4ecfba035e61fca4e9f64a2ecae838fc9f33ab798c62173ed75c/black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981", size = 1643986, upload-time = "2024-10-07T19:28:50.684Z" }, + { url = "https://files.pythonhosted.org/packages/37/d5/602d0ef5dfcace3fb4f79c436762f130abd9ee8d950fa2abdbf8bbc555e0/black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b", size = 1448085, upload-time = "2024-10-07T19:28:12.093Z" }, + { url = "https://files.pythonhosted.org/packages/47/6d/a3a239e938960df1a662b93d6230d4f3e9b4a22982d060fc38c42f45a56b/black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2", size = 1760928, upload-time = "2024-10-07T19:24:15.233Z" }, + { url = "https://files.pythonhosted.org/packages/dd/cf/af018e13b0eddfb434df4d9cd1b2b7892bab119f7a20123e93f6910982e8/black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b", size = 1436875, upload-time = "2024-10-07T19:24:42.762Z" }, + { url = "https://files.pythonhosted.org/packages/fe/02/f408c804e0ee78c367dcea0a01aedde4f1712af93b8b6e60df981e0228c7/black-24.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd", size = 1622516, upload-time = "2024-10-07T19:29:40.629Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b9/9b706ed2f55bfb28b436225a9c57da35990c9005b90b8c91f03924454ad7/black-24.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f", size = 1456181, upload-time = "2024-10-07T19:28:11.16Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1c/314d7f17434a5375682ad097f6f4cc0e3f414f3c95a9b1bb4df14a0f11f9/black-24.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800", size = 1752801, upload-time = "2024-10-07T19:23:56.594Z" }, + { url = "https://files.pythonhosted.org/packages/39/a7/20e5cd9237d28ad0b31438de5d9f01c8b99814576f4c0cda1edd62caf4b0/black-24.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7", size = 1413626, upload-time = "2024-10-07T19:24:46.133Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a7/4b27c50537ebca8bec139b872861f9d2bf501c5ec51fcf897cb924d9e264/black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d", size = 206898, upload-time = "2024-10-07T19:20:48.317Z" }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", + "python_full_version < '3.9'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "flake8" +version = "7.1.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "mccabe", marker = "python_full_version < '3.9'" }, + { name = "pycodestyle", version = "2.12.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "pyflakes", version = "3.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/16/3f2a0bb700ad65ac9663262905a025917c020a3f92f014d2ba8964b4602c/flake8-7.1.2.tar.gz", hash = "sha256:c586ffd0b41540951ae41af572e6790dbd49fc12b3aa2541685d253d9bd504bd", size = 48119, upload-time = "2025-02-16T18:45:44.296Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/f8/08d37b2cd89da306e3520bd27f8a85692122b42b56c0c2c3784ff09c022f/flake8-7.1.2-py2.py3-none-any.whl", hash = "sha256:1cbc62e65536f65e6d754dfe6f1bada7f5cf392d6f5db3c2b85892466c3e7c1a", size = 57745, upload-time = "2025-02-16T18:45:42.351Z" }, +] + +[[package]] +name = "flake8" +version = "7.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "mccabe", marker = "python_full_version >= '3.9'" }, + { name = "pycodestyle", version = "2.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pyflakes", version = "3.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/af/fbfe3c4b5a657d79e5c47a2827a362f9e1b763336a52f926126aa6dc7123/flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872", size = 48326, upload-time = "2025-06-20T19:31:35.838Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e", size = 57922, upload-time = "2025-06-20T19:31:34.425Z" }, +] + +[[package]] +name = "fluent-runtime" +version = "0.4.0" +source = { editable = "fluent.runtime" } +dependencies = [ + { name = "attrs", version = "25.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "attrs", version = "25.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "babel" }, + { name = "fluent-syntax" }, + { name = "pytz" }, + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] + +[package.metadata] +requires-dist = [ + { name = "attrs" }, + { name = "babel" }, + { name = "fluent-syntax", editable = "fluent.syntax" }, + { name = "pytz" }, + { name = "typing-extensions", specifier = ">=3.7,<5" }, +] + +[[package]] +name = "fluent-syntax" +version = "0.19.0" +source = { editable = "fluent.syntax" } +dependencies = [ + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] + +[package.metadata] +requires-dist = [{ name = "typing-extensions", specifier = ">=3.7,<5" }] + +[[package]] +name = "isort" +version = "5.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/87/f9/c1eb8635a24e87ade2efce21e3ce8cd6b8630bb685ddc9cdaca1349b2eb5/isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109", size = 175303, upload-time = "2023-12-13T20:37:26.124Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/b3/8def84f539e7d2289a02f0524b944b15d7c75dab7628bedf1c4f0992029c/isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6", size = 92310, upload-time = "2023-12-13T20:37:23.244Z" }, +] + +[[package]] +name = "librt" +version = "0.7.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/8a/071f6628363d83e803d4783e0cd24fb9c5b798164300fcfaaa47c30659c0/librt-0.7.5.tar.gz", hash = "sha256:de4221a1181fa9c8c4b5f35506ed6f298948f44003d84d2a8b9885d7e01e6cfa", size = 145868, upload-time = "2025-12-25T03:53:16.039Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/f2/3248d8419db99ab80bb36266735d1241f766ad5fd993071211f789b618a5/librt-0.7.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:81056e01bba1394f1d92904ec61a4078f66df785316275edbaf51d90da8c6e26", size = 54703, upload-time = "2025-12-25T03:51:48.394Z" }, + { url = "https://files.pythonhosted.org/packages/7b/30/7e179543dbcb1311f84b7e797658ad85cf2d4474c468f5dbafa13f2a98a5/librt-0.7.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d7c72c8756eeb3aefb1b9e3dac7c37a4a25db63640cac0ab6fc18e91a0edf05a", size = 56660, upload-time = "2025-12-25T03:51:49.791Z" }, + { url = "https://files.pythonhosted.org/packages/15/91/3ba03ac1ac1abd66757a134b3bd56d9674928b163d0e686ea065a2bbb92d/librt-0.7.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ddc4a16207f88f9597b397fc1f60781266d13b13de922ff61c206547a29e4bbd", size = 161026, upload-time = "2025-12-25T03:51:51.021Z" }, + { url = "https://files.pythonhosted.org/packages/0d/6e/b8365f547817d37b44c4be2ffa02630be995ef18be52d72698cecc3640c5/librt-0.7.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:63055d3dda433ebb314c9f1819942f16a19203c454508fdb2d167613f7017169", size = 169530, upload-time = "2025-12-25T03:51:52.417Z" }, + { url = "https://files.pythonhosted.org/packages/63/6a/8442eb0b6933c651a06e1888f863971f3391cc11338fdaa6ab969f7d1eac/librt-0.7.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f85f9b5db87b0f52e53c68ad2a0c5a53e00afa439bd54a1723742a2b1021276", size = 183272, upload-time = "2025-12-25T03:51:53.713Z" }, + { url = "https://files.pythonhosted.org/packages/90/c4/b1166df6ef8e1f68d309f50bf69e8e750a5ea12fe7e2cf202c771ff359fc/librt-0.7.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c566a4672564c5d54d8ab65cdaae5a87ee14c1564c1a2ddc7a9f5811c750f023", size = 179040, upload-time = "2025-12-25T03:51:55.048Z" }, + { url = "https://files.pythonhosted.org/packages/fc/30/8f3fd9fd975b16c37832d6c248b976d2a0e33f155063781e064f249b37f1/librt-0.7.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fee15c2a190ef389f14928135c6fb2d25cd3fdb7887bfd9a7b444bbdc8c06b96", size = 173506, upload-time = "2025-12-25T03:51:56.407Z" }, + { url = "https://files.pythonhosted.org/packages/75/71/c3d4d5658f9849bf8e07ffba99f892d49a0c9a4001323ed610db72aedc82/librt-0.7.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:584cb3e605ec45ba350962cec853e17be0a25a772f21f09f1e422f7044ae2a7d", size = 193573, upload-time = "2025-12-25T03:51:57.949Z" }, + { url = "https://files.pythonhosted.org/packages/86/7c/c1c8a0116a2eed3d58c8946c589a8f9e1354b9b825cc92eba58bb15f6fb1/librt-0.7.5-cp310-cp310-win32.whl", hash = "sha256:9c08527055fbb03c641c15bbc5b79dd2942fb6a3bd8dabf141dd7e97eeea4904", size = 42603, upload-time = "2025-12-25T03:51:59.215Z" }, + { url = "https://files.pythonhosted.org/packages/1d/00/b52c77ca294247420020b829b70465c6e6f2b9d59ab21d8051aac20432da/librt-0.7.5-cp310-cp310-win_amd64.whl", hash = "sha256:dd810f2d39c526c42ea205e0addad5dc08ef853c625387806a29d07f9d150d9b", size = 48977, upload-time = "2025-12-25T03:52:00.519Z" }, + { url = "https://files.pythonhosted.org/packages/11/89/42b3ccb702a7e5f7a4cf2afc8a0a8f8c5e7d4b4d3a7c3de6357673dddddb/librt-0.7.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f952e1a78c480edee8fb43aa2bf2e84dcd46c917d44f8065b883079d3893e8fc", size = 54705, upload-time = "2025-12-25T03:52:01.433Z" }, + { url = "https://files.pythonhosted.org/packages/bb/90/c16970b509c3c448c365041d326eeef5aeb2abaed81eb3187b26a3cd13f8/librt-0.7.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75965c1f4efb7234ff52a58b729d245a21e87e4b6a26a0ec08052f02b16274e4", size = 56667, upload-time = "2025-12-25T03:52:02.391Z" }, + { url = "https://files.pythonhosted.org/packages/ac/2f/da4bdf6c190503f4663fbb781dfae5564a2b1c3f39a2da8e1ac7536ac7bd/librt-0.7.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:732e0aa0385b59a1b2545159e781c792cc58ce9c134249233a7c7250a44684c4", size = 161705, upload-time = "2025-12-25T03:52:03.395Z" }, + { url = "https://files.pythonhosted.org/packages/fb/88/c5da8e1f5f22b23d56e1fbd87266799dcf32828d47bf69fabc6f9673c6eb/librt-0.7.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cdde31759bd8888f3ef0eebda80394a48961328a17c264dce8cc35f4b9cde35d", size = 171029, upload-time = "2025-12-25T03:52:04.798Z" }, + { url = "https://files.pythonhosted.org/packages/38/8a/8dfc00a6f1febc094ed9a55a448fc0b3a591b5dfd83be6cfd76d0910b1f0/librt-0.7.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df3146d52465b3b6397d25d513f428cb421c18df65b7378667bb5f1e3cc45805", size = 184704, upload-time = "2025-12-25T03:52:05.887Z" }, + { url = "https://files.pythonhosted.org/packages/ad/57/65dec835ff235f431801064a3b41268f2f5ee0d224dc3bbf46d911af5c1a/librt-0.7.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:29c8d2fae11d4379ea207ba7fc69d43237e42cf8a9f90ec6e05993687e6d648b", size = 180720, upload-time = "2025-12-25T03:52:06.925Z" }, + { url = "https://files.pythonhosted.org/packages/1e/27/92033d169bbcaa0d9a2dd476c179e5171ec22ed574b1b135a3c6104fb7d4/librt-0.7.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bb41f04046b4f22b1e7ba5ef513402cd2e3477ec610e5f92d38fe2bba383d419", size = 174538, upload-time = "2025-12-25T03:52:08.075Z" }, + { url = "https://files.pythonhosted.org/packages/44/5c/0127098743575d5340624d8d4ec508d4d5ff0877dcee6f55f54bf03e5ed0/librt-0.7.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8bb7883c1e94ceb87c2bf81385266f032da09cd040e804cc002f2c9d6b842e2f", size = 195240, upload-time = "2025-12-25T03:52:09.427Z" }, + { url = "https://files.pythonhosted.org/packages/47/0f/be028c3e906a8ee6d29a42fd362e6d57d4143057f2bc0c454d489a0f898b/librt-0.7.5-cp311-cp311-win32.whl", hash = "sha256:84d4a6b9efd6124f728558a18e79e7cc5c5d4efc09b2b846c910de7e564f5bad", size = 42941, upload-time = "2025-12-25T03:52:10.527Z" }, + { url = "https://files.pythonhosted.org/packages/ac/3a/2f0ed57f4c3ae3c841780a95dfbea4cd811c6842d9ee66171ce1af606d25/librt-0.7.5-cp311-cp311-win_amd64.whl", hash = "sha256:ab4b0d3bee6f6ff7017e18e576ac7e41a06697d8dea4b8f3ab9e0c8e1300c409", size = 49244, upload-time = "2025-12-25T03:52:11.832Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7c/d7932aedfa5a87771f9e2799e7185ec3a322f4a1f4aa87c234159b75c8c8/librt-0.7.5-cp311-cp311-win_arm64.whl", hash = "sha256:730be847daad773a3c898943cf67fb9845a3961d06fb79672ceb0a8cd8624cfa", size = 42614, upload-time = "2025-12-25T03:52:12.745Z" }, + { url = "https://files.pythonhosted.org/packages/33/9d/cb0a296cee177c0fee7999ada1c1af7eee0e2191372058814a4ca6d2baf0/librt-0.7.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba1077c562a046208a2dc6366227b3eeae8f2c2ab4b41eaf4fd2fa28cece4203", size = 55689, upload-time = "2025-12-25T03:52:14.041Z" }, + { url = "https://files.pythonhosted.org/packages/79/5c/d7de4d4228b74c5b81a3fbada157754bb29f0e1f8c38229c669a7f90422a/librt-0.7.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:654fdc971c76348a73af5240d8e2529265b9a7ba6321e38dd5bae7b0d4ab3abe", size = 57142, upload-time = "2025-12-25T03:52:15.336Z" }, + { url = "https://files.pythonhosted.org/packages/e5/b2/5da779184aae369b69f4ae84225f63741662a0fe422e91616c533895d7a4/librt-0.7.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6b7b58913d475911f6f33e8082f19dd9b120c4f4a5c911d07e395d67b81c6982", size = 165323, upload-time = "2025-12-25T03:52:16.384Z" }, + { url = "https://files.pythonhosted.org/packages/5a/40/6d5abc15ab6cc70e04c4d201bb28baffff4cfb46ab950b8e90935b162d58/librt-0.7.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8e0fd344bad57026a8f4ccfaf406486c2fc991838050c2fef156170edc3b775", size = 174218, upload-time = "2025-12-25T03:52:17.518Z" }, + { url = "https://files.pythonhosted.org/packages/0d/d0/5239a8507e6117a3cb59ce0095bdd258bd2a93d8d4b819a506da06d8d645/librt-0.7.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46aa91813c267c3f60db75d56419b42c0c0b9748ec2c568a0e3588e543fb4233", size = 189007, upload-time = "2025-12-25T03:52:18.585Z" }, + { url = "https://files.pythonhosted.org/packages/1f/a4/8eed1166ffddbb01c25363e4c4e655f4bac298debe9e5a2dcfaf942438a1/librt-0.7.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ddc0ab9dbc5f9ceaf2bf7a367bf01f2697660e908f6534800e88f43590b271db", size = 183962, upload-time = "2025-12-25T03:52:19.723Z" }, + { url = "https://files.pythonhosted.org/packages/a1/83/260e60aab2f5ccba04579c5c46eb3b855e51196fde6e2bcf6742d89140a8/librt-0.7.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7a488908a470451338607650f1c064175094aedebf4a4fa37890682e30ce0b57", size = 177611, upload-time = "2025-12-25T03:52:21.18Z" }, + { url = "https://files.pythonhosted.org/packages/c4/36/6dcfed0df41e9695665462bab59af15b7ed2b9c668d85c7ebadd022cbb76/librt-0.7.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e47fc52602ffc374e69bf1b76536dc99f7f6dd876bd786c8213eaa3598be030a", size = 199273, upload-time = "2025-12-25T03:52:22.25Z" }, + { url = "https://files.pythonhosted.org/packages/a6/b7/157149c8cffae6bc4293a52e0267860cee2398cb270798d94f1c8a69b9ae/librt-0.7.5-cp312-cp312-win32.whl", hash = "sha256:cda8b025875946ffff5a9a7590bf9acde3eb02cb6200f06a2d3e691ef3d9955b", size = 43191, upload-time = "2025-12-25T03:52:23.643Z" }, + { url = "https://files.pythonhosted.org/packages/f8/91/197dfeb8d3bdeb0a5344d0d8b3077f183ba5e76c03f158126f6072730998/librt-0.7.5-cp312-cp312-win_amd64.whl", hash = "sha256:b591c094afd0ffda820e931148c9e48dc31a556dc5b2b9b3cc552fa710d858e4", size = 49462, upload-time = "2025-12-25T03:52:24.637Z" }, + { url = "https://files.pythonhosted.org/packages/03/ea/052a79454cc52081dfaa9a1c4c10a529f7a6a6805b2fac5805fea5b25975/librt-0.7.5-cp312-cp312-win_arm64.whl", hash = "sha256:532ddc6a8a6ca341b1cd7f4d999043e4c71a212b26fe9fd2e7f1e8bb4e873544", size = 42830, upload-time = "2025-12-25T03:52:25.944Z" }, + { url = "https://files.pythonhosted.org/packages/9f/9a/8f61e16de0ff76590af893cfb5b1aa5fa8b13e5e54433d0809c7033f59ed/librt-0.7.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b1795c4b2789b458fa290059062c2f5a297ddb28c31e704d27e161386469691a", size = 55750, upload-time = "2025-12-25T03:52:26.975Z" }, + { url = "https://files.pythonhosted.org/packages/05/7c/a8a883804851a066f301e0bad22b462260b965d5c9e7fe3c5de04e6f91f8/librt-0.7.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2fcbf2e135c11f721193aa5f42ba112bb1046afafbffd407cbc81d8d735c74d0", size = 57170, upload-time = "2025-12-25T03:52:27.948Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5d/b3b47facf5945be294cf8a835b03589f70ee0e791522f99ec6782ed738b3/librt-0.7.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c039bbf79a9a2498404d1ae7e29a6c175e63678d7a54013a97397c40aee026c5", size = 165834, upload-time = "2025-12-25T03:52:29.09Z" }, + { url = "https://files.pythonhosted.org/packages/b4/b6/b26910cd0a4e43e5d02aacaaea0db0d2a52e87660dca08293067ee05601a/librt-0.7.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3919c9407faeeee35430ae135e3a78acd4ecaaaa73767529e2c15ca1d73ba325", size = 174820, upload-time = "2025-12-25T03:52:30.463Z" }, + { url = "https://files.pythonhosted.org/packages/a5/a3/81feddd345d4c869b7a693135a462ae275f964fcbbe793d01ea56a84c2ee/librt-0.7.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26b46620e1e0e45af510d9848ea0915e7040605dd2ae94ebefb6c962cbb6f7ec", size = 189609, upload-time = "2025-12-25T03:52:31.492Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a9/31310796ef4157d1d37648bf4a3b84555319f14cee3e9bad7bdd7bfd9a35/librt-0.7.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9bbb8facc5375476d392990dd6a71f97e4cb42e2ac66f32e860f6e47299d5e89", size = 184589, upload-time = "2025-12-25T03:52:32.59Z" }, + { url = "https://files.pythonhosted.org/packages/32/22/da3900544cb0ac6ab7a2857850158a0a093b86f92b264aa6c4a4f2355ff3/librt-0.7.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e9e9c988b5ffde7be02180f864cbd17c0b0c1231c235748912ab2afa05789c25", size = 178251, upload-time = "2025-12-25T03:52:33.745Z" }, + { url = "https://files.pythonhosted.org/packages/db/77/78e02609846e78b9b8c8e361753b3dbac9a07e6d5b567fe518de9e074ab0/librt-0.7.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:edf6b465306215b19dbe6c3fb63cf374a8f3e1ad77f3b4c16544b83033bbb67b", size = 199852, upload-time = "2025-12-25T03:52:34.826Z" }, + { url = "https://files.pythonhosted.org/packages/2a/25/05706f6b346429c951582f1b3561f4d5e1418d0d7ba1a0c181237cd77b3b/librt-0.7.5-cp313-cp313-win32.whl", hash = "sha256:060bde69c3604f694bd8ae21a780fe8be46bb3dbb863642e8dfc75c931ca8eee", size = 43250, upload-time = "2025-12-25T03:52:35.905Z" }, + { url = "https://files.pythonhosted.org/packages/d9/59/c38677278ac0b9ae1afc611382ef6c9ea87f52ad257bd3d8d65f0eacdc6a/librt-0.7.5-cp313-cp313-win_amd64.whl", hash = "sha256:a82d5a0ee43aeae2116d7292c77cc8038f4841830ade8aa922e098933b468b9e", size = 49421, upload-time = "2025-12-25T03:52:36.895Z" }, + { url = "https://files.pythonhosted.org/packages/c0/47/1d71113df4a81de5fdfbd3d7244e05d3d67e89f25455c3380ca50b92741e/librt-0.7.5-cp313-cp313-win_arm64.whl", hash = "sha256:3c98a8d0ac9e2a7cb8ff8c53e5d6e8d82bfb2839abf144fdeaaa832f2a12aa45", size = 42827, upload-time = "2025-12-25T03:52:37.856Z" }, + { url = "https://files.pythonhosted.org/packages/97/ae/8635b4efdc784220f1378be640d8b1a794332f7f6ea81bb4859bf9d18aa7/librt-0.7.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:9937574e6d842f359b8585903d04f5b4ab62277a091a93e02058158074dc52f2", size = 55191, upload-time = "2025-12-25T03:52:38.839Z" }, + { url = "https://files.pythonhosted.org/packages/52/11/ed7ef6955dc2032af37db9b0b31cd5486a138aa792e1bb9e64f0f4950e27/librt-0.7.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5cd3afd71e9bc146203b6c8141921e738364158d4aa7cdb9a874e2505163770f", size = 56894, upload-time = "2025-12-25T03:52:39.805Z" }, + { url = "https://files.pythonhosted.org/packages/24/f1/02921d4a66a1b5dcd0493b89ce76e2762b98c459fe2ad04b67b2ea6fdd39/librt-0.7.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9cffa3ef0af29687455161cb446eff059bf27607f95163d6a37e27bcb37180f6", size = 163726, upload-time = "2025-12-25T03:52:40.79Z" }, + { url = "https://files.pythonhosted.org/packages/65/87/27df46d2756fcb7a82fa7f6ca038a0c6064c3e93ba65b0b86fbf6a4f76a2/librt-0.7.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82f3f088482e2229387eadf8215c03f7726d56f69cce8c0c40f0795aebc9b361", size = 172470, upload-time = "2025-12-25T03:52:42.226Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a9/e65a35e5d423639f4f3d8e17301ff13cc41c2ff97677fe9c361c26dbfbb7/librt-0.7.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7aa33153a5bb0bac783d2c57885889b1162823384e8313d47800a0e10d0070e", size = 186807, upload-time = "2025-12-25T03:52:43.688Z" }, + { url = "https://files.pythonhosted.org/packages/d7/b0/ac68aa582a996b1241773bd419823290c42a13dc9f494704a12a17ddd7b6/librt-0.7.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:265729b551a2dd329cc47b323a182fb7961af42abf21e913c9dd7d3331b2f3c2", size = 181810, upload-time = "2025-12-25T03:52:45.095Z" }, + { url = "https://files.pythonhosted.org/packages/e1/c1/03f6717677f20acd2d690813ec2bbe12a2de305f32c61479c53f7b9413bc/librt-0.7.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:168e04663e126416ba712114050f413ac306759a1791d87b7c11d4428ba75760", size = 175599, upload-time = "2025-12-25T03:52:46.177Z" }, + { url = "https://files.pythonhosted.org/packages/01/d7/f976ff4c07c59b69bb5eec7e5886d43243075bbef834428124b073471c86/librt-0.7.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:553dc58987d1d853adda8aeadf4db8e29749f0b11877afcc429a9ad892818ae2", size = 196506, upload-time = "2025-12-25T03:52:47.327Z" }, + { url = "https://files.pythonhosted.org/packages/b7/74/004f068b8888e61b454568b5479f88018fceb14e511ac0609cccee7dd227/librt-0.7.5-cp314-cp314-win32.whl", hash = "sha256:263f4fae9eba277513357c871275b18d14de93fd49bf5e43dc60a97b81ad5eb8", size = 39747, upload-time = "2025-12-25T03:52:48.437Z" }, + { url = "https://files.pythonhosted.org/packages/37/b1/ea3ec8fcf5f0a00df21f08972af77ad799604a306db58587308067d27af8/librt-0.7.5-cp314-cp314-win_amd64.whl", hash = "sha256:85f485b7471571e99fab4f44eeb327dc0e1f814ada575f3fa85e698417d8a54e", size = 45970, upload-time = "2025-12-25T03:52:49.389Z" }, + { url = "https://files.pythonhosted.org/packages/5d/30/5e3fb7ac4614a50fc67e6954926137d50ebc27f36419c9963a94f931f649/librt-0.7.5-cp314-cp314-win_arm64.whl", hash = "sha256:49c596cd18e90e58b7caa4d7ca7606049c1802125fcff96b8af73fa5c3870e4d", size = 39075, upload-time = "2025-12-25T03:52:50.395Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7f/0af0a9306a06c2aabee3a790f5aa560c50ec0a486ab818a572dd3db6c851/librt-0.7.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:54d2aef0b0f5056f130981ad45081b278602ff3657fe16c88529f5058038e802", size = 57375, upload-time = "2025-12-25T03:52:51.439Z" }, + { url = "https://files.pythonhosted.org/packages/57/1f/c85e510baf6572a3d6ef40c742eacedc02973ed2acdb5dba2658751d9af8/librt-0.7.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0b4791202296ad51ac09a3ff58eb49d9da8e3a4009167a6d76ac418a974e5fd4", size = 59234, upload-time = "2025-12-25T03:52:52.687Z" }, + { url = "https://files.pythonhosted.org/packages/49/b1/bb6535e4250cd18b88d6b18257575a0239fa1609ebba925f55f51ae08e8e/librt-0.7.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e860909fea75baef941ee6436e0453612505883b9d0d87924d4fda27865b9a2", size = 183873, upload-time = "2025-12-25T03:52:53.705Z" }, + { url = "https://files.pythonhosted.org/packages/8e/49/ad4a138cca46cdaa7f0e15fa912ce3ccb4cc0d4090bfeb8ccc35766fa6d5/librt-0.7.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f02c4337bf271c4f06637f5ff254fad2238c0b8e32a3a480ebb2fc5e26f754a5", size = 194609, upload-time = "2025-12-25T03:52:54.884Z" }, + { url = "https://files.pythonhosted.org/packages/9c/2d/3b3cb933092d94bb2c1d3c9b503d8775f08d806588c19a91ee4d1495c2a8/librt-0.7.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7f51ffe59f4556243d3cc82d827bde74765f594fa3ceb80ec4de0c13ccd3416", size = 206777, upload-time = "2025-12-25T03:52:55.969Z" }, + { url = "https://files.pythonhosted.org/packages/3a/52/6e7611d3d1347812233dabc44abca4c8065ee97b83c9790d7ecc3f782bc8/librt-0.7.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0b7f080ba30601dfa3e3deed3160352273e1b9bc92e652f51103c3e9298f7899", size = 203208, upload-time = "2025-12-25T03:52:57.036Z" }, + { url = "https://files.pythonhosted.org/packages/27/aa/466ae4654bd2d45903fbf180815d41e3ae8903e5a1861f319f73c960a843/librt-0.7.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:fb565b4219abc8ea2402e61c7ba648a62903831059ed3564fa1245cc245d58d7", size = 196698, upload-time = "2025-12-25T03:52:58.481Z" }, + { url = "https://files.pythonhosted.org/packages/97/8f/424f7e4525bb26fe0d3e984d1c0810ced95e53be4fd867ad5916776e18a3/librt-0.7.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a3cfb15961e7333ea6ef033dc574af75153b5c230d5ad25fbcd55198f21e0cf", size = 217194, upload-time = "2025-12-25T03:52:59.575Z" }, + { url = "https://files.pythonhosted.org/packages/9e/33/13a4cb798a171b173f3c94db23adaf13a417130e1493933dc0df0d7fb439/librt-0.7.5-cp314-cp314t-win32.whl", hash = "sha256:118716de5ad6726332db1801bc90fa6d94194cd2e07c1a7822cebf12c496714d", size = 40282, upload-time = "2025-12-25T03:53:01.091Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f1/62b136301796399d65dad73b580f4509bcbd347dff885a450bff08e80cb6/librt-0.7.5-cp314-cp314t-win_amd64.whl", hash = "sha256:3dd58f7ce20360c6ce0c04f7bd9081c7f9c19fc6129a3c705d0c5a35439f201d", size = 46764, upload-time = "2025-12-25T03:53:02.381Z" }, + { url = "https://files.pythonhosted.org/packages/49/cb/940431d9410fda74f941f5cd7f0e5a22c63be7b0c10fa98b2b7022b48cb1/librt-0.7.5-cp314-cp314t-win_arm64.whl", hash = "sha256:08153ea537609d11f774d2bfe84af39d50d5c9ca3a4d061d946e0c9d8bce04a1", size = 39728, upload-time = "2025-12-25T03:53:03.306Z" }, + { url = "https://files.pythonhosted.org/packages/49/9e/882da7f7cb8dd36a5ede60dd612c481f2e8322f84d4aaee46921e167877d/librt-0.7.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:df2e210400b28e50994477ebf82f055698c79797b6ee47a1669d383ca33263e1", size = 54841, upload-time = "2025-12-25T03:53:04.404Z" }, + { url = "https://files.pythonhosted.org/packages/79/f3/ccb73a438a7e8519859941766efcb3382592e3e187ec1730d08778ab9c2b/librt-0.7.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d2cc7d187e8c6e9b7bdbefa9697ce897a704ea7a7ce844f2b4e0e2aa07ae51d3", size = 56804, upload-time = "2025-12-25T03:53:05.424Z" }, + { url = "https://files.pythonhosted.org/packages/7d/48/0b1d28f7627e1bdda82584974bb2def9cee9511ee3098772ba93c6677370/librt-0.7.5-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:39183abee670bc37b85f11e86c44a9cad1ed6efa48b580083e89ecee13dd9717", size = 159681, upload-time = "2025-12-25T03:53:06.452Z" }, + { url = "https://files.pythonhosted.org/packages/70/8e/cd2dc62b47c49c038728b2cedad54f4e73d371efbadbff9c22727ee0b94a/librt-0.7.5-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:191cbd42660446d67cf7a95ac7bfa60f49b8b3b0417c64f216284a1d86fc9335", size = 168516, upload-time = "2025-12-25T03:53:07.506Z" }, + { url = "https://files.pythonhosted.org/packages/35/43/ad71af75a76a0756338497487664b2b309a24d8daf15fa4ff335c830ae3e/librt-0.7.5-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea1b60b86595a5dc1f57b44a801a1c4d8209c0a69518391d349973a4491408e6", size = 182230, upload-time = "2025-12-25T03:53:08.584Z" }, + { url = "https://files.pythonhosted.org/packages/f0/b9/60b0821ca49998846279f977734c372619bd7d07c6ccbc52dc8f8b42a3db/librt-0.7.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:af69d9e159575e877c7546d1ee817b4ae089aa221dd1117e20c24ad8dc8659c7", size = 178270, upload-time = "2025-12-25T03:53:09.663Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b1/599d6167aa2ef5202eb9c63b4ab9730a348674bdafec6f470bfa31f7d5d5/librt-0.7.5-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0e2bf8f91093fac43e3eaebacf777f12fd539dce9ec5af3efc6d8424e96ccd49", size = 172568, upload-time = "2025-12-25T03:53:10.798Z" }, + { url = "https://files.pythonhosted.org/packages/5f/88/043a99e1453c82b501a44cc86e22788ef990eae57e4f2f5d75e2e11f47da/librt-0.7.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8dcae24de1bc9da93aa689cb6313c70e776d7cea2fcf26b9b6160fedfe6bd9af", size = 192747, upload-time = "2025-12-25T03:53:11.906Z" }, + { url = "https://files.pythonhosted.org/packages/4c/dc/85480f31db9049300bb757063f4364618400a5add4e1e45fa0fe055ab6f4/librt-0.7.5-cp39-cp39-win32.whl", hash = "sha256:cdb001a1a0e4f41e613bca2c0fc147fc8a7396f53fc94201cbfd8ec7cd69ca4b", size = 42552, upload-time = "2025-12-25T03:53:13.282Z" }, + { url = "https://files.pythonhosted.org/packages/bc/99/a743723826bf0e765eda50849c00f7c26102fcbd9a04e82adfd062ad2017/librt-0.7.5-cp39-cp39-win_amd64.whl", hash = "sha256:a9eacbf983319b26b5f340a2e0cd47ac1ee4725a7f3a72fd0f15063c934b69d6", size = 48959, upload-time = "2025-12-25T03:53:14.972Z" }, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, +] + +[[package]] +name = "mypy" +version = "1.14.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "mypy-extensions", marker = "python_full_version < '3.9'" }, + { name = "tomli", marker = "python_full_version < '3.9'" }, + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/eb/2c92d8ea1e684440f54fa49ac5d9a5f19967b7b472a281f419e69a8d228e/mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6", size = 3216051, upload-time = "2024-12-30T16:39:07.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/7a/87ae2adb31d68402da6da1e5f30c07ea6063e9f09b5e7cfc9dfa44075e74/mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb", size = 11211002, upload-time = "2024-12-30T16:37:22.435Z" }, + { url = "https://files.pythonhosted.org/packages/e1/23/eada4c38608b444618a132be0d199b280049ded278b24cbb9d3fc59658e4/mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0", size = 10358400, upload-time = "2024-12-30T16:37:53.526Z" }, + { url = "https://files.pythonhosted.org/packages/43/c9/d6785c6f66241c62fd2992b05057f404237deaad1566545e9f144ced07f5/mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d", size = 12095172, upload-time = "2024-12-30T16:37:50.332Z" }, + { url = "https://files.pythonhosted.org/packages/c3/62/daa7e787770c83c52ce2aaf1a111eae5893de9e004743f51bfcad9e487ec/mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b", size = 12828732, upload-time = "2024-12-30T16:37:29.96Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a2/5fb18318a3637f29f16f4e41340b795da14f4751ef4f51c99ff39ab62e52/mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427", size = 13012197, upload-time = "2024-12-30T16:38:05.037Z" }, + { url = "https://files.pythonhosted.org/packages/28/99/e153ce39105d164b5f02c06c35c7ba958aaff50a2babba7d080988b03fe7/mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f", size = 9780836, upload-time = "2024-12-30T16:37:19.726Z" }, + { url = "https://files.pythonhosted.org/packages/da/11/a9422850fd506edbcdc7f6090682ecceaf1f87b9dd847f9df79942da8506/mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c", size = 11120432, upload-time = "2024-12-30T16:37:11.533Z" }, + { url = "https://files.pythonhosted.org/packages/b6/9e/47e450fd39078d9c02d620545b2cb37993a8a8bdf7db3652ace2f80521ca/mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1", size = 10279515, upload-time = "2024-12-30T16:37:40.724Z" }, + { url = "https://files.pythonhosted.org/packages/01/b5/6c8d33bd0f851a7692a8bfe4ee75eb82b6983a3cf39e5e32a5d2a723f0c1/mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8", size = 12025791, upload-time = "2024-12-30T16:36:58.73Z" }, + { url = "https://files.pythonhosted.org/packages/f0/4c/e10e2c46ea37cab5c471d0ddaaa9a434dc1d28650078ac1b56c2d7b9b2e4/mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f", size = 12749203, upload-time = "2024-12-30T16:37:03.741Z" }, + { url = "https://files.pythonhosted.org/packages/88/55/beacb0c69beab2153a0f57671ec07861d27d735a0faff135a494cd4f5020/mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1", size = 12885900, upload-time = "2024-12-30T16:37:57.948Z" }, + { url = "https://files.pythonhosted.org/packages/a2/75/8c93ff7f315c4d086a2dfcde02f713004357d70a163eddb6c56a6a5eff40/mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae", size = 9777869, upload-time = "2024-12-30T16:37:33.428Z" }, + { url = "https://files.pythonhosted.org/packages/43/1b/b38c079609bb4627905b74fc6a49849835acf68547ac33d8ceb707de5f52/mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14", size = 11266668, upload-time = "2024-12-30T16:38:02.211Z" }, + { url = "https://files.pythonhosted.org/packages/6b/75/2ed0d2964c1ffc9971c729f7a544e9cd34b2cdabbe2d11afd148d7838aa2/mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9", size = 10254060, upload-time = "2024-12-30T16:37:46.131Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5f/7b8051552d4da3c51bbe8fcafffd76a6823779101a2b198d80886cd8f08e/mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11", size = 11933167, upload-time = "2024-12-30T16:37:43.534Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/f53971d3ac39d8b68bbaab9a4c6c58c8caa4d5fd3d587d16f5927eeeabe1/mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e", size = 12864341, upload-time = "2024-12-30T16:37:36.249Z" }, + { url = "https://files.pythonhosted.org/packages/03/d2/8bc0aeaaf2e88c977db41583559319f1821c069e943ada2701e86d0430b7/mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89", size = 12972991, upload-time = "2024-12-30T16:37:06.743Z" }, + { url = "https://files.pythonhosted.org/packages/6f/17/07815114b903b49b0f2cf7499f1c130e5aa459411596668267535fe9243c/mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b", size = 9879016, upload-time = "2024-12-30T16:37:15.02Z" }, + { url = "https://files.pythonhosted.org/packages/9e/15/bb6a686901f59222275ab228453de741185f9d54fecbaacec041679496c6/mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255", size = 11252097, upload-time = "2024-12-30T16:37:25.144Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b3/8b0f74dfd072c802b7fa368829defdf3ee1566ba74c32a2cb2403f68024c/mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34", size = 10239728, upload-time = "2024-12-30T16:38:08.634Z" }, + { url = "https://files.pythonhosted.org/packages/c5/9b/4fd95ab20c52bb5b8c03cc49169be5905d931de17edfe4d9d2986800b52e/mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a", size = 11924965, upload-time = "2024-12-30T16:38:12.132Z" }, + { url = "https://files.pythonhosted.org/packages/56/9d/4a236b9c57f5d8f08ed346914b3f091a62dd7e19336b2b2a0d85485f82ff/mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9", size = 12867660, upload-time = "2024-12-30T16:38:17.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/88/a61a5497e2f68d9027de2bb139c7bb9abaeb1be1584649fa9d807f80a338/mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd", size = 12969198, upload-time = "2024-12-30T16:38:32.839Z" }, + { url = "https://files.pythonhosted.org/packages/54/da/3d6fc5d92d324701b0c23fb413c853892bfe0e1dbe06c9138037d459756b/mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107", size = 9885276, upload-time = "2024-12-30T16:38:20.828Z" }, + { url = "https://files.pythonhosted.org/packages/39/02/1817328c1372be57c16148ce7d2bfcfa4a796bedaed897381b1aad9b267c/mypy-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7084fb8f1128c76cd9cf68fe5971b37072598e7c31b2f9f95586b65c741a9d31", size = 11143050, upload-time = "2024-12-30T16:38:29.743Z" }, + { url = "https://files.pythonhosted.org/packages/b9/07/99db9a95ece5e58eee1dd87ca456a7e7b5ced6798fd78182c59c35a7587b/mypy-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f845a00b4f420f693f870eaee5f3e2692fa84cc8514496114649cfa8fd5e2c6", size = 10321087, upload-time = "2024-12-30T16:38:14.739Z" }, + { url = "https://files.pythonhosted.org/packages/9a/eb/85ea6086227b84bce79b3baf7f465b4732e0785830726ce4a51528173b71/mypy-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44bf464499f0e3a2d14d58b54674dee25c031703b2ffc35064bd0df2e0fac319", size = 12066766, upload-time = "2024-12-30T16:38:47.038Z" }, + { url = "https://files.pythonhosted.org/packages/4b/bb/f01bebf76811475d66359c259eabe40766d2f8ac8b8250d4e224bb6df379/mypy-1.14.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c99f27732c0b7dc847adb21c9d47ce57eb48fa33a17bc6d7d5c5e9f9e7ae5bac", size = 12787111, upload-time = "2024-12-30T16:39:02.444Z" }, + { url = "https://files.pythonhosted.org/packages/2f/c9/84837ff891edcb6dcc3c27d85ea52aab0c4a34740ff5f0ccc0eb87c56139/mypy-1.14.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:bce23c7377b43602baa0bd22ea3265c49b9ff0b76eb315d6c34721af4cdf1d9b", size = 12974331, upload-time = "2024-12-30T16:38:23.849Z" }, + { url = "https://files.pythonhosted.org/packages/84/5f/901e18464e6a13f8949b4909535be3fa7f823291b8ab4e4b36cfe57d6769/mypy-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:8edc07eeade7ebc771ff9cf6b211b9a7d93687ff892150cb5692e4f4272b0837", size = 9763210, upload-time = "2024-12-30T16:38:36.299Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1f/186d133ae2514633f8558e78cd658070ba686c0e9275c5a5c24a1e1f0d67/mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35", size = 11200493, upload-time = "2024-12-30T16:38:26.935Z" }, + { url = "https://files.pythonhosted.org/packages/af/fc/4842485d034e38a4646cccd1369f6b1ccd7bc86989c52770d75d719a9941/mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc", size = 10357702, upload-time = "2024-12-30T16:38:50.623Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e6/457b83f2d701e23869cfec013a48a12638f75b9d37612a9ddf99072c1051/mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9", size = 12091104, upload-time = "2024-12-30T16:38:53.735Z" }, + { url = "https://files.pythonhosted.org/packages/f1/bf/76a569158db678fee59f4fd30b8e7a0d75bcbaeef49edd882a0d63af6d66/mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb", size = 12830167, upload-time = "2024-12-30T16:38:56.437Z" }, + { url = "https://files.pythonhosted.org/packages/43/bc/0bc6b694b3103de9fed61867f1c8bd33336b913d16831431e7cb48ef1c92/mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60", size = 13013834, upload-time = "2024-12-30T16:38:59.204Z" }, + { url = "https://files.pythonhosted.org/packages/b0/79/5f5ec47849b6df1e6943d5fd8e6632fbfc04b4fd4acfa5a5a9535d11b4e2/mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c", size = 9781231, upload-time = "2024-12-30T16:39:05.124Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b5/32dd67b69a16d088e533962e5044e51004176a9952419de0370cdaead0f8/mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1", size = 2752905, upload-time = "2024-12-30T16:38:42.021Z" }, +] + +[[package]] +name = "mypy" +version = "1.19.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "librt", marker = "python_full_version >= '3.9' and platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions", marker = "python_full_version >= '3.9'" }, + { name = "pathspec", marker = "python_full_version >= '3.9'" }, + { name = "tomli", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" }, + { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" }, + { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, + { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f7/88436084550ca9af5e610fa45286be04c3b63374df3e021c762fe8c4369f/mypy-1.19.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bcfc336a03a1aaa26dfce9fff3e287a3ba99872a157561cbfcebe67c13308e3", size = 13102606, upload-time = "2025-12-15T05:02:46.833Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a5/43dfad311a734b48a752790571fd9e12d61893849a01bff346a54011957f/mypy-1.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b7951a701c07ea584c4fe327834b92a30825514c868b1f69c30445093fdd9d5a", size = 12164496, upload-time = "2025-12-15T05:03:41.947Z" }, + { url = "https://files.pythonhosted.org/packages/88/f0/efbfa391395cce2f2771f937e0620cfd185ec88f2b9cd88711028a768e96/mypy-1.19.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b13cfdd6c87fc3efb69ea4ec18ef79c74c3f98b4e5498ca9b85ab3b2c2329a67", size = 12772068, upload-time = "2025-12-15T05:02:53.689Z" }, + { url = "https://files.pythonhosted.org/packages/25/05/58b3ba28f5aed10479e899a12d2120d582ba9fa6288851b20bf1c32cbb4f/mypy-1.19.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f28f99c824ecebcdaa2e55d82953e38ff60ee5ec938476796636b86afa3956e", size = 13520385, upload-time = "2025-12-15T05:02:38.328Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a0/c006ccaff50b31e542ae69b92fe7e2f55d99fba3a55e01067dd564325f85/mypy-1.19.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c608937067d2fc5a4dd1a5ce92fd9e1398691b8c5d012d66e1ddd430e9244376", size = 13796221, upload-time = "2025-12-15T05:03:22.147Z" }, + { url = "https://files.pythonhosted.org/packages/b2/ff/8bdb051cd710f01b880472241bd36b3f817a8e1c5d5540d0b761675b6de2/mypy-1.19.1-cp39-cp39-win_amd64.whl", hash = "sha256:409088884802d511ee52ca067707b90c883426bd95514e8cfda8281dc2effe24", size = 10055456, upload-time = "2025-12-15T05:03:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302, upload-time = "2024-09-17T19:06:50.688Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439, upload-time = "2024-09-17T19:06:49.212Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, +] + +[[package]] +name = "pycodestyle" +version = "2.12.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/43/aa/210b2c9aedd8c1cbeea31a50e42050ad56187754b34eb214c46709445801/pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521", size = 39232, upload-time = "2024-08-04T20:26:54.576Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/d8/a211b3f85e99a0daa2ddec96c949cac6824bd305b040571b82a03dd62636/pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3", size = 31284, upload-time = "2024-08-04T20:26:53.173Z" }, +] + +[[package]] +name = "pycodestyle" +version = "2.14.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", size = 39472, upload-time = "2025-06-20T18:49:48.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" }, +] + +[[package]] +name = "pyflakes" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/57/f9/669d8c9c86613c9d568757c7f5824bd3197d7b1c6c27553bc5618a27cce2/pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f", size = 63788, upload-time = "2024-01-05T00:28:47.703Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/d7/f1b7db88d8e4417c5d47adad627a93547f44bdc9028372dbd2313f34a855/pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a", size = 62725, upload-time = "2024-01-05T00:28:45.903Z" }, +] + +[[package]] +name = "pyflakes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/45/dc/fd034dc20b4b264b3d015808458391acbf9df40b1e54750ef175d39180b1/pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58", size = 64669, upload-time = "2025-06-20T18:45:27.834Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551, upload-time = "2025-06-20T18:45:26.937Z" }, +] + +[[package]] +name = "python-fluent" +version = "0" +source = { virtual = "." } +dependencies = [ + { name = "black", version = "24.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "black", version = "24.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "flake8", version = "7.1.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "flake8", version = "7.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "fluent-syntax" }, + { name = "isort" }, + { name = "mypy", version = "1.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "mypy", version = "1.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "types-babel" }, + { name = "types-pytz", version = "2024.2.0.20241221", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "types-pytz", version = "2025.2.0.20251108", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] + +[package.metadata] +requires-dist = [ + { name = "black", specifier = "~=24.0" }, + { name = "flake8", specifier = "~=7.0" }, + { name = "fluent-syntax", editable = "fluent.syntax" }, + { name = "isort", specifier = "~=5.0" }, + { name = "mypy", specifier = "~=1.0" }, + { name = "types-babel" }, + { name = "types-pytz" }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + +[[package]] +name = "tomli" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, +] + +[[package]] +name = "types-babel" +version = "2.11.0.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "types-pytz", version = "2024.2.0.20241221", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "types-pytz", version = "2025.2.0.20251108", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "types-setuptools", version = "75.8.0.20250110", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "types-setuptools", version = "80.9.0.20251223", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/b5/b18d2dd9375515126069616096470fd9efd27434abac9e8d601e71f61b12/types-babel-2.11.0.15.tar.gz", hash = "sha256:282c184c8c9d81e8269212c1b8fa0d39ee88fb8bc43be47980412781c9c85f7e", size = 14392, upload-time = "2023-07-20T15:21:10.686Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/6a/56538e54ca538552a1b83e5f3f05ba23668870276a9342df1252e4e07f73/types_babel-2.11.0.15-py3-none-any.whl", hash = "sha256:d0579f2e8adeaef3fbe2eb63e5a2ecf01767fc018e5f3f36a3c9d8b723bd62c7", size = 18442, upload-time = "2023-07-20T15:21:08.221Z" }, +] + +[[package]] +name = "types-pytz" +version = "2024.2.0.20241221" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/54/26/516311b02b5a215e721155fb65db8a965d061372e388d6125ebce8d674b0/types_pytz-2024.2.0.20241221.tar.gz", hash = "sha256:06d7cde9613e9f7504766a0554a270c369434b50e00975b3a4a0f6eed0f2c1a9", size = 10213, upload-time = "2024-12-21T02:40:48.654Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/db/c92ca6920cccd9c2998b013601542e2ac5e59bc805bcff94c94ad254b7df/types_pytz-2024.2.0.20241221-py3-none-any.whl", hash = "sha256:8fc03195329c43637ed4f593663df721fef919b60a969066e22606edf0b53ad5", size = 10008, upload-time = "2024-12-21T02:40:47.047Z" }, +] + +[[package]] +name = "types-pytz" +version = "2025.2.0.20251108" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/40/ff/c047ddc68c803b46470a357454ef76f4acd8c1088f5cc4891cdd909bfcf6/types_pytz-2025.2.0.20251108.tar.gz", hash = "sha256:fca87917836ae843f07129567b74c1929f1870610681b4c92cb86a3df5817bdb", size = 10961, upload-time = "2025-11-08T02:55:57.001Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/c1/56ef16bf5dcd255155cc736d276efa6ae0a5c26fd685e28f0412a4013c01/types_pytz-2025.2.0.20251108-py3-none-any.whl", hash = "sha256:0f1c9792cab4eb0e46c52f8845c8f77cf1e313cb3d68bf826aa867fe4717d91c", size = 10116, upload-time = "2025-11-08T02:55:56.194Z" }, +] + +[[package]] +name = "types-setuptools" +version = "75.8.0.20250110" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/42/5713e90d4f9683f2301d900f33e4fc2405ad8ac224dda30f6cb7f4cd215b/types_setuptools-75.8.0.20250110.tar.gz", hash = "sha256:96f7ec8bbd6e0a54ea180d66ad68ad7a1d7954e7281a710ea2de75e355545271", size = 48185, upload-time = "2025-01-10T02:45:52.085Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/a3/dbfd106751b11c728cec21cc62cbfe7ff7391b935c4b6e8f0bdc2e6fd541/types_setuptools-75.8.0.20250110-py3-none-any.whl", hash = "sha256:a9f12980bbf9bcdc23ecd80755789085bad6bfce4060c2275bc2b4ca9f2bc480", size = 71521, upload-time = "2025-01-10T02:45:49.873Z" }, +] + +[[package]] +name = "types-setuptools" +version = "80.9.0.20251223" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/00/07/d1b605230730990de20477150191d6dccf6aecc037da94c9960a5d563bc8/types_setuptools-80.9.0.20251223.tar.gz", hash = "sha256:d3411059ae2f5f03985217d86ac6084efea2c9e9cacd5f0869ef950f308169b2", size = 42420, upload-time = "2025-12-23T03:18:26.752Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/5c/b8877da94012dbc6643e4eeca22bca9b99b295be05d161f8a403ae9387c0/types_setuptools-80.9.0.20251223-py3-none-any.whl", hash = "sha256:1b36db79d724c2287d83dc052cf887b47c0da6a2fff044378be0b019545f56e6", size = 64318, upload-time = "2025-12-23T03:18:25.868Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] From 97a00c11a5d3f3fdf33b2e1167202aaf2a354703 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Mon, 29 Dec 2025 18:46:04 +0200 Subject: [PATCH 02/10] Drop support & CI tests for Python < 3.8 --- .github/workflows/ci.yml | 10 +++++----- fluent.runtime/pyproject.toml | 4 +--- fluent.syntax/pyproject.toml | 4 +--- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb466d26..d0202331 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: strategy: matrix: os: [ubuntu-22.04, windows-2022] - python-version: [3.7, 3.8, 3.9, "3.10", 3.11, 3.12, pypy3.9, pypy3.10] + python-version: [3.8, 3.9, "3.10", 3.11, 3.12, pypy3.9, pypy3.10] steps: - uses: actions/checkout@v6 - uses: astral-sh/setup-uv@v7 @@ -34,10 +34,9 @@ jobs: - run: uv run python -m unittest discover -s fluent.syntax - run: uv run python -m unittest discover -s fluent.runtime - # Test compatibility with the oldest Python version we claim to support, - # and for fluent.runtime's compatibility with a range of fluent.syntax versions. + # Test fluent.runtime's compatibility with a range of fluent.syntax versions. compatibility: - runs-on: ubuntu-20.04 # https://github.com/actions/setup-python/issues/544 + runs-on: ubuntu-22.04 strategy: matrix: fluent-syntax: @@ -49,7 +48,8 @@ jobs: - uses: actions/checkout@v6 - uses: astral-sh/setup-uv@v7 with: - python-version: 3.6 + python-version: 3.8 + - run: uv venv --python 3.8 - run: uv pip install ${{ matrix.fluent-syntax }} - run: uv pip install ./fluent.runtime - run: uv run python -m unittest discover -s fluent.runtime diff --git a/fluent.runtime/pyproject.toml b/fluent.runtime/pyproject.toml index 84d23cfa..163f7cb4 100644 --- a/fluent.runtime/pyproject.toml +++ b/fluent.runtime/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "fluent.runtime" version = "0.4.0" -requires-python = ">= 3.6" +requires-python = ">= 3.8" dependencies = [ "fluent.syntax>=0.17,<0.20", "attrs", @@ -21,8 +21,6 @@ classifiers = [ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3 :: Only", diff --git a/fluent.syntax/pyproject.toml b/fluent.syntax/pyproject.toml index 291da817..013291fb 100644 --- a/fluent.syntax/pyproject.toml +++ b/fluent.syntax/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "fluent.syntax" version = "0.19.0" -requires-python = ">= 3.6" +requires-python = ">= 3.8" dependencies = ["typing-extensions>=3.7,<5"] license = { text = "Apache-2.0" } description = "Localization library for expressive translations." @@ -14,8 +14,6 @@ classifiers = [ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3 :: Only", From 960863a11744f7bb9ad4e4d1c5a68b729e4b5dd6 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Mon, 29 Dec 2025 19:00:07 +0200 Subject: [PATCH 03/10] Include .vscode/settings.json to fix Pylance search paths --- .vscode/settings.json | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..df1d83b7 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "python.autoComplete.extraPaths": ["./fluent.syntax"], + "python.analysis.extraPaths": ["./fluent.syntax"], +} From 8c10de069e4432ced86f3d756c537379c87cb6f3 Mon Sep 17 00:00:00 2001 From: Zrekryu <145272593+zrekryu@users.noreply.github.com> Date: Tue, 30 Dec 2025 03:17:39 +0530 Subject: [PATCH 04/10] Fix type hint for FluentBundle functions arg (#214) --- fluent.runtime/fluent/runtime/builtins.py | 4 ++-- fluent.runtime/fluent/runtime/bundle.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/fluent.runtime/fluent/runtime/builtins.py b/fluent.runtime/fluent/runtime/builtins.py index cb16f9d8..02e817b8 100644 --- a/fluent.runtime/fluent/runtime/builtins.py +++ b/fluent.runtime/fluent/runtime/builtins.py @@ -1,4 +1,4 @@ -from typing import Any, Callable, Dict +from typing import Callable, Dict from .types import FluentType, fluent_date, fluent_number @@ -6,7 +6,7 @@ DATETIME = fluent_date -BUILTINS: Dict[str, Callable[[Any], FluentType]] = { +BUILTINS: Dict[str, Callable[..., FluentType]] = { "NUMBER": NUMBER, "DATETIME": DATETIME, } diff --git a/fluent.runtime/fluent/runtime/bundle.py b/fluent.runtime/fluent/runtime/bundle.py index 6a0f254d..a88fdab7 100644 --- a/fluent.runtime/fluent/runtime/bundle.py +++ b/fluent.runtime/fluent/runtime/bundle.py @@ -35,7 +35,7 @@ class FluentBundle: def __init__( self, locales: List[str], - functions: Union[Dict[str, Callable[[Any], "FluentType"]], None] = None, + functions: Union[Dict[str, Callable[..., "FluentType"]], None] = None, use_isolating: bool = True, ): self.locales = locales From ba386c2e03b4d101290a85034182d3d223a535d5 Mon Sep 17 00:00:00 2001 From: loudblow <145815114+loudblow@users.noreply.github.com> Date: Tue, 30 Dec 2025 13:58:02 +0400 Subject: [PATCH 05/10] runtime: Add FluentLocalization.format_message() (#212) --- fluent.runtime/docs/usage.rst | 35 ++++++++++++++ fluent.runtime/fluent/runtime/__init__.py | 3 +- fluent.runtime/fluent/runtime/fallback.py | 55 ++++++++++++++++----- fluent.runtime/tests/test_fallback.py | 59 +++++++++++++++++++++-- 4 files changed, 135 insertions(+), 17 deletions(-) diff --git a/fluent.runtime/docs/usage.rst b/fluent.runtime/docs/usage.rst index c4ad16af..c8068694 100644 --- a/fluent.runtime/docs/usage.rst +++ b/fluent.runtime/docs/usage.rst @@ -272,6 +272,41 @@ instances to indicate an error or missing data. Otherwise they should return unicode strings, or instances of a ``FluentType`` subclass as above. +Attributes +~~~~~~~~~~ +When rendering UI elements, it's handy to have a single translation that +contains everything you need in one variable. For example, a HTML +form input may have a value, but also a placeholder attribute, aria-label +attribute, and maybe a title attribute. + +.. code-block:: python + + >>> l10n = DemoLocalization(""" + ... login-input = Predefined value + ... .placeholder = { $email } + ... .aria-label = Login input value + ... .title = Type your login email + ... """) + >>> value, attributes = l10n.format_message( + ... "login-input", {"email": "email@example.com"} + ... ) + >>> value + 'Predefined value' + >>> attributes + {'placeholder': 'email@example.com', 'aria-label': 'Login input value', 'title': 'Type your login email'} + +You can also use the formatted message without unpacking it. + +.. code-block:: python + + >>> fmt_msg = l10n.format_message( + ... "login-input", {"email": "email@example.com"} + ... ) + >>> fmt_msg.value + 'Predefined value' + >>> fmt_msg.attributes + {'placeholder': 'email@example.com', 'aria-label': 'Login input value', 'title': 'Type your login email'} + Known limitations and bugs ~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/fluent.runtime/fluent/runtime/__init__.py b/fluent.runtime/fluent/runtime/__init__.py index a36d9578..beff3d29 100644 --- a/fluent.runtime/fluent/runtime/__init__.py +++ b/fluent.runtime/fluent/runtime/__init__.py @@ -2,7 +2,7 @@ from fluent.syntax.ast import Resource from .bundle import FluentBundle -from .fallback import AbstractResourceLoader, FluentLocalization, FluentResourceLoader +from .fallback import AbstractResourceLoader, FluentLocalization, FluentResourceLoader, FormattedMessage __all__ = [ "FluentLocalization", @@ -10,6 +10,7 @@ "FluentResourceLoader", "FluentResource", "FluentBundle", + "FormattedMessage", ] diff --git a/fluent.runtime/fluent/runtime/fallback.py b/fluent.runtime/fluent/runtime/fallback.py index f39bbf82..2fb6c85f 100644 --- a/fluent.runtime/fluent/runtime/fallback.py +++ b/fluent.runtime/fluent/runtime/fallback.py @@ -13,6 +13,7 @@ ) from fluent.syntax import FluentParser +from typing import NamedTuple from .bundle import FluentBundle @@ -22,6 +23,11 @@ from .types import FluentType +class FormattedMessage(NamedTuple): + value: Union[str, None] + attributes: Dict[str, str] + + class FluentLocalization: """ Generic API for Fluent applications. @@ -48,20 +54,47 @@ def __init__( self._bundle_cache: List[FluentBundle] = [] self._bundle_it = self._iterate_bundles() + def format_message( + self, msg_id: str, args: Union[Dict[str, Any], None] = None + ) -> FormattedMessage: + bundle, msg = next(( + (bundle, bundle.get_message(msg_id)) + for bundle in self._bundles() + if bundle.has_message(msg_id) + ), (None, None)) + if not bundle or not msg: + return FormattedMessage(msg_id, {}) + formatted_attrs = { + attr: cast( + str, + bundle.format_pattern(msg.attributes[attr], args)[0], + ) + for attr in msg.attributes + } + if not msg.value: + val = None + else: + val, _errors = bundle.format_pattern(msg.value, args) + return FormattedMessage( + # Never FluentNone when format_pattern called externally + cast(str, val), + formatted_attrs, + ) + def format_value( self, msg_id: str, args: Union[Dict[str, Any], None] = None ) -> str: - for bundle in self._bundles(): - if not bundle.has_message(msg_id): - continue - msg = bundle.get_message(msg_id) - if not msg.value: - continue - val, _errors = bundle.format_pattern(msg.value, args) - return cast( - str, val - ) # Never FluentNone when format_pattern called externally - return msg_id + bundle, msg = next(( + (bundle, bundle.get_message(msg_id)) + for bundle in self._bundles() + if bundle.has_message(msg_id) + ), (None, None)) + if not bundle or not msg or not msg.value: + return msg_id + val, _errors = bundle.format_pattern(msg.value, args) + return cast( + str, val + ) # Never FluentNone when format_pattern called externally def _create_bundle(self, locales: List[str]) -> FluentBundle: return self.bundle_class( diff --git a/fluent.runtime/tests/test_fallback.py b/fluent.runtime/tests/test_fallback.py index e1bc42fb..7192c022 100644 --- a/fluent.runtime/tests/test_fallback.py +++ b/fluent.runtime/tests/test_fallback.py @@ -12,11 +12,25 @@ def test_init(self): self.assertTrue(callable(l10n.format_value)) @patch_files({ - "de/one.ftl": "one = in German", - "de/two.ftl": "two = in German", - "fr/two.ftl": "three = in French", - "en/one.ftl": "four = exists", - "en/two.ftl": "five = exists", + "de/one.ftl": """one = in German + .foo = one in German + """, + "de/two.ftl": """two = in German + .foo = two in German + """, + "fr/two.ftl": """three = in French + .foo = three in French + """, + "en/one.ftl": """four = exists + .foo = four in English + """, + "en/two.ftl": """ +five = exists + .foo = five in English +bar = + .foo = bar in English +baz = baz in English + """, }) def test_bundles(self): l10n = FluentLocalization( @@ -39,6 +53,41 @@ def test_bundles(self): self.assertEqual(l10n.format_value("three"), "in French") self.assertEqual(l10n.format_value("four"), "exists") self.assertEqual(l10n.format_value("five"), "exists") + self.assertEqual(l10n.format_value("bar"), "bar") + self.assertEqual(l10n.format_value("baz"), "baz in English") + self.assertEqual(l10n.format_value("not-exists"), "not-exists") + self.assertEqual( + tuple(l10n.format_message("one")), + ("in German", {"foo": "one in German"}), + ) + self.assertEqual( + tuple(l10n.format_message("two")), + ("in German", {"foo": "two in German"}), + ) + self.assertEqual( + tuple(l10n.format_message("three")), + ("in French", {"foo": "three in French"}), + ) + self.assertEqual( + tuple(l10n.format_message("four")), + ("exists", {"foo": "four in English"}), + ) + self.assertEqual( + tuple(l10n.format_message("five")), + ("exists", {"foo": "five in English"}), + ) + self.assertEqual( + tuple(l10n.format_message("bar")), + (None, {"foo": "bar in English"}), + ) + self.assertEqual( + tuple(l10n.format_message("baz")), + ("baz in English", {}), + ) + self.assertEqual( + tuple(l10n.format_message("not-exists")), + ("not-exists", {}), + ) class TestResourceLoader(unittest.TestCase): From f73bf88eae016c45c48ca7b62e47119143881ebe Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Wed, 31 Dec 2025 12:38:51 +0200 Subject: [PATCH 06/10] Drop support for Python < 3.9 (#200) * style: Apply pyupgrade * Drop typing-extensions dependency as unnecessary * Advertise support & add CI tests for Python 3.13 and 3.14 --------- Co-authored-by: Francesco Lodolo --- .github/workflows/ci.yml | 10 +- fluent.runtime/fluent/runtime/builtins.py | 4 +- fluent.runtime/fluent/runtime/bundle.py | 19 +- fluent.runtime/fluent/runtime/fallback.py | 43 +-- fluent.runtime/fluent/runtime/prepare.py | 6 +- fluent.runtime/fluent/runtime/resolver.py | 31 +- fluent.runtime/fluent/runtime/types.py | 13 +- fluent.runtime/pyproject.toml | 9 +- .../tools/benchmarks/fluent_benchmark.py | 3 +- fluent.syntax/fluent/syntax/ast.py | 32 +- fluent.syntax/fluent/syntax/errors.py | 12 +- fluent.syntax/fluent/syntax/parser.py | 28 +- fluent.syntax/fluent/syntax/serializer.py | 32 +- fluent.syntax/fluent/syntax/stream.py | 4 +- fluent.syntax/fluent/syntax/visitor.py | 4 +- fluent.syntax/pyproject.toml | 9 +- pyproject.toml | 2 +- uv.lock | 305 ++---------------- 18 files changed, 152 insertions(+), 414 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d0202331..ba6bdf0f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: - uses: actions/checkout@v6 - uses: astral-sh/setup-uv@v7 with: - python-version: 3.8 + python-version: 3.9 - uses: actions/setup-python@v6 - run: uv sync --dev --all-packages - run: uv run python -m flake8 @@ -23,8 +23,8 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-22.04, windows-2022] - python-version: [3.8, 3.9, "3.10", 3.11, 3.12, pypy3.9, pypy3.10] + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v6 - uses: astral-sh/setup-uv@v7 @@ -48,8 +48,8 @@ jobs: - uses: actions/checkout@v6 - uses: astral-sh/setup-uv@v7 with: - python-version: 3.8 - - run: uv venv --python 3.8 + python-version: 3.12 + - run: uv venv - run: uv pip install ${{ matrix.fluent-syntax }} - run: uv pip install ./fluent.runtime - run: uv run python -m unittest discover -s fluent.runtime diff --git a/fluent.runtime/fluent/runtime/builtins.py b/fluent.runtime/fluent/runtime/builtins.py index 02e817b8..6833c172 100644 --- a/fluent.runtime/fluent/runtime/builtins.py +++ b/fluent.runtime/fluent/runtime/builtins.py @@ -1,4 +1,4 @@ -from typing import Callable, Dict +from typing import Callable from .types import FluentType, fluent_date, fluent_number @@ -6,7 +6,7 @@ DATETIME = fluent_date -BUILTINS: Dict[str, Callable[..., FluentType]] = { +BUILTINS: dict[str, Callable[..., FluentType]] = { "NUMBER": NUMBER, "DATETIME": DATETIME, } diff --git a/fluent.runtime/fluent/runtime/bundle.py b/fluent.runtime/fluent/runtime/bundle.py index a88fdab7..92db7d82 100644 --- a/fluent.runtime/fluent/runtime/bundle.py +++ b/fluent.runtime/fluent/runtime/bundle.py @@ -1,10 +1,9 @@ -from typing import TYPE_CHECKING, Any, Callable, Dict, List, Tuple, Union, cast +from typing import TYPE_CHECKING, Any, Callable, Literal, Union, cast import babel import babel.numbers import babel.plural from fluent.syntax import ast as FTL -from typing_extensions import Literal from .builtins import BUILTINS from .prepare import Compiler @@ -34,16 +33,16 @@ class FluentBundle: def __init__( self, - locales: List[str], - functions: Union[Dict[str, Callable[..., "FluentType"]], None] = None, + locales: list[str], + functions: Union[dict[str, Callable[..., "FluentType"]], None] = None, use_isolating: bool = True, ): self.locales = locales self._functions = {**BUILTINS, **(functions or {})} self.use_isolating = use_isolating - self._messages: Dict[str, Union[FTL.Message, FTL.Term]] = {} - self._terms: Dict[str, Union[FTL.Message, FTL.Term]] = {} - self._compiled: Dict[str, Message] = {} + self._messages: dict[str, Union[FTL.Message, FTL.Term]] = {} + self._terms: dict[str, Union[FTL.Message, FTL.Term]] = {} + self._compiled: dict[str, Message] = {} # The compiler is not typed, and this cast is only valid for the public API self._compiler = cast( Callable[[Union[FTL.Message, FTL.Term]], Message], Compiler() @@ -90,8 +89,8 @@ def _lookup(self, entry_id: str, term: bool = False) -> Message: return self._compiled[compiled_id] def format_pattern( - self, pattern: Pattern, args: Union[Dict[str, Any], None] = None - ) -> Tuple[Union[str, "FluentNone"], List[Exception]]: + self, pattern: Pattern, args: Union[dict[str, Any], None] = None + ) -> tuple[Union[str, "FluentNone"], list[Exception]]: if args is not None: fluent_args = { argname: native_to_fluent(argvalue) @@ -100,7 +99,7 @@ def format_pattern( else: fluent_args = {} - errors: List[Exception] = [] + errors: list[Exception] = [] env = ResolverEnvironment( context=self, current=CurrentEnvironment(args=fluent_args), errors=errors ) diff --git a/fluent.runtime/fluent/runtime/fallback.py b/fluent.runtime/fluent/runtime/fallback.py index 2fb6c85f..831d4288 100644 --- a/fluent.runtime/fluent/runtime/fallback.py +++ b/fluent.runtime/fluent/runtime/fallback.py @@ -1,16 +1,7 @@ import codecs import os -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Dict, - Generator, - List, - Type, - Union, - cast, -) +from collections.abc import Generator +from typing import TYPE_CHECKING, Any, Callable, Union, cast from fluent.syntax import FluentParser from typing import NamedTuple @@ -25,7 +16,7 @@ class FormattedMessage(NamedTuple): value: Union[str, None] - attributes: Dict[str, str] + attributes: dict[str, str] class FluentLocalization: @@ -38,12 +29,12 @@ class FluentLocalization: def __init__( self, - locales: List[str], - resource_ids: List[str], + locales: list[str], + resource_ids: list[str], resource_loader: "AbstractResourceLoader", use_isolating: bool = False, - bundle_class: Type[FluentBundle] = FluentBundle, - functions: Union[Dict[str, Callable[[Any], "FluentType"]], None] = None, + bundle_class: type[FluentBundle] = FluentBundle, + functions: Union[dict[str, Callable[[Any], "FluentType"]], None] = None, ): self.locales = locales self.resource_ids = resource_ids @@ -51,11 +42,11 @@ def __init__( self.use_isolating = use_isolating self.bundle_class = bundle_class self.functions = functions - self._bundle_cache: List[FluentBundle] = [] + self._bundle_cache: list[FluentBundle] = [] self._bundle_it = self._iterate_bundles() def format_message( - self, msg_id: str, args: Union[Dict[str, Any], None] = None + self, msg_id: str, args: Union[dict[str, Any], None] = None ) -> FormattedMessage: bundle, msg = next(( (bundle, bundle.get_message(msg_id)) @@ -82,7 +73,7 @@ def format_message( ) def format_value( - self, msg_id: str, args: Union[Dict[str, Any], None] = None + self, msg_id: str, args: Union[dict[str, Any], None] = None ) -> str: bundle, msg = next(( (bundle, bundle.get_message(msg_id)) @@ -96,7 +87,7 @@ def format_value( str, val ) # Never FluentNone when format_pattern called externally - def _create_bundle(self, locales: List[str]) -> FluentBundle: + def _create_bundle(self, locales: list[str]) -> FluentBundle: return self.bundle_class( locales, functions=self.functions, use_isolating=self.use_isolating ) @@ -128,8 +119,8 @@ class AbstractResourceLoader: """ def resources( - self, locale: str, resource_ids: List[str] - ) -> Generator[List["Resource"], None, None]: + self, locale: str, resource_ids: list[str] + ) -> Generator[list["Resource"], None, None]: """ Yield lists of FluentResource objects, corresponding to each of the resource_ids. @@ -151,7 +142,7 @@ class FluentResourceLoader(AbstractResourceLoader): different roots. """ - def __init__(self, roots: Union[str, List[str]]): + def __init__(self, roots: Union[str, list[str]]): """ Create a resource loader. The roots may be a string for a single location on disk, or a list of strings. @@ -159,10 +150,10 @@ def __init__(self, roots: Union[str, List[str]]): self.roots = [roots] if isinstance(roots, str) else roots def resources( - self, locale: str, resource_ids: List[str] - ) -> Generator[List["Resource"], None, None]: + self, locale: str, resource_ids: list[str] + ) -> Generator[list["Resource"], None, None]: for root in self.roots: - resources: List[Any] = [] + resources: list[Any] = [] for resource_id in resource_ids: path = self.localize_path(os.path.join(root, resource_id), locale) if not os.path.isfile(path): diff --git a/fluent.runtime/fluent/runtime/prepare.py b/fluent.runtime/fluent/runtime/prepare.py index 418b33ba..b14aea0b 100644 --- a/fluent.runtime/fluent/runtime/prepare.py +++ b/fluent.runtime/fluent/runtime/prepare.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List +from typing import Any from fluent.syntax import ast as FTL @@ -17,7 +17,7 @@ def compile(self, node: Any) -> Any: nodename: str = type(node).__name__ if not hasattr(resolver, nodename): return node - kwargs: Dict[str, Any] = vars(node).copy() + kwargs: dict[str, Any] = vars(node).copy() for propname, propvalue in kwargs.items(): kwargs[propname] = self(propvalue) handler = getattr(self, "compile_" + nodename, self.compile_generic) @@ -31,7 +31,7 @@ def compile_Placeable(self, _: Any, expression: Any, **kwargs: Any) -> Any: return expression return resolver.Placeable(expression=expression, **kwargs) - def compile_Pattern(self, _: Any, elements: List[Any], **kwargs: Any) -> Any: + def compile_Pattern(self, _: Any, elements: list[Any], **kwargs: Any) -> Any: if len(elements) == 1 and isinstance(elements[0], resolver.Placeable): # Don't isolate isolated placeables return resolver.NeverIsolatingPlaceable(elements[0].expression) diff --git a/fluent.runtime/fluent/runtime/resolver.py b/fluent.runtime/fluent/runtime/resolver.py index 1961a960..b42b14c3 100644 --- a/fluent.runtime/fluent/runtime/resolver.py +++ b/fluent.runtime/fluent/runtime/resolver.py @@ -1,5 +1,6 @@ import contextlib -from typing import TYPE_CHECKING, Any, Dict, Generator, List, Set, Union, cast +from collections.abc import Generator +from typing import TYPE_CHECKING, Any, Union, cast import attr from fluent.syntax import ast as FTL @@ -42,7 +43,7 @@ class CurrentEnvironment: # For Messages, VariableReference nodes are interpreted as external args, # but for Terms they are the values explicitly passed using CallExpression # syntax. So we have to be able to change 'args' for this purpose. - args: Dict[str, Any] = attr.ib(factory=dict) + args: dict[str, Any] = attr.ib(factory=dict) # This controls whether we need to report an error if a VariableReference # refers to an arg that is not present in the args dict. error_for_missing_arg: bool = attr.ib(default=True) @@ -51,9 +52,9 @@ class CurrentEnvironment: @attr.s class ResolverEnvironment: context: "FluentBundle" = attr.ib() - errors: List[Exception] = attr.ib() + errors: list[Exception] = attr.ib() part_count: int = attr.ib(default=0, init=False) - active_patterns: Set[FTL.Pattern] = attr.ib(factory=set, init=False) + active_patterns: set[FTL.Pattern] = attr.ib(factory=set, init=False) current: CurrentEnvironment = attr.ib(factory=CurrentEnvironment) @contextlib.contextmanager @@ -72,7 +73,7 @@ def modified( self.current = old_current def modified_for_term_reference( - self, args: Union[Dict[str, Any], None] = None + self, args: Union[dict[str, Any], None] = None ) -> Any: return self.modified( args=args if args is not None else {}, error_for_missing_arg=False @@ -100,13 +101,13 @@ class Literal(BaseResolver): class Message(FTL.Entry, BaseResolver): id: "Identifier" value: Union["Pattern", None] - attributes: Dict[str, "Pattern"] + attributes: dict[str, "Pattern"] def __init__( self, id: "Identifier", value: Union["Pattern", None] = None, - attributes: Union[List["Attribute"], None] = None, + attributes: Union[list["Attribute"], None] = None, comment: Any = None, **kwargs: Any, ): @@ -121,13 +122,13 @@ def __init__( class Term(FTL.Entry, BaseResolver): id: "Identifier" value: "Pattern" - attributes: Dict[str, "Pattern"] + attributes: dict[str, "Pattern"] def __init__( self, id: "Identifier", value: "Pattern", - attributes: Union[List["Attribute"], None] = None, + attributes: Union[list["Attribute"], None] = None, comment: Any = None, **kwargs: Any, ): @@ -143,7 +144,7 @@ class Pattern(FTL.Pattern, BaseResolver): # Prevent messages with too many sub parts, for CPI DOS protection MAX_PARTS = 1000 - elements: List[Union["TextElement", "Placeable"]] # type: ignore + elements: list[Union["TextElement", "Placeable"]] # type: ignore def __init__(self, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) @@ -294,7 +295,7 @@ def __call__(self, env: ResolverEnvironment) -> Any: if isinstance(arg_val, (FluentType, str)): return arg_val env.errors.append( - TypeError("Unsupported external type: {}, {}".format(name, type(arg_val))) + TypeError(f"Unsupported external type: {name}, {type(arg_val)}") ) return FluentNone(name) @@ -306,7 +307,7 @@ class Attribute(FTL.Attribute, BaseResolver): class SelectExpression(FTL.SelectExpression, BaseResolver): selector: "InlineExpression" - variants: List["Variant"] # type: ignore + variants: list["Variant"] # type: ignore def __call__(self, env: ResolverEnvironment) -> Union[str, FluentNone]: key = self.selector(env) @@ -368,8 +369,8 @@ def __call__(self, env: ResolverEnvironment) -> str: class CallArguments(FTL.CallArguments, BaseResolver): - positional: List[Union["InlineExpression", Placeable]] # type: ignore - named: List["NamedArgument"] # type: ignore + positional: list[Union["InlineExpression", Placeable]] # type: ignore + named: list["NamedArgument"] # type: ignore class FunctionReference(FTL.FunctionReference, BaseResolver): @@ -384,7 +385,7 @@ def __call__(self, env: ResolverEnvironment) -> Any: function = env.context._functions[function_name] except LookupError: env.errors.append( - FluentReferenceError("Unknown function: {}".format(function_name)) + FluentReferenceError(f"Unknown function: {function_name}") ) return FluentNone(function_name + "()") diff --git a/fluent.runtime/fluent/runtime/types.py b/fluent.runtime/fluent/runtime/types.py index 697a18b7..c6717194 100644 --- a/fluent.runtime/fluent/runtime/types.py +++ b/fluent.runtime/fluent/runtime/types.py @@ -1,14 +1,13 @@ import warnings from datetime import date, datetime from decimal import Decimal -from typing import Any, Dict, Type, TypeVar, Union, cast +from typing import Any, Literal, TypeVar, Union, cast import attr import pytz from babel import Locale from babel.dates import format_date, format_time, get_datetime_format, get_timezone from babel.numbers import NumberPattern, parse_pattern -from typing_extensions import Literal FORMAT_STYLE_DECIMAL = "decimal" FORMAT_STYLE_CURRENCY = "currency" @@ -106,7 +105,7 @@ def __new__( return self._init(value, kwargs) def _init( - self, value: Union[int, float, Decimal, "FluentNumber"], kwargs: Dict[str, Any] + self, value: Union[int, float, Decimal, "FluentNumber"], kwargs: dict[str, Any] ) -> "FluentNumber": self.options = merge_options( NumberFormatOptions, @@ -211,7 +210,7 @@ def replacer(s: str) -> str: def merge_options( - options_class: Type[Options], base: Union[Options, None], kwargs: Dict[str, Any] + options_class: type[Options], base: Union[Options, None], kwargs: dict[str, Any] ) -> Options: """ Given an 'options_class', an optional 'base' object to copy from, @@ -346,7 +345,7 @@ class FluentDateType(FluentType): # So we leave those alone, and implement another `_init_options` # which is called from other constructors. def _init_options( - self, dt_obj: Union[date, datetime], kwargs: Dict[str, Any] + self, dt_obj: Union[date, datetime], kwargs: dict[str, Any] ) -> None: if "timeStyle" in kwargs and not isinstance(self, datetime): raise TypeError( @@ -437,6 +436,4 @@ def fluent_date( elif isinstance(dt, FluentNone): return dt else: - raise TypeError( - "Can't use fluent_date with object {} of type {}".format(dt, type(dt)) - ) + raise TypeError(f"Can't use fluent_date with object {dt} of type {type(dt)}") diff --git a/fluent.runtime/pyproject.toml b/fluent.runtime/pyproject.toml index 163f7cb4..8679443d 100644 --- a/fluent.runtime/pyproject.toml +++ b/fluent.runtime/pyproject.toml @@ -1,13 +1,12 @@ [project] name = "fluent.runtime" version = "0.4.0" -requires-python = ">= 3.8" +requires-python = ">= 3.9" dependencies = [ "fluent.syntax>=0.17,<0.20", "attrs", "babel", "pytz", - "typing-extensions>=3.7,<5", ] license = { text = "Apache-2.0" } description = "Localization library for expressive translations." @@ -21,8 +20,12 @@ classifiers = [ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3 :: Only", ] diff --git a/fluent.runtime/tools/benchmarks/fluent_benchmark.py b/fluent.runtime/tools/benchmarks/fluent_benchmark.py index d80de407..83f42912 100644 --- a/fluent.runtime/tools/benchmarks/fluent_benchmark.py +++ b/fluent.runtime/tools/benchmarks/fluent_benchmark.py @@ -1,7 +1,6 @@ #!/usr/bin/env python # This should be run using pytest -from __future__ import unicode_literals import sys @@ -48,7 +47,7 @@ def fluent_template(bundle): ) -class TestBenchmark(object): +class TestBenchmark: def test_template(self, fluent_bundle, benchmark): benchmark(lambda: fluent_template(fluent_bundle)) diff --git a/fluent.syntax/fluent/syntax/ast.py b/fluent.syntax/fluent/syntax/ast.py index c47244e5..c13ee1c7 100644 --- a/fluent.syntax/fluent/syntax/ast.py +++ b/fluent.syntax/fluent/syntax/ast.py @@ -1,10 +1,10 @@ import json import re import sys -from typing import Any, Callable, Dict, List, TypeVar, Union, cast +from typing import Any, Callable, TypeVar, Union, cast Node = TypeVar("Node", bound="BaseNode") -ToJsonFn = Callable[[Dict[str, Any]], Any] +ToJsonFn = Callable[[dict[str, Any]], Any] def to_json(value: Any, fn: Union[ToJsonFn, None] = None) -> Any: @@ -29,7 +29,7 @@ def from_json(value: Any) -> Any: return value -def scalars_equal(node1: Any, node2: Any, ignored_fields: List[str]) -> bool: +def scalars_equal(node1: Any, node2: Any, ignored_fields: list[str]) -> bool: """Compare two nodes which are not lists.""" if type(node1) is not type(node2): @@ -66,7 +66,7 @@ def visit(value: Any) -> Any: **{name: visit(value) for name, value in vars(self).items()} ) - def equals(self, other: "BaseNode", ignored_fields: List[str] = ["span"]) -> bool: + def equals(self, other: "BaseNode", ignored_fields: list[str] = ["span"]) -> bool: """Compare two nodes. Nodes are deeply compared on a field by field basis. If possible, False @@ -126,7 +126,7 @@ def add_span(self, start: int, end: int) -> None: class Resource(SyntaxNode): - def __init__(self, body: Union[List["EntryType"], None] = None, **kwargs: Any): + def __init__(self, body: Union[list["EntryType"], None] = None, **kwargs: Any): super().__init__(**kwargs) self.body = body or [] @@ -140,7 +140,7 @@ def __init__( self, id: "Identifier", value: Union["Pattern", None] = None, - attributes: Union[List["Attribute"], None] = None, + attributes: Union[list["Attribute"], None] = None, comment: Union["Comment", None] = None, **kwargs: Any ): @@ -156,7 +156,7 @@ def __init__( self, id: "Identifier", value: "Pattern", - attributes: Union[List["Attribute"], None] = None, + attributes: Union[list["Attribute"], None] = None, comment: Union["Comment", None] = None, **kwargs: Any ): @@ -169,7 +169,7 @@ def __init__( class Pattern(SyntaxNode): def __init__( - self, elements: List[Union["TextElement", "Placeable"]], **kwargs: Any + self, elements: list[Union["TextElement", "Placeable"]], **kwargs: Any ): super().__init__(**kwargs) self.elements = elements @@ -206,12 +206,12 @@ def __init__(self, value: str, **kwargs: Any): super().__init__(**kwargs) self.value = value - def parse(self) -> Dict[str, Any]: + def parse(self) -> dict[str, Any]: return {"value": self.value} class StringLiteral(Literal): - def parse(self) -> Dict[str, str]: + def parse(self) -> dict[str, str]: def from_escape_sequence(matchobj: Any) -> str: c, codepoint4, codepoint6 = matchobj.groups() if c: @@ -233,7 +233,7 @@ def from_escape_sequence(matchobj: Any) -> str: class NumberLiteral(Literal): - def parse(self) -> Dict[str, Union[float, int]]: + def parse(self) -> dict[str, Union[float, int]]: value = float(self.value) decimal_position = self.value.find(".") precision = 0 @@ -283,7 +283,7 @@ def __init__(self, id: "Identifier", arguments: "CallArguments", **kwargs: Any): class SelectExpression(Expression): def __init__( - self, selector: "InlineExpression", variants: List["Variant"], **kwargs: Any + self, selector: "InlineExpression", variants: list["Variant"], **kwargs: Any ): super().__init__(**kwargs) self.selector = selector @@ -293,8 +293,8 @@ def __init__( class CallArguments(SyntaxNode): def __init__( self, - positional: Union[List[Union["InlineExpression", Placeable]], None] = None, - named: Union[List["NamedArgument"], None] = None, + positional: Union[list[Union["InlineExpression", Placeable]], None] = None, + named: Union[list["NamedArgument"], None] = None, **kwargs: Any ): super().__init__(**kwargs) @@ -366,7 +366,7 @@ class Junk(SyntaxNode): def __init__( self, content: Union[str, None] = None, - annotations: Union[List["Annotation"], None] = None, + annotations: Union[list["Annotation"], None] = None, **kwargs: Any ): super().__init__(**kwargs) @@ -388,7 +388,7 @@ class Annotation(SyntaxNode): def __init__( self, code: str, - arguments: Union[List[Any], None] = None, + arguments: Union[list[Any], None] = None, message: Union[str, None] = None, **kwargs: Any ): diff --git a/fluent.syntax/fluent/syntax/errors.py b/fluent.syntax/fluent/syntax/errors.py index b6a59f5b..baa98113 100644 --- a/fluent.syntax/fluent/syntax/errors.py +++ b/fluent.syntax/fluent/syntax/errors.py @@ -1,4 +1,4 @@ -from typing import Tuple, Union +from typing import Union class ParseError(Exception): @@ -8,15 +8,15 @@ def __init__(self, code: str, *args: Union[str, None]): self.message = get_error_message(code, args) -def get_error_message(code: str, args: Tuple[Union[str, None], ...]) -> str: +def get_error_message(code: str, args: tuple[Union[str, None], ...]) -> str: if code == "E00001": return "Generic error" if code == "E0002": return "Expected an entry start" if code == "E0003": - return 'Expected token: "{}"'.format(args[0]) + return f'Expected token: "{args[0]}"' if code == "E0004": - return 'Expected a character from range: "{}"'.format(args[0]) + return f'Expected a character from range: "{args[0]}"' if code == "E0005": msg = 'Expected message "{}" to have a value or attributes' return msg.format(args[0]) @@ -58,9 +58,9 @@ def get_error_message(code: str, args: Tuple[Union[str, None], ...]) -> str: if code == "E0024": return "Cannot access variants of a message." if code == "E0025": - return "Unknown escape sequence: \\{}.".format(args[0]) + return f"Unknown escape sequence: \\{args[0]}." if code == "E0026": - return "Invalid Unicode escape sequence: {}.".format(args[0]) + return f"Invalid Unicode escape sequence: {args[0]}." if code == "E0027": return "Unbalanced closing brace in TextElement." if code == "E0028": diff --git a/fluent.syntax/fluent/syntax/parser.py b/fluent.syntax/fluent/syntax/parser.py index 5999f97c..5888c448 100644 --- a/fluent.syntax/fluent/syntax/parser.py +++ b/fluent.syntax/fluent/syntax/parser.py @@ -1,5 +1,5 @@ import re -from typing import Any, Callable, List, Set, TypeVar, Union, cast +from typing import Any, Callable, TypeVar, Union, cast from . import ast from .errors import ParseError @@ -45,7 +45,7 @@ def parse(self, source: str) -> ast.Resource: ps = FluentParserStream(source) ps.skip_blank_block() - entries: List[ast.EntryType] = [] + entries: list[ast.EntryType] = [] last_comment = None while ps.current_char: @@ -236,8 +236,8 @@ def get_attribute(self, ps: FluentParserStream) -> ast.Attribute: return ast.Attribute(key, value) - def get_attributes(self, ps: FluentParserStream) -> List[ast.Attribute]: - attrs: List[ast.Attribute] = [] + def get_attributes(self, ps: FluentParserStream) -> list[ast.Attribute]: + attrs: list[ast.Attribute] = [] ps.peek_blank() while ps.is_attribute_start(): @@ -299,8 +299,8 @@ def get_variant(self, ps: FluentParserStream, has_default: bool) -> ast.Variant: return ast.Variant(key, value, default_index) - def get_variants(self, ps: FluentParserStream) -> List[ast.Variant]: - variants: List[ast.Variant] = [] + def get_variants(self, ps: FluentParserStream) -> list[ast.Variant]: + variants: list[ast.Variant] = [] has_default = False ps.skip_blank() @@ -376,7 +376,7 @@ def maybe_get_pattern(self, ps: FluentParserStream) -> Union[ast.Pattern, None]: @with_span def get_pattern(self, ps: FluentParserStream, is_block: bool) -> ast.Pattern: - elements: List[Any] = [] + elements: list[Any] = [] if is_block: # A block pattern is a pattern which starts on a new line. Measure # the indent of this first line for the dedentation logic. @@ -422,20 +422,20 @@ def get_pattern(self, ps: FluentParserStream, is_block: bool) -> ast.Pattern: class Indent(ast.SyntaxNode): def __init__(self, value: str, start: int, end: int): - super(FluentParser.Indent, self).__init__() + super().__init__() self.value = value self.add_span(start, end) def dedent( self, - elements: List[Union[ast.TextElement, ast.Placeable, Indent]], + elements: list[Union[ast.TextElement, ast.Placeable, Indent]], common_indent: int, - ) -> List[Union[ast.TextElement, ast.Placeable]]: + ) -> list[Union[ast.TextElement, ast.Placeable]]: """Dedent a list of elements by removing the maximum common indent from the beginning of text lines. The common indent is calculated in get_pattern. """ - trimmed: List[Union[ast.TextElement, ast.Placeable]] = [] + trimmed: list[Union[ast.TextElement, ast.Placeable]] = [] for element in elements: if isinstance(element, ast.Placeable): @@ -660,9 +660,9 @@ def get_call_argument( @with_span def get_call_arguments(self, ps: FluentParserStream) -> ast.CallArguments: - positional: List[Union[ast.InlineExpression, ast.Placeable]] = [] - named: List[ast.NamedArgument] = [] - argument_names: Set[str] = set() + positional: list[Union[ast.InlineExpression, ast.Placeable]] = [] + named: list[ast.NamedArgument] = [] + argument_names: set[str] = set() ps.expect_char("(") ps.skip_blank() diff --git a/fluent.syntax/fluent/syntax/serializer.py b/fluent.syntax/fluent/syntax/serializer.py index 3659154c..414dce19 100644 --- a/fluent.syntax/fluent/syntax/serializer.py +++ b/fluent.syntax/fluent/syntax/serializer.py @@ -1,4 +1,4 @@ -from typing import List, Union +from typing import Union from . import ast @@ -46,11 +46,11 @@ def __init__(self, with_junk: bool = False): def serialize(self, resource: ast.Resource) -> str: "Serialize a :class:`.ast.Resource` to a string." if not isinstance(resource, ast.Resource): - raise Exception("Unknown resource type: {}".format(type(resource))) + raise Exception(f"Unknown resource type: {type(resource)}") state = 0 - parts: List[str] = [] + parts: list[str] = [] for entry in resource.body: if not isinstance(entry, ast.Junk) or self.with_junk: parts.append(self.serialize_entry(entry, state)) @@ -79,7 +79,7 @@ def serialize_entry(self, entry: ast.EntryType, state: int = 0) -> str: return "{}\n".format(serialize_comment(entry, "###")) if isinstance(entry, ast.Junk): return serialize_junk(entry) - raise Exception("Unknown entry type: {}".format(type(entry))) + raise Exception(f"Unknown entry type: {type(entry)}") def serialize_comment( @@ -104,7 +104,7 @@ def serialize_junk(junk: ast.Junk) -> str: def serialize_message(message: ast.Message) -> str: - parts: List[str] = [] + parts: list[str] = [] if message.comment: parts.append(serialize_comment(message.comment)) @@ -123,7 +123,7 @@ def serialize_message(message: ast.Message) -> str: def serialize_term(term: ast.Term) -> str: - parts: List[str] = [] + parts: list[str] = [] if term.comment: parts.append(serialize_comment(term.comment)) @@ -160,20 +160,20 @@ def serialize_element(element: ast.PatternElement) -> str: return element.value if isinstance(element, ast.Placeable): return serialize_placeable(element) - raise Exception("Unknown element type: {}".format(type(element))) + raise Exception(f"Unknown element type: {type(element)}") def serialize_placeable(placeable: ast.Placeable) -> str: expr = placeable.expression if isinstance(expr, ast.Placeable): - return "{{{}}}".format(serialize_placeable(expr)) + return f"{{{serialize_placeable(expr)}}}" if isinstance(expr, ast.SelectExpression): # Special-case select expressions to control the withespace around the # opening and the closing brace. - return "{{ {}}}".format(serialize_expression(expr)) + return f"{{ {serialize_expression(expr)}}}" if isinstance(expr, ast.Expression): - return "{{ {} }}".format(serialize_expression(expr)) - raise Exception("Unknown expression type: {}".format(type(expr))) + return f"{{ {serialize_expression(expr)} }}" + raise Exception(f"Unknown expression type: {type(expr)}") def serialize_expression(expression: Union[ast.Expression, ast.Placeable]) -> str: @@ -199,13 +199,13 @@ def serialize_expression(expression: Union[ast.Expression, ast.Placeable]) -> st args = serialize_call_arguments(expression.arguments) return f"{expression.id.name}{args}" if isinstance(expression, ast.SelectExpression): - out = "{} ->".format(serialize_expression(expression.selector)) + out = f"{serialize_expression(expression.selector)} ->" for variant in expression.variants: out += serialize_variant(variant) return f"{out}\n" if isinstance(expression, ast.Placeable): return serialize_placeable(expression) - raise Exception("Unknown expression type: {}".format(type(expression))) + raise Exception(f"Unknown expression type: {type(expression)}") def serialize_variant(variant: ast.Variant) -> str: @@ -221,11 +221,11 @@ def serialize_call_arguments(expr: ast.CallArguments) -> str: named = ", ".join(serialize_named_argument(arg) for arg in expr.named) if len(expr.positional) > 0 and len(expr.named) > 0: return f"({positional}, {named})" - return "({})".format(positional or named) + return f"({positional or named})" def serialize_named_argument(arg: ast.NamedArgument) -> str: - return "{}: {}".format(arg.name.name, serialize_expression(arg.value)) + return f"{arg.name.name}: {serialize_expression(arg.value)}" def serialize_variant_key(key: Union[ast.Identifier, ast.NumberLiteral]) -> str: @@ -233,4 +233,4 @@ def serialize_variant_key(key: Union[ast.Identifier, ast.NumberLiteral]) -> str: return key.name if isinstance(key, ast.NumberLiteral): return key.value - raise Exception("Unknown variant key type: {}".format(type(key))) + raise Exception(f"Unknown variant key type: {type(key)}") diff --git a/fluent.syntax/fluent/syntax/stream.py b/fluent.syntax/fluent/syntax/stream.py index 0e276fc3..2a485e22 100644 --- a/fluent.syntax/fluent/syntax/stream.py +++ b/fluent.syntax/fluent/syntax/stream.py @@ -1,6 +1,4 @@ -from typing import Callable, Union - -from typing_extensions import Literal +from typing import Callable, Literal, Union from .errors import ParseError diff --git a/fluent.syntax/fluent/syntax/visitor.py b/fluent.syntax/fluent/syntax/visitor.py index 2f637afb..53fea500 100644 --- a/fluent.syntax/fluent/syntax/visitor.py +++ b/fluent.syntax/fluent/syntax/visitor.py @@ -1,4 +1,4 @@ -from typing import Any, List +from typing import Any from .ast import BaseNode, Node @@ -50,7 +50,7 @@ def visit(self, node: Any) -> Any: def generic_visit(self, node: Node) -> Node: # type: ignore for propname, propvalue in vars(node).items(): if isinstance(propvalue, list): - new_vals: List[Any] = [] + new_vals: list[Any] = [] for child in propvalue: new_val = self.visit(child) if new_val is not None: diff --git a/fluent.syntax/pyproject.toml b/fluent.syntax/pyproject.toml index 013291fb..5a0ba99a 100644 --- a/fluent.syntax/pyproject.toml +++ b/fluent.syntax/pyproject.toml @@ -1,8 +1,7 @@ [project] name = "fluent.syntax" version = "0.19.0" -requires-python = ">= 3.8" -dependencies = ["typing-extensions>=3.7,<5"] +requires-python = ">= 3.9" license = { text = "Apache-2.0" } description = "Localization library for expressive translations." keywords = ["fluent", "localization", "l10n"] @@ -14,8 +13,12 @@ classifiers = [ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3 :: Only", ] diff --git a/pyproject.toml b/pyproject.toml index 567f3ca0..8ff24004 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "python-fluent" version = "0" -requires-python = ">= 3.8.1" +requires-python = ">= 3.9" dependencies = [ "fluent.syntax", "black ~= 24.0", diff --git a/uv.lock b/uv.lock index 777227e3..44fcc13e 100644 --- a/uv.lock +++ b/uv.lock @@ -1,10 +1,9 @@ version = 1 revision = 3 -requires-python = ">=3.8.1" +requires-python = ">=3.9" resolution-markers = [ "python_full_version >= '3.10'", - "python_full_version == '3.9.*'", - "python_full_version < '3.9'", + "python_full_version < '3.10'", ] [manifest] @@ -14,26 +13,10 @@ members = [ "python-fluent", ] -[[package]] -name = "attrs" -version = "25.3.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.9'", -] -sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, -] - [[package]] name = "attrs" version = "25.4.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", - "python_full_version == '3.9.*'", -] sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, @@ -43,73 +26,25 @@ wheels = [ name = "babel" version = "2.17.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytz", marker = "python_full_version < '3.9'" }, -] sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, ] -[[package]] -name = "black" -version = "24.8.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.9'", -] -dependencies = [ - { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "mypy-extensions", marker = "python_full_version < '3.9'" }, - { name = "packaging", marker = "python_full_version < '3.9'" }, - { name = "pathspec", marker = "python_full_version < '3.9'" }, - { name = "platformdirs", version = "4.3.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "tomli", marker = "python_full_version < '3.9'" }, - { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/04/b0/46fb0d4e00372f4a86a6f8efa3cb193c9f64863615e39010b1477e010578/black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f", size = 644810, upload-time = "2024-08-02T17:43:18.405Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/6e/74e29edf1fba3887ed7066930a87f698ffdcd52c5dbc263eabb06061672d/black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6", size = 1632092, upload-time = "2024-08-02T17:47:26.911Z" }, - { url = "https://files.pythonhosted.org/packages/ab/49/575cb6c3faee690b05c9d11ee2e8dba8fbd6d6c134496e644c1feb1b47da/black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb", size = 1457529, upload-time = "2024-08-02T17:47:29.109Z" }, - { url = "https://files.pythonhosted.org/packages/7a/b4/d34099e95c437b53d01c4aa37cf93944b233066eb034ccf7897fa4e5f286/black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42", size = 1757443, upload-time = "2024-08-02T17:46:20.306Z" }, - { url = "https://files.pythonhosted.org/packages/87/a0/6d2e4175ef364b8c4b64f8441ba041ed65c63ea1db2720d61494ac711c15/black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a", size = 1418012, upload-time = "2024-08-02T17:47:20.33Z" }, - { url = "https://files.pythonhosted.org/packages/08/a6/0a3aa89de9c283556146dc6dbda20cd63a9c94160a6fbdebaf0918e4a3e1/black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1", size = 1615080, upload-time = "2024-08-02T17:48:05.467Z" }, - { url = "https://files.pythonhosted.org/packages/db/94/b803d810e14588bb297e565821a947c108390a079e21dbdcb9ab6956cd7a/black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af", size = 1438143, upload-time = "2024-08-02T17:47:30.247Z" }, - { url = "https://files.pythonhosted.org/packages/a5/b5/f485e1bbe31f768e2e5210f52ea3f432256201289fd1a3c0afda693776b0/black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4", size = 1738774, upload-time = "2024-08-02T17:46:17.837Z" }, - { url = "https://files.pythonhosted.org/packages/a8/69/a000fc3736f89d1bdc7f4a879f8aaf516fb03613bb51a0154070383d95d9/black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af", size = 1427503, upload-time = "2024-08-02T17:46:22.654Z" }, - { url = "https://files.pythonhosted.org/packages/a2/a8/05fb14195cfef32b7c8d4585a44b7499c2a4b205e1662c427b941ed87054/black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368", size = 1646132, upload-time = "2024-08-02T17:49:52.843Z" }, - { url = "https://files.pythonhosted.org/packages/41/77/8d9ce42673e5cb9988f6df73c1c5c1d4e9e788053cccd7f5fb14ef100982/black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed", size = 1448665, upload-time = "2024-08-02T17:47:54.479Z" }, - { url = "https://files.pythonhosted.org/packages/cc/94/eff1ddad2ce1d3cc26c162b3693043c6b6b575f538f602f26fe846dfdc75/black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018", size = 1762458, upload-time = "2024-08-02T17:46:19.384Z" }, - { url = "https://files.pythonhosted.org/packages/28/ea/18b8d86a9ca19a6942e4e16759b2fa5fc02bbc0eb33c1b866fcd387640ab/black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2", size = 1436109, upload-time = "2024-08-02T17:46:52.97Z" }, - { url = "https://files.pythonhosted.org/packages/9f/d4/ae03761ddecc1a37d7e743b89cccbcf3317479ff4b88cfd8818079f890d0/black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd", size = 1617322, upload-time = "2024-08-02T17:51:20.203Z" }, - { url = "https://files.pythonhosted.org/packages/14/4b/4dfe67eed7f9b1ddca2ec8e4418ea74f0d1dc84d36ea874d618ffa1af7d4/black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2", size = 1442108, upload-time = "2024-08-02T17:50:40.824Z" }, - { url = "https://files.pythonhosted.org/packages/97/14/95b3f91f857034686cae0e73006b8391d76a8142d339b42970eaaf0416ea/black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e", size = 1745786, upload-time = "2024-08-02T17:46:02.939Z" }, - { url = "https://files.pythonhosted.org/packages/95/54/68b8883c8aa258a6dde958cd5bdfada8382bec47c5162f4a01e66d839af1/black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920", size = 1426754, upload-time = "2024-08-02T17:46:38.603Z" }, - { url = "https://files.pythonhosted.org/packages/13/b2/b3f24fdbb46f0e7ef6238e131f13572ee8279b70f237f221dd168a9dba1a/black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c", size = 1631706, upload-time = "2024-08-02T17:49:57.606Z" }, - { url = "https://files.pythonhosted.org/packages/d9/35/31010981e4a05202a84a3116423970fd1a59d2eda4ac0b3570fbb7029ddc/black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e", size = 1457429, upload-time = "2024-08-02T17:49:12.764Z" }, - { url = "https://files.pythonhosted.org/packages/27/25/3f706b4f044dd569a20a4835c3b733dedea38d83d2ee0beb8178a6d44945/black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47", size = 1756488, upload-time = "2024-08-02T17:46:08.067Z" }, - { url = "https://files.pythonhosted.org/packages/63/72/79375cd8277cbf1c5670914e6bd4c1b15dea2c8f8e906dc21c448d0535f0/black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb", size = 1417721, upload-time = "2024-08-02T17:46:42.637Z" }, - { url = "https://files.pythonhosted.org/packages/27/1e/83fa8a787180e1632c3d831f7e58994d7aaf23a0961320d21e84f922f919/black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed", size = 206504, upload-time = "2024-08-02T17:43:15.747Z" }, -] - [[package]] name = "black" version = "24.10.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", - "python_full_version == '3.9.*'", -] dependencies = [ - { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "mypy-extensions", marker = "python_full_version >= '3.9'" }, - { name = "packaging", marker = "python_full_version >= '3.9'" }, - { name = "pathspec", marker = "python_full_version >= '3.9'" }, - { name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "platformdirs", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "tomli", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, - { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d8/0d/cc2fb42b8c50d80143221515dd7e4766995bd07c56c9a3ed30baf080b6dc/black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875", size = 645813, upload-time = "2024-10-07T19:20:50.361Z" } wheels = [ @@ -141,8 +76,7 @@ name = "click" version = "8.1.8" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version == '3.9.*'", - "python_full_version < '3.9'", + "python_full_version < '3.10'", ] dependencies = [ { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, @@ -176,35 +110,14 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] -[[package]] -name = "flake8" -version = "7.1.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.9'", -] -dependencies = [ - { name = "mccabe", marker = "python_full_version < '3.9'" }, - { name = "pycodestyle", version = "2.12.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "pyflakes", version = "3.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/58/16/3f2a0bb700ad65ac9663262905a025917c020a3f92f014d2ba8964b4602c/flake8-7.1.2.tar.gz", hash = "sha256:c586ffd0b41540951ae41af572e6790dbd49fc12b3aa2541685d253d9bd504bd", size = 48119, upload-time = "2025-02-16T18:45:44.296Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/35/f8/08d37b2cd89da306e3520bd27f8a85692122b42b56c0c2c3784ff09c022f/flake8-7.1.2-py2.py3-none-any.whl", hash = "sha256:1cbc62e65536f65e6d754dfe6f1bada7f5cf392d6f5db3c2b85892466c3e7c1a", size = 57745, upload-time = "2025-02-16T18:45:42.351Z" }, -] - [[package]] name = "flake8" version = "7.3.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", - "python_full_version == '3.9.*'", -] dependencies = [ - { name = "mccabe", marker = "python_full_version >= '3.9'" }, - { name = "pycodestyle", version = "2.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, - { name = "pyflakes", version = "3.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "mccabe" }, + { name = "pycodestyle" }, + { name = "pyflakes" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9b/af/fbfe3c4b5a657d79e5c47a2827a362f9e1b763336a52f926126aa6dc7123/flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872", size = 48326, upload-time = "2025-06-20T19:31:35.838Z" } wheels = [ @@ -216,13 +129,10 @@ name = "fluent-runtime" version = "0.4.0" source = { editable = "fluent.runtime" } dependencies = [ - { name = "attrs", version = "25.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "attrs", version = "25.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "attrs" }, { name = "babel" }, { name = "fluent-syntax" }, { name = "pytz" }, - { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, ] [package.metadata] @@ -231,20 +141,12 @@ requires-dist = [ { name = "babel" }, { name = "fluent-syntax", editable = "fluent.syntax" }, { name = "pytz" }, - { name = "typing-extensions", specifier = ">=3.7,<5" }, ] [[package]] name = "fluent-syntax" version = "0.19.0" source = { editable = "fluent.syntax" } -dependencies = [ - { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, -] - -[package.metadata] -requires-dist = [{ name = "typing-extensions", specifier = ">=3.7,<5" }] [[package]] name = "isort" @@ -347,73 +249,16 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, ] -[[package]] -name = "mypy" -version = "1.14.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.9'", -] -dependencies = [ - { name = "mypy-extensions", marker = "python_full_version < '3.9'" }, - { name = "tomli", marker = "python_full_version < '3.9'" }, - { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b9/eb/2c92d8ea1e684440f54fa49ac5d9a5f19967b7b472a281f419e69a8d228e/mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6", size = 3216051, upload-time = "2024-12-30T16:39:07.335Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/7a/87ae2adb31d68402da6da1e5f30c07ea6063e9f09b5e7cfc9dfa44075e74/mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb", size = 11211002, upload-time = "2024-12-30T16:37:22.435Z" }, - { url = "https://files.pythonhosted.org/packages/e1/23/eada4c38608b444618a132be0d199b280049ded278b24cbb9d3fc59658e4/mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0", size = 10358400, upload-time = "2024-12-30T16:37:53.526Z" }, - { url = "https://files.pythonhosted.org/packages/43/c9/d6785c6f66241c62fd2992b05057f404237deaad1566545e9f144ced07f5/mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d", size = 12095172, upload-time = "2024-12-30T16:37:50.332Z" }, - { url = "https://files.pythonhosted.org/packages/c3/62/daa7e787770c83c52ce2aaf1a111eae5893de9e004743f51bfcad9e487ec/mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b", size = 12828732, upload-time = "2024-12-30T16:37:29.96Z" }, - { url = "https://files.pythonhosted.org/packages/1b/a2/5fb18318a3637f29f16f4e41340b795da14f4751ef4f51c99ff39ab62e52/mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427", size = 13012197, upload-time = "2024-12-30T16:38:05.037Z" }, - { url = "https://files.pythonhosted.org/packages/28/99/e153ce39105d164b5f02c06c35c7ba958aaff50a2babba7d080988b03fe7/mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f", size = 9780836, upload-time = "2024-12-30T16:37:19.726Z" }, - { url = "https://files.pythonhosted.org/packages/da/11/a9422850fd506edbcdc7f6090682ecceaf1f87b9dd847f9df79942da8506/mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c", size = 11120432, upload-time = "2024-12-30T16:37:11.533Z" }, - { url = "https://files.pythonhosted.org/packages/b6/9e/47e450fd39078d9c02d620545b2cb37993a8a8bdf7db3652ace2f80521ca/mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1", size = 10279515, upload-time = "2024-12-30T16:37:40.724Z" }, - { url = "https://files.pythonhosted.org/packages/01/b5/6c8d33bd0f851a7692a8bfe4ee75eb82b6983a3cf39e5e32a5d2a723f0c1/mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8", size = 12025791, upload-time = "2024-12-30T16:36:58.73Z" }, - { url = "https://files.pythonhosted.org/packages/f0/4c/e10e2c46ea37cab5c471d0ddaaa9a434dc1d28650078ac1b56c2d7b9b2e4/mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f", size = 12749203, upload-time = "2024-12-30T16:37:03.741Z" }, - { url = "https://files.pythonhosted.org/packages/88/55/beacb0c69beab2153a0f57671ec07861d27d735a0faff135a494cd4f5020/mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1", size = 12885900, upload-time = "2024-12-30T16:37:57.948Z" }, - { url = "https://files.pythonhosted.org/packages/a2/75/8c93ff7f315c4d086a2dfcde02f713004357d70a163eddb6c56a6a5eff40/mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae", size = 9777869, upload-time = "2024-12-30T16:37:33.428Z" }, - { url = "https://files.pythonhosted.org/packages/43/1b/b38c079609bb4627905b74fc6a49849835acf68547ac33d8ceb707de5f52/mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14", size = 11266668, upload-time = "2024-12-30T16:38:02.211Z" }, - { url = "https://files.pythonhosted.org/packages/6b/75/2ed0d2964c1ffc9971c729f7a544e9cd34b2cdabbe2d11afd148d7838aa2/mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9", size = 10254060, upload-time = "2024-12-30T16:37:46.131Z" }, - { url = "https://files.pythonhosted.org/packages/a1/5f/7b8051552d4da3c51bbe8fcafffd76a6823779101a2b198d80886cd8f08e/mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11", size = 11933167, upload-time = "2024-12-30T16:37:43.534Z" }, - { url = "https://files.pythonhosted.org/packages/04/90/f53971d3ac39d8b68bbaab9a4c6c58c8caa4d5fd3d587d16f5927eeeabe1/mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e", size = 12864341, upload-time = "2024-12-30T16:37:36.249Z" }, - { url = "https://files.pythonhosted.org/packages/03/d2/8bc0aeaaf2e88c977db41583559319f1821c069e943ada2701e86d0430b7/mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89", size = 12972991, upload-time = "2024-12-30T16:37:06.743Z" }, - { url = "https://files.pythonhosted.org/packages/6f/17/07815114b903b49b0f2cf7499f1c130e5aa459411596668267535fe9243c/mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b", size = 9879016, upload-time = "2024-12-30T16:37:15.02Z" }, - { url = "https://files.pythonhosted.org/packages/9e/15/bb6a686901f59222275ab228453de741185f9d54fecbaacec041679496c6/mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255", size = 11252097, upload-time = "2024-12-30T16:37:25.144Z" }, - { url = "https://files.pythonhosted.org/packages/f8/b3/8b0f74dfd072c802b7fa368829defdf3ee1566ba74c32a2cb2403f68024c/mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34", size = 10239728, upload-time = "2024-12-30T16:38:08.634Z" }, - { url = "https://files.pythonhosted.org/packages/c5/9b/4fd95ab20c52bb5b8c03cc49169be5905d931de17edfe4d9d2986800b52e/mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a", size = 11924965, upload-time = "2024-12-30T16:38:12.132Z" }, - { url = "https://files.pythonhosted.org/packages/56/9d/4a236b9c57f5d8f08ed346914b3f091a62dd7e19336b2b2a0d85485f82ff/mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9", size = 12867660, upload-time = "2024-12-30T16:38:17.342Z" }, - { url = "https://files.pythonhosted.org/packages/40/88/a61a5497e2f68d9027de2bb139c7bb9abaeb1be1584649fa9d807f80a338/mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd", size = 12969198, upload-time = "2024-12-30T16:38:32.839Z" }, - { url = "https://files.pythonhosted.org/packages/54/da/3d6fc5d92d324701b0c23fb413c853892bfe0e1dbe06c9138037d459756b/mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107", size = 9885276, upload-time = "2024-12-30T16:38:20.828Z" }, - { url = "https://files.pythonhosted.org/packages/39/02/1817328c1372be57c16148ce7d2bfcfa4a796bedaed897381b1aad9b267c/mypy-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7084fb8f1128c76cd9cf68fe5971b37072598e7c31b2f9f95586b65c741a9d31", size = 11143050, upload-time = "2024-12-30T16:38:29.743Z" }, - { url = "https://files.pythonhosted.org/packages/b9/07/99db9a95ece5e58eee1dd87ca456a7e7b5ced6798fd78182c59c35a7587b/mypy-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f845a00b4f420f693f870eaee5f3e2692fa84cc8514496114649cfa8fd5e2c6", size = 10321087, upload-time = "2024-12-30T16:38:14.739Z" }, - { url = "https://files.pythonhosted.org/packages/9a/eb/85ea6086227b84bce79b3baf7f465b4732e0785830726ce4a51528173b71/mypy-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44bf464499f0e3a2d14d58b54674dee25c031703b2ffc35064bd0df2e0fac319", size = 12066766, upload-time = "2024-12-30T16:38:47.038Z" }, - { url = "https://files.pythonhosted.org/packages/4b/bb/f01bebf76811475d66359c259eabe40766d2f8ac8b8250d4e224bb6df379/mypy-1.14.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c99f27732c0b7dc847adb21c9d47ce57eb48fa33a17bc6d7d5c5e9f9e7ae5bac", size = 12787111, upload-time = "2024-12-30T16:39:02.444Z" }, - { url = "https://files.pythonhosted.org/packages/2f/c9/84837ff891edcb6dcc3c27d85ea52aab0c4a34740ff5f0ccc0eb87c56139/mypy-1.14.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:bce23c7377b43602baa0bd22ea3265c49b9ff0b76eb315d6c34721af4cdf1d9b", size = 12974331, upload-time = "2024-12-30T16:38:23.849Z" }, - { url = "https://files.pythonhosted.org/packages/84/5f/901e18464e6a13f8949b4909535be3fa7f823291b8ab4e4b36cfe57d6769/mypy-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:8edc07eeade7ebc771ff9cf6b211b9a7d93687ff892150cb5692e4f4272b0837", size = 9763210, upload-time = "2024-12-30T16:38:36.299Z" }, - { url = "https://files.pythonhosted.org/packages/ca/1f/186d133ae2514633f8558e78cd658070ba686c0e9275c5a5c24a1e1f0d67/mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35", size = 11200493, upload-time = "2024-12-30T16:38:26.935Z" }, - { url = "https://files.pythonhosted.org/packages/af/fc/4842485d034e38a4646cccd1369f6b1ccd7bc86989c52770d75d719a9941/mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc", size = 10357702, upload-time = "2024-12-30T16:38:50.623Z" }, - { url = "https://files.pythonhosted.org/packages/b4/e6/457b83f2d701e23869cfec013a48a12638f75b9d37612a9ddf99072c1051/mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9", size = 12091104, upload-time = "2024-12-30T16:38:53.735Z" }, - { url = "https://files.pythonhosted.org/packages/f1/bf/76a569158db678fee59f4fd30b8e7a0d75bcbaeef49edd882a0d63af6d66/mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb", size = 12830167, upload-time = "2024-12-30T16:38:56.437Z" }, - { url = "https://files.pythonhosted.org/packages/43/bc/0bc6b694b3103de9fed61867f1c8bd33336b913d16831431e7cb48ef1c92/mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60", size = 13013834, upload-time = "2024-12-30T16:38:59.204Z" }, - { url = "https://files.pythonhosted.org/packages/b0/79/5f5ec47849b6df1e6943d5fd8e6632fbfc04b4fd4acfa5a5a9535d11b4e2/mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c", size = 9781231, upload-time = "2024-12-30T16:39:05.124Z" }, - { url = "https://files.pythonhosted.org/packages/a0/b5/32dd67b69a16d088e533962e5044e51004176a9952419de0370cdaead0f8/mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1", size = 2752905, upload-time = "2024-12-30T16:38:42.021Z" }, -] - [[package]] name = "mypy" version = "1.19.1" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", - "python_full_version == '3.9.*'", -] dependencies = [ - { name = "librt", marker = "python_full_version >= '3.9' and platform_python_implementation != 'PyPy'" }, - { name = "mypy-extensions", marker = "python_full_version >= '3.9'" }, - { name = "pathspec", marker = "python_full_version >= '3.9'" }, - { name = "tomli", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, - { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } wheels = [ @@ -483,24 +328,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, ] -[[package]] -name = "platformdirs" -version = "4.3.6" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.9'", -] -sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302, upload-time = "2024-09-17T19:06:50.688Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439, upload-time = "2024-09-17T19:06:49.212Z" }, -] - [[package]] name = "platformdirs" version = "4.4.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version == '3.9.*'", + "python_full_version < '3.10'", ] sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } wheels = [ @@ -519,51 +352,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, ] -[[package]] -name = "pycodestyle" -version = "2.12.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.9'", -] -sdist = { url = "https://files.pythonhosted.org/packages/43/aa/210b2c9aedd8c1cbeea31a50e42050ad56187754b34eb214c46709445801/pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521", size = 39232, upload-time = "2024-08-04T20:26:54.576Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/d8/a211b3f85e99a0daa2ddec96c949cac6824bd305b040571b82a03dd62636/pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3", size = 31284, upload-time = "2024-08-04T20:26:53.173Z" }, -] - [[package]] name = "pycodestyle" version = "2.14.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", - "python_full_version == '3.9.*'", -] sdist = { url = "https://files.pythonhosted.org/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", size = 39472, upload-time = "2025-06-20T18:49:48.75Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" }, ] -[[package]] -name = "pyflakes" -version = "3.2.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.9'", -] -sdist = { url = "https://files.pythonhosted.org/packages/57/f9/669d8c9c86613c9d568757c7f5824bd3197d7b1c6c27553bc5618a27cce2/pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f", size = 63788, upload-time = "2024-01-05T00:28:47.703Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/d7/f1b7db88d8e4417c5d47adad627a93547f44bdc9028372dbd2313f34a855/pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a", size = 62725, upload-time = "2024-01-05T00:28:45.903Z" }, -] - [[package]] name = "pyflakes" version = "3.4.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", - "python_full_version == '3.9.*'", -] sdist = { url = "https://files.pythonhosted.org/packages/45/dc/fd034dc20b4b264b3d015808458391acbf9df40b1e54750ef175d39180b1/pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58", size = 64669, upload-time = "2025-06-20T18:45:27.834Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551, upload-time = "2025-06-20T18:45:26.937Z" }, @@ -574,17 +375,13 @@ name = "python-fluent" version = "0" source = { virtual = "." } dependencies = [ - { name = "black", version = "24.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "black", version = "24.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, - { name = "flake8", version = "7.1.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "flake8", version = "7.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "black" }, + { name = "flake8" }, { name = "fluent-syntax" }, { name = "isort" }, - { name = "mypy", version = "1.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "mypy", version = "1.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "mypy" }, { name = "types-babel" }, - { name = "types-pytz", version = "2024.2.0.20241221", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "types-pytz", version = "2025.2.0.20251108", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "types-pytz" }, ] [package.metadata] @@ -661,86 +458,36 @@ name = "types-babel" version = "2.11.0.15" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "types-pytz", version = "2024.2.0.20241221", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "types-pytz", version = "2025.2.0.20251108", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, - { name = "types-setuptools", version = "75.8.0.20250110", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "types-setuptools", version = "80.9.0.20251223", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "types-pytz" }, + { name = "types-setuptools" }, ] sdist = { url = "https://files.pythonhosted.org/packages/64/b5/b18d2dd9375515126069616096470fd9efd27434abac9e8d601e71f61b12/types-babel-2.11.0.15.tar.gz", hash = "sha256:282c184c8c9d81e8269212c1b8fa0d39ee88fb8bc43be47980412781c9c85f7e", size = 14392, upload-time = "2023-07-20T15:21:10.686Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d7/6a/56538e54ca538552a1b83e5f3f05ba23668870276a9342df1252e4e07f73/types_babel-2.11.0.15-py3-none-any.whl", hash = "sha256:d0579f2e8adeaef3fbe2eb63e5a2ecf01767fc018e5f3f36a3c9d8b723bd62c7", size = 18442, upload-time = "2023-07-20T15:21:08.221Z" }, ] -[[package]] -name = "types-pytz" -version = "2024.2.0.20241221" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.9'", -] -sdist = { url = "https://files.pythonhosted.org/packages/54/26/516311b02b5a215e721155fb65db8a965d061372e388d6125ebce8d674b0/types_pytz-2024.2.0.20241221.tar.gz", hash = "sha256:06d7cde9613e9f7504766a0554a270c369434b50e00975b3a4a0f6eed0f2c1a9", size = 10213, upload-time = "2024-12-21T02:40:48.654Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/74/db/c92ca6920cccd9c2998b013601542e2ac5e59bc805bcff94c94ad254b7df/types_pytz-2024.2.0.20241221-py3-none-any.whl", hash = "sha256:8fc03195329c43637ed4f593663df721fef919b60a969066e22606edf0b53ad5", size = 10008, upload-time = "2024-12-21T02:40:47.047Z" }, -] - [[package]] name = "types-pytz" version = "2025.2.0.20251108" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", - "python_full_version == '3.9.*'", -] sdist = { url = "https://files.pythonhosted.org/packages/40/ff/c047ddc68c803b46470a357454ef76f4acd8c1088f5cc4891cdd909bfcf6/types_pytz-2025.2.0.20251108.tar.gz", hash = "sha256:fca87917836ae843f07129567b74c1929f1870610681b4c92cb86a3df5817bdb", size = 10961, upload-time = "2025-11-08T02:55:57.001Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e7/c1/56ef16bf5dcd255155cc736d276efa6ae0a5c26fd685e28f0412a4013c01/types_pytz-2025.2.0.20251108-py3-none-any.whl", hash = "sha256:0f1c9792cab4eb0e46c52f8845c8f77cf1e313cb3d68bf826aa867fe4717d91c", size = 10116, upload-time = "2025-11-08T02:55:56.194Z" }, ] -[[package]] -name = "types-setuptools" -version = "75.8.0.20250110" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.9'", -] -sdist = { url = "https://files.pythonhosted.org/packages/f7/42/5713e90d4f9683f2301d900f33e4fc2405ad8ac224dda30f6cb7f4cd215b/types_setuptools-75.8.0.20250110.tar.gz", hash = "sha256:96f7ec8bbd6e0a54ea180d66ad68ad7a1d7954e7281a710ea2de75e355545271", size = 48185, upload-time = "2025-01-10T02:45:52.085Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cf/a3/dbfd106751b11c728cec21cc62cbfe7ff7391b935c4b6e8f0bdc2e6fd541/types_setuptools-75.8.0.20250110-py3-none-any.whl", hash = "sha256:a9f12980bbf9bcdc23ecd80755789085bad6bfce4060c2275bc2b4ca9f2bc480", size = 71521, upload-time = "2025-01-10T02:45:49.873Z" }, -] - [[package]] name = "types-setuptools" version = "80.9.0.20251223" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", - "python_full_version == '3.9.*'", -] sdist = { url = "https://files.pythonhosted.org/packages/00/07/d1b605230730990de20477150191d6dccf6aecc037da94c9960a5d563bc8/types_setuptools-80.9.0.20251223.tar.gz", hash = "sha256:d3411059ae2f5f03985217d86ac6084efea2c9e9cacd5f0869ef950f308169b2", size = 42420, upload-time = "2025-12-23T03:18:26.752Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/78/5c/b8877da94012dbc6643e4eeca22bca9b99b295be05d161f8a403ae9387c0/types_setuptools-80.9.0.20251223-py3-none-any.whl", hash = "sha256:1b36db79d724c2287d83dc052cf887b47c0da6a2fff044378be0b019545f56e6", size = 64318, upload-time = "2025-12-23T03:18:25.868Z" }, ] -[[package]] -name = "typing-extensions" -version = "4.13.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.9'", -] -sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, -] - [[package]] name = "typing-extensions" version = "4.15.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", - "python_full_version == '3.9.*'", -] sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, From addd1fc9a8b4f89a99525a4831d06d1a7a022195 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Fri, 2 Jan 2026 13:25:23 +0200 Subject: [PATCH 07/10] chore: Specify mypy ignore path in VS Code settings --- .vscode/settings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.vscode/settings.json b/.vscode/settings.json index df1d83b7..607dbfde 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,5 @@ { "python.autoComplete.extraPaths": ["./fluent.syntax"], "python.analysis.extraPaths": ["./fluent.syntax"], + "mypy-type-checker.ignorePatterns": ["**/tests/**"] } From ca8503035fd7cebcf9bedd4c8622d4e40b06cfdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pred=C3=A4?= <46051820+PredaaA@users.noreply.github.com> Date: Fri, 2 Jan 2026 13:35:05 +0100 Subject: [PATCH 08/10] Replace codecs.open() with built-in open() (#210) * Set newline arg in open() to disable universal newlines * Refactor test util patch_files() to use TemporaryDirectory --------- Co-authored-by: Eemeli Aro --- fluent.runtime/fluent/runtime/fallback.py | 4 +- fluent.runtime/tests/test_fallback.py | 71 ++++++++++---------- fluent.runtime/tests/test_utils.py | 52 ++++++-------- fluent.runtime/tests/utils.py | 67 +++++++----------- fluent.syntax/tests/syntax/test_reference.py | 3 +- fluent.syntax/tests/syntax/test_structure.py | 3 +- tools/fluentfmt.py | 3 +- tools/parse.py | 3 +- tools/serialize.py | 3 +- 9 files changed, 88 insertions(+), 121 deletions(-) diff --git a/fluent.runtime/fluent/runtime/fallback.py b/fluent.runtime/fluent/runtime/fallback.py index 831d4288..178450dc 100644 --- a/fluent.runtime/fluent/runtime/fallback.py +++ b/fluent.runtime/fluent/runtime/fallback.py @@ -1,4 +1,3 @@ -import codecs import os from collections.abc import Generator from typing import TYPE_CHECKING, Any, Callable, Union, cast @@ -158,7 +157,8 @@ def resources( path = self.localize_path(os.path.join(root, resource_id), locale) if not os.path.isfile(path): continue - content = codecs.open(path, "r", "utf-8").read() + with open(path, "r", encoding="utf-8", newline="\n") as file: + content = file.read() resources.append(FluentParser().parse(content)) if resources: yield resources diff --git a/fluent.runtime/tests/test_fallback.py b/fluent.runtime/tests/test_fallback.py index 7192c022..6e43f795 100644 --- a/fluent.runtime/tests/test_fallback.py +++ b/fluent.runtime/tests/test_fallback.py @@ -1,4 +1,5 @@ import unittest +from os.path import join from .utils import patch_files from fluent.runtime import FluentLocalization, FluentResourceLoader @@ -11,30 +12,26 @@ def test_init(self): ) self.assertTrue(callable(l10n.format_value)) - @patch_files({ - "de/one.ftl": """one = in German - .foo = one in German - """, - "de/two.ftl": """two = in German - .foo = two in German - """, - "fr/two.ftl": """three = in French - .foo = three in French - """, - "en/one.ftl": """four = exists - .foo = four in English - """, - "en/two.ftl": """ -five = exists - .foo = five in English -bar = - .foo = bar in English -baz = baz in English - """, - }) - def test_bundles(self): + @patch_files( + { + "de": { + "one.ftl": "one = in German\n .foo = one in German\n", + "two.ftl": "two = in German\n .foo = two in German\n", + }, + "fr": {"two.ftl": "three = in French\n .foo = three in French\n"}, + "en": { + "one.ftl": "four = exists\n .foo = four in English\n", + "two.ftl": "five = exists\n .foo = five in English\n" + + "bar =\n .foo = bar in English\n" + + "baz = baz in English\n", + }, + } + ) + def test_bundles(self, root): l10n = FluentLocalization( - ["de", "fr", "en"], ["one.ftl", "two.ftl"], FluentResourceLoader("{locale}") + ["de", "fr", "en"], + ["one.ftl", "two.ftl"], + FluentResourceLoader(join(root, "{locale}")), ) bundles_gen = l10n._bundles() bundle_de = next(bundles_gen) @@ -91,29 +88,31 @@ def test_bundles(self): class TestResourceLoader(unittest.TestCase): - @patch_files({ - "en/one.ftl": "one = exists", - "en/two.ftl": "two = exists", - }) - def test_all_exist(self): - loader = FluentResourceLoader("{locale}") + @patch_files( + { + "en": { + "one.ftl": "one = exists", + "two.ftl": "two = exists", + } + } + ) + def test_all_exist(self, root): + loader = FluentResourceLoader(join(root, "{locale}")) resources_list = list(loader.resources("en", ["one.ftl", "two.ftl"])) self.assertEqual(len(resources_list), 1) resources = resources_list[0] self.assertEqual(len(resources), 2) - @patch_files({ - "en/two.ftl": "two = exists", - }) - def test_one_exists(self): - loader = FluentResourceLoader("{locale}") + @patch_files({"en": {"two.ftl": "two = exists"}}) + def test_one_exists(self, root): + loader = FluentResourceLoader(join(root, "{locale}")) resources_list = list(loader.resources("en", ["one.ftl", "two.ftl"])) self.assertEqual(len(resources_list), 1) resources = resources_list[0] self.assertEqual(len(resources), 1) @patch_files({}) - def test_none_exist(self): - loader = FluentResourceLoader("{locale}") + def test_none_exist(self, root): + loader = FluentResourceLoader(join(root, "{locale}")) resources_list = list(loader.resources("en", ["one.ftl", "two.ftl"])) self.assertEqual(len(resources_list), 0) diff --git a/fluent.runtime/tests/test_utils.py b/fluent.runtime/tests/test_utils.py index 32c0ab0e..24ea14b0 100644 --- a/fluent.runtime/tests/test_utils.py +++ b/fluent.runtime/tests/test_utils.py @@ -1,42 +1,30 @@ import unittest from .utils import patch_files -import os -import codecs +from os.path import isdir, isfile, join class TestFileSimulate(unittest.TestCase): def test_basic(self): - @patch_files({ - "the.txt": "The", - "en/one.txt": "One", - "en/two.txt": "Two" - }) - def patch_me(a, b): + @patch_files( + { + "the.txt": "The", + "en": { + "one.txt": "One", + "two.txt": "Two", + }, + } + ) + def patch_me(a, b, root): self.assertEqual(a, 10) self.assertEqual(b, "b") - self.assertFileIs(os.path.basename(__file__), None) - self.assertFileIs("the.txt", "The") - self.assertFileIs("en/one.txt", "One") - self.assertFileIs("en\\one.txt", "One") - self.assertFileIs("en/two.txt", "Two") - self.assertFileIs("en\\two.txt", "Two") - self.assertFileIs("en/three.txt", None) - self.assertFileIs("en\\three.txt", None) + with open(join(root, "the.txt")) as f: + self.assertEqual(f.read(), "The") + with open(join(root, "en", "one.txt")) as f: + self.assertEqual(f.read(), "One") + with open(join(root, "en", "two.txt")) as f: + self.assertEqual(f.read(), "Two") + self.assertTrue(isdir(join(root, "en"))) + self.assertFalse(isfile(join(root, "none.txt"))) + self.assertFalse(isfile(join(root, "en", "three.txt"))) - with self.assertRaises(ValueError): - os.path.isfile("en/") patch_me(10, "b") - - def assertFileIs(self, filename, expect_contents): - """ - expect_contents is None: Expect file does not exist - expect_contents is a str: Expect file to exist and contents to match - """ - if expect_contents is None: - self.assertFalse(os.path.isfile(filename), - f"Expected {filename} to not exist.") - else: - self.assertTrue(os.path.isfile(filename), - f"Expected {filename} to exist.") - with codecs.open(filename, "r", "utf-8") as f: - self.assertEqual(f.read(), expect_contents) diff --git a/fluent.runtime/tests/utils.py b/fluent.runtime/tests/utils.py index fec04180..f4d6a01b 100644 --- a/fluent.runtime/tests/utils.py +++ b/fluent.runtime/tests/utils.py @@ -1,50 +1,35 @@ """Utilities for testing.""" import textwrap -from pathlib import PureWindowsPath, PurePosixPath -from unittest import mock -from io import StringIO -import functools +from functools import wraps +from os import mkdir +from os.path import join +from tempfile import TemporaryDirectory def dedent_ftl(text): return textwrap.dedent(f"{text.rstrip()}\n") -# Needed in test_falllback.py because it uses dict + string compare to make a virtual file structure -def _normalize_file_path(path): - """Note: Does not support absolute paths or paths that - contain '.' or '..' parts.""" - # Cannot use os.path or PurePath, because they only recognize - # one kind of path separator - if PureWindowsPath(path).is_absolute() or PurePosixPath(path).is_absolute(): - raise ValueError(f"Unsupported path: {path}") - parts = path.replace("\\", "/").split("/") - if "." in parts or ".." in parts: - raise ValueError(f"Unsupported path: {path}") - if parts and parts[-1] == "": - # path ends with a trailing pathsep - raise ValueError(f"Path appears to be a directory, not a file: {path}") - return "/".join(parts) - - -def patch_files(files: dict): - """Decorate a function to simulate files ``files`` during the function. - - The keys of ``files`` are file names and must use '/' for path separator. - The values are file contents. Directories or relative paths are not supported. - Example: ``{"en/one.txt": "One", "en/two.txt": "Two"}`` - - The implementation may be changed to match the mechanism used. - """ - - # Here it is possible to validate file names, but skipped - - def then(func): - @mock.patch("os.path.isfile", side_effect=lambda p: _normalize_file_path(p) in files) - @mock.patch("codecs.open", side_effect=lambda p, _, __: StringIO(files[_normalize_file_path(p)])) - @functools.wraps(func) # Make ret look like func to later decorators - def ret(*args, **kwargs): - func(*args[:-2], **kwargs) - return ret - return then +def patch_files(tree: dict): + def build_file_tree(root: str, tree: dict) -> None: + for name, value in tree.items(): + path = join(root, name) + if isinstance(value, str): + with open(path, "x", encoding="utf-8", newline="\n") as file: + if value: + file.write(value) + else: + mkdir(path) + build_file_tree(path, value) + + def decorator(fn): + @wraps(fn) + def wrapper(*args, **kwargs): + with TemporaryDirectory() as root: + build_file_tree(root, tree) + return fn(*args, root, **kwargs) + + return wrapper + + return decorator diff --git a/fluent.syntax/tests/syntax/test_reference.py b/fluent.syntax/tests/syntax/test_reference.py index 0de996e0..17463c62 100644 --- a/fluent.syntax/tests/syntax/test_reference.py +++ b/fluent.syntax/tests/syntax/test_reference.py @@ -1,4 +1,3 @@ -import codecs import json import os import unittest @@ -7,7 +6,7 @@ def read_file(path): - with codecs.open(path, "r", encoding="utf-8") as file: + with open(path, "r", encoding="utf-8", newline="\n") as file: text = file.read() return text diff --git a/fluent.syntax/tests/syntax/test_structure.py b/fluent.syntax/tests/syntax/test_structure.py index 067187fb..8d7254b3 100644 --- a/fluent.syntax/tests/syntax/test_structure.py +++ b/fluent.syntax/tests/syntax/test_structure.py @@ -1,4 +1,3 @@ -import codecs import json import os import unittest @@ -7,7 +6,7 @@ def read_file(path): - with codecs.open(path, "r", encoding="utf-8") as file: + with open(path, "r", encoding="utf-8", newline="\n") as file: text = file.read() return text diff --git a/tools/fluentfmt.py b/tools/fluentfmt.py index 9d6b75a2..cab445b4 100755 --- a/tools/fluentfmt.py +++ b/tools/fluentfmt.py @@ -1,6 +1,5 @@ #!/usr/bin/python -import codecs import sys from fluent.syntax import parse, serialize @@ -9,7 +8,7 @@ def read_file(path): - with codecs.open(path, "r", encoding="utf-8") as file: + with open(path, "r", encoding="utf-8", newline="\n") as file: text = file.read() return text diff --git a/tools/parse.py b/tools/parse.py index f3fece98..8ad92c58 100755 --- a/tools/parse.py +++ b/tools/parse.py @@ -1,6 +1,5 @@ #!/usr/bin/python -import codecs import json import sys @@ -10,7 +9,7 @@ def read_file(path): - with codecs.open(path, "r", encoding="utf-8") as file: + with open(path, "r", encoding="utf-8", newline="\n") as file: text = file.read() return text diff --git a/tools/serialize.py b/tools/serialize.py index dccaa8d6..623b8326 100755 --- a/tools/serialize.py +++ b/tools/serialize.py @@ -1,6 +1,5 @@ #!/usr/bin/python -import codecs import json import sys @@ -10,7 +9,7 @@ def read_json(path): - with codecs.open(path, "r", encoding="utf-8") as file: + with open(path, "r", encoding="utf-8", newline="\n") as file: return json.load(file) From e95b07ea07966dec064d09398a265222742bcfa2 Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Tue, 26 May 2026 15:50:35 +0300 Subject: [PATCH 09/10] Switch from unittest to pytest (#217) --- .github/workflows/ci.yml | 7 +- fluent.pygments/tests/pygments/test_lexer.py | 13 +- fluent.runtime/tests/format/test_arguments.py | 93 ++-- .../tests/format/test_attributes.py | 261 +++++---- fluent.runtime/tests/format/test_builtins.py | 251 +++++---- fluent.runtime/tests/format/test_functions.py | 259 ++++----- fluent.runtime/tests/format/test_isolating.py | 98 ++-- .../tests/format/test_parameterized_terms.py | 362 ++++++------- .../tests/format/test_placeables.py | 253 +++++---- .../tests/format/test_primitives.py | 271 +++++----- .../tests/format/test_select_expression.py | 496 +++++++++--------- fluent.runtime/tests/test_bomb.py | 85 ++- fluent.runtime/tests/test_bundle.py | 133 ++--- fluent.runtime/tests/test_fallback.py | 162 +++--- fluent.runtime/tests/test_types.py | 255 +++++---- fluent.runtime/tests/test_utils.py | 30 -- fluent.runtime/tests/utils.py | 34 +- fluent.syntax/tests/syntax/test_ast_json.py | 22 +- fluent.syntax/tests/syntax/test_entry.py | 55 +- fluent.syntax/tests/syntax/test_equals.py | 121 ++--- fluent.syntax/tests/syntax/test_literal.py | 77 +-- fluent.syntax/tests/syntax/test_reference.py | 74 +-- fluent.syntax/tests/syntax/test_serializer.py | 152 +++--- fluent.syntax/tests/syntax/test_stream.py | 189 ++++--- fluent.syntax/tests/syntax/test_structure.py | 96 ++-- fluent.syntax/tests/syntax/test_visitor.py | 65 ++- pyproject.toml | 1 + uv.lock | 99 ++++ 28 files changed, 1916 insertions(+), 2098 deletions(-) delete mode 100644 fluent.runtime/tests/test_utils.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ba6bdf0f..89b4d2f2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,8 +31,9 @@ jobs: with: python-version: ${{ matrix.python-version }} - run: uv sync --dev --all-packages - - run: uv run python -m unittest discover -s fluent.syntax - - run: uv run python -m unittest discover -s fluent.runtime + - run: uv run pytest fluent.syntax + - run: uv run pytest fluent.runtime + - run: uv run pytest fluent.pygments # Test fluent.runtime's compatibility with a range of fluent.syntax versions. compatibility: @@ -52,4 +53,4 @@ jobs: - run: uv venv - run: uv pip install ${{ matrix.fluent-syntax }} - run: uv pip install ./fluent.runtime - - run: uv run python -m unittest discover -s fluent.runtime + - run: uv run pytest fluent.runtime diff --git a/fluent.pygments/tests/pygments/test_lexer.py b/fluent.pygments/tests/pygments/test_lexer.py index a8e8a701..394b9b87 100644 --- a/fluent.pygments/tests/pygments/test_lexer.py +++ b/fluent.pygments/tests/pygments/test_lexer.py @@ -1,20 +1,15 @@ -import unittest - from fluent.pygments.lexer import FluentLexer from pygments.token import Token -class LexerTest(unittest.TestCase): - def setUp(self): - self.lexer = FluentLexer() - +class TestLexer: def test_comment(self): fragment = "# comment\n" tokens = [ (Token.Comment.Multiline, "# comment"), (Token.Punctuation, "\n"), ] - self.assertEqual(tokens, list(self.lexer.get_tokens(fragment))) + assert list(FluentLexer().get_tokens(fragment)) == tokens def test_message(self): fragment = "msg = some value\n" @@ -24,7 +19,7 @@ def test_message(self): (Token.Literal, "some value"), (Token.Punctuation, "\n"), ] - self.assertEqual(tokens, list(self.lexer.get_tokens(fragment))) + assert list(FluentLexer().get_tokens(fragment)) == tokens def test_message_with_comment(self): fragment = "# good comment\nmsg = some value\n" @@ -36,4 +31,4 @@ def test_message_with_comment(self): (Token.Literal, "some value"), (Token.Punctuation, "\n"), ] - self.assertEqual(tokens, list(self.lexer.get_tokens(fragment))) + assert list(FluentLexer().get_tokens(fragment)) == tokens diff --git a/fluent.runtime/tests/format/test_arguments.py b/fluent.runtime/tests/format/test_arguments.py index 6016d5f7..5d49f526 100644 --- a/fluent.runtime/tests/format/test_arguments.py +++ b/fluent.runtime/tests/format/test_arguments.py @@ -1,74 +1,61 @@ -import unittest +import pytest from fluent.runtime import FluentBundle, FluentResource from ..utils import dedent_ftl -class TestNumbersInValues(unittest.TestCase): - def setUp(self): - self.bundle = FluentBundle(["en-US"], use_isolating=False) - self.bundle.add_resource( +class TestNumbersInValues: + @pytest.fixture + def bundle(self): + bundle = FluentBundle(["en-US"], use_isolating=False) + bundle.add_resource( FluentResource( dedent_ftl( """ - foo = Foo { $num } - bar = { foo } - baz = - .attr = Baz Attribute { $num } - qux = { "a" -> - *[a] Baz Variant A { $num } - } - """ + foo = Foo { $num } + bar = { foo } + baz = + .attr = Baz Attribute { $num } + qux = { "a" -> + *[a] Baz Variant A { $num } + } + """ ) ) ) + return bundle - def test_can_be_used_in_the_message_value(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("foo").value, {"num": 3} - ) - self.assertEqual(val, "Foo 3") - self.assertEqual(len(errs), 0) - - def test_can_be_used_in_the_message_value_which_is_referenced(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("bar").value, {"num": 3} - ) - self.assertEqual(val, "Foo 3") - self.assertEqual(len(errs), 0) + def test_can_be_used_in_the_message_value(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("foo").value, {"num": 3}) + assert val == "Foo 3" + assert len(errs) == 0 - def test_can_be_used_in_an_attribute(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("baz").attributes["attr"], {"num": 3} - ) - self.assertEqual(val, "Baz Attribute 3") - self.assertEqual(len(errs), 0) + def test_can_be_used_in_the_message_value_which_is_referenced(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("bar").value, {"num": 3}) + assert val == "Foo 3" + assert len(errs) == 0 - def test_can_be_used_in_a_variant(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("qux").value, {"num": 3} + def test_can_be_used_in_an_attribute(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("baz").attributes["attr"], {"num": 3} ) - self.assertEqual(val, "Baz Variant A 3") - self.assertEqual(len(errs), 0) + assert val == "Baz Attribute 3" + assert len(errs) == 0 + def test_can_be_used_in_a_variant(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("qux").value, {"num": 3}) + assert val == "Baz Variant A 3" + assert len(errs) == 0 -class TestStrings(unittest.TestCase): - def setUp(self): - self.bundle = FluentBundle(["en-US"], use_isolating=False) - self.bundle.add_resource( - FluentResource( - dedent_ftl( - """ - foo = { $arg } - """ - ) - ) - ) +class TestStrings: def test_can_be_a_string(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("foo").value, {"arg": "Argument"} + bundle = FluentBundle(["en-US"], use_isolating=False) + bundle.add_resource(FluentResource("foo = { $arg }")) + + val, errs = bundle.format_pattern( + bundle.get_message("foo").value, {"arg": "Argument"} ) - self.assertEqual(val, "Argument") - self.assertEqual(len(errs), 0) + assert val == "Argument" + assert len(errs) == 0 diff --git a/fluent.runtime/tests/format/test_attributes.py b/fluent.runtime/tests/format/test_attributes.py index 040cda9b..915fb165 100644 --- a/fluent.runtime/tests/format/test_attributes.py +++ b/fluent.runtime/tests/format/test_attributes.py @@ -1,190 +1,173 @@ -import unittest - +import pytest from fluent.runtime import FluentBundle, FluentResource from fluent.runtime.errors import FluentReferenceError from ..utils import dedent_ftl -class TestAttributesWithStringValues(unittest.TestCase): - - def setUp(self): - self.bundle = FluentBundle(["en-US"], use_isolating=False) - self.bundle.add_resource( +class TestAttributesWithStringValues: + @pytest.fixture + def bundle(self): + bundle = FluentBundle(["en-US"], use_isolating=False) + bundle.add_resource( FluentResource( dedent_ftl( """ - foo = Foo - .attr = Foo Attribute - bar = { foo } Bar - .attr = Bar Attribute - ref-foo = { foo.attr } - ref-bar = { bar.attr } - """ + foo = Foo + .attr = Foo Attribute + bar = { foo } Bar + .attr = Bar Attribute + ref-foo = { foo.attr } + ref-bar = { bar.attr } + """ ) ) ) + return bundle - def test_can_be_referenced_for_entities_with_string_values(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("ref-foo").value, {} - ) - self.assertEqual(val, "Foo Attribute") - self.assertEqual(len(errs), 0) + def test_can_be_referenced_for_entities_with_string_values(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("ref-foo").value, {}) + assert val == "Foo Attribute" + assert len(errs) == 0 - def test_can_be_referenced_for_entities_with_pattern_values(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("ref-bar").value, {} - ) - self.assertEqual(val, "Bar Attribute") - self.assertEqual(len(errs), 0) + def test_can_be_referenced_for_entities_with_pattern_values(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("ref-bar").value, {}) + assert val == "Bar Attribute" + assert len(errs) == 0 - def test_can_be_formatted_directly_for_entities_with_string_values(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("foo").attributes["attr"], {} + def test_can_be_formatted_directly_for_entities_with_string_values(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("foo").attributes["attr"], {} ) - self.assertEqual(val, "Foo Attribute") - self.assertEqual(len(errs), 0) + assert val == "Foo Attribute" + assert len(errs) == 0 - def test_can_be_formatted_directly_for_entities_with_pattern_values(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("bar").attributes["attr"], {} + def test_can_be_formatted_directly_for_entities_with_pattern_values(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("bar").attributes["attr"], {} ) - self.assertEqual(val, "Bar Attribute") - self.assertEqual(len(errs), 0) - + assert val == "Bar Attribute" + assert len(errs) == 0 -class TestAttributesWithSimplePatternValues(unittest.TestCase): - def setUp(self): - self.bundle = FluentBundle(["en-US"], use_isolating=False) - self.bundle.add_resource( +class TestAttributesWithSimplePatternValues: + @pytest.fixture + def bundle(self): + bundle = FluentBundle(["en-US"], use_isolating=False) + bundle.add_resource( FluentResource( dedent_ftl( """ - foo = Foo - bar = Bar - .attr = { foo } Attribute - baz = { foo } Baz - .attr = { foo } Attribute - qux = Qux - .attr = { qux } Attribute - ref-bar = { bar.attr } - ref-baz = { baz.attr } - ref-qux = { qux.attr } - """ + foo = Foo + bar = Bar + .attr = { foo } Attribute + baz = { foo } Baz + .attr = { foo } Attribute + qux = Qux + .attr = { qux } Attribute + ref-bar = { bar.attr } + ref-baz = { baz.attr } + ref-qux = { qux.attr } + """ ) ) ) + return bundle - def test_can_be_referenced_for_entities_with_string_values(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("ref-bar").value, {} - ) - self.assertEqual(val, "Foo Attribute") - self.assertEqual(len(errs), 0) + def test_can_be_referenced_for_entities_with_string_values(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("ref-bar").value, {}) + assert val == "Foo Attribute" + assert len(errs) == 0 - def test_can_be_formatted_directly_for_entities_with_string_values(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("bar").attributes["attr"], {} + def test_can_be_formatted_directly_for_entities_with_string_values(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("bar").attributes["attr"], {} ) - self.assertEqual(val, "Foo Attribute") - self.assertEqual(len(errs), 0) + assert val == "Foo Attribute" + assert len(errs) == 0 - def test_can_be_referenced_for_entities_with_pattern_values(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("ref-baz").value, {} - ) - self.assertEqual(val, "Foo Attribute") - self.assertEqual(len(errs), 0) + def test_can_be_referenced_for_entities_with_pattern_values(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("ref-baz").value, {}) + assert val == "Foo Attribute" + assert len(errs) == 0 - def test_can_be_formatted_directly_for_entities_with_pattern_values(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("baz").attributes["attr"], {} + def test_can_be_formatted_directly_for_entities_with_pattern_values(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("baz").attributes["attr"], {} ) - self.assertEqual(val, "Foo Attribute") - self.assertEqual(len(errs), 0) + assert val == "Foo Attribute" + assert len(errs) == 0 - def test_works_with_self_references(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("ref-qux").value, {} - ) - self.assertEqual(val, "Qux Attribute") - self.assertEqual(len(errs), 0) + def test_works_with_self_references(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("ref-qux").value, {}) + assert val == "Qux Attribute" + assert len(errs) == 0 - def test_works_with_self_references_direct(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("qux").attributes["attr"], {} + def test_works_with_self_references_direct(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("qux").attributes["attr"], {} ) - self.assertEqual(val, "Qux Attribute") - self.assertEqual(len(errs), 0) + assert val == "Qux Attribute" + assert len(errs) == 0 -class TestMissing(unittest.TestCase): - def setUp(self): - self.bundle = FluentBundle(["en-US"], use_isolating=False) - self.bundle.add_resource( +class TestMissing: + @pytest.fixture + def bundle(self): + bundle = FluentBundle(["en-US"], use_isolating=False) + bundle.add_resource( FluentResource( dedent_ftl( """ - foo = Foo - bar = Bar - .attr = Bar Attribute - baz = { foo } Baz - qux = { foo } Qux - .attr = Qux Attribute - ref-foo = { foo.missing } - ref-bar = { bar.missing } - ref-baz = { baz.missing } - ref-qux = { qux.missing } - attr-only = - .attr = Attr Only Attribute - ref-double-missing = { missing.attr } - """ + foo = Foo + bar = Bar + .attr = Bar Attribute + baz = { foo } Baz + qux = { foo } Qux + .attr = Qux Attribute + ref-foo = { foo.missing } + ref-bar = { bar.missing } + ref-baz = { baz.missing } + ref-qux = { qux.missing } + attr-only = + .attr = Attr Only Attribute + ref-double-missing = { missing.attr } + """ ) ) ) + return bundle - def test_msg_with_string_value_and_no_attributes(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("ref-foo").value, {} - ) - self.assertEqual(val, "{foo.missing}") - self.assertEqual(errs, [FluentReferenceError("Unknown attribute: foo.missing")]) + def test_msg_with_string_value_and_no_attributes(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("ref-foo").value, {}) + assert val == "{foo.missing}" + assert errs == [FluentReferenceError("Unknown attribute: foo.missing")] - def test_msg_with_string_value_and_other_attributes(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("ref-bar").value, {} - ) - self.assertEqual(val, "{bar.missing}") - self.assertEqual(errs, [FluentReferenceError("Unknown attribute: bar.missing")]) + def test_msg_with_string_value_and_other_attributes(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("ref-bar").value, {}) + assert val == "{bar.missing}" + assert errs == [FluentReferenceError("Unknown attribute: bar.missing")] - def test_msg_with_pattern_value_and_no_attributes(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("ref-baz").value, {} - ) - self.assertEqual(val, "{baz.missing}") - self.assertEqual(errs, [FluentReferenceError("Unknown attribute: baz.missing")]) + def test_msg_with_pattern_value_and_no_attributes(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("ref-baz").value, {}) + assert val == "{baz.missing}" + assert errs == [FluentReferenceError("Unknown attribute: baz.missing")] - def test_msg_with_pattern_value_and_other_attributes(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("ref-qux").value, {} - ) - self.assertEqual(val, "{qux.missing}") - self.assertEqual(errs, [FluentReferenceError("Unknown attribute: qux.missing")]) + def test_msg_with_pattern_value_and_other_attributes(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("ref-qux").value, {}) + assert val == "{qux.missing}" + assert errs == [FluentReferenceError("Unknown attribute: qux.missing")] - def test_attr_only_attribute(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("attr-only").attributes["attr"], {} + def test_attr_only_attribute(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("attr-only").attributes["attr"], {} ) - self.assertEqual(val, "Attr Only Attribute") - self.assertEqual(len(errs), 0) + assert val == "Attr Only Attribute" + assert len(errs) == 0 - def test_missing_message_and_attribute(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("ref-double-missing").value, {} - ) - self.assertEqual(val, "{missing.attr}") - self.assertEqual( - errs, [FluentReferenceError("Unknown attribute: missing.attr")] + def test_missing_message_and_attribute(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("ref-double-missing").value, {} ) + assert val == "{missing.attr}" + assert errs == [FluentReferenceError("Unknown attribute: missing.attr")] diff --git a/fluent.runtime/tests/format/test_builtins.py b/fluent.runtime/tests/format/test_builtins.py index ea5b3383..8f4c0fd3 100644 --- a/fluent.runtime/tests/format/test_builtins.py +++ b/fluent.runtime/tests/format/test_builtins.py @@ -1,7 +1,8 @@ -import unittest from datetime import date, datetime from decimal import Decimal +import pytest + from fluent.runtime import FluentBundle, FluentResource from fluent.runtime.errors import FluentReferenceError from fluent.runtime.types import fluent_date, fluent_number @@ -9,187 +10,181 @@ from ..utils import dedent_ftl -class TestNumberBuiltin(unittest.TestCase): - - def setUp(self): - self.bundle = FluentBundle(["en-US"], use_isolating=False) - self.bundle.add_resource( +class TestNumberBuiltin: + @pytest.fixture + def bundle(self): + bundle = FluentBundle(["en-US"], use_isolating=False) + bundle.add_resource( FluentResource( dedent_ftl( """ - implicit-call = { 123456 } - implicit-call2 = { $arg } - defaults = { NUMBER(123456) } - percent-style = { NUMBER(1.234, style: "percent") } - currency-style = { NUMBER(123456, style: "currency", currency: "USD") } - from-arg = { NUMBER($arg) } - merge-params = { NUMBER($arg, useGrouping: 0) } - """ + implicit-call = { 123456 } + implicit-call2 = { $arg } + defaults = { NUMBER(123456) } + percent-style = { NUMBER(1.234, style: "percent") } + currency-style = { NUMBER(123456, style: "currency", currency: "USD") } + from-arg = { NUMBER($arg) } + merge-params = { NUMBER($arg, useGrouping: 0) } + """ ) ) ) + return bundle - def test_implicit_call(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("implicit-call").value, {} - ) - self.assertEqual(val, "123,456") - self.assertEqual(len(errs), 0) + def test_implicit_call(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("implicit-call").value, {}) + assert val == "123,456" + assert len(errs) == 0 - def test_implicit_call2_int(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("implicit-call2").value, {"arg": 123456} + def test_implicit_call2_int(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("implicit-call2").value, {"arg": 123456} ) - self.assertEqual(val, "123,456") - self.assertEqual(len(errs), 0) + assert val == "123,456" + assert len(errs) == 0 - def test_implicit_call2_float(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("implicit-call2").value, {"arg": 123456.0} + def test_implicit_call2_float(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("implicit-call2").value, {"arg": 123456.0} ) - self.assertEqual(val, "123,456") - self.assertEqual(len(errs), 0) + assert val == "123,456" + assert len(errs) == 0 - def test_implicit_call2_decimal(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("implicit-call2").value, + def test_implicit_call2_decimal(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("implicit-call2").value, {"arg": Decimal("123456.0")}, ) - self.assertEqual(val, "123,456") - self.assertEqual(len(errs), 0) + assert val == "123,456" + assert len(errs) == 0 - def test_defaults(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("defaults").value, {} - ) - self.assertEqual(val, "123,456") - self.assertEqual(len(errs), 0) + def test_defaults(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("defaults").value, {}) + assert val == "123,456" + assert len(errs) == 0 - def test_percent_style(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("percent-style").value, {} - ) - self.assertEqual(val, "123%") - self.assertEqual(len(errs), 0) + def test_percent_style(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("percent-style").value, {}) + assert val == "123%" + assert len(errs) == 0 - def test_currency_style(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("currency-style").value, {} + def test_currency_style(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("currency-style").value, {} ) - self.assertEqual(val, "$123,456.00") - self.assertEqual(len(errs), 0) + assert val == "$123,456.00" + assert len(errs) == 0 - def test_from_arg_int(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("from-arg").value, {"arg": 123456} + def test_from_arg_int(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("from-arg").value, {"arg": 123456} ) - self.assertEqual(val, "123,456") - self.assertEqual(len(errs), 0) + assert val == "123,456" + assert len(errs) == 0 - def test_from_arg_float(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("from-arg").value, {"arg": 123456.0} + def test_from_arg_float(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("from-arg").value, {"arg": 123456.0} ) - self.assertEqual(val, "123,456") - self.assertEqual(len(errs), 0) + assert val == "123,456" + assert len(errs) == 0 - def test_from_arg_decimal(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("from-arg").value, {"arg": Decimal("123456.0")} + def test_from_arg_decimal(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("from-arg").value, {"arg": Decimal("123456.0")} ) - self.assertEqual(val, "123,456") - self.assertEqual(len(errs), 0) + assert val == "123,456" + assert len(errs) == 0 - def test_from_arg_missing(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("from-arg").value, {} - ) - self.assertEqual(val, "arg") - self.assertEqual(len(errs), 1) - self.assertEqual(errs, [FluentReferenceError("Unknown external: arg")]) + def test_from_arg_missing(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("from-arg").value, {}) + assert val == "arg" + assert len(errs) == 1 + assert errs == [FluentReferenceError("Unknown external: arg")] - def test_partial_application(self): + def test_partial_application(self, bundle): number = fluent_number(123456.78, currency="USD", style="currency") - val, errs = self.bundle.format_pattern( - self.bundle.get_message("from-arg").value, {"arg": number} + val, errs = bundle.format_pattern( + bundle.get_message("from-arg").value, {"arg": number} ) - self.assertEqual(val, "$123,456.78") - self.assertEqual(len(errs), 0) + assert val == "$123,456.78" + assert len(errs) == 0 - def test_merge_params(self): + def test_merge_params(self, bundle): number = fluent_number(123456.78, currency="USD", style="currency") - val, errs = self.bundle.format_pattern( - self.bundle.get_message("merge-params").value, {"arg": number} + val, errs = bundle.format_pattern( + bundle.get_message("merge-params").value, {"arg": number} ) - self.assertEqual(val, "$123456.78") - self.assertEqual(len(errs), 0) + assert val == "$123456.78" + assert len(errs) == 0 -class TestDatetimeBuiltin(unittest.TestCase): - - def setUp(self): - self.bundle = FluentBundle(["en-US"], use_isolating=False) - self.bundle.add_resource( +class TestDatetimeBuiltin: + @pytest.fixture + def bundle(self): + bundle = FluentBundle(["en-US"], use_isolating=False) + bundle.add_resource( FluentResource( dedent_ftl( """ - implicit-call = { $date } - explicit-call = { DATETIME($date) } - call-with-arg = { DATETIME($date, dateStyle: "long") } - """ + implicit-call = { $date } + explicit-call = { DATETIME($date) } + call-with-arg = { DATETIME($date, dateStyle: "long") } + """ ) ) ) + return bundle - def test_implicit_call_date(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("implicit-call").value, {"date": date(2018, 2, 1)} + def test_implicit_call_date(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("implicit-call").value, {"date": date(2018, 2, 1)} ) - self.assertEqual(val, "Feb 1, 2018") - self.assertEqual(len(errs), 0) + assert val == "Feb 1, 2018" + assert len(errs) == 0 - def test_implicit_call_datetime(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("implicit-call").value, + def test_implicit_call_datetime(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("implicit-call").value, {"date": datetime(2018, 2, 1, 14, 15, 16)}, ) - self.assertEqual(val, "Feb 1, 2018") - self.assertEqual(len(errs), 0) + assert val == "Feb 1, 2018" + assert len(errs) == 0 - def test_explicit_call_date(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("explicit-call").value, {"date": date(2018, 2, 1)} + def test_explicit_call_date(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("explicit-call").value, {"date": date(2018, 2, 1)} ) - self.assertEqual(val, "Feb 1, 2018") - self.assertEqual(len(errs), 0) + assert val == "Feb 1, 2018" + assert len(errs) == 0 - def test_explicit_call_datetime(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("explicit-call").value, + def test_explicit_call_datetime(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("explicit-call").value, {"date": datetime(2018, 2, 1, 14, 15, 16)}, ) - self.assertEqual(val, "Feb 1, 2018") - self.assertEqual(len(errs), 0) + assert val == "Feb 1, 2018" + assert len(errs) == 0 - def test_explicit_call_date_fluent_date(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("explicit-call").value, + def test_explicit_call_date_fluent_date(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("explicit-call").value, {"date": fluent_date(date(2018, 2, 1), dateStyle="short")}, ) - self.assertEqual(val, "2/1/18") - self.assertEqual(len(errs), 0) + assert val == "2/1/18" + assert len(errs) == 0 - def test_arg(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("call-with-arg").value, {"date": date(2018, 2, 1)} + def test_arg(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("call-with-arg").value, {"date": date(2018, 2, 1)} ) - self.assertEqual(val, "February 1, 2018") - self.assertEqual(len(errs), 0) + assert val == "February 1, 2018" + assert len(errs) == 0 - def test_arg_overrides_fluent_date(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("call-with-arg").value, + def test_arg_overrides_fluent_date(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("call-with-arg").value, {"date": fluent_date(date(2018, 2, 1), dateStyle="short")}, ) - self.assertEqual(val, "February 1, 2018") - self.assertEqual(len(errs), 0) + assert val == "February 1, 2018" + assert len(errs) == 0 diff --git a/fluent.runtime/tests/format/test_functions.py b/fluent.runtime/tests/format/test_functions.py index e4011328..2135b3f4 100644 --- a/fluent.runtime/tests/format/test_functions.py +++ b/fluent.runtime/tests/format/test_functions.py @@ -1,5 +1,4 @@ -import unittest - +import pytest from fluent.runtime import FluentBundle, FluentResource from fluent.runtime.errors import FluentReferenceError from fluent.runtime.types import FluentNone @@ -7,203 +6,171 @@ from ..utils import dedent_ftl -class TestFunctionCalls(unittest.TestCase): - - def setUp(self): - self.bundle = FluentBundle( +class TestFunctionCalls: + @pytest.fixture + def bundle(self): + bundle = FluentBundle( ["en-US"], use_isolating=False, functions={"IDENTITY": lambda x: x} ) - self.bundle.add_resource( + bundle.add_resource( FluentResource( dedent_ftl( """ - foo = Foo - .attr = Attribute - pass-nothing = { IDENTITY() } - pass-string = { IDENTITY("a") } - pass-number = { IDENTITY(1) } - pass-message = { IDENTITY(foo) } - pass-attr = { IDENTITY(foo.attr) } - pass-external = { IDENTITY($ext) } - pass-function-call = { IDENTITY(IDENTITY(1)) } - """ + foo = Foo + .attr = Attribute + pass-nothing = { IDENTITY() } + pass-string = { IDENTITY("a") } + pass-number = { IDENTITY(1) } + pass-message = { IDENTITY(foo) } + pass-attr = { IDENTITY(foo.attr) } + pass-external = { IDENTITY($ext) } + pass-function-call = { IDENTITY(IDENTITY(1)) } + """ ) ) ) + return bundle - def test_accepts_strings(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("pass-string").value, {} - ) - self.assertEqual(val, "a") - self.assertEqual(len(errs), 0) + def test_accepts_strings(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("pass-string").value, {}) + assert val == "a" + assert len(errs) == 0 - def test_accepts_numbers(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("pass-number").value, {} - ) - self.assertEqual(val, "1") - self.assertEqual(len(errs), 0) + def test_accepts_numbers(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("pass-number").value, {}) + assert val == "1" + assert len(errs) == 0 - def test_accepts_entities(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("pass-message").value, {} - ) - self.assertEqual(val, "Foo") - self.assertEqual(len(errs), 0) + def test_accepts_entities(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("pass-message").value, {}) + assert val == "Foo" + assert len(errs) == 0 - def test_accepts_attributes(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("pass-attr").value, {} - ) - self.assertEqual(val, "Attribute") - self.assertEqual(len(errs), 0) + def test_accepts_attributes(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("pass-attr").value, {}) + assert val == "Attribute" + assert len(errs) == 0 - def test_accepts_externals(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("pass-external").value, {"ext": "Ext"} + def test_accepts_externals(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("pass-external").value, {"ext": "Ext"} ) - self.assertEqual(val, "Ext") - self.assertEqual(len(errs), 0) + assert val == "Ext" + assert len(errs) == 0 - def test_accepts_function_calls(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("pass-function-call").value, {} + def test_accepts_function_calls(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("pass-function-call").value, {} ) - self.assertEqual(val, "1") - self.assertEqual(len(errs), 0) + assert val == "1" + assert len(errs) == 0 - def test_wrong_arity(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("pass-nothing").value, {} - ) - self.assertEqual(val, "IDENTITY()") - self.assertEqual(len(errs), 1) - self.assertEqual(type(errs[0]), TypeError) + def test_wrong_arity(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("pass-nothing").value, {}) + assert val == "IDENTITY()" + assert len(errs) == 1 + assert isinstance(errs[0], TypeError) -class TestMissing(unittest.TestCase): - - def setUp(self): - self.bundle = FluentBundle(["en-US"], use_isolating=False) - self.bundle.add_resource( - FluentResource( - dedent_ftl( - """ - missing = { MISSING(1) } - """ - ) - ) - ) - +class TestMissing: def test_falls_back_to_name_of_function(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("missing").value, {} - ) - self.assertEqual(val, "MISSING()") - self.assertEqual(errs, [FluentReferenceError("Unknown function: MISSING")]) + bundle = FluentBundle(["en-US"], use_isolating=False) + bundle.add_resource(FluentResource("missing = { MISSING(1) }")) + val, errs = bundle.format_pattern(bundle.get_message("missing").value, {}) + assert val == "MISSING()" + assert errs == [FluentReferenceError("Unknown function: MISSING")] -class TestResolving(unittest.TestCase): - def setUp(self): - self.args_passed = [] +class TestResolving: + @pytest.fixture + def args_passed(self): + return [] + @pytest.fixture + def bundle(self, args_passed): def number_processor(number): - self.args_passed.append(number) + args_passed.append(number) return number - self.bundle = FluentBundle( + bundle = FluentBundle( ["en-US"], use_isolating=False, functions={"NUMBER_PROCESSOR": number_processor}, ) - - self.bundle.add_resource( + bundle.add_resource( FluentResource( - dedent_ftl( - """ - pass-number = { NUMBER_PROCESSOR(1) } - pass-arg = { NUMBER_PROCESSOR($arg) } - """ - ) + "pass-number = { NUMBER_PROCESSOR(1) }\n" + + "pass-arg = { NUMBER_PROCESSOR($arg) }\n" ) ) + return bundle - def test_args_passed_as_numbers(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("pass-arg").value, {"arg": 1} - ) - self.assertEqual(val, "1") - self.assertEqual(len(errs), 0) - self.assertEqual(self.args_passed, [1]) - - def test_literals_passed_as_numbers(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("pass-number").value, {} + def test_args_passed_as_numbers(self, args_passed, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("pass-arg").value, {"arg": 1} ) - self.assertEqual(val, "1") - self.assertEqual(len(errs), 0) - self.assertEqual(self.args_passed, [1]) + assert val == "1" + assert len(errs) == 0 + assert args_passed == [1] + def test_literals_passed_as_numbers(self, args_passed, bundle): + val, errs = bundle.format_pattern(bundle.get_message("pass-number").value, {}) + assert val == "1" + assert len(errs) == 0 + assert args_passed == [1] -class TestKeywordArgs(unittest.TestCase): - def setUp(self): - self.args_passed = [] +class TestKeywordArgs: + @pytest.fixture + def args_passed(self): + return [] + @pytest.fixture + def bundle(self, args_passed): def my_function(arg, kwarg1=None, kwarg2="default"): - self.args_passed.append((arg, kwarg1, kwarg2)) + args_passed.append((arg, kwarg1, kwarg2)) return arg - self.bundle = FluentBundle( + bundle = FluentBundle( ["en-US"], use_isolating=False, functions={"MYFUNC": my_function} ) - self.bundle.add_resource( + bundle.add_resource( FluentResource( dedent_ftl( """ - pass-arg = { MYFUNC("a") } - pass-kwarg1 = { MYFUNC("a", kwarg1: 1) } - pass-kwarg2 = { MYFUNC("a", kwarg2: "other") } - pass-kwargs = { MYFUNC("a", kwarg1: 1, kwarg2: "other") } - pass-user-arg = { MYFUNC($arg) } - """ + pass-arg = { MYFUNC("a") } + pass-kwarg1 = { MYFUNC("a", kwarg1: 1) } + pass-kwarg2 = { MYFUNC("a", kwarg2: "other") } + pass-kwargs = { MYFUNC("a", kwarg1: 1, kwarg2: "other") } + pass-user-arg = { MYFUNC($arg) } + """ ) ) ) + return bundle - def test_defaults(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("pass-arg").value, {} - ) - self.assertEqual(self.args_passed, [("a", None, "default")]) - self.assertEqual(len(errs), 0) + def test_defaults(self, args_passed, bundle): + val, errs = bundle.format_pattern(bundle.get_message("pass-arg").value, {}) + assert args_passed == [("a", None, "default")] + assert len(errs) == 0 - def test_pass_kwarg1(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("pass-kwarg1").value, {} - ) - self.assertEqual(self.args_passed, [("a", 1, "default")]) - self.assertEqual(len(errs), 0) + def test_pass_kwarg1(self, args_passed, bundle): + val, errs = bundle.format_pattern(bundle.get_message("pass-kwarg1").value, {}) + assert args_passed == [("a", 1, "default")] + assert len(errs) == 0 - def test_pass_kwarg2(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("pass-kwarg2").value, {} - ) - self.assertEqual(self.args_passed, [("a", None, "other")]) - self.assertEqual(len(errs), 0) + def test_pass_kwarg2(self, args_passed, bundle): + val, errs = bundle.format_pattern(bundle.get_message("pass-kwarg2").value, {}) + assert args_passed == [("a", None, "other")] + assert len(errs) == 0 - def test_pass_kwargs(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("pass-kwargs").value, {} - ) - self.assertEqual(self.args_passed, [("a", 1, "other")]) - self.assertEqual(len(errs), 0) + def test_pass_kwargs(self, args_passed, bundle): + val, errs = bundle.format_pattern(bundle.get_message("pass-kwargs").value, {}) + assert args_passed == [("a", 1, "other")] + assert len(errs) == 0 - def test_missing_arg(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("pass-user-arg").value, {} - ) - self.assertEqual(self.args_passed, [(FluentNone("arg"), None, "default")]) - self.assertEqual(len(errs), 1) + def test_missing_arg(self, args_passed, bundle): + val, errs = bundle.format_pattern(bundle.get_message("pass-user-arg").value, {}) + assert args_passed == [(FluentNone("arg"), None, "default")] + assert len(errs) == 1 diff --git a/fluent.runtime/tests/format/test_isolating.py b/fluent.runtime/tests/format/test_isolating.py index fff01e97..6c39333e 100644 --- a/fluent.runtime/tests/format/test_isolating.py +++ b/fluent.runtime/tests/format/test_isolating.py @@ -1,4 +1,4 @@ -import unittest +import pytest from fluent.runtime import FluentBundle, FluentResource @@ -9,76 +9,76 @@ PDI = "\u2069" -class TestUseIsolating(unittest.TestCase): - - def setUp(self): - self.bundle = FluentBundle(["en-US"]) - self.bundle.add_resource( +class TestUseIsolating: + @pytest.fixture + def bundle(self): + bundle = FluentBundle(["en-US"]) + bundle.add_resource( FluentResource( dedent_ftl( """ - foo = Foo - bar = { foo } Bar - baz = { $arg } Baz - qux = { bar } { baz } - """ + foo = Foo + bar = { foo } Bar + baz = { $arg } Baz + qux = { bar } { baz } + """ ) ) ) + return bundle - def test_isolates_interpolated_message_references(self): - val, errs = self.bundle.format_pattern(self.bundle.get_message("bar").value, {}) - self.assertEqual(val, FSI + "Foo" + PDI + " Bar") - self.assertEqual(len(errs), 0) + def test_isolates_interpolated_message_references(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("bar").value, {}) + assert val == FSI + "Foo" + PDI + " Bar" + assert len(errs) == 0 - def test_isolates_interpolated_string_typed_variable_references(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("baz").value, {"arg": "Arg"} + def test_isolates_interpolated_string_typed_variable_references(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("baz").value, {"arg": "Arg"} ) - self.assertEqual(val, FSI + "Arg" + PDI + " Baz") - self.assertEqual(len(errs), 0) + assert val == FSI + "Arg" + PDI + " Baz" + assert len(errs) == 0 - def test_isolates_interpolated_number_typed_variable_references(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("baz").value, {"arg": 1} - ) - self.assertEqual(val, FSI + "1" + PDI + " Baz") - self.assertEqual(len(errs), 0) + def test_isolates_interpolated_number_typed_variable_references(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("baz").value, {"arg": 1}) + assert val == FSI + "1" + PDI + " Baz" + assert len(errs) == 0 - def test_isolates_complex_interpolations(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("qux").value, {"arg": "Arg"} + def test_isolates_complex_interpolations(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("qux").value, {"arg": "Arg"} ) expected_bar = FSI + FSI + "Foo" + PDI + " Bar" + PDI expected_baz = FSI + FSI + "Arg" + PDI + " Baz" + PDI - self.assertEqual(val, expected_bar + " " + expected_baz) - self.assertEqual(len(errs), 0) + assert val == expected_bar + " " + expected_baz + assert len(errs) == 0 -class TestSkipIsolating(unittest.TestCase): - - def setUp(self): - self.bundle = FluentBundle(["en-US"]) - self.bundle.add_resource( +class TestSkipIsolating: + @pytest.fixture + def bundle(self): + bundle = FluentBundle(["en-US"]) + bundle.add_resource( FluentResource( dedent_ftl( """ - -brand-short-name = Amaya - foo = { -brand-short-name } - with-arg = { $arg } - """ + -brand-short-name = Amaya + foo = { -brand-short-name } + with-arg = { $arg } + """ ) ) ) + return bundle - def test_skip_isolating_chars_if_just_one_message_ref(self): - val, errs = self.bundle.format_pattern(self.bundle.get_message("foo").value, {}) - self.assertEqual(val, "Amaya") - self.assertEqual(len(errs), 0) + def test_skip_isolating_chars_if_just_one_message_ref(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("foo").value, {}) + assert val == "Amaya" + assert len(errs) == 0 - def test_skip_isolating_chars_if_just_one_placeable_arg(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("with-arg").value, {"arg": "Arg"} + def test_skip_isolating_chars_if_just_one_placeable_arg(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("with-arg").value, {"arg": "Arg"} ) - self.assertEqual(val, "Arg") - self.assertEqual(len(errs), 0) + assert val == "Arg" + assert len(errs) == 0 diff --git a/fluent.runtime/tests/format/test_parameterized_terms.py b/fluent.runtime/tests/format/test_parameterized_terms.py index 8aed061a..5f7c5d96 100644 --- a/fluent.runtime/tests/format/test_parameterized_terms.py +++ b/fluent.runtime/tests/format/test_parameterized_terms.py @@ -1,265 +1,247 @@ -import unittest - +import pytest from fluent.runtime import FluentBundle, FluentResource from fluent.runtime.errors import FluentFormatError, FluentReferenceError from ..utils import dedent_ftl -class TestParameterizedTerms(unittest.TestCase): - - def setUp(self): - self.bundle = FluentBundle(["en-US"], use_isolating=False) - self.bundle.add_resource( +class TestParameterizedTerms: + @pytest.fixture + def bundle(self): + bundle = FluentBundle(["en-US"], use_isolating=False) + bundle.add_resource( FluentResource( dedent_ftl( """ - -thing = { $article -> - *[definite] the thing - [indefinite] a thing - [none] thing - } - thing-no-arg = { -thing } - thing-no-arg-alt = { -thing() } - thing-with-arg = { -thing(article: "indefinite") } - thing-positional-arg = { -thing("foo") } - thing-fallback = { -thing(article: "somethingelse") } - bad-term = { -missing() } - """ + -thing = { $article -> + *[definite] the thing + [indefinite] a thing + [none] thing + } + thing-no-arg = { -thing } + thing-no-arg-alt = { -thing() } + thing-with-arg = { -thing(article: "indefinite") } + thing-positional-arg = { -thing("foo") } + thing-fallback = { -thing(article: "somethingelse") } + bad-term = { -missing() } + """ ) ) ) + return bundle - def test_argument_omitted(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("thing-no-arg").value, {} - ) - self.assertEqual(val, "the thing") - self.assertEqual(errs, []) + def test_argument_omitted(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("thing-no-arg").value, {}) + assert val == "the thing" + assert errs == [] - def test_argument_omitted_alt(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("thing-no-arg-alt").value, {} + def test_argument_omitted_alt(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("thing-no-arg-alt").value, {} ) - self.assertEqual(val, "the thing") - self.assertEqual(errs, []) + assert val == "the thing" + assert errs == [] - def test_with_argument(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("thing-with-arg").value, {} + def test_with_argument(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("thing-with-arg").value, {} ) - self.assertEqual(val, "a thing") - self.assertEqual(errs, []) + assert val == "a thing" + assert errs == [] - def test_positional_arg(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("thing-positional-arg").value, {} - ) - self.assertEqual(val, "the thing") - self.assertEqual( - errs, - [FluentFormatError("Ignored positional arguments passed to term '-thing'")], + def test_positional_arg(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("thing-positional-arg").value, {} ) + assert val == "the thing" + assert errs == [ + FluentFormatError("Ignored positional arguments passed to term '-thing'") + ] - def test_fallback(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("thing-fallback").value, {} + def test_fallback(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("thing-fallback").value, {} ) - self.assertEqual(val, "the thing") - self.assertEqual(errs, []) + assert val == "the thing" + assert errs == [] - def test_no_implicit_access_to_external_args(self): + def test_no_implicit_access_to_external_args(self, bundle): # The '-thing' term should not get passed article="indefinite" - val, errs = self.bundle.format_pattern( - self.bundle.get_message("thing-no-arg").value, {"article": "indefinite"} - ) - self.assertEqual(val, "the thing") - self.assertEqual(errs, []) - - def test_no_implicit_access_to_external_args_but_term_args_still_passed(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("thing-with-arg").value, {"article": "none"} + val, errs = bundle.format_pattern( + bundle.get_message("thing-no-arg").value, {"article": "indefinite"} ) - self.assertEqual(val, "a thing") - self.assertEqual(errs, []) + assert val == "the thing" + assert errs == [] - def test_bad_term(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("bad-term").value, {} + def test_no_implicit_access_to_external_args_but_term_args_still_passed( + self, bundle + ): + val, errs = bundle.format_pattern( + bundle.get_message("thing-with-arg").value, {"article": "none"} ) - self.assertEqual(val, "{-missing}") - self.assertEqual(errs, [FluentReferenceError("Unknown term: -missing")]) + assert val == "a thing" + assert errs == [] + def test_bad_term(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("bad-term").value, {}) + assert val == "{-missing}" + assert errs == [FluentReferenceError("Unknown term: -missing")] -class TestParameterizedTermAttributes(unittest.TestCase): - def setUp(self): - self.bundle = FluentBundle(["en-US"], use_isolating=False) - self.bundle.add_resource( +class TestParameterizedTermAttributes: + @pytest.fixture + def bundle(self): + bundle = FluentBundle(["en-US"], use_isolating=False) + bundle.add_resource( FluentResource( dedent_ftl( """ - -brand = Cool Thing - .status = { $version -> - [v2] available - *[v1] deprecated - } - - attr-with-arg = { -brand } is { -brand.status(version: "v2") -> - [available] available, yay! - *[deprecated] deprecated, sorry - } - - -other = { $arg -> - [a] ABC - *[d] DEF - } - - missing-attr-ref = { -other.missing(arg: "a") -> - [ABC] ABC option - *[DEF] DEF option - } - """ + -brand = Cool Thing + .status = { $version -> + [v2] available + *[v1] deprecated + } + + attr-with-arg = { -brand } is { -brand.status(version: "v2") -> + [available] available, yay! + *[deprecated] deprecated, sorry + } + + -other = { $arg -> + [a] ABC + *[d] DEF + } + + missing-attr-ref = { -other.missing(arg: "a") -> + [ABC] ABC option + *[DEF] DEF option + } + """ ) ) ) + return bundle - def test_with_argument(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("attr-with-arg").value, {} - ) - self.assertEqual(val, "Cool Thing is available, yay!") - self.assertEqual(errs, []) + def test_with_argument(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("attr-with-arg").value, {}) + assert val == "Cool Thing is available, yay!" + assert errs == [] - def test_missing_attr(self): + def test_missing_attr(self, bundle): # We don't fall back from attributes, get default. - val, errs = self.bundle.format_pattern( - self.bundle.get_message("missing-attr-ref").value, {} + val, errs = bundle.format_pattern( + bundle.get_message("missing-attr-ref").value, {} ) - self.assertEqual(val, "DEF option") - self.assertEqual( - errs, [FluentReferenceError("Unknown attribute: -other.missing")] - ) - + assert val == "DEF option" + assert errs == [FluentReferenceError("Unknown attribute: -other.missing")] -class TestNestedParameterizedTerms(unittest.TestCase): - def setUp(self): - self.bundle = FluentBundle(["en-US"], use_isolating=False) - self.bundle.add_resource( +class TestNestedParameterizedTerms: + @pytest.fixture + def bundle(self): + bundle = FluentBundle(["en-US"], use_isolating=False) + bundle.add_resource( FluentResource( dedent_ftl( """ - -thing = { $article -> - *[definite] { $first-letter -> - *[lower] the thing - [upper] The thing - } - [indefinite] { $first-letter -> - *[lower] a thing - [upper] A thing - } - } - - both-args = { -thing(first-letter: "upper", article: "indefinite") }. - outer-arg = This is { -thing(article: "indefinite") }. - inner-arg = { -thing(first-letter: "upper") }. - neither-arg = { -thing() }. - """ + -thing = { $article -> + *[definite] { $first-letter -> + *[lower] the thing + [upper] The thing + } + [indefinite] { $first-letter -> + *[lower] a thing + [upper] A thing + } + } + + both-args = { -thing(first-letter: "upper", article: "indefinite") }. + outer-arg = This is { -thing(article: "indefinite") }. + inner-arg = { -thing(first-letter: "upper") }. + neither-arg = { -thing() }. + """ ) ) ) + return bundle - def test_both_args(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("both-args").value, {} - ) - self.assertEqual(val, "A thing.") - self.assertEqual(errs, []) + def test_both_args(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("both-args").value, {}) + assert val == "A thing." + assert errs == [] - def test_outer_arg(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("outer-arg").value, {} - ) - self.assertEqual(val, "This is a thing.") - self.assertEqual(errs, []) + def test_outer_arg(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("outer-arg").value, {}) + assert val == "This is a thing." + assert errs == [] - def test_inner_arg(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("inner-arg").value, {} - ) - self.assertEqual(val, "The thing.") - self.assertEqual(errs, []) - - def test_inner_arg_with_external_args(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("inner-arg").value, {"article": "indefinite"} - ) - self.assertEqual(val, "The thing.") - self.assertEqual(errs, []) + def test_inner_arg(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("inner-arg").value, {}) + assert val == "The thing." + assert errs == [] - def test_neither_arg(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("neither-arg").value, {} + def test_inner_arg_with_external_args(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("inner-arg").value, {"article": "indefinite"} ) - self.assertEqual(val, "the thing.") - self.assertEqual(errs, []) + assert val == "The thing." + assert errs == [] + def test_neither_arg(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("neither-arg").value, {}) + assert val == "the thing." + assert errs == [] -class TestTermsCalledFromTerms(unittest.TestCase): - def setUp(self): - self.bundle = FluentBundle(["en-US"], use_isolating=False) - self.bundle.add_resource( +class TestTermsCalledFromTerms: + @pytest.fixture + def bundle(self): + bundle = FluentBundle(["en-US"], use_isolating=False) + bundle.add_resource( FluentResource( dedent_ftl( """ - -foo = {$a} {$b} - -bar = {-foo(b: 2)} - -baz = {-foo} - ref-bar = {-bar(a: 1)} - ref-baz = {-baz(a: 1)} - """ + -foo = {$a} {$b} + -bar = {-foo(b: 2)} + -baz = {-foo} + ref-bar = {-bar(a: 1)} + ref-baz = {-baz(a: 1)} + """ ) ) ) + return bundle - def test_term_args_isolated_with_call_syntax(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("ref-bar").value, {} - ) - self.assertEqual(val, "a 2") - self.assertEqual(errs, []) - - def test_term_args_isolated_without_call_syntax(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("ref-baz").value, {} - ) - self.assertEqual(val, "a b") - self.assertEqual(errs, []) + def test_term_args_isolated_with_call_syntax(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("ref-bar").value, {}) + assert val == "a 2" + assert errs == [] + def test_term_args_isolated_without_call_syntax(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("ref-baz").value, {}) + assert val == "a b" + assert errs == [] -class TestMessagesCalledFromTerms(unittest.TestCase): - def setUp(self): - self.bundle = FluentBundle(["en-US"], use_isolating=False) - self.bundle.add_resource( +class TestMessagesCalledFromTerms: + def test_messages_inherit_term_args(self): + bundle = FluentBundle(["en-US"], use_isolating=False) + bundle.add_resource( FluentResource( dedent_ftl( """ - msg = Msg is {$arg} - -foo = {msg} - ref-foo = {-foo(arg: 1)} - """ + msg = Msg is {$arg} + -foo = {msg} + ref-foo = {-foo(arg: 1)} + """ ) ) ) - def test_messages_inherit_term_args(self): # This behaviour may change in future, message calls might be # disallowed from inside terms - val, errs = self.bundle.format_pattern( - self.bundle.get_message("ref-foo").value, {"arg": 2} + val, errs = bundle.format_pattern( + bundle.get_message("ref-foo").value, {"arg": 2} ) - self.assertEqual(val, "Msg is 1") - self.assertEqual(errs, []) + assert val == "Msg is 1" + assert errs == [] diff --git a/fluent.runtime/tests/format/test_placeables.py b/fluent.runtime/tests/format/test_placeables.py index 460c34b2..d1abd21f 100644 --- a/fluent.runtime/tests/format/test_placeables.py +++ b/fluent.runtime/tests/format/test_placeables.py @@ -1,169 +1,156 @@ -import unittest - +import pytest from fluent.runtime import FluentBundle, FluentResource from fluent.runtime.errors import FluentCyclicReferenceError, FluentReferenceError from ..utils import dedent_ftl -class TestPlaceables(unittest.TestCase): - def setUp(self): - self.bundle = FluentBundle(["en-US"], use_isolating=False) - self.bundle.add_resource( +class TestPlaceables: + @pytest.fixture + def bundle(self): + bundle = FluentBundle(["en-US"], use_isolating=False) + bundle.add_resource( FluentResource( dedent_ftl( """ - message = Message - .attr = Message Attribute - -term = Term - .attr = Term Attribute - -term2 = { - *[variant1] Term Variant 1 - [variant2] Term Variant 2 - } - - uses-message = { message } - uses-message-attr = { message.attr } - uses-term = { -term } - - bad-message-ref = Text { not-a-message } - bad-message-attr-ref = Text { message.not-an-attr } - bad-term-ref = Text { -not-a-term } - - self-referencing-message = Text { self-referencing-message } - cyclic-msg1 = Text1 { cyclic-msg2 } - cyclic-msg2 = Text2 { cyclic-msg1 } - self-cyclic-message = Parent { self-cyclic-message.attr } - .attr = Attribute { self-cyclic-message } - - self-attribute-ref-ok = Parent { self-attribute-ref-ok.attr } - .attr = Attribute - self-parent-ref-ok = Parent - .attr = Attribute { self-parent-ref-ok } - """ + message = Message + .attr = Message Attribute + -term = Term + .attr = Term Attribute + -term2 = { + *[variant1] Term Variant 1 + [variant2] Term Variant 2 + } + + uses-message = { message } + uses-message-attr = { message.attr } + uses-term = { -term } + + bad-message-ref = Text { not-a-message } + bad-message-attr-ref = Text { message.not-an-attr } + bad-term-ref = Text { -not-a-term } + + self-referencing-message = Text { self-referencing-message } + cyclic-msg1 = Text1 { cyclic-msg2 } + cyclic-msg2 = Text2 { cyclic-msg1 } + self-cyclic-message = Parent { self-cyclic-message.attr } + .attr = Attribute { self-cyclic-message } + + self-attribute-ref-ok = Parent { self-attribute-ref-ok.attr } + .attr = Attribute + self-parent-ref-ok = Parent + .attr = Attribute { self-parent-ref-ok } + """ ) ) ) + return bundle - def test_placeable_message(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("uses-message").value, {} - ) - self.assertEqual(val, "Message") - self.assertEqual(len(errs), 0) + def test_placeable_message(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("uses-message").value, {}) + assert val == "Message" + assert len(errs) == 0 - def test_placeable_message_attr(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("uses-message-attr").value, {} + def test_placeable_message_attr(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("uses-message-attr").value, {} ) - self.assertEqual(val, "Message Attribute") - self.assertEqual(len(errs), 0) + assert val == "Message Attribute" + assert len(errs) == 0 - def test_placeable_term(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("uses-term").value, {} - ) - self.assertEqual(val, "Term") - self.assertEqual(len(errs), 0) + def test_placeable_term(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("uses-term").value, {}) + assert val == "Term" + assert len(errs) == 0 - def test_placeable_bad_message(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("bad-message-ref").value, {} + def test_placeable_bad_message(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("bad-message-ref").value, {} ) - self.assertEqual(val, "Text {not-a-message}") - self.assertEqual(len(errs), 1) - self.assertEqual(errs, [FluentReferenceError("Unknown message: not-a-message")]) + assert val == "Text {not-a-message}" + assert len(errs) == 1 + assert errs == [FluentReferenceError("Unknown message: not-a-message")] - def test_placeable_bad_message_attr(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("bad-message-attr-ref").value, {} + def test_placeable_bad_message_attr(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("bad-message-attr-ref").value, {} ) - self.assertEqual(val, "Text {message.not-an-attr}") - self.assertEqual(len(errs), 1) - self.assertEqual( - errs, [FluentReferenceError("Unknown attribute: message.not-an-attr")] + assert val == "Text {message.not-an-attr}" + assert len(errs) == 1 + assert errs == [FluentReferenceError("Unknown attribute: message.not-an-attr")] + + def test_placeable_bad_term(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("bad-term-ref").value, {}) + assert val == "Text {-not-a-term}" + assert len(errs) == 1 + assert errs == [FluentReferenceError("Unknown term: -not-a-term")] + + def test_cycle_detection(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("self-referencing-message").value, {} ) - - def test_placeable_bad_term(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("bad-term-ref").value, {} + assert val == "Text ???" + assert len(errs) == 1 + assert errs == [FluentCyclicReferenceError("Cyclic reference")] + + def test_mutual_cycle_detection(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("cyclic-msg1").value, {}) + assert val == "Text1 Text2 ???" + assert len(errs) == 1 + assert errs == [FluentCyclicReferenceError("Cyclic reference")] + + def test_allowed_self_reference(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("self-attribute-ref-ok").value, {} ) - self.assertEqual(val, "Text {-not-a-term}") - self.assertEqual(len(errs), 1) - self.assertEqual(errs, [FluentReferenceError("Unknown term: -not-a-term")]) - - def test_cycle_detection(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("self-referencing-message").value, {} + assert val == "Parent Attribute" + assert len(errs) == 0 + val, errs = bundle.format_pattern( + bundle.get_message("self-parent-ref-ok").attributes["attr"], {} ) - self.assertEqual(val, "Text ???") - self.assertEqual(len(errs), 1) - self.assertEqual(errs, [FluentCyclicReferenceError("Cyclic reference")]) + assert val == "Attribute Parent" + assert len(errs) == 0 - def test_mutual_cycle_detection(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("cyclic-msg1").value, {} - ) - self.assertEqual(val, "Text1 Text2 ???") - self.assertEqual(len(errs), 1) - self.assertEqual(errs, [FluentCyclicReferenceError("Cyclic reference")]) - def test_allowed_self_reference(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("self-attribute-ref-ok").value, {} - ) - self.assertEqual(val, "Parent Attribute") - self.assertEqual(len(errs), 0) - val, errs = self.bundle.format_pattern( - self.bundle.get_message("self-parent-ref-ok").attributes["attr"], {} - ) - self.assertEqual(val, "Attribute Parent") - self.assertEqual(len(errs), 0) - - -class TestSingleElementPattern(unittest.TestCase): +class TestSingleElementPattern: def test_single_literal_number_isolating(self): - self.bundle = FluentBundle(["en-US"], use_isolating=True) - self.bundle.add_resource(FluentResource("foo = { 1 }")) - val, errs = self.bundle.format_pattern(self.bundle.get_message("foo").value) - self.assertEqual(val, "1") - self.assertEqual(errs, []) + bundle = FluentBundle(["en-US"], use_isolating=True) + bundle.add_resource(FluentResource("foo = { 1 }")) + val, errs = bundle.format_pattern(bundle.get_message("foo").value) + assert val == "1" + assert errs == [] def test_single_literal_number_non_isolating(self): - self.bundle = FluentBundle(["en-US"], use_isolating=False) - self.bundle.add_resource(FluentResource("foo = { 1 }")) - val, errs = self.bundle.format_pattern(self.bundle.get_message("foo").value) - self.assertEqual(val, "1") - self.assertEqual(errs, []) + bundle = FluentBundle(["en-US"], use_isolating=False) + bundle.add_resource(FluentResource("foo = { 1 }")) + val, errs = bundle.format_pattern(bundle.get_message("foo").value) + assert val == "1" + assert errs == [] def test_single_arg_number_isolating(self): - self.bundle = FluentBundle(["en-US"], use_isolating=True) - self.bundle.add_resource(FluentResource("foo = { $arg }")) - val, errs = self.bundle.format_pattern( - self.bundle.get_message("foo").value, {"arg": 1} - ) - self.assertEqual(val, "1") - self.assertEqual(errs, []) + bundle = FluentBundle(["en-US"], use_isolating=True) + bundle.add_resource(FluentResource("foo = { $arg }")) + val, errs = bundle.format_pattern(bundle.get_message("foo").value, {"arg": 1}) + assert val == "1" + assert errs == [] def test_single_arg_number_non_isolating(self): - self.bundle = FluentBundle(["en-US"], use_isolating=False) - self.bundle.add_resource(FluentResource("foo = { $arg }")) - val, errs = self.bundle.format_pattern( - self.bundle.get_message("foo").value, {"arg": 1} - ) - self.assertEqual(val, "1") - self.assertEqual(errs, []) + bundle = FluentBundle(["en-US"], use_isolating=False) + bundle.add_resource(FluentResource("foo = { $arg }")) + val, errs = bundle.format_pattern(bundle.get_message("foo").value, {"arg": 1}) + assert val == "1" + assert errs == [] def test_single_arg_missing_isolating(self): - self.bundle = FluentBundle(["en-US"], use_isolating=True) - self.bundle.add_resource(FluentResource("foo = { $arg }")) - val, errs = self.bundle.format_pattern(self.bundle.get_message("foo").value) - self.assertEqual(val, "arg") - self.assertEqual(len(errs), 1) + bundle = FluentBundle(["en-US"], use_isolating=True) + bundle.add_resource(FluentResource("foo = { $arg }")) + val, errs = bundle.format_pattern(bundle.get_message("foo").value) + assert val == "arg" + assert len(errs) == 1 def test_single_arg_missing_non_isolating(self): - self.bundle = FluentBundle(["en-US"], use_isolating=False) - self.bundle.add_resource(FluentResource("foo = { $arg }")) - val, errs = self.bundle.format_pattern(self.bundle.get_message("foo").value) - self.assertEqual(val, "arg") - self.assertEqual(len(errs), 1) + bundle = FluentBundle(["en-US"], use_isolating=False) + bundle.add_resource(FluentResource("foo = { $arg }")) + val, errs = bundle.format_pattern(bundle.get_message("foo").value) + assert val == "arg" + assert len(errs) == 1 diff --git a/fluent.runtime/tests/format/test_primitives.py b/fluent.runtime/tests/format/test_primitives.py index 38e94e36..e50091ae 100644 --- a/fluent.runtime/tests/format/test_primitives.py +++ b/fluent.runtime/tests/format/test_primitives.py @@ -1,194 +1,189 @@ -import unittest - +import pytest from fluent.runtime import FluentBundle, FluentResource from ..utils import dedent_ftl -class TestSimpleStringValue(unittest.TestCase): - def setUp(self): - self.bundle = FluentBundle(["en-US"], use_isolating=False) - self.bundle.add_resource( +class TestSimpleStringValue: + @pytest.fixture + def bundle(self): + bundle = FluentBundle(["en-US"], use_isolating=False) + bundle.add_resource( FluentResource( dedent_ftl( r""" - foo = Foo - placeable-literal = { "Foo" } Bar - placeable-message = { foo } Bar - selector-literal = { "Foo" -> - [Foo] Member 1 - *[Bar] Member 2 - } - bar = - .attr = Bar Attribute - placeable-attr = { bar.attr } - -baz = Baz - .attr = BazAttribute - selector-attr = { -baz.attr -> - [BazAttribute] Member 3 - *[other] Member 4 - } - escapes = {" "}stuff{"\u0258}\"\\end"} - """ + foo = Foo + placeable-literal = { "Foo" } Bar + placeable-message = { foo } Bar + selector-literal = { "Foo" -> + [Foo] Member 1 + *[Bar] Member 2 + } + bar = + .attr = Bar Attribute + placeable-attr = { bar.attr } + -baz = Baz + .attr = BazAttribute + selector-attr = { -baz.attr -> + [BazAttribute] Member 3 + *[other] Member 4 + } + escapes = {" "}stuff{"\u0258}\"\\end"} + """ ) ) ) + return bundle - def test_can_be_used_as_a_value(self): - val, errs = self.bundle.format_pattern(self.bundle.get_message("foo").value, {}) - self.assertEqual(val, "Foo") - self.assertEqual(len(errs), 0) + def test_can_be_used_as_a_value(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("foo").value, {}) + assert val == "Foo" + assert len(errs) == 0 - def test_can_be_used_in_a_placeable(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("placeable-literal").value, {} + def test_can_be_used_in_a_placeable(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("placeable-literal").value, {} ) - self.assertEqual(val, "Foo Bar") - self.assertEqual(len(errs), 0) + assert val == "Foo Bar" + assert len(errs) == 0 - def test_can_be_a_value_of_a_message_referenced_in_a_placeable(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("placeable-message").value, {} + def test_can_be_a_value_of_a_message_referenced_in_a_placeable(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("placeable-message").value, {} ) - self.assertEqual(val, "Foo Bar") - self.assertEqual(len(errs), 0) + assert val == "Foo Bar" + assert len(errs) == 0 - def test_can_be_a_selector(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("selector-literal").value, {} + def test_can_be_a_selector(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("selector-literal").value, {} ) - self.assertEqual(val, "Member 1") - self.assertEqual(len(errs), 0) + assert val == "Member 1" + assert len(errs) == 0 - def test_can_be_used_as_an_attribute_value(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("bar").attributes["attr"], {} + def test_can_be_used_as_an_attribute_value(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("bar").attributes["attr"], {} ) - self.assertEqual(val, "Bar Attribute") - self.assertEqual(len(errs), 0) + assert val == "Bar Attribute" + assert len(errs) == 0 - def test_can_be_a_value_of_an_attribute_used_in_a_placeable(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("placeable-attr").value, {} + def test_can_be_a_value_of_an_attribute_used_in_a_placeable(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("placeable-attr").value, {} ) - self.assertEqual(val, "Bar Attribute") - self.assertEqual(len(errs), 0) + assert val == "Bar Attribute" + assert len(errs) == 0 - def test_can_be_a_value_of_an_attribute_used_as_a_selector(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("selector-attr").value, {} - ) - self.assertEqual(val, "Member 3") - self.assertEqual(len(errs), 0) + def test_can_be_a_value_of_an_attribute_used_as_a_selector(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("selector-attr").value, {}) + assert val == "Member 3" + assert len(errs) == 0 - def test_escapes(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("escapes").value, {} - ) - self.assertEqual(val, r' stuffɘ}"\end') - self.assertEqual(len(errs), 0) + def test_escapes(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("escapes").value, {}) + assert val == r' stuffɘ}"\end' + assert len(errs) == 0 -class TestComplexStringValue(unittest.TestCase): - def setUp(self): - self.bundle = FluentBundle(["en-US"], use_isolating=False) - self.bundle.add_resource( +class TestComplexStringValue: + @pytest.fixture + def bundle(self): + bundle = FluentBundle(["en-US"], use_isolating=False) + bundle.add_resource( FluentResource( dedent_ftl( """ - foo = Foo - bar = { foo }Bar + foo = Foo + bar = { foo }Bar - placeable-message = { bar }Baz + placeable-message = { bar }Baz - baz = - .attr = { bar }BazAttribute + baz = + .attr = { bar }BazAttribute - -qux = Qux - .attr = { bar }QuxAttribute + -qux = Qux + .attr = { bar }QuxAttribute - placeable-attr = { baz.attr } + placeable-attr = { baz.attr } - selector-attr = { -qux.attr -> - [FooBarQuxAttribute] FooBarQux - *[other] Other - } - """ + selector-attr = { -qux.attr -> + [FooBarQuxAttribute] FooBarQux + *[other] Other + } + """ ) ) ) + return bundle - def test_can_be_used_as_a_value(self): - val, errs = self.bundle.format_pattern(self.bundle.get_message("bar").value, {}) - self.assertEqual(val, "FooBar") - self.assertEqual(len(errs), 0) + def test_can_be_used_as_a_value(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("bar").value, {}) + assert val == "FooBar" + assert len(errs) == 0 - def test_can_be_value_of_a_message_referenced_in_a_placeable(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("placeable-message").value, {} + def test_can_be_value_of_a_message_referenced_in_a_placeable(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("placeable-message").value, {} ) - self.assertEqual(val, "FooBarBaz") - self.assertEqual(len(errs), 0) + assert val == "FooBarBaz" + assert len(errs) == 0 - def test_can_be_used_as_an_attribute_value(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("baz").attributes["attr"], {} + def test_can_be_used_as_an_attribute_value(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("baz").attributes["attr"], {} ) - self.assertEqual(val, "FooBarBazAttribute") - self.assertEqual(len(errs), 0) + assert val == "FooBarBazAttribute" + assert len(errs) == 0 - def test_can_be_a_value_of_an_attribute_used_in_a_placeable(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("placeable-attr").value, {} + def test_can_be_a_value_of_an_attribute_used_in_a_placeable(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("placeable-attr").value, {} ) - self.assertEqual(val, "FooBarBazAttribute") - self.assertEqual(len(errs), 0) + assert val == "FooBarBazAttribute" + assert len(errs) == 0 - def test_can_be_a_value_of_an_attribute_used_as_a_selector(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("selector-attr").value, {} - ) - self.assertEqual(val, "FooBarQux") - self.assertEqual(len(errs), 0) + def test_can_be_a_value_of_an_attribute_used_as_a_selector(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("selector-attr").value, {}) + assert val == "FooBarQux" + assert len(errs) == 0 -class TestNumbers(unittest.TestCase): - def setUp(self): - self.bundle = FluentBundle(["en-US"], use_isolating=False) - self.bundle.add_resource( +class TestNumbers: + @pytest.fixture + def bundle(self): + bundle = FluentBundle(["en-US"], use_isolating=False) + bundle.add_resource( FluentResource( dedent_ftl( """ - one = { 1 } - one_point_two = { 1.2 } - select = { 1 -> - *[0] Zero - [1] One - } - position = { NUMBER(1, type: "ordinal") -> - *[other] Zero - [one] ${1}st - } - """ + one = { 1 } + one_point_two = { 1.2 } + select = { 1 -> + *[0] Zero + [1] One + } + position = { NUMBER(1, type: "ordinal") -> + *[other] Zero + [one] ${1}st + } + """ ) ) ) + return bundle - def test_int_number_used_in_placeable(self): - val, errs = self.bundle.format_pattern(self.bundle.get_message("one").value, {}) - self.assertEqual(val, "1") - self.assertEqual(len(errs), 0) + def test_int_number_used_in_placeable(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("one").value, {}) + assert val == "1" + assert len(errs) == 0 - def test_float_number_used_in_placeable(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("one_point_two").value, {} - ) - self.assertEqual(val, "1.2") - self.assertEqual(len(errs), 0) + def test_float_number_used_in_placeable(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("one_point_two").value, {}) + assert val == "1.2" + assert len(errs) == 0 - def test_can_be_used_as_a_selector(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("select").value, {} - ) - self.assertEqual(val, "One") - self.assertEqual(len(errs), 0) + def test_can_be_used_as_a_selector(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("select").value, {}) + assert val == "One" + assert len(errs) == 0 diff --git a/fluent.runtime/tests/format/test_select_expression.py b/fluent.runtime/tests/format/test_select_expression.py index d4402996..cf33e416 100644 --- a/fluent.runtime/tests/format/test_select_expression.py +++ b/fluent.runtime/tests/format/test_select_expression.py @@ -1,337 +1,315 @@ -import unittest - +import pytest from fluent.runtime import FluentBundle, FluentResource from fluent.runtime.errors import FluentReferenceError from ..utils import dedent_ftl -class TestSelectExpressionWithStrings(unittest.TestCase): - - def setUp(self): - self.bundle = FluentBundle(["en-US"], use_isolating=False) +class TestSelectExpressionWithStrings: + @pytest.fixture + def bundle(self): + return FluentBundle(["en-US"], use_isolating=False) - def test_with_a_matching_selector(self): - self.bundle.add_resource( + def test_with_a_matching_selector(self, bundle): + bundle.add_resource( FluentResource( dedent_ftl( """ - foo = { "a" -> - [a] A - *[b] B - } - """ + foo = { "a" -> + [a] A + *[b] B + } + """ ) ) ) - val, errs = self.bundle.format_pattern(self.bundle.get_message("foo").value, {}) - self.assertEqual(val, "A") - self.assertEqual(len(errs), 0) + val, errs = bundle.format_pattern(bundle.get_message("foo").value, {}) + assert val == "A" + assert len(errs) == 0 - def test_with_a_non_matching_selector(self): - self.bundle.add_resource( + def test_with_a_non_matching_selector(self, bundle): + bundle.add_resource( FluentResource( dedent_ftl( """ - foo = { "c" -> - [a] A - *[b] B - } - """ + foo = { "c" -> + [a] A + *[b] B + } + """ ) ) ) - val, errs = self.bundle.format_pattern(self.bundle.get_message("foo").value, {}) - self.assertEqual(val, "B") - self.assertEqual(len(errs), 0) + val, errs = bundle.format_pattern(bundle.get_message("foo").value, {}) + assert val == "B" + assert len(errs) == 0 - def test_with_a_missing_selector(self): - self.bundle.add_resource( + def test_with_a_missing_selector(self, bundle): + bundle.add_resource( FluentResource( dedent_ftl( """ - foo = { $none -> - [a] A - *[b] B - } - """ + foo = { $none -> + [a] A + *[b] B + } + """ ) ) ) - val, errs = self.bundle.format_pattern(self.bundle.get_message("foo").value, {}) - self.assertEqual(val, "B") - self.assertEqual(errs, [FluentReferenceError("Unknown external: none")]) + val, errs = bundle.format_pattern(bundle.get_message("foo").value, {}) + assert val == "B" + assert errs == [FluentReferenceError("Unknown external: none")] - def test_with_argument_expression(self): - self.bundle.add_resource( + def test_with_argument_expression(self, bundle): + bundle.add_resource( FluentResource( dedent_ftl( """ - foo = { $arg -> - [a] A - *[b] B - } - """ + foo = { $arg -> + [a] A + *[b] B + } + """ ) ) ) - val, errs = self.bundle.format_pattern( - self.bundle.get_message("foo").value, {"arg": "a"} - ) - self.assertEqual(val, "A") - + val, errs = bundle.format_pattern(bundle.get_message("foo").value, {"arg": "a"}) + assert val == "A" -class TestSelectExpressionWithNumbers(unittest.TestCase): - def setUp(self): - self.bundle = FluentBundle(["en-US"], use_isolating=False) - self.bundle.add_resource( +class TestSelectExpressionWithNumbers: + @pytest.fixture + def bundle(self): + bundle = FluentBundle(["en-US"], use_isolating=False) + bundle.add_resource( FluentResource( dedent_ftl( """ - foo = { 1 -> - *[0] A - [1] B - } - - bar = { 2 -> - *[0] A - [1] B - } - - baz = { $num -> - *[0] A - [1] B - } - - qux = { 1.0 -> - *[0] A - [1] B - } - """ + foo = { 1 -> + *[0] A + [1] B + } + + bar = { 2 -> + *[0] A + [1] B + } + + baz = { $num -> + *[0] A + [1] B + } + + qux = { 1.0 -> + *[0] A + [1] B + } + """ ) ) ) + return bundle - def test_selects_the_right_variant(self): - val, errs = self.bundle.format_pattern(self.bundle.get_message("foo").value, {}) - self.assertEqual(val, "B") - self.assertEqual(len(errs), 0) - - def test_with_a_non_matching_selector(self): - val, errs = self.bundle.format_pattern(self.bundle.get_message("bar").value, {}) - self.assertEqual(val, "A") - self.assertEqual(len(errs), 0) + def test_selects_the_right_variant(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("foo").value, {}) + assert val == "B" + assert len(errs) == 0 - def test_with_a_missing_selector(self): - val, errs = self.bundle.format_pattern(self.bundle.get_message("baz").value, {}) - self.assertEqual(val, "A") - self.assertEqual(errs, [FluentReferenceError("Unknown external: num")]) + def test_with_a_non_matching_selector(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("bar").value, {}) + assert val == "A" + assert len(errs) == 0 - def test_with_argument_int(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("baz").value, {"num": 1} - ) - self.assertEqual(val, "B") + def test_with_a_missing_selector(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("baz").value, {}) + assert val == "A" + assert errs == [FluentReferenceError("Unknown external: num")] - def test_with_argument_float(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("baz").value, {"num": 1.0} - ) - self.assertEqual(val, "B") + def test_with_argument_int(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("baz").value, {"num": 1}) + assert val == "B" - def test_with_float(self): - val, errs = self.bundle.format_pattern(self.bundle.get_message("qux").value, {}) - self.assertEqual(val, "B") + def test_with_argument_float(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("baz").value, {"num": 1.0}) + assert val == "B" + def test_with_float(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("qux").value, {}) + assert val == "B" -class TestSelectExpressionWithPluralCategories(unittest.TestCase): - def setUp(self): - self.bundle = FluentBundle(["en-US"], use_isolating=False) - self.bundle.add_resource( +class TestSelectExpressionWithPluralCategories: + @pytest.fixture + def bundle(self): + bundle = FluentBundle(["en-US"], use_isolating=False) + bundle.add_resource( FluentResource( dedent_ftl( """ - foo = { 1 -> - [one] A - *[other] B - } - - bar = { 1 -> - [1] A - *[other] B - } - - baz = { "not a number" -> - [one] A - *[other] B - } - - qux = { $num -> - [one] A - *[other] B - } - - count = { NUMBER($num, type: "cardinal") -> - *[other] B - [one] A - } - - order = { NUMBER($num, type: "ordinal") -> - *[other] {$num}th - [one] {$num}st - [two] {$num}nd - [few] {$num}rd - } - """ + foo = { 1 -> + [one] A + *[other] B + } + + bar = { 1 -> + [1] A + *[other] B + } + + baz = { "not a number" -> + [one] A + *[other] B + } + + qux = { $num -> + [one] A + *[other] B + } + + count = { NUMBER($num, type: "cardinal") -> + *[other] B + [one] A + } + + order = { NUMBER($num, type: "ordinal") -> + *[other] {$num}th + [one] {$num}st + [two] {$num}nd + [few] {$num}rd + } + """ ) ) ) - - def test_selects_the_right_category(self): - val, errs = self.bundle.format_pattern(self.bundle.get_message("foo").value, {}) - self.assertEqual(val, "A") - self.assertEqual(len(errs), 0) - - def test_selects_exact_match(self): - val, errs = self.bundle.format_pattern(self.bundle.get_message("bar").value, {}) - self.assertEqual(val, "A") - self.assertEqual(len(errs), 0) - - def test_selects_default_with_invalid_selector(self): - val, errs = self.bundle.format_pattern(self.bundle.get_message("baz").value, {}) - self.assertEqual(val, "B") - self.assertEqual(len(errs), 0) - - def test_with_a_missing_selector(self): - val, errs = self.bundle.format_pattern(self.bundle.get_message("qux").value, {}) - self.assertEqual(val, "B") - self.assertEqual(errs, [FluentReferenceError("Unknown external: num")]) - - def test_with_argument_integer(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("qux").value, {"num": 1} - ) - self.assertEqual(val, "A") - self.assertEqual(len(errs), 0) - - val, errs = self.bundle.format_pattern( - self.bundle.get_message("qux").value, {"num": 2} - ) - self.assertEqual(val, "B") - self.assertEqual(len(errs), 0) - - def test_with_argument_float(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("qux").value, {"num": 1.0} - ) - self.assertEqual(val, "A") - self.assertEqual(len(errs), 0) - - def test_with_cardinal_integer(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("count").value, {"num": 1} + return bundle + + def test_selects_the_right_category(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("foo").value, {}) + assert val == "A" + assert len(errs) == 0 + + def test_selects_exact_match(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("bar").value, {}) + assert val == "A" + assert len(errs) == 0 + + def test_selects_default_with_invalid_selector(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("baz").value, {}) + assert val == "B" + assert len(errs) == 0 + + def test_with_a_missing_selector(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("qux").value, {}) + assert val == "B" + assert errs == [FluentReferenceError("Unknown external: num")] + + def test_with_argument_integer(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("qux").value, {"num": 1}) + assert val == "A" + assert len(errs) == 0 + + val, errs = bundle.format_pattern(bundle.get_message("qux").value, {"num": 2}) + assert val == "B" + assert len(errs) == 0 + + def test_with_argument_float(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("qux").value, {"num": 1.0}) + assert val == "A" + assert len(errs) == 0 + + def test_with_cardinal_integer(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("count").value, {"num": 1}) + assert val == "A" + assert len(errs) == 0 + + val, errs = bundle.format_pattern(bundle.get_message("count").value, {"num": 2}) + assert val == "B" + assert len(errs) == 0 + + def test_with_cardinal_float(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("count").value, {"num": 1.0} ) - self.assertEqual(val, "A") - self.assertEqual(len(errs), 0) + assert val == "A" + assert len(errs) == 0 - val, errs = self.bundle.format_pattern( - self.bundle.get_message("count").value, {"num": 2} - ) - self.assertEqual(val, "B") - self.assertEqual(len(errs), 0) + def test_with_ordinal_integer(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("order").value, {"num": 1}) + assert val == "1st" + assert len(errs) == 0 - def test_with_cardinal_float(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("count").value, {"num": 1.0} - ) - self.assertEqual(val, "A") - self.assertEqual(len(errs), 0) + val, errs = bundle.format_pattern(bundle.get_message("order").value, {"num": 2}) + assert val == "2nd" + assert len(errs) == 0 - def test_with_ordinal_integer(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("order").value, {"num": 1} + val, errs = bundle.format_pattern( + bundle.get_message("order").value, {"num": 11} ) - self.assertEqual(val, "1st") - self.assertEqual(len(errs), 0) + assert val == "11th" + assert len(errs) == 0 - val, errs = self.bundle.format_pattern( - self.bundle.get_message("order").value, {"num": 2} + val, errs = bundle.format_pattern( + bundle.get_message("order").value, {"num": 21} ) - self.assertEqual(val, "2nd") - self.assertEqual(len(errs), 0) + assert val == "21st" + assert len(errs) == 0 - val, errs = self.bundle.format_pattern( - self.bundle.get_message("order").value, {"num": 11} + def test_with_ordinal_float(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("order").value, {"num": 1.0} ) - self.assertEqual(val, "11th") - self.assertEqual(len(errs), 0) + assert val == "1st" + assert len(errs) == 0 - val, errs = self.bundle.format_pattern( - self.bundle.get_message("order").value, {"num": 21} - ) - self.assertEqual(val, "21st") - self.assertEqual(len(errs), 0) - def test_with_ordinal_float(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("order").value, {"num": 1.0} - ) - self.assertEqual(val, "1st") - self.assertEqual(len(errs), 0) - - -class TestSelectExpressionWithTerms(unittest.TestCase): - - def setUp(self): - self.bundle = FluentBundle(["en-US"], use_isolating=False) - self.bundle.add_resource( +class TestSelectExpressionWithTerms: + @pytest.fixture + def bundle(self): + bundle = FluentBundle(["en-US"], use_isolating=False) + bundle.add_resource( FluentResource( dedent_ftl( """ - -my-term = term - .attr = termattribute - - ref-term-attr = { -my-term.attr -> - [termattribute] Term Attribute - *[other] Other - } - - ref-term-attr-other = { -my-term.attr -> - [x] Term Attribute - *[other] Other - } - - ref-term-attr-missing = { -my-term.missing -> - [x] Term Attribute - *[other] Other - } - """ + -my-term = term + .attr = termattribute + + ref-term-attr = { -my-term.attr -> + [termattribute] Term Attribute + *[other] Other + } + + ref-term-attr-other = { -my-term.attr -> + [x] Term Attribute + *[other] Other + } + + ref-term-attr-missing = { -my-term.missing -> + [x] Term Attribute + *[other] Other + } + """ ) ) ) + return bundle - def test_ref_term_attribute(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("ref-term-attr").value - ) - self.assertEqual(val, "Term Attribute") - self.assertEqual(len(errs), 0) + def test_ref_term_attribute(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("ref-term-attr").value) + assert val == "Term Attribute" + assert len(errs) == 0 - def test_ref_term_attribute_fallback(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("ref-term-attr-other").value + def test_ref_term_attribute_fallback(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("ref-term-attr-other").value ) - self.assertEqual(val, "Other") - self.assertEqual(len(errs), 0) + assert val == "Other" + assert len(errs) == 0 - def test_ref_term_attribute_missing(self): - val, errs = self.bundle.format_pattern( - self.bundle.get_message("ref-term-attr-missing").value - ) - self.assertEqual(val, "Other") - self.assertEqual(len(errs), 1) - self.assertEqual( - errs, [FluentReferenceError("Unknown attribute: -my-term.missing")] + def test_ref_term_attribute_missing(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("ref-term-attr-missing").value ) + assert val == "Other" + assert len(errs) == 1 + assert errs == [FluentReferenceError("Unknown attribute: -my-term.missing")] diff --git a/fluent.runtime/tests/test_bomb.py b/fluent.runtime/tests/test_bomb.py index 479747d6..687e9141 100644 --- a/fluent.runtime/tests/test_bomb.py +++ b/fluent.runtime/tests/test_bomb.py @@ -1,49 +1,44 @@ -import unittest - +import pytest from fluent.runtime import FluentBundle, FluentResource -from .utils import dedent_ftl - - -class TestBillionLaughs(unittest.TestCase): - - def setUp(self): - self.ctx = FluentBundle(["en-US"], use_isolating=False) - self.ctx.add_resource( - FluentResource( - dedent_ftl( - """ - lol0 = 01234567890123456789012345678901234567890123456789 - lol1 = {lol0}{lol0}{lol0}{lol0}{lol0}{lol0}{lol0}{lol0}{lol0}{lol0} - lol2 = {lol1}{lol1}{lol1}{lol1}{lol1}{lol1}{lol1}{lol1}{lol1}{lol1} - lol3 = {lol2}{lol2}{lol2}{lol2}{lol2}{lol2}{lol2}{lol2}{lol2}{lol2} - lol4 = {lol3}{lol3}{lol3}{lol3}{lol3}{lol3}{lol3}{lol3}{lol3}{lol3} - lolz = {lol4} - - elol0 = { "" } - elol1 = {elol0}{elol0}{elol0}{elol0}{elol0}{elol0}{elol0}{elol0}{elol0}{elol0} - elol2 = {elol1}{elol1}{elol1}{elol1}{elol1}{elol1}{elol1}{elol1}{elol1}{elol1} - elol3 = {elol2}{elol2}{elol2}{elol2}{elol2}{elol2}{elol2}{elol2}{elol2}{elol2} - elol4 = {elol3}{elol3}{elol3}{elol3}{elol3}{elol3}{elol3}{elol3}{elol3}{elol3} - elol5 = {elol4}{elol4}{elol4}{elol4}{elol4}{elol4}{elol4}{elol4}{elol4}{elol4} - elol6 = {elol5}{elol5}{elol5}{elol5}{elol5}{elol5}{elol5}{elol5}{elol5}{elol5} - emptylolz = {elol6} - - """ - ) - ) - ) - - def test_max_length_protection(self): - val, errs = self.ctx.format_pattern(self.ctx.get_message("lolz").value) - self.assertEqual(val, "{???}") - self.assertNotEqual(len(errs), 0) - self.assertIn("Too many characters", str(errs[-1])) - - def test_max_expansions_protection(self): + +bomb_source = """\ +lol0 = 01234567890123456789012345678901234567890123456789 +lol1 = {lol0}{lol0}{lol0}{lol0}{lol0}{lol0}{lol0}{lol0}{lol0}{lol0} +lol2 = {lol1}{lol1}{lol1}{lol1}{lol1}{lol1}{lol1}{lol1}{lol1}{lol1} +lol3 = {lol2}{lol2}{lol2}{lol2}{lol2}{lol2}{lol2}{lol2}{lol2}{lol2} +lol4 = {lol3}{lol3}{lol3}{lol3}{lol3}{lol3}{lol3}{lol3}{lol3}{lol3} +lolz = {lol4} + +elol0 = { "" } +elol1 = {elol0}{elol0}{elol0}{elol0}{elol0}{elol0}{elol0}{elol0}{elol0}{elol0} +elol2 = {elol1}{elol1}{elol1}{elol1}{elol1}{elol1}{elol1}{elol1}{elol1}{elol1} +elol3 = {elol2}{elol2}{elol2}{elol2}{elol2}{elol2}{elol2}{elol2}{elol2}{elol2} +elol4 = {elol3}{elol3}{elol3}{elol3}{elol3}{elol3}{elol3}{elol3}{elol3}{elol3} +elol5 = {elol4}{elol4}{elol4}{elol4}{elol4}{elol4}{elol4}{elol4}{elol4}{elol4} +elol6 = {elol5}{elol5}{elol5}{elol5}{elol5}{elol5}{elol5}{elol5}{elol5}{elol5} +emptylolz = {elol6} +""" + + +@pytest.fixture +def bundle(): + bundle = FluentBundle(["en-US"], use_isolating=False) + bundle.add_resource(FluentResource(bomb_source)) + return bundle + + +class TestBillionLaughs: + def test_max_length_protection(self, bundle): + val, errs = bundle.format_pattern(bundle.get_message("lolz").value) + assert val == "{???}" + assert len(errs) != 0 + assert "Too many characters" in str(errs[-1]) + + def test_max_expansions_protection(self, bundle): # Without protection, emptylolz will take a really long time to # evaluate, although it generates an empty message. - val, errs = self.ctx.format_pattern(self.ctx.get_message("emptylolz").value) - self.assertEqual(val, "{???}") - self.assertEqual(len(errs), 1) - self.assertIn("Too many parts", str(errs[-1])) + val, errs = bundle.format_pattern(bundle.get_message("emptylolz").value) + assert val == "{???}" + assert len(errs) == 1 + assert "Too many parts" in str(errs[-1]) diff --git a/fluent.runtime/tests/test_bundle.py b/fluent.runtime/tests/test_bundle.py index d15eb362..82809222 100644 --- a/fluent.runtime/tests/test_bundle.py +++ b/fluent.runtime/tests/test_bundle.py @@ -1,16 +1,17 @@ -import unittest - +import pytest from fluent.runtime import FluentBundle, FluentResource from .utils import dedent_ftl -class TestFluentBundle(unittest.TestCase): - def setUp(self): - self.bundle = FluentBundle(["en-US"]) +@pytest.fixture +def bundle(): + return FluentBundle(["en-US"]) + - def test_add_resource(self): - self.bundle.add_resource( +class TestFluentBundle: + def test_add_resource(self, bundle): + bundle.add_resource( FluentResource( dedent_ftl( """ @@ -21,12 +22,12 @@ def test_add_resource(self): ) ) ) - self.assertIn("foo", self.bundle._messages) - self.assertIn("bar", self.bundle._messages) - self.assertIn("baz", self.bundle._terms) + assert "foo" in bundle._messages + assert "bar" in bundle._messages + assert "baz" in bundle._terms - def test_has_message(self): - self.bundle.add_resource( + def test_has_message(self, bundle): + bundle.add_resource( FluentResource( dedent_ftl( """ @@ -36,11 +37,11 @@ def test_has_message(self): ) ) - self.assertTrue(self.bundle.has_message("foo")) - self.assertFalse(self.bundle.has_message("bar")) + assert bundle.has_message("foo") + assert not bundle.has_message("bar") - def test_has_message_for_term(self): - self.bundle.add_resource( + def test_has_message_for_term(self, bundle): + bundle.add_resource( FluentResource( dedent_ftl( """ @@ -50,10 +51,10 @@ def test_has_message_for_term(self): ) ) - self.assertFalse(self.bundle.has_message("-foo")) + assert not bundle.has_message("-foo") - def test_has_message_with_attribute(self): - self.bundle.add_resource( + def test_has_message_with_attribute(self, bundle): + bundle.add_resource( FluentResource( dedent_ftl( """ @@ -64,53 +65,51 @@ def test_has_message_with_attribute(self): ) ) - self.assertTrue(self.bundle.has_message("foo")) - self.assertFalse(self.bundle.has_message("foo.attr")) - self.assertFalse(self.bundle.has_message("foo.other-attribute")) + assert bundle.has_message("foo") + assert not bundle.has_message("foo.attr") + assert not bundle.has_message("foo.other-attribute") - def test_plural_form_english_ints(self): - bundle = FluentBundle(["en-US"]) - self.assertEqual(bundle._plural_form(0), "other") - self.assertEqual(bundle._plural_form(1), "one") - self.assertEqual(bundle._plural_form(2), "other") + def test_plural_form_english_ints(self, bundle): + assert bundle._plural_form(0) == "other" + assert bundle._plural_form(1) == "one" + assert bundle._plural_form(2) == "other" - def test_plural_form_english_floats(self): - bundle = FluentBundle(["en-US"]) - self.assertEqual(bundle._plural_form(0.0), "other") - self.assertEqual(bundle._plural_form(1.0), "one") - self.assertEqual(bundle._plural_form(2.0), "other") - self.assertEqual(bundle._plural_form(0.5), "other") + def test_plural_form_english_floats(self, bundle): + assert bundle._plural_form(0.0) == "other" + assert bundle._plural_form(1.0) == "one" + assert bundle._plural_form(2.0) == "other" + assert bundle._plural_form(0.5) == "other" def test_plural_form_french(self): # Just spot check one other, to ensure that we # are not getting the EN locale by accident or bundle = FluentBundle(["fr"]) - self.assertEqual(bundle._plural_form(0), "one") - self.assertEqual(bundle._plural_form(1), "one") - self.assertEqual(bundle._plural_form(2), "other") - - def test_ordinal_form_english_ints(self): - bundle = FluentBundle(["en-US"]) - self.assertEqual(bundle._ordinal_form(0), "other") - self.assertEqual(bundle._ordinal_form(1), "one") - self.assertEqual(bundle._ordinal_form(2), "two") - self.assertEqual(bundle._ordinal_form(3), "few") - self.assertEqual(bundle._ordinal_form(11), "other") - self.assertEqual(bundle._ordinal_form(21), "one") - - def test_format_args(self): - self.bundle.add_resource(FluentResource("foo = Foo")) - val, errs = self.bundle.format_pattern(self.bundle.get_message("foo").value) - self.assertEqual(val, "Foo") - - val, errs = self.bundle.format_pattern(self.bundle.get_message("foo").value, {}) - self.assertEqual(val, "Foo") - - def test_format_missing(self): - self.assertRaises(LookupError, self.bundle.get_message, "a-missing-message") - - def test_format_term(self): - self.bundle.add_resource( + assert bundle._plural_form(0) == "one" + assert bundle._plural_form(1) == "one" + assert bundle._plural_form(2) == "other" + + def test_ordinal_form_english_ints(self, bundle): + assert bundle._ordinal_form(0) == "other" + assert bundle._ordinal_form(1) == "one" + assert bundle._ordinal_form(2) == "two" + assert bundle._ordinal_form(3) == "few" + assert bundle._ordinal_form(11) == "other" + assert bundle._ordinal_form(21) == "one" + + def test_format_args(self, bundle): + bundle.add_resource(FluentResource("foo = Foo")) + val, errs = bundle.format_pattern(bundle.get_message("foo").value) + assert val == "Foo" + + val, errs = bundle.format_pattern(bundle.get_message("foo").value, {}) + assert val == "Foo" + + def test_format_missing(self, bundle): + with pytest.raises(LookupError): + bundle.get_message("a-missing-message") + + def test_format_term(self, bundle): + bundle.add_resource( FluentResource( dedent_ftl( """ @@ -119,11 +118,13 @@ def test_format_term(self): ) ) ) - self.assertRaises(LookupError, self.bundle.get_message, "-foo") - self.assertRaises(LookupError, self.bundle.get_message, "foo") + with pytest.raises(LookupError): + bundle.get_message("-foo") + with pytest.raises(LookupError): + bundle.get_message("foo") - def test_message_and_term_separate(self): - self.bundle.add_resource( + def test_message_and_term_separate(self, bundle): + bundle.add_resource( FluentResource( dedent_ftl( """ @@ -133,6 +134,6 @@ def test_message_and_term_separate(self): ) ) ) - val, errs = self.bundle.format_pattern(self.bundle.get_message("foo").value, {}) - self.assertEqual(val, "Refers to \u2068Foo\u2069") - self.assertEqual(errs, []) + val, errs = bundle.format_pattern(bundle.get_message("foo").value, {}) + assert val == "Refers to \u2068Foo\u2069" + assert errs == [] diff --git a/fluent.runtime/tests/test_fallback.py b/fluent.runtime/tests/test_fallback.py index 6e43f795..71cd8fb0 100644 --- a/fluent.runtime/tests/test_fallback.py +++ b/fluent.runtime/tests/test_fallback.py @@ -1,118 +1,110 @@ -import unittest from os.path import join -from .utils import patch_files from fluent.runtime import FluentLocalization, FluentResourceLoader +from .utils import build_file_tree -class TestLocalization(unittest.TestCase): + +class TestLocalization: def test_init(self): l10n = FluentLocalization( ["en"], ["file.ftl"], FluentResourceLoader("{locale}") ) - self.assertTrue(callable(l10n.format_value)) + assert callable(l10n.format_value) - @patch_files( - { - "de": { - "one.ftl": "one = in German\n .foo = one in German\n", - "two.ftl": "two = in German\n .foo = two in German\n", - }, - "fr": {"two.ftl": "three = in French\n .foo = three in French\n"}, - "en": { - "one.ftl": "four = exists\n .foo = four in English\n", - "two.ftl": "five = exists\n .foo = five in English\n" - + "bar =\n .foo = bar in English\n" - + "baz = baz in English\n", + def test_bundles(self, tmp_path): + build_file_tree( + tmp_path, + { + "de": { + "one.ftl": "one = in German\n .foo = one in German\n", + "two.ftl": "two = in German\n .foo = two in German\n", + }, + "fr": {"two.ftl": "three = in French\n .foo = three in French\n"}, + "en": { + "one.ftl": "four = exists\n .foo = four in English\n", + "two.ftl": "five = exists\n .foo = five in English\n" + + "bar =\n .foo = bar in English\n" + + "baz = baz in English\n", + }, }, - } - ) - def test_bundles(self, root): + ) l10n = FluentLocalization( ["de", "fr", "en"], ["one.ftl", "two.ftl"], - FluentResourceLoader(join(root, "{locale}")), + FluentResourceLoader(join(tmp_path, "{locale}")), ) bundles_gen = l10n._bundles() bundle_de = next(bundles_gen) - self.assertEqual(bundle_de.locales[0], "de") - self.assertTrue(bundle_de.has_message("one")) - self.assertTrue(bundle_de.has_message("two")) + assert bundle_de.locales[0] == "de" + assert bundle_de.has_message("one") + assert bundle_de.has_message("two") bundle_fr = next(bundles_gen) - self.assertEqual(bundle_fr.locales[0], "fr") - self.assertFalse(bundle_fr.has_message("one")) - self.assertTrue(bundle_fr.has_message("three")) - self.assertListEqual(list(l10n._bundles())[:2], [bundle_de, bundle_fr]) + assert bundle_fr.locales[0] == "fr" + assert not bundle_fr.has_message("one") + assert bundle_fr.has_message("three") + assert list(l10n._bundles())[:2] == [bundle_de, bundle_fr] bundle_en = next(bundles_gen) - self.assertEqual(bundle_en.locales[0], "en") - self.assertEqual(l10n.format_value("one"), "in German") - self.assertEqual(l10n.format_value("two"), "in German") - self.assertEqual(l10n.format_value("three"), "in French") - self.assertEqual(l10n.format_value("four"), "exists") - self.assertEqual(l10n.format_value("five"), "exists") - self.assertEqual(l10n.format_value("bar"), "bar") - self.assertEqual(l10n.format_value("baz"), "baz in English") - self.assertEqual(l10n.format_value("not-exists"), "not-exists") - self.assertEqual( - tuple(l10n.format_message("one")), - ("in German", {"foo": "one in German"}), - ) - self.assertEqual( - tuple(l10n.format_message("two")), - ("in German", {"foo": "two in German"}), + assert bundle_en.locales[0] == "en" + assert l10n.format_value("one") == "in German" + assert l10n.format_value("two") == "in German" + assert l10n.format_value("three") == "in French" + assert l10n.format_value("four") == "exists" + assert l10n.format_value("five") == "exists" + assert l10n.format_value("bar") == "bar" + assert l10n.format_value("baz") == "baz in English" + assert l10n.format_value("not-exists") == "not-exists" + assert tuple(l10n.format_message("one")) == ( + "in German", + {"foo": "one in German"}, ) - self.assertEqual( - tuple(l10n.format_message("three")), - ("in French", {"foo": "three in French"}), + assert tuple(l10n.format_message("two")) == ( + "in German", + {"foo": "two in German"}, ) - self.assertEqual( - tuple(l10n.format_message("four")), - ("exists", {"foo": "four in English"}), + assert tuple(l10n.format_message("three")) == ( + "in French", + {"foo": "three in French"}, ) - self.assertEqual( - tuple(l10n.format_message("five")), - ("exists", {"foo": "five in English"}), + assert tuple(l10n.format_message("four")) == ( + "exists", + {"foo": "four in English"}, ) - self.assertEqual( - tuple(l10n.format_message("bar")), - (None, {"foo": "bar in English"}), - ) - self.assertEqual( - tuple(l10n.format_message("baz")), - ("baz in English", {}), - ) - self.assertEqual( - tuple(l10n.format_message("not-exists")), - ("not-exists", {}), + assert tuple(l10n.format_message("five")) == ( + "exists", + {"foo": "five in English"}, ) + assert tuple(l10n.format_message("bar")) == (None, {"foo": "bar in English"}) + assert tuple(l10n.format_message("baz")) == ("baz in English", {}) + assert tuple(l10n.format_message("not-exists")) == ("not-exists", {}) -class TestResourceLoader(unittest.TestCase): - @patch_files( - { - "en": { - "one.ftl": "one = exists", - "two.ftl": "two = exists", - } - } - ) - def test_all_exist(self, root): - loader = FluentResourceLoader(join(root, "{locale}")) +class TestResourceLoader: + def test_all_exist(self, tmp_path): + build_file_tree( + tmp_path, + { + "en": { + "one.ftl": "one = exists", + "two.ftl": "two = exists", + } + }, + ) + loader = FluentResourceLoader(join(tmp_path, "{locale}")) resources_list = list(loader.resources("en", ["one.ftl", "two.ftl"])) - self.assertEqual(len(resources_list), 1) + assert len(resources_list) == 1 resources = resources_list[0] - self.assertEqual(len(resources), 2) + assert len(resources) == 2 - @patch_files({"en": {"two.ftl": "two = exists"}}) - def test_one_exists(self, root): - loader = FluentResourceLoader(join(root, "{locale}")) + def test_one_exists(self, tmp_path): + build_file_tree(tmp_path, {"en": {"two.ftl": "two = exists"}}) + loader = FluentResourceLoader(join(tmp_path, "{locale}")) resources_list = list(loader.resources("en", ["one.ftl", "two.ftl"])) - self.assertEqual(len(resources_list), 1) + assert len(resources_list) == 1 resources = resources_list[0] - self.assertEqual(len(resources), 1) + assert len(resources) == 1 - @patch_files({}) - def test_none_exist(self, root): - loader = FluentResourceLoader(join(root, "{locale}")) + def test_none_exist(self, tmp_path): + loader = FluentResourceLoader(join(tmp_path, "{locale}")) resources_list = list(loader.resources("en", ["one.ftl", "two.ftl"])) - self.assertEqual(len(resources_list), 0) + assert len(resources_list) == 0 diff --git a/fluent.runtime/tests/test_types.py b/fluent.runtime/tests/test_types.py index 99f288c1..df4ca825 100644 --- a/fluent.runtime/tests/test_types.py +++ b/fluent.runtime/tests/test_types.py @@ -1,8 +1,9 @@ -import unittest +import re import warnings from datetime import date, datetime from decimal import Decimal +import pytest import pytz from babel import Locale from fluent.runtime.types import ( @@ -12,215 +13,210 @@ fluent_number, ) +en_US = Locale.parse("en_US") +cur_pos = fluent_number(123456.78123, currency="USD", style="currency") +cur_neg = fluent_number(-123456.78123, currency="USD", style="currency") -class TestFluentNumber(unittest.TestCase): - - locale = Locale.parse("en_US") - - def setUp(self): - self.cur_pos = fluent_number(123456.78123, currency="USD", style="currency") - self.cur_neg = fluent_number(-123456.78123, currency="USD", style="currency") +class TestFluentNumber: def test_int(self): i = fluent_number(1) - self.assertTrue(isinstance(i, int)) - self.assertTrue(isinstance(i, FluentNumber)) - self.assertEqual(i + 1, 2) + assert isinstance(i, int) + assert isinstance(i, FluentNumber) + assert i + 1 == 2 def test_float(self): f = fluent_number(1.1) - self.assertTrue(isinstance(f, float)) - self.assertTrue(isinstance(f, FluentNumber)) - self.assertEqual(f + 1, 2.1) + assert isinstance(f, float) + assert isinstance(f, FluentNumber) + assert f + 1 == 2.1 def test_decimal(self): d = Decimal("1.1") - self.assertTrue(isinstance(fluent_number(d), Decimal)) - self.assertTrue(isinstance(fluent_number(d), FluentNumber)) - self.assertEqual(d + 1, Decimal("2.1")) + assert isinstance(fluent_number(d), Decimal) + assert isinstance(fluent_number(d), FluentNumber) + assert d + 1 == Decimal("2.1") def test_disallow_nonexistant_options(self): - self.assertRaises( - TypeError, - fluent_number, - 1, - not_a_real_option=True, - ) + with pytest.raises(TypeError): + fluent_number( + 1, + not_a_real_option=True, + ) def test_style_validation(self): - self.assertRaises(ValueError, fluent_number, 1, style="xyz") + with pytest.raises(ValueError): + fluent_number(1, style="xyz") def test_use_grouping(self): f1 = fluent_number(123456.78, useGrouping=True) f2 = fluent_number(123456.78, useGrouping=False) - self.assertEqual(f1.format(self.locale), "123,456.78") - self.assertEqual(f2.format(self.locale), "123456.78") + assert f1.format(en_US) == "123,456.78" + assert f2.format(en_US) == "123456.78" # ensure we didn't mutate anything when we created the new # NumberPattern: - self.assertEqual(f1.format(self.locale), "123,456.78") + assert f1.format(en_US) == "123,456.78" def test_use_grouping_decimal(self): d = Decimal("123456.78") f1 = fluent_number(d, useGrouping=True) f2 = fluent_number(d, useGrouping=False) - self.assertEqual(f1.format(self.locale), "123,456.78") - self.assertEqual(f2.format(self.locale), "123456.78") + assert f1.format(en_US) == "123,456.78" + assert f2.format(en_US) == "123456.78" def test_minimum_integer_digits(self): f = fluent_number(1.23, minimumIntegerDigits=3) - self.assertEqual(f.format(self.locale), "001.23") + assert f.format(en_US) == "001.23" def test_minimum_integer_digits_decimal(self): f = fluent_number(Decimal("1.23"), minimumIntegerDigits=3) - self.assertEqual(f.format(self.locale), "001.23") + assert f.format(en_US) == "001.23" def test_minimum_fraction_digits(self): f = fluent_number(1.2, minimumFractionDigits=3) - self.assertEqual(f.format(self.locale), "1.200") + assert f.format(en_US) == "1.200" def test_maximum_fraction_digits(self): f1 = fluent_number(1.23456) - self.assertEqual(f1.format(self.locale), "1.235") + assert f1.format(en_US) == "1.235" f2 = fluent_number(1.23456, maximumFractionDigits=5) - self.assertEqual(f2.format(self.locale), "1.23456") + assert f2.format(en_US) == "1.23456" def test_minimum_significant_digits(self): f1 = fluent_number(123, minimumSignificantDigits=5) - self.assertEqual(f1.format(self.locale), "123.00") + assert f1.format(en_US) == "123.00" f2 = fluent_number(12.3, minimumSignificantDigits=5) - self.assertEqual(f2.format(self.locale), "12.300") + assert f2.format(en_US) == "12.300" def test_maximum_significant_digits(self): f1 = fluent_number(123456, maximumSignificantDigits=3) - self.assertEqual(f1.format(self.locale), "123,000") + assert f1.format(en_US) == "123,000" f2 = fluent_number(12.3456, maximumSignificantDigits=3) - self.assertEqual(f2.format(self.locale), "12.3") + assert f2.format(en_US) == "12.3" f3 = fluent_number(12, maximumSignificantDigits=5) - self.assertEqual(f3.format(self.locale), "12") + assert f3.format(en_US) == "12" def test_currency(self): # This test the default currencyDisplay value - self.assertEqual(self.cur_pos.format(self.locale), "$123,456.78") + assert cur_pos.format(en_US) == "$123,456.78" def test_currency_display_validation(self): - self.assertRaises(ValueError, fluent_number, 1234, currencyDisplay="junk") + with pytest.raises(ValueError): + fluent_number(1234, currencyDisplay="junk") def test_currency_display_symbol(self): - cur_pos_sym = fluent_number(self.cur_pos, currencyDisplay="symbol") - cur_neg_sym = fluent_number(self.cur_neg, currencyDisplay="symbol") - self.assertEqual(cur_pos_sym.format(self.locale), "$123,456.78") - self.assertEqual(cur_neg_sym.format(self.locale), "-$123,456.78") + cur_pos_sym = fluent_number(cur_pos, currencyDisplay="symbol") + cur_neg_sym = fluent_number(cur_neg, currencyDisplay="symbol") + assert cur_pos_sym.format(en_US) == "$123,456.78" + assert cur_neg_sym.format(en_US) == "-$123,456.78" def test_currency_display_code(self): # Outputs here were determined by comparing with Javascrpt # Intl.NumberFormat in Firefox. - cur_pos_code = fluent_number(self.cur_pos, currencyDisplay="code") - cur_neg_code = fluent_number(self.cur_neg, currencyDisplay="code") - self.assertEqual(cur_pos_code.format(self.locale), "USD123,456.78") - self.assertEqual(cur_neg_code.format(self.locale), "-USD123,456.78") + cur_pos_code = fluent_number(cur_pos, currencyDisplay="code") + cur_neg_code = fluent_number(cur_neg, currencyDisplay="code") + assert cur_pos_code.format(en_US) == "USD123,456.78" + assert cur_neg_code.format(en_US) == "-USD123,456.78" - @unittest.skip("Babel doesn't provide support for this yet") + @pytest.mark.skip("Babel doesn't provide support for this yet") def test_currency_display_name(self): - cur_pos_name = fluent_number(self.cur_pos, currencyDisplay="name") - cur_neg_name = fluent_number(self.cur_neg, currencyDisplay="name") - self.assertEqual(cur_pos_name.format(self.locale), "123,456.78 US dollars") - self.assertEqual(cur_neg_name.format(self.locale), "-123,456.78 US dollars") + cur_pos_name = fluent_number(cur_pos, currencyDisplay="name") + cur_neg_name = fluent_number(cur_neg, currencyDisplay="name") + assert cur_pos_name.format(en_US) == "123,456.78 US dollars" + assert cur_neg_name.format(en_US) == "-123,456.78 US dollars" # Some others locales: hr_BA = Locale.parse("hr_BA") - self.assertEqual(cur_pos_name.format(hr_BA), "123.456,78 američkih dolara") + assert cur_pos_name.format(hr_BA) == "123.456,78 američkih dolara" es_GT = Locale.parse("es_GT") - self.assertEqual( - cur_pos_name.format(es_GT), "dólares estadounidenses 123,456.78" - ) + assert cur_pos_name.format(es_GT) == "dólares estadounidenses 123,456.78" def test_copy_attributes(self): f1 = fluent_number(123456.78, useGrouping=False) - self.assertEqual(f1.options.useGrouping, False) + assert f1.options.useGrouping is False # Check we didn't mutate anything - self.assertIs(FluentNumber.default_number_format_options.useGrouping, True) + assert FluentNumber.default_number_format_options.useGrouping is True f2 = fluent_number(f1, style="percent") - self.assertEqual(f2.options.style, "percent") + assert f2.options.style == "percent" # Check we copied - self.assertEqual(f2.options.useGrouping, False) + assert f2.options.useGrouping is False # and didn't mutate anything - self.assertEqual(f1.options.style, "decimal") - self.assertEqual(FluentNumber.default_number_format_options.style, "decimal") - + assert f1.options.style == "decimal" + assert FluentNumber.default_number_format_options.style == "decimal" -class TestFluentDate(unittest.TestCase): - locale = Locale.parse("en_US") +a_date = date(2018, 2, 1) +a_datetime = datetime(2018, 2, 1, 14, 15, 16, 123456, tzinfo=pytz.UTC) - def setUp(self): - self.a_date = date(2018, 2, 1) - self.a_datetime = datetime(2018, 2, 1, 14, 15, 16, 123456, tzinfo=pytz.UTC) +class TestFluentDate: def test_date(self): - fd = fluent_date(self.a_date) - self.assertTrue(isinstance(fd, date)) - self.assertTrue(isinstance(fd, FluentDateType)) - self.assertEqual(fd.year, self.a_date.year) - self.assertEqual(fd.month, self.a_date.month) - self.assertEqual(fd.day, self.a_date.day) + fd = fluent_date(a_date) + assert isinstance(fd, date) + assert isinstance(fd, FluentDateType) + assert fd.year == a_date.year + assert fd.month == a_date.month + assert fd.day == a_date.day def test_datetime(self): - fd = fluent_date(self.a_datetime) - self.assertTrue(isinstance(fd, datetime)) - self.assertTrue(isinstance(fd, FluentDateType)) - self.assertEqual(fd.year, self.a_datetime.year) - self.assertEqual(fd.month, self.a_datetime.month) - self.assertEqual(fd.day, self.a_datetime.day) - self.assertEqual(fd.hour, self.a_datetime.hour) - self.assertEqual(fd.minute, self.a_datetime.minute) - self.assertEqual(fd.second, self.a_datetime.second) - self.assertEqual(fd.microsecond, self.a_datetime.microsecond) - self.assertEqual(fd.tzinfo, self.a_datetime.tzinfo) + fd = fluent_date(a_datetime) + assert isinstance(fd, datetime) + assert isinstance(fd, FluentDateType) + assert fd.year == a_datetime.year + assert fd.month == a_datetime.month + assert fd.day == a_datetime.day + assert fd.hour == a_datetime.hour + assert fd.minute == a_datetime.minute + assert fd.second == a_datetime.second + assert fd.microsecond == a_datetime.microsecond + assert fd.tzinfo == a_datetime.tzinfo def test_format_defaults(self): - fd = fluent_date(self.a_date) + fd = fluent_date(a_date) en_US = Locale.parse("en_US") en_GB = Locale.parse("en_GB") - self.assertEqual(fd.format(en_GB), "1 Feb 2018") - self.assertEqual(fd.format(en_US), "Feb 1, 2018") + assert fd.format(en_GB) == "1 Feb 2018" + assert fd.format(en_US) == "Feb 1, 2018" def test_dateStyle_date(self): - fd = fluent_date(self.a_date, dateStyle="long") + fd = fluent_date(a_date, dateStyle="long") en_US = Locale.parse("en_US") en_GB = Locale.parse("en_GB") - self.assertEqual(fd.format(en_GB), "1 February 2018") - self.assertEqual(fd.format(en_US), "February 1, 2018") + assert fd.format(en_GB) == "1 February 2018" + assert fd.format(en_US) == "February 1, 2018" def test_dateStyle_datetime(self): - fd = fluent_date(self.a_datetime, dateStyle="long") + fd = fluent_date(a_datetime, dateStyle="long") en_US = Locale.parse("en_US") en_GB = Locale.parse("en_GB") - self.assertEqual(fd.format(en_GB), "1 February 2018") - self.assertEqual(fd.format(en_US), "February 1, 2018") + assert fd.format(en_GB) == "1 February 2018" + assert fd.format(en_US) == "February 1, 2018" def test_timeStyle_datetime(self): - fd = fluent_date(self.a_datetime, timeStyle="short") + fd = fluent_date(a_datetime, timeStyle="short") en_US = Locale.parse("en_US") en_GB = Locale.parse("en_GB") - self.assertRegex(fd.format(en_US), "^2:15\\sPM$") - self.assertEqual(fd.format(en_GB), "14:15") + assert re.search("^2:15\\sPM$", fd.format(en_US)) + assert fd.format(en_GB) == "14:15" def test_dateStyle_and_timeStyle_datetime(self): - fd = fluent_date(self.a_datetime, timeStyle="short", dateStyle="short") + fd = fluent_date(a_datetime, timeStyle="short", dateStyle="short") en_US = Locale.parse("en_US") en_GB = Locale.parse("en_GB") - self.assertRegex(fd.format(en_US), "^2/1/18, 2:15\\sPM$") - self.assertEqual(fd.format(en_GB), "01/02/2018, 14:15") + assert re.search("^2/1/18, 2:15\\sPM$", fd.format(en_US)) + assert fd.format(en_GB) == "01/02/2018, 14:15" def test_validate_dateStyle(self): - self.assertRaises(ValueError, fluent_date, self.a_date, dateStyle="nothing") + with pytest.raises(ValueError): + fluent_date(a_date, dateStyle="nothing") def test_validate_timeStyle(self): - self.assertRaises(ValueError, fluent_date, self.a_datetime, timeStyle="nothing") + with pytest.raises(ValueError): + fluent_date(a_datetime, timeStyle="nothing") def test_timeZone(self): en_GB = Locale.parse("en_GB") @@ -231,41 +227,41 @@ def test_timeZone(self): # datetime object with tzinfo set to BST dt1 = datetime(2018, 7, 1, 23, 30, 0, tzinfo=pytz.UTC).astimezone(LondonTZ) fd1 = fluent_date(dt1, dateStyle="short", timeStyle="short") - self.assertEqual(fd1.format(en_GB), "02/07/2018, 00:30") + assert fd1.format(en_GB) == "02/07/2018, 00:30" fd1b = fluent_date(dt1, dateStyle="full", timeStyle="full") - self.assertRegex( - fd1b.format(en_GB), + assert re.search( "^Monday,? 2 July 2018(,| at) 00:30:00 British Summer Time$", + fd1b.format(en_GB), ) fd1c = fluent_date(dt1, dateStyle="short") - self.assertEqual(fd1c.format(en_GB), "02/07/2018") + assert fd1c.format(en_GB) == "02/07/2018" fd1d = fluent_date(dt1, timeStyle="short") - self.assertEqual(fd1d.format(en_GB), "00:30") + assert fd1d.format(en_GB) == "00:30" # datetime object with no TZ, TZ passed in to fluent_date dt2 = datetime(2018, 7, 1, 23, 30, 0) # Assumed UTC fd2 = fluent_date( dt2, dateStyle="short", timeStyle="short", timeZone="Europe/London" ) - self.assertEqual(fd2.format(en_GB), "02/07/2018, 00:30") + assert fd2.format(en_GB) == "02/07/2018, 00:30" fd2b = fluent_date( dt2, dateStyle="full", timeStyle="full", timeZone="Europe/London" ) - self.assertRegex( - fd2b.format(en_GB), + assert re.search( "^Monday,? 2 July 2018(,| at) 00:30:00 British Summer Time$", + fd2b.format(en_GB), ) fd2c = fluent_date(dt2, dateStyle="short", timeZone="Europe/London") - self.assertEqual(fd2c.format(en_GB), "02/07/2018") + assert fd2c.format(en_GB) == "02/07/2018" fd2d = fluent_date(dt1, timeStyle="short", timeZone="Europe/London") - self.assertEqual(fd2d.format(en_GB), "00:30") + assert fd2d.format(en_GB) == "00:30" def test_allow_unsupported_options(self): # We are just checking that these don't raise exceptions with warnings.catch_warnings(): warnings.simplefilter("ignore") fluent_date( - self.a_date, + a_date, hour12=True, weekday="narrow", era="narrow", @@ -279,32 +275,31 @@ def test_allow_unsupported_options(self): ) def test_disallow_nonexistant_options(self): - self.assertRaises( - TypeError, - fluent_date, - self.a_date, - not_a_real_option=True, - ) + with pytest.raises(TypeError): + fluent_date( + a_date, + not_a_real_option=True, + ) def test_dont_wrap_unnecessarily(self): - f1 = fluent_date(self.a_date) + f1 = fluent_date(a_date) f2 = fluent_date(f1) - self.assertIs(f1, f2) + assert f1 is f2 def test_copy_attributes(self): - f1 = fluent_date(self.a_date, dateStyle="long", hour12=False) - self.assertEqual(f1.options.dateStyle, "long") + f1 = fluent_date(a_date, dateStyle="long", hour12=False) + assert f1.options.dateStyle == "long" f2 = fluent_date(f1, hour12=False) # Check we copied other attributes: - self.assertEqual(f2.options.dateStyle, "long") - self.assertEqual(f2.options.hour12, False) + assert f2.options.dateStyle == "long" + assert f2.options.hour12 is False # Check we can override f3 = fluent_date(f2, dateStyle="full") - self.assertEqual(f3.options.dateStyle, "full") + assert f3.options.dateStyle == "full" # and didn't mutate anything - self.assertEqual(f1.options.dateStyle, "long") - self.assertEqual(f2.options.dateStyle, "long") + assert f1.options.dateStyle == "long" + assert f2.options.dateStyle == "long" diff --git a/fluent.runtime/tests/test_utils.py b/fluent.runtime/tests/test_utils.py deleted file mode 100644 index 24ea14b0..00000000 --- a/fluent.runtime/tests/test_utils.py +++ /dev/null @@ -1,30 +0,0 @@ -import unittest -from .utils import patch_files -from os.path import isdir, isfile, join - - -class TestFileSimulate(unittest.TestCase): - def test_basic(self): - @patch_files( - { - "the.txt": "The", - "en": { - "one.txt": "One", - "two.txt": "Two", - }, - } - ) - def patch_me(a, b, root): - self.assertEqual(a, 10) - self.assertEqual(b, "b") - with open(join(root, "the.txt")) as f: - self.assertEqual(f.read(), "The") - with open(join(root, "en", "one.txt")) as f: - self.assertEqual(f.read(), "One") - with open(join(root, "en", "two.txt")) as f: - self.assertEqual(f.read(), "Two") - self.assertTrue(isdir(join(root, "en"))) - self.assertFalse(isfile(join(root, "none.txt"))) - self.assertFalse(isfile(join(root, "en", "three.txt"))) - - patch_me(10, "b") diff --git a/fluent.runtime/tests/utils.py b/fluent.runtime/tests/utils.py index f4d6a01b..8a54f6b0 100644 --- a/fluent.runtime/tests/utils.py +++ b/fluent.runtime/tests/utils.py @@ -1,35 +1,21 @@ """Utilities for testing.""" import textwrap -from functools import wraps from os import mkdir from os.path import join -from tempfile import TemporaryDirectory def dedent_ftl(text): return textwrap.dedent(f"{text.rstrip()}\n") -def patch_files(tree: dict): - def build_file_tree(root: str, tree: dict) -> None: - for name, value in tree.items(): - path = join(root, name) - if isinstance(value, str): - with open(path, "x", encoding="utf-8", newline="\n") as file: - if value: - file.write(value) - else: - mkdir(path) - build_file_tree(path, value) - - def decorator(fn): - @wraps(fn) - def wrapper(*args, **kwargs): - with TemporaryDirectory() as root: - build_file_tree(root, tree) - return fn(*args, root, **kwargs) - - return wrapper - - return decorator +def build_file_tree(root: str, tree: dict) -> None: + for name, value in tree.items(): + path = join(root, name) + if isinstance(value, str): + with open(path, "x", encoding="utf-8", newline="\n") as file: + if value: + file.write(value) + else: + mkdir(path) + build_file_tree(path, value) diff --git a/fluent.syntax/tests/syntax/test_ast_json.py b/fluent.syntax/tests/syntax/test_ast_json.py index 557a66a0..1fe41393 100644 --- a/fluent.syntax/tests/syntax/test_ast_json.py +++ b/fluent.syntax/tests/syntax/test_ast_json.py @@ -1,27 +1,21 @@ -import unittest - from fluent.syntax.ast import from_json from fluent.syntax.parser import FluentParser -from tests.syntax import dedent_ftl - -class TestASTJSON(unittest.TestCase): - maxDiff = None +from . import dedent_ftl - def setUp(self): - self.parser = FluentParser() +class TestASTJSON: def test_simple_resource(self): input = """\ foo = Foo """ - ast1 = self.parser.parse(dedent_ftl(input)) + ast1 = FluentParser().parse(dedent_ftl(input)) json1 = ast1.to_json() ast2 = from_json(json1) json2 = ast2.to_json() - self.assertEqual(json1, json2) + assert json1 == json2 def test_complex_resource(self): input = """\ @@ -47,21 +41,21 @@ def test_complex_resource(self): } post. """ - ast1 = self.parser.parse(dedent_ftl(input)) + ast1 = FluentParser().parse(dedent_ftl(input)) json1 = ast1.to_json() ast2 = from_json(json1) json2 = ast2.to_json() - self.assertEqual(json1, json2) + assert json1 == json2 def test_syntax_error(self): input = """\ foo = Foo { """ - ast1 = self.parser.parse(dedent_ftl(input)) + ast1 = FluentParser().parse(dedent_ftl(input)) json1 = ast1.to_json() ast2 = from_json(json1) json2 = ast2.to_json() - self.assertEqual(json1, json2) + assert json1 == json2 diff --git a/fluent.syntax/tests/syntax/test_entry.py b/fluent.syntax/tests/syntax/test_entry.py index 250f8488..411d6e42 100644 --- a/fluent.syntax/tests/syntax/test_entry.py +++ b/fluent.syntax/tests/syntax/test_entry.py @@ -1,21 +1,19 @@ -import unittest +import pytest from fluent.syntax.ast import from_json from fluent.syntax.parser import FluentParser from fluent.syntax.serializer import FluentSerializer -from tests.syntax import dedent_ftl +from . import dedent_ftl -class TestParseEntry(unittest.TestCase): - maxDiff = None - def setUp(self): - self.parser = FluentParser(with_spans=False) +class TestParseEntry: + @pytest.fixture + def parser(self): + return FluentParser(with_spans=False) - def test_simple_message(self): - input = """\ - foo = Foo - """ + def test_simple_message(self, parser): + input = "foo = Foo\n" output = { "comment": None, "value": { @@ -29,10 +27,10 @@ def test_simple_message(self): "id": {"span": None, "type": "Identifier", "name": "foo"}, } - message = self.parser.parse_entry(dedent_ftl(input)) - self.assertEqual(message.to_json(), output) + message = parser.parse_entry(dedent_ftl(input)) + assert message.to_json() == output - def test_ignore_attached_comment(self): + def test_ignore_attached_comment(self, parser): input = """\ # Attached Comment foo = Foo @@ -50,10 +48,10 @@ def test_ignore_attached_comment(self): "span": None, } - message = self.parser.parse_entry(dedent_ftl(input)) - self.assertEqual(message.to_json(), output) + message = parser.parse_entry(dedent_ftl(input)) + assert message.to_json() == output - def test_return_junk(self): + def test_return_junk(self, parser): input = """\ # Attached Comment junk @@ -73,10 +71,10 @@ def test_return_junk(self): "type": "Junk", } - message = self.parser.parse_entry(dedent_ftl(input)) - self.assertEqual(message.to_json(), output) + message = parser.parse_entry(dedent_ftl(input)) + assert message.to_json() == output - def test_ignore_all_valid_comments(self): + def test_ignore_all_valid_comments(self, parser): input = """\ # Attached Comment ## Group Comment @@ -96,10 +94,10 @@ def test_ignore_all_valid_comments(self): "id": {"name": "foo", "span": None, "type": "Identifier"}, } - message = self.parser.parse_entry(dedent_ftl(input)) - self.assertEqual(message.to_json(), output) + message = parser.parse_entry(dedent_ftl(input)) + assert message.to_json() == output - def test_do_not_ignore_invalid_comments(self): + def test_do_not_ignore_invalid_comments(self, parser): input = """\ # Attached Comment ##Invalid Comment @@ -119,14 +117,11 @@ def test_do_not_ignore_invalid_comments(self): "type": "Junk", } - message = self.parser.parse_entry(dedent_ftl(input)) - self.assertEqual(message.to_json(), output) - + message = parser.parse_entry(dedent_ftl(input)) + assert message.to_json() == output -class TestSerializeEntry(unittest.TestCase): - def setUp(self): - self.serializer = FluentSerializer() +class TestSerializeEntry: def test_simple_message(self): input = { "comment": None, @@ -142,5 +137,5 @@ def test_simple_message(self): foo = Foo """ - message = self.serializer.serialize_entry(from_json(input)) - self.assertEqual(message, dedent_ftl(output)) + message = FluentSerializer().serialize_entry(from_json(input)) + assert message == dedent_ftl(output) diff --git a/fluent.syntax/tests/syntax/test_equals.py b/fluent.syntax/tests/syntax/test_equals.py index ceb81122..6fb08c9a 100644 --- a/fluent.syntax/tests/syntax/test_equals.py +++ b/fluent.syntax/tests/syntax/test_equals.py @@ -1,28 +1,25 @@ -import unittest - from fluent.syntax.parser import FluentParser -from tests.syntax import dedent_ftl + +from . import dedent_ftl -class TestEntryEqualToSelf(unittest.TestCase): - def setUp(self): - self.parser = FluentParser() +def parse_ftl_entry(string): + return FluentParser().parse_entry(dedent_ftl(string)) - def parse_ftl_entry(self, string): - return self.parser.parse_entry(dedent_ftl(string)) +class TestEntryEqualToSelf: def test_same_simple_message(self): - message1 = self.parse_ftl_entry( + message1 = parse_ftl_entry( """\ foo = Foo """ ) - self.assertTrue(message1.equals(message1)) - self.assertTrue(message1.equals(message1.clone())) + assert message1.equals(message1) + assert message1.equals(message1.clone()) def test_same_selector_message(self): - message1 = self.parse_ftl_entry( + message1 = parse_ftl_entry( """\ foo = { $num -> @@ -35,32 +32,32 @@ def test_same_selector_message(self): """ ) - self.assertTrue(message1.equals(message1)) - self.assertTrue(message1.equals(message1.clone())) + assert message1.equals(message1) + assert message1.equals(message1.clone()) def test_same_complex_placeable_message(self): - message1 = self.parse_ftl_entry( + message1 = parse_ftl_entry( """\ foo = Foo { NUMBER($num, style: "decimal") } Bar """ ) - self.assertTrue(message1.equals(message1)) - self.assertTrue(message1.equals(message1.clone())) + assert message1.equals(message1) + assert message1.equals(message1.clone()) def test_same_message_with_attribute(self): - message1 = self.parse_ftl_entry( + message1 = parse_ftl_entry( """\ foo = .attr = Attr """ ) - self.assertTrue(message1.equals(message1)) - self.assertTrue(message1.equals(message1.clone())) + assert message1.equals(message1) + assert message1.equals(message1.clone()) def test_same_message_with_attributes(self): - message1 = self.parse_ftl_entry( + message1 = parse_ftl_entry( """\ foo = .attr1 = Attr 1 @@ -68,36 +65,30 @@ def test_same_message_with_attributes(self): """ ) - self.assertTrue(message1.equals(message1)) - self.assertTrue(message1.equals(message1.clone())) + assert message1.equals(message1) + assert message1.equals(message1.clone()) def test_same_junk(self): - message1 = self.parse_ftl_entry( + message1 = parse_ftl_entry( """\ foo = Foo { """ ) - self.assertTrue(message1.equals(message1)) - self.assertTrue(message1.equals(message1.clone())) - + assert message1.equals(message1) + assert message1.equals(message1.clone()) -class TestNonEquals(unittest.TestCase): - def setUp(self): - self.parser = FluentParser() - - def parse_ftl_entry(self, string): - return self.parser.parse_entry(dedent_ftl(string)) +class TestNonEquals: def test_attributes(self): - message1 = self.parse_ftl_entry( + message1 = parse_ftl_entry( """\ foo = .attr1 = Attr1 .attr2 = Attr2 """ ) - message2 = self.parse_ftl_entry( + message2 = parse_ftl_entry( """\ foo = .attr2 = Attr2 @@ -105,10 +96,10 @@ def test_attributes(self): """ ) - self.assertFalse(message1.equals(message2)) + assert not message1.equals(message2) def test_variants(self): - message1 = self.parse_ftl_entry( + message1 = parse_ftl_entry( """\ foo = { $num -> @@ -117,7 +108,7 @@ def test_variants(self): } """ ) - message2 = self.parse_ftl_entry( + message2 = parse_ftl_entry( """\ foo = { $num -> @@ -127,10 +118,10 @@ def test_variants(self): """ ) - self.assertFalse(message1.equals(message2)) + assert not message1.equals(message2) def test_variants_with_numbers(self): - message1 = self.parse_ftl_entry( + message1 = parse_ftl_entry( """\ foo = { $num -> @@ -139,7 +130,7 @@ def test_variants_with_numbers(self): } """ ) - message2 = self.parse_ftl_entry( + message2 = parse_ftl_entry( """\ foo = { $num -> @@ -149,10 +140,10 @@ def test_variants_with_numbers(self): """ ) - self.assertFalse(message1.equals(message2)) + assert not message1.equals(message2) -class TestEqualWithSpans(unittest.TestCase): +class TestEqualWithSpans: def test_default_behavior(self): parser = FluentParser() @@ -165,7 +156,7 @@ def test_default_behavior(self): messages = [(parser.parse_entry(a), parser.parse_entry(b)) for a, b in strings] for a, b in messages: - self.assertTrue(a.equals(b)) + assert a.equals(b) def test_parser_without_spans(self): parser = FluentParser(with_spans=False) @@ -179,7 +170,7 @@ def test_parser_without_spans(self): messages = [(parser.parse_entry(a), parser.parse_entry(b)) for a, b in strings] for a, b in messages: - self.assertTrue(a.equals(b)) + assert a.equals(b) def test_equals_with_spans(self): parser = FluentParser() @@ -192,7 +183,7 @@ def test_equals_with_spans(self): messages = [(parser.parse_entry(a), parser.parse_entry(b)) for a, b in strings] for a, b in messages: - self.assertTrue(a.equals(b, ignored_fields=None)) + assert a.equals(b, ignored_fields=None) def test_parser_without_spans_equals_with_spans(self): parser = FluentParser(with_spans=False) @@ -207,7 +198,7 @@ def test_parser_without_spans_equals_with_spans(self): messages = [(parser.parse_entry(a), parser.parse_entry(b)) for a, b in strings] for a, b in messages: - self.assertTrue(a.equals(b, ignored_fields=None)) + assert a.equals(b, ignored_fields=None) def test_differ_with_spans(self): parser = FluentParser() @@ -220,49 +211,43 @@ def test_differ_with_spans(self): messages = [(parser.parse_entry(a), parser.parse_entry(b)) for a, b in strings] for a, b in messages: - self.assertFalse(a.equals(b, ignored_fields=None)) - - -class TestIgnoredFields(unittest.TestCase): - def setUp(self): - self.parser = FluentParser() + assert not a.equals(b, ignored_fields=None) - def parse_ftl_entry(self, string): - return self.parser.parse_entry(dedent_ftl(string)) +class TestIgnoredFields: def test_ignore_value(self): - a = self.parse_ftl_entry("foo = Foo") - b = self.parse_ftl_entry("foo = Bar") + a = parse_ftl_entry("foo = Foo") + b = parse_ftl_entry("foo = Bar") - self.assertTrue(a.equals(b, ignored_fields=["value"])) + assert a.equals(b, ignored_fields=["value"]) def test_ignore_value_span(self): - a = self.parse_ftl_entry("foo = Foo") - b = self.parse_ftl_entry("foo = Foobar") + a = parse_ftl_entry("foo = Foo") + b = parse_ftl_entry("foo = Foobar") - self.assertTrue(a.equals(b, ignored_fields=["span", "value"])) - self.assertFalse(a.equals(b, ignored_fields=["value"])) + assert a.equals(b, ignored_fields=["span", "value"]) + assert not a.equals(b, ignored_fields=["value"]) def test_ignore_comments(self): - a = self.parse_ftl_entry( + a = parse_ftl_entry( """\ # Comment A foo = Foo """ ) - b = self.parse_ftl_entry( + b = parse_ftl_entry( """\ # Comment B foo = Foo """ ) - c = self.parse_ftl_entry( + c = parse_ftl_entry( """\ # Comment CC foo = Foo """ ) - self.assertTrue(a.equals(b, ignored_fields=["comment"])) - self.assertFalse(a.equals(c, ignored_fields=["comment"])) - self.assertTrue(a.equals(c, ignored_fields=["comment", "span"])) + assert a.equals(b, ignored_fields=["comment"]) + assert not a.equals(c, ignored_fields=["comment"]) + assert a.equals(c, ignored_fields=["comment", "span"]) diff --git a/fluent.syntax/tests/syntax/test_literal.py b/fluent.syntax/tests/syntax/test_literal.py index ab22dfcf..ea89919e 100644 --- a/fluent.syntax/tests/syntax/test_literal.py +++ b/fluent.syntax/tests/syntax/test_literal.py @@ -1,5 +1,4 @@ import sys -import unittest from fluent.syntax.parser import FluentParser @@ -11,70 +10,52 @@ def parse_literal(input): return expr.parse() -class TestStringLiteralParse(unittest.TestCase): +class TestStringLiteralParse: def test_no_escape_sequences(self): - self.assertDictEqual(parse_literal('x = {"abc"}'), {"value": "abc"}) + assert parse_literal('x = {"abc"}') == {"value": "abc"} def test_double_quote_backslash(self): - self.assertDictEqual(parse_literal(r'x = {"\""}'), {"value": '"'}) - self.assertDictEqual(parse_literal(r'x = {"\\"}'), {"value": "\\"}) + assert parse_literal(r'x = {"\""}') == {"value": '"'} + assert parse_literal(r'x = {"\\"}') == {"value": "\\"} def test_unicode_escape(self): - self.assertDictEqual(parse_literal('x = {"\\u0041"}'), {"value": "A"}) - self.assertDictEqual(parse_literal('x = {"\\\\u0041"}'), {"value": "\\u0041"}) + assert parse_literal('x = {"\\u0041"}') == {"value": "A"} + assert parse_literal('x = {"\\\\u0041"}') == {"value": "\\u0041"} if sys.maxunicode > 0xFFFF: - self.assertDictEqual(parse_literal('x = {"\\U01F602"}'), {"value": "😂"}) - self.assertDictEqual( - parse_literal('x = {"\\\\U01F602"}'), {"value": "\\U01F602"} - ) + assert parse_literal('x = {"\\U01F602"}') == {"value": "😂"} + assert parse_literal('x = {"\\\\U01F602"}') == {"value": "\\U01F602"} def test_trailing_number(self): - self.assertDictEqual(parse_literal('x = {"\\u004100"}'), {"value": "A00"}) + assert parse_literal('x = {"\\u004100"}') == {"value": "A00"} if sys.maxunicode > 0xFFFF: - self.assertDictEqual( - parse_literal('x = {"\\U01F60200"}'), {"value": "😂00"} - ) + assert parse_literal('x = {"\\U01F60200"}') == {"value": "😂00"} -class TestNumberLiteralParse(unittest.TestCase): +class TestNumberLiteralParse: def test_integers(self): - self.assertDictEqual(parse_literal("x = {0}"), {"value": 0, "precision": 0}) - self.assertDictEqual(parse_literal("x = {1}"), {"value": 1, "precision": 0}) - self.assertDictEqual(parse_literal("x = {-0}"), {"value": 0, "precision": 0}) - self.assertDictEqual(parse_literal("x = {-1}"), {"value": -1, "precision": 0}) + assert parse_literal("x = {0}") == {"value": 0, "precision": 0} + assert parse_literal("x = {1}") == {"value": 1, "precision": 0} + assert parse_literal("x = {-0}") == {"value": 0, "precision": 0} + assert parse_literal("x = {-1}") == {"value": -1, "precision": 0} def test_padded_integers(self): - self.assertDictEqual(parse_literal("x = {00}"), {"value": 0, "precision": 0}) - self.assertDictEqual(parse_literal("x = {01}"), {"value": 1, "precision": 0}) - self.assertDictEqual(parse_literal("x = {-00}"), {"value": 0, "precision": 0}) - self.assertDictEqual(parse_literal("x = {-01}"), {"value": -1, "precision": 0}) + assert parse_literal("x = {00}") == {"value": 0, "precision": 0} + assert parse_literal("x = {01}") == {"value": 1, "precision": 0} + assert parse_literal("x = {-00}") == {"value": 0, "precision": 0} + assert parse_literal("x = {-01}") == {"value": -1, "precision": 0} def test_positive_floats(self): - self.assertDictEqual(parse_literal("x = {0.0}"), {"value": 0, "precision": 1}) - self.assertDictEqual( - parse_literal("x = {0.01}"), {"value": 0.01, "precision": 2} - ) - self.assertDictEqual( - parse_literal("x = {1.03}"), {"value": 1.03, "precision": 2} - ) - self.assertDictEqual(parse_literal("x = {1.000}"), {"value": 1, "precision": 3}) + assert parse_literal("x = {0.0}") == {"value": 0, "precision": 1} + assert parse_literal("x = {0.01}") == {"value": 0.01, "precision": 2} + assert parse_literal("x = {1.03}") == {"value": 1.03, "precision": 2} + assert parse_literal("x = {1.000}") == {"value": 1, "precision": 3} def test_negative_floats(self): - self.assertDictEqual(parse_literal("x = {-0.0}"), {"value": 0, "precision": 1}) - self.assertDictEqual( - parse_literal("x = {-0.01}"), {"value": -0.01, "precision": 2} - ) - self.assertDictEqual( - parse_literal("x = {-1.03}"), {"value": -1.03, "precision": 2} - ) - self.assertDictEqual( - parse_literal("x = {-1.000}"), {"value": -1, "precision": 3} - ) + assert parse_literal("x = {-0.0}") == {"value": 0, "precision": 1} + assert parse_literal("x = {-0.01}") == {"value": -0.01, "precision": 2} + assert parse_literal("x = {-1.03}") == {"value": -1.03, "precision": 2} + assert parse_literal("x = {-1.000}") == {"value": -1, "precision": 3} def test_padded_floats(self): - self.assertDictEqual( - parse_literal("x = {-00.00}"), {"value": 0, "precision": 2} - ) - self.assertDictEqual( - parse_literal("x = {-01.000}"), {"value": -1, "precision": 3} - ) + assert parse_literal("x = {-00.00}") == {"value": 0, "precision": 2} + assert parse_literal("x = {-01.000}") == {"value": -1, "precision": 3} diff --git a/fluent.syntax/tests/syntax/test_reference.py b/fluent.syntax/tests/syntax/test_reference.py index 17463c62..316007d2 100644 --- a/fluent.syntax/tests/syntax/test_reference.py +++ b/fluent.syntax/tests/syntax/test_reference.py @@ -1,58 +1,36 @@ -import json -import os -import unittest - -from fluent.syntax import parse - - -def read_file(path): - with open(path, "r", encoding="utf-8", newline="\n") as file: - text = file.read() - return text - - -fixtures = os.path.join(os.path.dirname(__file__), "fixtures_reference") +from json import loads +from os import listdir +from os.path import dirname, join, splitext +import pytest -class TestReferenceMeta(type): - def __new__(mcs, name, bases, attrs): - - def remove_untested(obj): - if obj["type"] == "Junk": - obj["annotations"] = [] - if "span" in obj: - del obj["span"] - return obj - - def gen_test(file_name): - def test(self): - ftl_path = os.path.join(fixtures, file_name + ".ftl") - ast_path = os.path.join(fixtures, file_name + ".json") - - source = read_file(ftl_path) - expected = read_file(ast_path) - - ast = parse(source) - self.assertEqual(ast.to_json(remove_untested), json.loads(expected)) +from fluent.syntax import parse - return test +fixtures_dir = join(dirname(__file__), "fixtures_reference") +fixture_names = [ + fp[0] for fn in listdir(fixtures_dir) if (fp := splitext(fn))[1] == ".ftl" +] - for f in os.listdir(fixtures): - file_name, ext = os.path.splitext(f) - if ext != ".ftl": - continue +@pytest.mark.parametrize("name", fixture_names) +def test_reference(name): + if name in ("leading_dots",): + pytest.skip("Known difference between reference and tooling parsers") - # Skip fixtures which are known to differ between the reference - # parser and the tooling parser. - if file_name in ("leading_dots", "variant_lists"): - continue + ftl_path = join(fixtures_dir, name + ".ftl") + ast_path = join(fixtures_dir, name + ".json") - test_name = f"test_{file_name}" - attrs[test_name] = gen_test(file_name) + with open(ftl_path, "r", encoding="utf-8", newline="\n") as file: + ast = parse(file.read()) + with open(ast_path, "r", encoding="utf-8", newline="\n") as file: + expected = loads(file.read()) - return type.__new__(mcs, name, bases, attrs) + assert ast.to_json(remove_untested) == expected -class TestReference(unittest.TestCase, metaclass=TestReferenceMeta): - maxDiff = None +def remove_untested(obj): + if obj["type"] == "Junk": + obj["annotations"] = [] + if "span" in obj: + del obj["span"] + return obj diff --git a/fluent.syntax/tests/syntax/test_serializer.py b/fluent.syntax/tests/syntax/test_serializer.py index 80d78407..49a329b9 100644 --- a/fluent.syntax/tests/syntax/test_serializer.py +++ b/fluent.syntax/tests/syntax/test_serializer.py @@ -1,11 +1,11 @@ -import unittest - +import pytest from fluent.syntax import FluentParser, FluentSerializer from fluent.syntax.serializer import serialize_expression, serialize_variant_key -from tests.syntax import dedent_ftl + +from . import dedent_ftl -class TestSerializeResource(unittest.TestCase): +class TestSerializeResource: @staticmethod def pretty_ftl(text): parser = FluentParser() @@ -16,30 +16,30 @@ def pretty_ftl(text): def test_invalid_resource(self): serializer = FluentSerializer() - with self.assertRaisesRegex(Exception, "Unknown resource type"): + with pytest.raises(Exception, match="Unknown resource type"): serializer.serialize(None) - with self.assertRaisesRegex(Exception, "Unknown resource type"): + with pytest.raises(Exception, match="Unknown resource type"): serializer.serialize(object()) def test_simple_message(self): input = """\ foo = Foo """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) def test_simple_term(self): input = """\ -foo = Foo """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) def test_two_simple_messages(self): input = """\ foo = Foo bar = Bar """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) def test_block_multiline(self): input = """\ @@ -47,7 +47,7 @@ def test_block_multiline(self): Foo Bar """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) def test_inline_multiline(self): input = """\ @@ -59,43 +59,43 @@ def test_inline_multiline(self): Foo Bar """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(output)) + assert self.pretty_ftl(input) == dedent_ftl(output) def test_message_reference(self): input = """\ foo = Foo { bar } """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) def test_term_reference(self): input = """\ foo = Foo { -bar } """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) def test_variable_reference(self): input = """\ foo = Foo { $bar } """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) def test_number_element(self): input = """\ foo = Foo { 1 } """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) def test_string_element(self): input = """\ foo = Foo { "bar" } """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) def test_attribute_expression(self): input = """\ foo = Foo { bar.baz } """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) def test_comment_resource(self): input = """\ @@ -104,7 +104,7 @@ def test_comment_resource(self): foo = Foo """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) def test_comment_message(self): input = """\ @@ -114,7 +114,7 @@ def test_comment_message(self): # bar = Bar """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) def test_comment_group(self): input = """\ @@ -131,7 +131,7 @@ def test_comment_group(self): baz = Baz """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) def test_comment_standalone(self): input = """\ @@ -141,7 +141,7 @@ def test_comment_standalone(self): bar = Bar """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) def test_multiline_starting_inline(self): input = """\ @@ -153,14 +153,14 @@ def test_multiline_starting_inline(self): Foo Bar """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(output)) + assert self.pretty_ftl(input) == dedent_ftl(output) def test_multiline_starting_inline_with_special_char(self): input = """\ foo = *Foo Bar """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) def test_multiline_with_placeable(self): input = """\ @@ -168,14 +168,14 @@ def test_multiline_with_placeable(self): Foo { bar } Baz """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) def test_attribute(self): input = """\ foo = .attr = Foo Attr """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) def test_attribute_multiline(self): input = """\ @@ -184,7 +184,7 @@ def test_attribute_multiline(self): Foo Attr Continued """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) def test_two_attributes(self): input = """\ @@ -192,7 +192,7 @@ def test_two_attributes(self): .attr-a = Foo Attr A .attr-b = Foo Attr B """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) def test_value_and_attributes(self): input = """\ @@ -200,7 +200,7 @@ def test_value_and_attributes(self): .attr-a = Foo Attr A .attr-b = Foo Attr B """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) def test_multiline_value_and_attributes(self): input = """\ @@ -210,7 +210,7 @@ def test_multiline_value_and_attributes(self): .attr-a = Foo Attr A .attr-b = Foo Attr B """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) def test_select_expression(self): input = """\ @@ -220,7 +220,7 @@ def test_select_expression(self): [b] B } """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) def test_variant_multiline(self): input = """\ @@ -231,7 +231,7 @@ def test_variant_multiline(self): BBB } """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) def test_variant_multiline_first_inline(self): input = """\ @@ -249,7 +249,7 @@ def test_variant_multiline_first_inline(self): BBB } """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(output)) + assert self.pretty_ftl(input) == dedent_ftl(output) def test_variant_key_number(self): input = """\ @@ -258,7 +258,7 @@ def test_variant_key_number(self): *[1] 1 } """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) def test_select_expression_in_block_value(self): input = """\ @@ -268,7 +268,7 @@ def test_select_expression_in_block_value(self): [b] B } """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) def test_select_expression_in_inline_value(self): input = """\ @@ -284,7 +284,7 @@ def test_select_expression_in_inline_value(self): [b] B } """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(output)) + assert self.pretty_ftl(input) == dedent_ftl(output) def test_select_expression_in_inline_value_starting_with_special_char(self): input = """\ @@ -293,7 +293,7 @@ def test_select_expression_in_inline_value_starting_with_special_char(self): [b] B } """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) def test_select_expression_in_multi_multiline(self): input = """\ @@ -304,7 +304,7 @@ def test_select_expression_in_multi_multiline(self): [b] B } """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) def test_select_expression_nested(self): input = """\ @@ -316,7 +316,7 @@ def test_select_expression_nested(self): } } """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) def test_selector_variable_reference(self): input = """\ @@ -325,7 +325,7 @@ def test_selector_variable_reference(self): *[a] A } """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) def test_selector_number_expression(self): input = """\ @@ -334,7 +334,7 @@ def test_selector_number_expression(self): *[a] A } """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) def test_selector_string_expression(self): input = """\ @@ -343,7 +343,7 @@ def test_selector_string_expression(self): *[a] A } """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) def test_selector_attribute_expression(self): input = """\ @@ -352,100 +352,100 @@ def test_selector_attribute_expression(self): *[a] A } """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) def test_call_expression(self): input = """\ foo = { FOO() } """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) def test_call_expression_with_string_expression(self): input = """\ foo = { FOO("bar") } """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) def test_call_expression_with_number_expression(self): input = """\ foo = { FOO(1) } """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) def test_call_expression_with_message_reference(self): input = """\ foo = { FOO(bar) } """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) def test_call_expression_with_variable_reference(self): input = """\ foo = { FOO($bar) } """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) def test_call_expression_with_two_positional_arguments(self): input = """\ foo = { FOO(bar, baz) } """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) def test_call_expression_with_named_argument_number(self): input = """\ foo = { FOO(bar: 1) } """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) def test_call_expression_with_named_argument_string(self): input = """\ foo = { FOO(bar: "bar") } """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) def test_call_expression_with_two_named_arguments(self): input = """\ foo = { FOO(bar: "bar", baz: "baz") } """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) def test_call_expression_with_positional_and_named_arguments(self): input = """\ foo = { FOO(bar, 1, baz: "baz") } """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) def test_macro_call(self): input = """\ foo = { -term() } """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) def test_nested_placeables(self): input = """\ foo = {{ FOO() }} """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) def test_backslash_in_text(self): input = """\ foo = \\{ placeable } """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) def test_escaped_special_in_string_literal(self): input = """\ foo = { "Escaped \\" quote" } """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) def test_escaped_unicode_sequence(self): input = """\ foo = { "\\u0065" } """ - self.assertEqual(self.pretty_ftl(input), dedent_ftl(input)) + assert self.pretty_ftl(input) == dedent_ftl(input) -class TestSerializeExpression(unittest.TestCase): +class TestSerializeExpression: @staticmethod def pretty_expr(text): parser = FluentParser() @@ -454,47 +454,47 @@ def pretty_expr(text): return serialize_expression(expr) def test_invalid_expression(self): - with self.assertRaisesRegex(Exception, "Unknown expression type"): + with pytest.raises(Exception, match="Unknown expression type"): serialize_expression(None) - with self.assertRaisesRegex(Exception, "Unknown expression type"): + with pytest.raises(Exception, match="Unknown expression type"): serialize_expression(object()) def test_string_expression(self): input = """\ foo = { "str" } """ - self.assertEqual(self.pretty_expr(input), '"str"') + assert self.pretty_expr(input) == '"str"' def test_number_expression(self): input = """\ foo = { 3 } """ - self.assertEqual(self.pretty_expr(input), "3") + assert self.pretty_expr(input) == "3" def test_message_reference(self): input = """\ foo = { msg } """ - self.assertEqual(self.pretty_expr(input), "msg") + assert self.pretty_expr(input) == "msg" def test_variable_reference(self): input = """\ foo = { $ext } """ - self.assertEqual(self.pretty_expr(input), "$ext") + assert self.pretty_expr(input) == "$ext" def test_attribute_expression(self): input = """\ foo = { msg.attr } """ - self.assertEqual(self.pretty_expr(input), "msg.attr") + assert self.pretty_expr(input) == "msg.attr" def test_call_expression(self): input = """\ foo = { BUILTIN(3.14, kwarg: "value") } """ - self.assertEqual(self.pretty_expr(input), 'BUILTIN(3.14, kwarg: "value")') + assert self.pretty_expr(input) == 'BUILTIN(3.14, kwarg: "value")' def test_select_expression(self): input = """\ @@ -503,10 +503,10 @@ def test_select_expression(self): *[one] One } """ - self.assertEqual(self.pretty_expr(input), "$num ->\n *[one] One\n") + assert self.pretty_expr(input) == "$num ->\n *[one] One\n" -class TestSerializeVariantKey(unittest.TestCase): +class TestSerializeVariantKey: @staticmethod def pretty_variant_key(text, index): parser = FluentParser() @@ -515,10 +515,10 @@ def pretty_variant_key(text, index): return serialize_variant_key(variants[index].key) def test_invalid_expression(self): - with self.assertRaisesRegex(Exception, "Unknown variant key type"): + with pytest.raises(Exception, match="Unknown variant key type"): serialize_variant_key(None) - with self.assertRaisesRegex(Exception, "Unknown variant key type"): + with pytest.raises(Exception, match="Unknown variant key type"): serialize_variant_key(object()) def test_identifiers(self): @@ -528,8 +528,8 @@ def test_identifiers(self): *[other] Other } """ - self.assertEqual(self.pretty_variant_key(input, 0), "one") - self.assertEqual(self.pretty_variant_key(input, 1), "other") + assert self.pretty_variant_key(input, 0) == "one" + assert self.pretty_variant_key(input, 1) == "other" def test_number_literals(self): input = """\ @@ -540,7 +540,7 @@ def test_number_literals(self): [007] James } """ - self.assertEqual(self.pretty_variant_key(input, 0), "-123456789") - self.assertEqual(self.pretty_variant_key(input, 1), "0") - self.assertEqual(self.pretty_variant_key(input, 2), "3.14") - self.assertEqual(self.pretty_variant_key(input, 3), "007") + assert self.pretty_variant_key(input, 0) == "-123456789" + assert self.pretty_variant_key(input, 1) == "0" + assert self.pretty_variant_key(input, 2) == "3.14" + assert self.pretty_variant_key(input, 3) == "007" diff --git a/fluent.syntax/tests/syntax/test_stream.py b/fluent.syntax/tests/syntax/test_stream.py index d8bef4bb..dff2a040 100644 --- a/fluent.syntax/tests/syntax/test_stream.py +++ b/fluent.syntax/tests/syntax/test_stream.py @@ -1,98 +1,95 @@ -import unittest - from fluent.syntax.stream import ParserStream -class TestParserStream(unittest.TestCase): - +class TestParserStream: def test_next(self): ps = ParserStream("abcd") - self.assertEqual("a", ps.current_char) - self.assertEqual(0, ps.index) + assert "a" == ps.current_char + assert 0 == ps.index - self.assertEqual("b", ps.next()) - self.assertEqual("b", ps.current_char) - self.assertEqual(1, ps.index) + assert "b" == ps.next() + assert "b" == ps.current_char + assert 1 == ps.index - self.assertEqual("c", ps.next()) - self.assertEqual("c", ps.current_char) - self.assertEqual(2, ps.index) + assert "c" == ps.next() + assert "c" == ps.current_char + assert 2 == ps.index - self.assertEqual("d", ps.next()) - self.assertEqual("d", ps.current_char) - self.assertEqual(3, ps.index) + assert "d" == ps.next() + assert "d" == ps.current_char + assert 3 == ps.index - self.assertEqual(None, ps.next()) - self.assertEqual(None, ps.current_char) - self.assertEqual(4, ps.index) + assert ps.next() is None + assert ps.current_char is None + assert 4 == ps.index def test_peek(self): ps = ParserStream("abcd") - self.assertEqual("a", ps.current_peek) - self.assertEqual(0, ps.peek_offset) + assert "a" == ps.current_peek + assert 0 == ps.peek_offset - self.assertEqual("b", ps.peek()) - self.assertEqual("b", ps.current_peek) - self.assertEqual(1, ps.peek_offset) + assert "b" == ps.peek() + assert "b" == ps.current_peek + assert 1 == ps.peek_offset - self.assertEqual("c", ps.peek()) - self.assertEqual("c", ps.current_peek) - self.assertEqual(2, ps.peek_offset) + assert "c" == ps.peek() + assert "c" == ps.current_peek + assert 2 == ps.peek_offset - self.assertEqual("d", ps.peek()) - self.assertEqual("d", ps.current_peek) - self.assertEqual(3, ps.peek_offset) + assert "d" == ps.peek() + assert "d" == ps.current_peek + assert 3 == ps.peek_offset - self.assertEqual(None, ps.peek()) - self.assertEqual(None, ps.current_peek) - self.assertEqual(4, ps.peek_offset) + assert ps.peek() is None + assert ps.current_peek is None + assert 4 == ps.peek_offset def test_peek_and_next(self): ps = ParserStream("abcd") - self.assertEqual("b", ps.peek()) - self.assertEqual(1, ps.peek_offset) - self.assertEqual(0, ps.index) + assert "b" == ps.peek() + assert 1 == ps.peek_offset + assert 0 == ps.index - self.assertEqual("b", ps.next()) - self.assertEqual(0, ps.peek_offset) - self.assertEqual(1, ps.index) + assert "b" == ps.next() + assert 0 == ps.peek_offset + assert 1 == ps.index - self.assertEqual("c", ps.peek()) - self.assertEqual(1, ps.peek_offset) - self.assertEqual(1, ps.index) + assert "c" == ps.peek() + assert 1 == ps.peek_offset + assert 1 == ps.index - self.assertEqual("c", ps.next()) - self.assertEqual(0, ps.peek_offset) - self.assertEqual(2, ps.index) - self.assertEqual("c", ps.current_char) - self.assertEqual("c", ps.current_peek) + assert "c" == ps.next() + assert 0 == ps.peek_offset + assert 2 == ps.index + assert "c" == ps.current_char + assert "c" == ps.current_peek - self.assertEqual("d", ps.peek()) - self.assertEqual(1, ps.peek_offset) - self.assertEqual(2, ps.index) + assert "d" == ps.peek() + assert 1 == ps.peek_offset + assert 2 == ps.index - self.assertEqual("d", ps.next()) - self.assertEqual(0, ps.peek_offset) - self.assertEqual(3, ps.index) - self.assertEqual("d", ps.current_char) - self.assertEqual("d", ps.current_peek) + assert "d" == ps.next() + assert 0 == ps.peek_offset + assert 3 == ps.index + assert "d" == ps.current_char + assert "d" == ps.current_peek - self.assertEqual(None, ps.peek()) - self.assertEqual(1, ps.peek_offset) - self.assertEqual(3, ps.index) - self.assertEqual("d", ps.current_char) - self.assertEqual(None, ps.current_peek) + assert ps.peek() is None + assert 1 == ps.peek_offset + assert 3 == ps.index + assert "d" == ps.current_char + assert ps.current_peek is None - self.assertEqual(None, ps.peek()) - self.assertEqual(2, ps.peek_offset) - self.assertEqual(3, ps.index) + assert ps.peek() is None + assert 2 == ps.peek_offset + assert 3 == ps.index - self.assertEqual(None, ps.next()) - self.assertEqual(0, ps.peek_offset) - self.assertEqual(4, ps.index) + assert ps.next() is None + assert 0 == ps.peek_offset + assert 4 == ps.index def test_skip_to_peek(self): ps = ParserStream("abcd") @@ -102,24 +99,24 @@ def test_skip_to_peek(self): ps.skip_to_peek() - self.assertEqual("c", ps.current_char) - self.assertEqual("c", ps.current_peek) - self.assertEqual(0, ps.peek_offset) - self.assertEqual(2, ps.index) + assert "c" == ps.current_char + assert "c" == ps.current_peek + assert 0 == ps.peek_offset + assert 2 == ps.index ps.peek() - self.assertEqual("c", ps.current_char) - self.assertEqual("d", ps.current_peek) - self.assertEqual(1, ps.peek_offset) - self.assertEqual(2, ps.index) + assert "c" == ps.current_char + assert "d" == ps.current_peek + assert 1 == ps.peek_offset + assert 2 == ps.index ps.next() - self.assertEqual("d", ps.current_char) - self.assertEqual("d", ps.current_peek) - self.assertEqual(0, ps.peek_offset) - self.assertEqual(3, ps.index) + assert "d" == ps.current_char + assert "d" == ps.current_peek + assert 0 == ps.peek_offset + assert 3 == ps.index def test_reset_peek(self): ps = ParserStream("abcd") @@ -129,33 +126,33 @@ def test_reset_peek(self): ps.peek() ps.reset_peek() - self.assertEqual("b", ps.current_char) - self.assertEqual("b", ps.current_peek) - self.assertEqual(0, ps.peek_offset) - self.assertEqual(1, ps.index) + assert "b" == ps.current_char + assert "b" == ps.current_peek + assert 0 == ps.peek_offset + assert 1 == ps.index ps.peek() - self.assertEqual("b", ps.current_char) - self.assertEqual("c", ps.current_peek) - self.assertEqual(1, ps.peek_offset) - self.assertEqual(1, ps.index) + assert "b" == ps.current_char + assert "c" == ps.current_peek + assert 1 == ps.peek_offset + assert 1 == ps.index ps.peek() ps.peek() ps.peek() ps.reset_peek() - self.assertEqual("b", ps.current_char) - self.assertEqual("b", ps.current_peek) - self.assertEqual(0, ps.peek_offset) - self.assertEqual(1, ps.index) + assert "b" == ps.current_char + assert "b" == ps.current_peek + assert 0 == ps.peek_offset + assert 1 == ps.index - self.assertEqual("c", ps.peek()) - self.assertEqual("b", ps.current_char) - self.assertEqual("c", ps.current_peek) - self.assertEqual(1, ps.peek_offset) - self.assertEqual(1, ps.index) + assert "c" == ps.peek() + assert "b" == ps.current_char + assert "c" == ps.current_peek + assert 1 == ps.peek_offset + assert 1 == ps.index - self.assertEqual("d", ps.peek()) - self.assertEqual(None, ps.peek()) + assert "d" == ps.peek() + assert ps.peek() is None diff --git a/fluent.syntax/tests/syntax/test_structure.py b/fluent.syntax/tests/syntax/test_structure.py index 8d7254b3..2004ed7f 100644 --- a/fluent.syntax/tests/syntax/test_structure.py +++ b/fluent.syntax/tests/syntax/test_structure.py @@ -1,71 +1,53 @@ -import json -import os -import unittest +from json import loads +from os import listdir +from os.path import dirname, join, splitext + +import pytest from fluent.syntax import parse +fixtures_dir = join(dirname(__file__), "fixtures_structure") +fixture_names = [ + fp[0] for fn in listdir(fixtures_dir) if (fp := splitext(fn))[1] == ".ftl" +] + + +@pytest.mark.parametrize("name", fixture_names) +def test_structure_with_spans(name): + source, expected = read_fixtures(name) + ast = parse(source, with_spans=True) + assert ast.to_json() == expected + + +@pytest.mark.parametrize("name", fixture_names) +def test_structure_without_spans(name): + source, expected = read_fixtures(name) + ast = parse(source, with_spans=False) + assert ast.to_json() == without_spans(expected) + + +def read_fixtures(name: str): + ftl_path = join(fixtures_dir, name + ".ftl") + ast_path = join(fixtures_dir, name + ".json") -def read_file(path): - with open(path, "r", encoding="utf-8", newline="\n") as file: - text = file.read() - return text + with open(ftl_path, "r", encoding="utf-8", newline="\n") as file: + source = file.read() + with open(ast_path, "r", encoding="utf-8", newline="\n") as file: + expected = loads(file.read()) + + return source, expected def without_spans(expected): """ - Given an expected JSON fragment with span information, recursively replace all of the spans - with None. + Recursively replace all of the spans with None. """ if isinstance(expected, dict): - result = {} - for key, value in expected.items(): - if key == "span": - result[key] = None - else: - result[key] = without_spans(value) - - return result + return { + key: None if key == "span" else without_spans(value) + for key, value in expected.items() + } elif isinstance(expected, list): return [without_spans(item) for item in expected] else: - # We have been passed something which would not have span information in it return expected - - -fixtures = os.path.join(os.path.dirname(__file__), "fixtures_structure") - - -class TestStructureMeta(type): - def __new__(mcs, name, bases, attrs): - - def gen_test(file_name, with_spans): - def test(self): - ftl_path = os.path.join(fixtures, file_name + ".ftl") - ast_path = os.path.join(fixtures, file_name + ".json") - - source = read_file(ftl_path) - expected = json.loads(read_file(ast_path)) - - if not with_spans: - expected = without_spans(expected) - - ast = parse(source, with_spans=with_spans) - - self.assertEqual(ast.to_json(), expected) - - return test - - for f in os.listdir(fixtures): - file_name, ext = os.path.splitext(f) - - if ext != ".ftl": - continue - - attrs[f"test_{file_name}_with_spans"] = gen_test(file_name, with_spans=True) - attrs[f"test_{file_name}_without_spans"] = gen_test(file_name, with_spans=False) - - return type.__new__(mcs, name, bases, attrs) - - -class TestStructure(unittest.TestCase, metaclass=TestStructureMeta): - maxDiff = None diff --git a/fluent.syntax/tests/syntax/test_visitor.py b/fluent.syntax/tests/syntax/test_visitor.py index 1b3f4f16..b08dfb94 100644 --- a/fluent.syntax/tests/syntax/test_visitor.py +++ b/fluent.syntax/tests/syntax/test_visitor.py @@ -1,9 +1,9 @@ -import unittest from collections import defaultdict from fluent.syntax import ast, visitor from fluent.syntax.parser import FluentParser -from tests.syntax import dedent_ftl + +from . import dedent_ftl class MockVisitor(visitor.Visitor): @@ -19,55 +19,52 @@ def visit_Pattern(self, node): self.pattern_calls += 1 -class TestVisitor(unittest.TestCase): +class TestVisitor: def test_resource(self): resource = FluentParser().parse( dedent_ftl( """\ - one = Message - # Comment - two = Messages - three = Messages with - .an = Attribute - """ + one = Message + # Comment + two = Messages + three = Messages with + .an = Attribute + """ ) ) mv = MockVisitor() mv.visit(resource) - self.assertEqual(mv.pattern_calls, 4) - self.assertDictEqual( - mv.calls, - { - "Resource": 1, - "Comment": 1, - "Message": 3, - "Identifier": 4, - "Attribute": 1, - "Span": 10, - }, - ) - - -class TestTransformer(unittest.TestCase): - def test(self): + assert mv.pattern_calls == 4 + assert mv.calls == { + "Resource": 1, + "Comment": 1, + "Message": 3, + "Identifier": 4, + "Attribute": 1, + "Span": 10, + } + + +class TestTransformer: + def test_transformer(self): resource = FluentParser().parse( dedent_ftl( """\ - one = Message - two = Messages - three = Has a - .an = Message string in the Attribute - """ + one = Message + two = Messages + three = Has a + .an = Message string in the Attribute + """ ) ) prior_res_id = id(resource) prior_msg_id = id(resource.body[1].value) backup = resource.clone() transformed = ReplaceTransformer("Message", "Term").visit(resource) - self.assertEqual(prior_res_id, id(transformed)) - self.assertEqual(prior_msg_id, id(transformed.body[1].value)) - self.assertFalse(transformed.equals(backup)) - self.assertEqual(transformed.body[1].value.elements[0].value, "Terms") + assert prior_res_id == id(transformed) + assert prior_msg_id == id(transformed.body[1].value) + assert not transformed.equals(backup) + assert transformed.body[1].value.elements[0].value == "Terms" class WordCounter: diff --git a/pyproject.toml b/pyproject.toml index 8ff24004..7d9088c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,7 @@ dependencies = [ "flake8 ~= 7.0", "isort ~= 5.0", "mypy ~= 1.0", + "pytest>=8.4.2", "types-babel", "types-pytz", ] diff --git a/uv.lock b/uv.lock index 44fcc13e..b7bcf2af 100644 --- a/uv.lock +++ b/uv.lock @@ -110,6 +110,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + [[package]] name = "flake8" version = "7.3.0" @@ -148,6 +160,30 @@ name = "fluent-syntax" version = "0.19.0" source = { editable = "fluent.syntax" } +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + [[package]] name = "isort" version = "5.13.2" @@ -352,6 +388,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, ] +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + [[package]] name = "pycodestyle" version = "2.14.0" @@ -370,6 +415,57 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551, upload-time = "2025-06-20T18:45:26.937Z" }, ] +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "pluggy", marker = "python_full_version < '3.10'" }, + { name = "pygments", marker = "python_full_version < '3.10'" }, + { name = "tomli", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, + { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "pluggy", marker = "python_full_version >= '3.10'" }, + { name = "pygments", marker = "python_full_version >= '3.10'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + [[package]] name = "python-fluent" version = "0" @@ -380,6 +476,8 @@ dependencies = [ { name = "fluent-syntax" }, { name = "isort" }, { name = "mypy" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "types-babel" }, { name = "types-pytz" }, ] @@ -391,6 +489,7 @@ requires-dist = [ { name = "fluent-syntax", editable = "fluent.syntax" }, { name = "isort", specifier = "~=5.0" }, { name = "mypy", specifier = "~=1.0" }, + { name = "pytest", specifier = ">=8.4.2" }, { name = "types-babel" }, { name = "types-pytz" }, ] From 724a6cf0cba068df12cb43dd9d0071eaec14f18e Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Tue, 26 May 2026 18:47:31 +0300 Subject: [PATCH 10/10] syntax: Fix typo in Generic error code (#216) --- fluent.syntax/fluent/syntax/errors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fluent.syntax/fluent/syntax/errors.py b/fluent.syntax/fluent/syntax/errors.py index baa98113..66f8bddf 100644 --- a/fluent.syntax/fluent/syntax/errors.py +++ b/fluent.syntax/fluent/syntax/errors.py @@ -9,7 +9,7 @@ def __init__(self, code: str, *args: Union[str, None]): def get_error_message(code: str, args: tuple[Union[str, None], ...]) -> str: - if code == "E00001": + if code == "E0001": return "Generic error" if code == "E0002": return "Expected an entry start"