diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 11667d614..ac831e7f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,15 +27,15 @@ jobs: contents: read runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: persist-credentials: false - - uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6 + - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: cache-dependency-glob: ".github/workflows/*.yml" cache-suffix: pre-commit-uv - run: uv tool install pre-commit --with pre-commit-uv --force-reinstall - - uses: actions/cache@v5 + - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 with: path: ~/.cache/pre-commit key: pre-commit-uv-v1-${{ hashFiles('.pre-commit-config.yaml') }} @@ -67,15 +67,15 @@ jobs: BABEL_CLDR_QUIET: "1" PIP_DISABLE_PIP_VERSION_CHECK: "1" steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: persist-credentials: false - - uses: actions/cache@v5 + - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 with: path: cldr key: cldr-${{ hashFiles('scripts/*cldr*') }} - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 with: python-version: ${{ matrix.python-version }} allow-prereleases: true @@ -90,7 +90,7 @@ jobs: env: COVERAGE_XML_PATH: ${{ runner.temp }} BABEL_TOX_EXTRA_DEPS: pytest-github-actions-annotate-failures - - uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 + - uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 with: directory: ${{ runner.temp }} flags: ${{ matrix.os }}-${{ matrix.python-version }} @@ -103,10 +103,10 @@ jobs: runs-on: ubuntu-24.04 needs: lint steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: persist-credentials: false - - uses: actions/setup-python@v6 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 with: python-version: "3.14" cache: "pip" @@ -114,7 +114,7 @@ jobs: - run: pip install build -e . - run: make import-cldr - run: python -m build - - uses: actions/upload-artifact@v6 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: dist path: dist @@ -130,12 +130,12 @@ jobs: permissions: id-token: write # Required for Trusted Publishing action steps: - - uses: actions/download-artifact@v7 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: dist path: dist/ - name: Publish package distributions to PyPI - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 with: verbose: true print-hash: true diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index e4f467e38..83ae82929 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -22,8 +22,8 @@ jobs: security-events: write # via Zizmor example steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: persist-credentials: false - name: Run zizmor - uses: zizmorcore/zizmor-action@e639db99335bc9038abc0e066dfcd72e23d26fb4 # v0.3.0 + uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3 diff --git a/Makefile b/Makefile index 05f4d8434..fba85b899 100644 --- a/Makefile +++ b/Makefile @@ -20,4 +20,7 @@ develop: tox-test: tox -.PHONY: test develop tox-test clean-pyc clean-cldr import-cldr clean standalone-test +update-gha: + uvx gha-tools@latest autoupdate --pin all -s specific --first-party-version-strategy=major --write .github/workflows/ + +.PHONY: test develop tox-test clean-pyc clean-cldr import-cldr clean update-gha diff --git a/babel/localedata.py b/babel/localedata.py index 2b225a142..4648e6626 100644 --- a/babel/localedata.py +++ b/babel/localedata.py @@ -60,6 +60,7 @@ def resolve_locale_filename(name: os.PathLike[str] | str) -> str: return os.path.join(_dirname, f"{name}.dat") +@lru_cache(maxsize=None) def exists(name: str) -> bool: """Check whether locale data is available for the given locale. @@ -72,7 +73,7 @@ def exists(name: str) -> bool: if name in _cache: return True file_found = os.path.exists(resolve_locale_filename(name)) - return True if file_found else bool(normalize_locale(name)) + return file_found or bool(normalize_locale(name)) @lru_cache(maxsize=None) diff --git a/babel/units.py b/babel/units.py index 88ebb909c..2c3462422 100644 --- a/babel/units.py +++ b/babel/units.py @@ -121,7 +121,7 @@ def format_unit( .. versionadded:: 2.2.0 - :param value: the value to format. If this is a string, no number formatting will be attempted. + :param value: the value to format. If this is a string, no number formatting will be attempted and the number is assumed to be singular for the purposes of unit formatting. :param measurement_unit: the code of a measurement unit. Known units can be found in the CLDR Unit Validity XML file: https://unicode.org/repos/cldr/tags/latest/common/validity/unit.xml @@ -137,7 +137,6 @@ def format_unit( q_unit = _find_unit_pattern(measurement_unit, locale=locale) if not q_unit: raise UnknownUnitError(unit=measurement_unit, locale=locale) - unit_patterns = locale._data["unit_patterns"][q_unit].get(length, {}) if isinstance(value, str): # Assume the value is a preformatted singular. formatted_value = value @@ -151,8 +150,21 @@ def format_unit( ) plural_form = locale.plural_form(value) - if plural_form in unit_patterns: - return unit_patterns[plural_form].format(formatted_value) + unit_patterns = locale._data["unit_patterns"][q_unit] + + # We do not support `` tags at all while ingesting CLDR data, + # so these aliases specified in `root.xml` are hard-coded here: + # + # + lengths_to_check = [length, "short"] if length in ("long", "narrow") else [length] + + for real_length in lengths_to_check: + length_patterns = unit_patterns.get(real_length, {}) + # Fall back from the correct plural form to "other" + # (this is specified in LDML "Lateral Inheritance") + pat = length_patterns.get(plural_form) or length_patterns.get("other") + if pat: + return pat.format(formatted_value) # Fall back to a somewhat bad representation. # nb: This is marked as no-cover, as the current CLDR seemingly has no way for this to happen. diff --git a/docs/api/dates.rst b/docs/api/dates.rst index cbdac593b..9f0c80b57 100644 --- a/docs/api/dates.rst +++ b/docs/api/dates.rst @@ -40,8 +40,6 @@ Timezone Functionality A timezone object for the computer's local timezone. -.. autoclass:: TimezoneTransition - Data Access ----------- @@ -69,3 +67,9 @@ Basic Parsing .. autofunction:: parse_time .. autofunction:: parse_pattern + +Exceptions +---------- + +.. autoexception:: ParseError + :members: diff --git a/docs/api/numbers.rst b/docs/api/numbers.rst index d3ab8b116..c43bd56d2 100644 --- a/docs/api/numbers.rst +++ b/docs/api/numbers.rst @@ -36,19 +36,47 @@ Exceptions .. autoexception:: NumberFormatError :members: +.. autoexception:: UnknownCurrencyError + :members: + +.. autoexception:: UnknownCurrencyFormatError + :members: + +.. autoexception:: UnsupportedNumberingSystemError + :members: + Data Access ----------- +.. autofunction:: get_decimal_symbol + +.. autofunction:: get_exponential_symbol + +.. autofunction:: get_group_symbol + +.. autofunction:: get_infinity_symbol + +.. autofunction:: get_minus_sign_symbol + +.. autofunction:: get_plus_sign_symbol + +Currency Utilities +------------------ + .. autofunction:: get_currency_name +.. autofunction:: get_currency_precision + .. autofunction:: get_currency_symbol .. autofunction:: get_currency_unit_pattern -.. autofunction:: get_decimal_symbol +.. autofunction:: get_territory_currencies -.. autofunction:: get_plus_sign_symbol +.. autofunction:: is_currency -.. autofunction:: get_minus_sign_symbol +.. autofunction:: list_currencies -.. autofunction:: get_territory_currencies +.. autofunction:: normalize_currency + +.. autofunction:: validate_currency diff --git a/docs/api/plural.rst b/docs/api/plural.rst index d6934b576..c40f29f04 100644 --- a/docs/api/plural.rst +++ b/docs/api/plural.rst @@ -21,3 +21,9 @@ Conversion Functionality .. autofunction:: to_python .. autofunction:: to_gettext + +Exceptions +---------- + +.. autoexception:: RuleError + :members: diff --git a/docs/api/units.rst b/docs/api/units.rst index 2b99be547..db94b7055 100644 --- a/docs/api/units.rst +++ b/docs/api/units.rst @@ -11,3 +11,9 @@ locales. .. autofunction:: format_compound_unit .. autofunction:: get_unit_name + +Exceptions +---------- + +.. autoexception:: UnknownUnitError + :members: diff --git a/docs/conf.py b/docs/conf.py index b22b78c4f..eb8175796 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -44,16 +44,16 @@ # General information about the project. project = 'Babel' -copyright = '2025, The Babel Team' +copyright = '2026, The Babel Team' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '2.17' +version = '2.18' # The full version, including alpha/beta/rc tags. -release = '2.17.0' +release = '2.18.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/requirements.txt b/docs/requirements.txt index c133306c7..4864c69dd 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1 +1 @@ -Sphinx~=5.3.0 +Sphinx~=9.1.0 diff --git a/tests/test_localedata.py b/tests/test_localedata.py index 03cbed1dc..42810b992 100644 --- a/tests/test_localedata.py +++ b/tests/test_localedata.py @@ -109,10 +109,10 @@ def test_locale_argument_acceptance(): assert normalized_locale is None assert not localedata.exists(None) - # Testing list input. + # Testing tuple input. normalized_locale = localedata.normalize_locale(['en_us', None]) assert normalized_locale is None - assert not localedata.exists(['en_us', None]) + assert not localedata.exists(('en_us', None)) def test_locale_identifiers_cache(monkeypatch): diff --git a/tests/test_units.py b/tests/test_units.py index 7c6ad6b4d..27b7535fe 100644 --- a/tests/test_units.py +++ b/tests/test_units.py @@ -17,3 +17,15 @@ ]) def test_new_cldr46_units(unit, count, expected): assert format_unit(count, unit, locale='cs_CZ') == expected + + +@pytest.mark.parametrize('count, unit, locale, length, expected', [ + (1, 'duration-month', 'et', 'long', '1 kuu'), + (1, 'duration-minute', 'et', 'narrow', '1 min'), + (2, 'duration-minute', 'et', 'narrow', '2 min'), + (2, 'digital-byte', 'et', 'long', '2 baiti'), + (1, 'duration-day', 'it', 'long', '1 giorno'), + (1, 'duration-day', 'it', 'short', '1 giorno'), +]) +def test_issue_1217(count, unit, locale, length, expected): + assert format_unit(count, unit, length, locale=locale) == expected diff --git a/tox.ini b/tox.ini index a48dca512..d5f3e326b 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,11 @@ envlist = [testenv] extras = dev -deps = {env:BABEL_TOX_EXTRA_DEPS:} +deps = + # On Python 3.9, we need a version of Setuptools that still has pkg_resources + # to be able to run the Jinja extractor tests. + py39: setuptools<82 + {env:BABEL_TOX_EXTRA_DEPS:} allowlist_externals = make commands = make clean-cldr test setenv =