diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0797decb5..981f49e0c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,18 +7,22 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12"] numpy: [null, "numpy>=1.23,<2.0.0", "numpy>=2.0.0rc1"] uncertainties: [null, "uncertainties==3.1.6", "uncertainties>=3.1.6,<4.0.0"] extras: [null] include: - python-version: "3.10" # Minimal versions - numpy: "numpy" - extras: matplotlib==2.2.5 + numpy: "numpy>=1.23,<2.0.0" + extras: matplotlib==3.5.3 - python-version: "3.10" numpy: "numpy" uncertainties: "uncertainties" - extras: "sparse xarray netCDF4 dask[complete]==2023.4.0 graphviz babel==2.8 mip>=1.13" + extras: "sparse xarray netCDF4 dask[complete]==2024.5.1 graphviz babel==2.8 mip>=1.13" + - python-version: "3.10" + numpy: "numpy==1.26.1" + uncertainties: null + extras: "babel==2.15 matplotlib==3.9.0" runs-on: ubuntu-latest env: diff --git a/CHANGES b/CHANGES index 8243e3471..11df3542b 100644 --- a/CHANGES +++ b/CHANGES @@ -1,7 +1,15 @@ Pint Changelog ============== -0.24 (unreleased) +0.24.1 (unreleased) +----------------- + +- Fix custom formatter needing the registry object. (PR #2011) +- Support python 3.9 following difficulties installing with NumPy 2. (PR #2019) +- Fix default formatting of dimensionless unit issue. (PR #2012) +- Fix bug preventing custom formatters with modifiers working. (PR #2021) + +0.24 (2024-06-07) ----------------- - Fix detection of invalid conversion between offset and delta units. (PR #1905) @@ -9,6 +17,8 @@ Pint Changelog - NumPy 2.0 support (PR #1985, #1971) - Implement numpy roll (Related to issue #981) +- Implement numpy correlate + (PR #1990) - Add `dim_sort` function to _formatter_helpers. - Add `dim_order` and `default_sort_func` properties to FullFormatter. (PR #1926, fixes Issue #1841) @@ -18,6 +28,7 @@ Pint Changelog array resulting in incorrect units. (PR #1677) - Fix LaTeX siuntix formatting when using non_int_type=decimal.Decimal. + (PR #1977) - Added refractive index units. (PR #1816) - Fix converting to offset units of higher dimension e.g. gauge pressure diff --git a/README.rst b/README.rst index a839fcdd7..3c16a4541 100644 --- a/README.rst +++ b/README.rst @@ -3,7 +3,6 @@ :alt: Latest Version .. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json - :target: https://github.com/python/black :target: https://github.com/astral-sh/ruff :alt: Ruff diff --git a/docs/advanced/currencies.rst b/docs/advanced/currencies.rst index 26b66b531..addc94785 100644 --- a/docs/advanced/currencies.rst +++ b/docs/advanced/currencies.rst @@ -84,3 +84,16 @@ currency on its own dimension, and then implement transformations:: More sophisticated formulas, e.g. dealing with flat fees and thresholds, can be implemented with arbitrary python code by programmatically defining a context (see :ref:`contexts`). + +Currency Symbols +---------------- + +Many common currency symbols are not supported by the pint parser. A preprocessor can be used as a workaround: + +.. doctest:: + + >>> import pint + >>> ureg = pint.UnitRegistry(preprocessors = [lambda s: s.replace("€", "EUR")]) + >>> ureg.define("euro = [currency] = € = EUR") + >>> print(ureg.Quantity("1 €")) + 1 euro diff --git a/docs/ecosystem.rst b/docs/ecosystem.rst index 751c49726..c83c52f49 100644 --- a/docs/ecosystem.rst +++ b/docs/ecosystem.rst @@ -21,3 +21,4 @@ Packages using pint: - `thermo `_ Thermodynamic equilibrium calculations - `Taurus `_ Control system UI creation - `InstrumentKit `_ Interacting with laboratory equipment over various buses. +- `NEMO `_ Electricity production cost model diff --git a/docs/user/formatting.rst b/docs/user/formatting.rst index d45fc1e13..fbf2fae42 100644 --- a/docs/user/formatting.rst +++ b/docs/user/formatting.rst @@ -98,7 +98,8 @@ formats: '2.3e-06 kilogram ** -1 * meter ** 3 * second ** -2' where ``unit`` is a :py:class:`dict` subclass containing the unit names and -their exponents. +their exponents, ``registry`` is the current instance of :py:class:``UnitRegistry`` and +``options`` is not yet implemented. You can choose to replace the complete formatter. Briefly, the formatter if an object with the following methods: `format_magnitude`, `format_unit`, `format_quantity`, `format_uncertainty`, diff --git a/docs/user/log_units.rst b/docs/user/log_units.rst index 03e007914..096397350 100644 --- a/docs/user/log_units.rst +++ b/docs/user/log_units.rst @@ -111,16 +111,16 @@ will not work: .. doctest:: >>> -161.0 * ureg('dBm/Hz') == (-161.0 * ureg.dBm / ureg.Hz) - False + np.False_ But this will: .. doctest:: >>> ureg('-161.0 dBm/Hz') == (-161.0 * ureg.dBm / ureg.Hz) - True + np.True_ >>> Q_(-161.0, 'dBm') / ureg.Hz == (-161.0 * ureg.dBm / ureg.Hz) - True + np.True_ To begin using this feature while avoiding problems, define logarithmic units as single-unit quantities and convert them to their base units as quickly as diff --git a/pint/compat.py b/pint/compat.py index 277662410..32ad04afb 100644 --- a/pint/compat.py +++ b/pint/compat.py @@ -19,9 +19,13 @@ from typing import ( Any, NoReturn, - TypeAlias, # noqa ) +if sys.version_info >= (3, 10): + from typing import TypeAlias # noqa +else: + from typing_extensions import TypeAlias # noqa + if sys.version_info >= (3, 11): from typing import Self # noqa else: diff --git a/pint/delegates/formatter/_compound_unit_helpers.py b/pint/delegates/formatter/_compound_unit_helpers.py index 34ffe146b..c8ac1e2d9 100644 --- a/pint/delegates/formatter/_compound_unit_helpers.py +++ b/pint/delegates/formatter/_compound_unit_helpers.py @@ -20,12 +20,11 @@ TYPE_CHECKING, Any, Literal, - TypeAlias, TypedDict, TypeVar, ) -from ...compat import babel_parse +from ...compat import TypeAlias, babel_parse from ...util import NonReducingUnitsContainer, UnitsContainer T = TypeVar("T") @@ -83,11 +82,19 @@ def localize_per( locale = babel_parse(locale) patterns = locale._data["compound_unit_patterns"].get("per", None) + if patterns is None: + return default or "{}/{}" + patterns = patterns.get(length, None) if patterns is None: return default or "{}/{}" - return patterns.get(length, default or "{}/{}") + # babel 2.8 + if isinstance(patterns, str): + return patterns + + # babe; 2.15 + return patterns.get("compound", default or "{}/{}") @functools.lru_cache @@ -259,6 +266,12 @@ def prepare_compount_unit( # out: unit_name, unit_exponent + if len(out) == 0: + if "~" in spec: + return ([], []) + else: + return ([("dimensionless", 1)], []) + if "~" in spec: if registry is None: raise ValueError( diff --git a/pint/delegates/formatter/_format_helpers.py b/pint/delegates/formatter/_format_helpers.py index 501c72c7a..065263126 100644 --- a/pint/delegates/formatter/_format_helpers.py +++ b/pint/delegates/formatter/_format_helpers.py @@ -131,6 +131,8 @@ def join_mu(joint_fstring: str, mstr: str, ustr: str) -> str: This avoids that `3 and `1 / m` becomes `3 1 / m` """ + if ustr == "": + return mstr if ustr.startswith("1 / "): return joint_fstring.format(mstr, ustr[2:]) return joint_fstring.format(mstr, ustr) diff --git a/pint/delegates/formatter/_to_register.py b/pint/delegates/formatter/_to_register.py index 0f8f46788..697973716 100644 --- a/pint/delegates/formatter/_to_register.py +++ b/pint/delegates/formatter/_to_register.py @@ -61,6 +61,8 @@ def wrapper(func: Callable[[PlainUnit, UnitRegistry], str]): raise ValueError(f"format {name!r} already exists") # or warn instead class NewFormatter(BaseFormatter): + spec = name + def format_magnitude( self, magnitude: Magnitude, diff --git a/pint/delegates/formatter/full.py b/pint/delegates/formatter/full.py index c3bf49ef4..8a633b4c7 100644 --- a/pint/delegates/formatter/full.py +++ b/pint/delegates/formatter/full.py @@ -107,12 +107,20 @@ def get_formatter(self, spec: str): if k in spec: return v - try: - return REGISTERED_FORMATTERS[spec] - except KeyError: - pass + for k, v in REGISTERED_FORMATTERS.items(): + if k in spec: + orphan_fmt = REGISTERED_FORMATTERS[k] + break + else: + return self._formatters["D"] - return self._formatters["D"] + try: + fmt = orphan_fmt.__class__(self._registry) + spec = getattr(fmt, "spec", spec) + self._formatters[spec] = fmt + return fmt + except Exception: + return orphan_fmt def format_magnitude( self, magnitude: Magnitude, mspec: str = "", **babel_kwds: Unpack[BabelKwds] diff --git a/pint/testsuite/helpers.py b/pint/testsuite/helpers.py index c9106b75a..d317e0755 100644 --- a/pint/testsuite/helpers.py +++ b/pint/testsuite/helpers.py @@ -128,9 +128,26 @@ def requires_numpy_at_least(version): ) -requires_babel = pytest.mark.skipif( - not HAS_BABEL, reason="Requires Babel with units support" -) +def requires_babel(tested_locales=[]): + if not HAS_BABEL: + return pytest.mark.skip("Requires Babel with units support") + + import locale + + default_locale = locale.getlocale(locale.LC_NUMERIC) + locales_unavailable = False + try: + for loc in tested_locales: + locale.setlocale(locale.LC_NUMERIC, loc) + except locale.Error: + locales_unavailable = True + locale.setlocale(locale.LC_NUMERIC, default_locale) + + return pytest.mark.skipif( + locales_unavailable, reason="Tested locales not available." + ) + + requires_not_babel = pytest.mark.skipif( HAS_BABEL, reason="Requires Babel not to be installed" ) diff --git a/pint/testsuite/test_babel.py b/pint/testsuite/test_babel.py index 2dd66d58d..9adcb04a9 100644 --- a/pint/testsuite/test_babel.py +++ b/pint/testsuite/test_babel.py @@ -16,7 +16,7 @@ def test_no_babel(func_registry): distance.format_babel(locale="fr_FR", length="long") -@helpers.requires_babel() +@helpers.requires_babel(["fr_FR", "ro_RO"]) def test_format(func_registry): ureg = func_registry dirname = os.path.dirname(__file__) @@ -36,7 +36,7 @@ def test_format(func_registry): assert mks.format_babel(locale="fr_FR") == "métrique" -@helpers.requires_babel() +@helpers.requires_babel(["fr_FR", "ro_RO"]) def test_registry_locale(): ureg = UnitRegistry(fmt_locale="fr_FR") dirname = os.path.dirname(__file__) @@ -60,7 +60,7 @@ def test_registry_locale(): assert mks.format_babel(locale="fr_FR") == "métrique" -@helpers.requires_babel() +@helpers.requires_babel(["fr_FR"]) def test_unit_format_babel(): ureg = UnitRegistry(fmt_locale="fr_FR") volume = ureg.Unit("ml") @@ -85,7 +85,7 @@ def test_no_registry_locale(func_registry): distance.format_babel() -@helpers.requires_babel() +@helpers.requires_babel(["fr_FR"]) def test_str(func_registry): ureg = func_registry d = 24.1 * ureg.meter diff --git a/pint/testsuite/test_formatting.py b/pint/testsuite/test_formatting.py index e74c09c50..d8f10715b 100644 --- a/pint/testsuite/test_formatting.py +++ b/pint/testsuite/test_formatting.py @@ -59,6 +59,8 @@ def test_split_format(format, default, flag, expected): def test_register_unit_format(func_registry): @fmt.register_unit_format("custom") def format_custom(unit, registry, **options): + # Ensure the registry is correct.. + registry.Unit(unit) return "" quantity = 1.0 * func_registry.meter diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index 2a0b7edf6..97eca3cde 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -7,7 +7,12 @@ import pytest -from pint import Context, DimensionalityError, UnitRegistry, get_application_registry +from pint import ( + Context, + DimensionalityError, + UnitRegistry, + get_application_registry, +) from pint.compat import np from pint.delegates.formatter._compound_unit_helpers import sort_by_dimensionality from pint.facets.plain.unit import UnitsContainer @@ -908,7 +913,7 @@ def test_issue1674(self, module_registry): arr_of_q * q_arr, np.array([Q_(2, "m^2"), Q_(8, "m s")], dtype="object") ) - @helpers.requires_babel() + @helpers.requires_babel(["es_ES"]) def test_issue_1400(self, sess_registry): q1 = 3.1 * sess_registry.W q2 = 3.1 * sess_registry.W / sess_registry.cm @@ -1255,3 +1260,46 @@ def test_issue1949(registry_empty): def test_issue1772(given, expected): ureg = UnitRegistry(non_int_type=decimal.Decimal) assert f"{ureg(given):Lx}" == expected + + +def test_issue2017(): + ureg = UnitRegistry() + + from pint import formatting as fmt + + @fmt.register_unit_format("test") + def _test_format(unit, registry, **options): + print("format called") + proc = {u.replace("µ", "u"): e for u, e in unit.items()} + return fmt.formatter( + proc.items(), + as_ratio=True, + single_denominator=False, + product_fmt="*", + division_fmt="/", + power_fmt="{}{}", + parentheses_fmt="({})", + **options, + ) + + base_unit = ureg.microsecond + assert f"{base_unit:~test}" == "us" + assert f"{base_unit:test}" == "microsecond" + + +def test_issue2007(): + ureg = UnitRegistry() + q = ureg.Quantity(1, "") + assert f"{q:P}" == "1 dimensionless" + assert f"{q:C}" == "1 dimensionless" + assert f"{q:D}" == "1 dimensionless" + assert f"{q:H}" == "1 dimensionless" + + assert f"{q:L}" == "1\\ \\mathrm{dimensionless}" + # L returned '1\\ dimensionless' in pint 0.23 + + assert f"{q:Lx}" == "\\SI[]{1}{}" + assert f"{q:~P}" == "1" + assert f"{q:~C}" == "1" + assert f"{q:~D}" == "1" + assert f"{q:~H}" == "1" diff --git a/pyproject.toml b/pyproject.toml index a376bd6a4..9f29f8f92 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,11 +22,12 @@ classifiers = [ "Programming Language :: Python", "Topic :: Scientific/Engineering", "Topic :: Software Development :: Libraries", + "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", ] -requires-python = ">=3.10" +requires-python = ">=3.9" dynamic = ["version", "dependencies"] [tool.setuptools.package-data]