diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 981f49e0c..dfebe5bdc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 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] diff --git a/CHANGES b/CHANGES index de1c3bccb..07328fb1c 100644 --- a/CHANGES +++ b/CHANGES @@ -1,8 +1,26 @@ Pint Changelog ============== -0.25 (unreleased) ------------------ +0.25.0 (unreleased) +------------------- + +- Add docs to the functions in ``pint.testing`` (PR #2070) + + +0.24.4 (2024-11-07) +------------------- + +- add error for prefixed non multi units (#1998) +- build: typing_extensions version +- build: switch from appdirs to platformdirs +- fix GenericPlainRegistry getattr type (#2045) +- Replace references to the deprecated `UnitRegistry.default_format` (#2058) +- fix: upgrade to flexparser>=0.4, exceptions are no longer dataclasses. + (required for Python 3.13) + + +0.24.2 (2024-07-28) +------------------- - Fix the default behaviour for pint-convert (cli) for importing uncertainties package (PR #2032, Issue #2016) - Added mu and mc as alternatives for SI micro prefix @@ -14,7 +32,7 @@ Pint Changelog 0.24.1 (2024-06-24) ------------------ +------------------- - Fix custom formatter needing the registry object. (PR #2011) - Support python 3.9 following difficulties installing with NumPy 2. (PR #2019) diff --git a/docs/conf.py b/docs/conf.py index d856e1075..b27bff94a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -45,6 +45,7 @@ "sphinx_design", ] + # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] diff --git a/docs/ecosystem.rst b/docs/ecosystem.rst index c83c52f49..95a73bd45 100644 --- a/docs/ecosystem.rst +++ b/docs/ecosystem.rst @@ -13,7 +13,7 @@ Pint integrations: Packages using pint: ------------------- +-------------------- - `fluids `_ Practical fluid dynamics calculations - `ht `_ Practical heat transfer calculations diff --git a/pint/delegates/txt_defparser/common.py b/pint/delegates/txt_defparser/common.py index ebdabc062..def901d88 100644 --- a/pint/delegates/txt_defparser/common.py +++ b/pint/delegates/txt_defparser/common.py @@ -12,7 +12,7 @@ from __future__ import annotations -from dataclasses import dataclass, field +from dataclasses import dataclass import flexparser as fp @@ -20,7 +20,6 @@ from ..base_defparser import ParserConfig -@dataclass(frozen=True) class DefinitionSyntaxError(errors.DefinitionSyntaxError, fp.ParsingError): """A syntax error was found in a definition. Combines: @@ -30,7 +29,11 @@ class DefinitionSyntaxError(errors.DefinitionSyntaxError, fp.ParsingError): and an extra location attribute in which the filename or reseource is stored. """ - location: str = field(init=False, default="") + msg: str + + def __init__(self, msg: str, location: str = ""): + self.msg = msg + self.location = location def __str__(self) -> str: msg = ( diff --git a/pint/errors.py b/pint/errors.py index 59d3b4569..d1882dbdd 100644 --- a/pint/errors.py +++ b/pint/errors.py @@ -11,7 +11,6 @@ from __future__ import annotations import typing as ty -from dataclasses import dataclass, fields OFFSET_ERROR_DOCS_HTML = "https://pint.readthedocs.io/en/stable/user/nonmult.html" LOG_ERROR_DOCS_HTML = "https://pint.readthedocs.io/en/stable/user/log_units.html" @@ -81,12 +80,10 @@ def def_err(self, msg: str): return DefinitionError(self.name, self.__class__, msg) -@dataclass(frozen=False) class PintError(Exception): """Base exception for all Pint errors.""" -@dataclass(frozen=False) class DefinitionError(ValueError, PintError): """Raised when a definition is not properly constructed.""" @@ -94,69 +91,76 @@ class DefinitionError(ValueError, PintError): definition_type: type msg: str + def __init__(self, name: str, definition_type: type, msg: str): + self.name = name + self.definition_type = definition_type + self.msg = msg + def __str__(self): msg = f"Cannot define '{self.name}' ({self.definition_type}): {self.msg}" return msg def __reduce__(self): - return self.__class__, tuple(getattr(self, f.name) for f in fields(self)) + return self.__class__, (self.name, self.definition_type, self.msg) -@dataclass(frozen=False) class DefinitionSyntaxError(ValueError, PintError): """Raised when a textual definition has a syntax error.""" msg: str + def __init__(self, msg: str): + self.msg = msg + def __str__(self): return self.msg def __reduce__(self): - return self.__class__, tuple(getattr(self, f.name) for f in fields(self)) + return self.__class__, (self.msg,) -@dataclass(frozen=False) class RedefinitionError(ValueError, PintError): """Raised when a unit or prefix is redefined.""" name: str definition_type: type + def __init__(self, name: str, definition_type: type): + self.name = name + self.definition_type = definition_type + def __str__(self): msg = f"Cannot redefine '{self.name}' ({self.definition_type})" return msg def __reduce__(self): - return self.__class__, tuple(getattr(self, f.name) for f in fields(self)) + return self.__class__, (self.name, self.definition_type) -@dataclass(frozen=False) class UndefinedUnitError(AttributeError, PintError): """Raised when the units are not defined in the unit registry.""" - unit_names: str | tuple[str, ...] + unit_names: tuple[str, ...] + + def __init__(self, unit_names: str | ty.Iterable[str]): + if isinstance(unit_names, str): + self.unit_names = (unit_names,) + else: + self.unit_names = tuple(unit_names) def __str__(self): - if isinstance(self.unit_names, str): - return f"'{self.unit_names}' is not defined in the unit registry" - if ( - isinstance(self.unit_names, (tuple, list, set)) - and len(self.unit_names) == 1 - ): + if len(self.unit_names) == 1: return f"'{tuple(self.unit_names)[0]}' is not defined in the unit registry" return f"{tuple(self.unit_names)} are not defined in the unit registry" def __reduce__(self): - return self.__class__, tuple(getattr(self, f.name) for f in fields(self)) + return self.__class__, (self.unit_names,) -@dataclass(frozen=False) class PintTypeError(TypeError, PintError): - def __reduce__(self): - return self.__class__, tuple(getattr(self, f.name) for f in fields(self)) + pass -@dataclass(frozen=False) class DimensionalityError(PintTypeError): """Raised when trying to convert between incompatible units.""" @@ -166,6 +170,20 @@ class DimensionalityError(PintTypeError): dim2: str = "" extra_msg: str = "" + def __init__( + self, + units1: ty.Any, + units2: ty.Any, + dim1: str = "", + dim2: str = "", + extra_msg: str = "", + ) -> None: + self.units1 = units1 + self.units2 = units2 + self.dim1 = dim1 + self.dim2 = dim2 + self.extra_msg = extra_msg + def __str__(self): if self.dim1 or self.dim2: dim1 = f" ({self.dim1})" @@ -180,16 +198,25 @@ def __str__(self): ) def __reduce__(self): - return self.__class__, tuple(getattr(self, f.name) for f in fields(self)) + return self.__class__, ( + self.units1, + self.units2, + self.dim1, + self.dim2, + self.extra_msg, + ) -@dataclass(frozen=False) class OffsetUnitCalculusError(PintTypeError): """Raised on ambiguous operations with offset units.""" units1: ty.Any units2: ty.Optional[ty.Any] = None + def __init__(self, units1: ty.Any, units2: ty.Optional[ty.Any] = None) -> None: + self.units1 = units1 + self.units2 = units2 + def yield_units(self): yield self.units1 if self.units2: @@ -205,16 +232,19 @@ def __str__(self): ) def __reduce__(self): - return self.__class__, tuple(getattr(self, f.name) for f in fields(self)) + return self.__class__, (self.units1, self.units2) -@dataclass(frozen=False) class LogarithmicUnitCalculusError(PintTypeError): """Raised on inappropriate operations with logarithmic units.""" units1: ty.Any units2: ty.Optional[ty.Any] = None + def __init__(self, units1: ty.Any, units2: ty.Optional[ty.Any] = None) -> None: + self.units1 = units1 + self.units2 = units2 + def yield_units(self): yield self.units1 if self.units2: @@ -230,26 +260,28 @@ def __str__(self): ) def __reduce__(self): - return self.__class__, tuple(getattr(self, f.name) for f in fields(self)) + return self.__class__, (self.units1, self.units2) -@dataclass(frozen=False) class UnitStrippedWarning(UserWarning, PintError): msg: str + def __init__(self, msg: str): + self.msg = msg + def __reduce__(self): - return self.__class__, tuple(getattr(self, f.name) for f in fields(self)) + return self.__class__, (self.msg,) -@dataclass(frozen=False) class UnexpectedScaleInContainer(Exception): - def __reduce__(self): - return self.__class__, tuple(getattr(self, f.name) for f in fields(self)) + pass -@dataclass(frozen=False) class UndefinedBehavior(UserWarning, PintError): msg: str + def __init__(self, msg: str): + self.msg = msg + def __reduce__(self): - return self.__class__, tuple(getattr(self, f.name) for f in fields(self)) + return self.__class__, (self.msg,) diff --git a/pint/testing.py b/pint/testing.py index 21a1f55dd..c5508d8d2 100644 --- a/pint/testing.py +++ b/pint/testing.py @@ -1,3 +1,13 @@ +""" + pint.testing + ~~~~~~~~~~~~ + + Functions for testing whether pint quantities are equal. + + :copyright: 2016 by Pint Authors, see AUTHORS for more details.. + :license: BSD, see LICENSE for more details. +""" + from __future__ import annotations import math @@ -35,6 +45,25 @@ def _get_comparable_magnitudes(first, second, msg): def assert_equal(first, second, msg: str | None = None) -> None: + """ + Assert that two quantities are equal + + Parameters + ---------- + first + First quantity to compare + + second + Second quantity to compare + + msg + If supplied, message to show if the two quantities aren't equal. + + Raises + ------ + AssertionError + The two quantities are not equal. + """ if msg is None: msg = f"Comparing {first!r} and {second!r}. " @@ -60,6 +89,33 @@ def assert_equal(first, second, msg: str | None = None) -> None: def assert_allclose( first, second, rtol: float = 1e-07, atol: float = 0, msg: str | None = None ) -> None: + """ + Assert that two quantities are all close + + Unlike numpy, this uses a symmetric check of closeness. + + Parameters + ---------- + first + First quantity to compare + + second + Second quantity to compare + + rtol + Relative tolerance to use when checking for closeness. + + atol + Absolute tolerance to use when checking for closeness. + + msg + If supplied, message to show if the two quantities aren't equal. + + Raises + ------ + AssertionError + The two quantities are not close to within the supplied tolerance. + """ if msg is None: try: msg = f"Comparing {first!r} and {second!r}. " diff --git a/requirements.txt b/requirements.txt index c62365819..b63f8da99 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ platformdirs>=2.1.0 typing_extensions>=4.0.0 flexcache>=0.3 -flexparser>=0.3 +flexparser>=0.4