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]