Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add docs for testing module #2070

Merged
merged 11 commits into from
Dec 3, 2024
Merged
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
24 changes: 21 additions & 3 deletions CHANGES
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
Expand Down
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"sphinx_design",
]


# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]

Expand Down
2 changes: 1 addition & 1 deletion docs/ecosystem.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Pint integrations:


Packages using pint:
------------------
--------------------

- `fluids <https://github.com/CalebBell/fluids>`_ Practical fluid dynamics calculations
- `ht <https://github.com/CalebBell/ht/>`_ Practical heat transfer calculations
Expand Down
9 changes: 6 additions & 3 deletions pint/delegates/txt_defparser/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,14 @@

from __future__ import annotations

from dataclasses import dataclass, field
from dataclasses import dataclass

import flexparser as fp

from ... import errors
from ..base_defparser import ParserConfig


@dataclass(frozen=True)
class DefinitionSyntaxError(errors.DefinitionSyntaxError, fp.ParsingError):
"""A syntax error was found in a definition. Combines:

Expand All @@ -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 = (
Expand Down
98 changes: 65 additions & 33 deletions pint/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -81,82 +80,87 @@ 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."""

name: str
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."""

Expand All @@ -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})"
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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,)
56 changes: 56 additions & 0 deletions pint/testing.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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}. "

Expand All @@ -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}. "
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
platformdirs>=2.1.0
typing_extensions>=4.0.0
flexcache>=0.3
flexparser>=0.3
flexparser>=0.4
Loading