TypedUnit 0.2.17__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: TypedUnit
3
- Version: 0.2.17
3
+ Version: 0.3.0
4
4
  Summary: A package wrapping pint for handling typed units in Python.
5
5
  Author-email: Martin Poinsinet de Sivry-Houle <martin.poinsinet.de.sivry@gmail.com>
6
6
  License: MIT License
@@ -47,15 +47,17 @@ Requires-Dist: numpy
47
47
  Requires-Dist: matplotlib
48
48
  Requires-Dist: pint_pandas
49
49
  Provides-Extra: release
50
- Requires-Dist: martinpdes-maintenance==0.2.2; extra == "release"
50
+ Requires-Dist: martinpdes-maintenance==0.2.3; extra == "release"
51
51
  Provides-Extra: testing
52
- Requires-Dist: pytest<10.0,>=7.4; extra == "testing"
53
- Requires-Dist: pytest-cov<7,>=2; extra == "testing"
52
+ Requires-Dist: martinpdes-maintenance[testing]==0.2.3; extra == "testing"
54
53
  Requires-Dist: pytest-json-report~=1.5; extra == "testing"
55
- Requires-Dist: flake8==7.3.0; extra == "testing"
54
+ Requires-Dist: pytest-cov<7; extra == "testing"
56
55
  Requires-Dist: coverage~=7.6; extra == "testing"
57
56
  Provides-Extra: documentation
58
57
  Requires-Dist: MartinPdeS-docs<0.5,>=0.4.0; extra == "documentation"
58
+ Provides-Extra: dev
59
+ Requires-Dist: martinpdes-maintenance[quality]==0.2.3; extra == "dev"
60
+ Requires-Dist: flake8==7.3.0; extra == "dev"
59
61
  Dynamic: license-file
60
62
 
61
63
  TypedUnit
@@ -8,3 +8,4 @@ from TypedUnit.helper import validate_units as validate_units
8
8
  from TypedUnit.units import * # noqa: F403 -- Re-export the unit module's public types.
9
9
  from TypedUnit.units import Quantity as Quantity
10
10
  from TypedUnit.units import ureg as ureg
11
+ from TypedUnit.validation import validate_quantity # noqa: F401 -- public API
@@ -1,5 +1,5 @@
1
1
  """Release version maintained by martinpdes-maintenance."""
2
2
 
3
- __version__ = version = "0.2.17"
3
+ __version__ = version = "0.3.0"
4
4
  __version_tuple__ = version_tuple = tuple(int(part) for part in version.split("."))
5
5
  __commit_id__ = commit_id = None
@@ -0,0 +1,73 @@
1
+ """Direct validation of Pint quantities without modifying their values."""
2
+
3
+ # ruff: noqa: I001
4
+ from __future__ import annotations
5
+ import numpy as np
6
+ from pint import Quantity, UnitRegistry
7
+
8
+
9
+ def validate_quantity(
10
+ *,
11
+ value: Quantity,
12
+ unit: str,
13
+ name: str,
14
+ registry: UnitRegistry | None = None,
15
+ shape: tuple[int, ...] | None = None,
16
+ positive: bool = False,
17
+ nonnegative: bool = False,
18
+ ) -> None:
19
+ """Validate a quantity's registry, dimensions, shape, and magnitudes.
20
+
21
+ Parameters
22
+ ----------
23
+ value : pint.Quantity
24
+ Quantity to validate; it is not changed or replaced.
25
+ unit : str
26
+ Unit compatible with the required dimensionality. Magnitudes are
27
+ checked after conversion to this unit.
28
+ name : str
29
+ Parameter name included in error messages.
30
+ registry : pint.UnitRegistry or None, optional
31
+ If supplied, require a quantity created by this exact registry. None
32
+ accepts quantities from any Pint registry. Default is None.
33
+ shape : tuple of int or None, optional
34
+ Required magnitude shape. An empty tuple requires a scalar; None leaves
35
+ the shape unrestricted. Default is None.
36
+ positive : bool, optional
37
+ Require every magnitude to be strictly positive. Default is False.
38
+ nonnegative : bool, optional
39
+ Require every magnitude to be nonnegative. Default is False.
40
+
41
+ Returns
42
+ -------
43
+ None
44
+ The supplied quantity is preserved and no converted value is returned.
45
+
46
+ Raises
47
+ ------
48
+ TypeError
49
+ If value is not a Pint quantity or belongs to a different registry.
50
+ pint.DimensionalityError
51
+ If the supplied units are incompatible with unit.
52
+ ValueError
53
+ If magnitudes are nonfinite or violate shape or sign requirements.
54
+ """
55
+ if not isinstance(value, Quantity):
56
+ raise TypeError(f"{name} must be a Pint quantity.")
57
+
58
+ if registry is not None and not isinstance(value, registry.Quantity):
59
+ raise TypeError(f"{name} must belong to the specified unit registry.")
60
+
61
+ magnitude = np.asarray(value.to(unit).magnitude, dtype=float)
62
+
63
+ if shape is not None and magnitude.shape != shape:
64
+ raise ValueError(f"{name} must have shape {shape}.")
65
+
66
+ if not np.all(np.isfinite(magnitude)):
67
+ raise ValueError(f"{name} must be finite.")
68
+
69
+ if positive and np.any(magnitude <= 0):
70
+ raise ValueError(f"{name} must be positive.")
71
+
72
+ if nonnegative and np.any(magnitude < 0):
73
+ raise ValueError(f"{name} must be nonnegative.")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: TypedUnit
3
- Version: 0.2.17
3
+ Version: 0.3.0
4
4
  Summary: A package wrapping pint for handling typed units in Python.
5
5
  Author-email: Martin Poinsinet de Sivry-Houle <martin.poinsinet.de.sivry@gmail.com>
6
6
  License: MIT License
@@ -47,15 +47,17 @@ Requires-Dist: numpy
47
47
  Requires-Dist: matplotlib
48
48
  Requires-Dist: pint_pandas
49
49
  Provides-Extra: release
50
- Requires-Dist: martinpdes-maintenance==0.2.2; extra == "release"
50
+ Requires-Dist: martinpdes-maintenance==0.2.3; extra == "release"
51
51
  Provides-Extra: testing
52
- Requires-Dist: pytest<10.0,>=7.4; extra == "testing"
53
- Requires-Dist: pytest-cov<7,>=2; extra == "testing"
52
+ Requires-Dist: martinpdes-maintenance[testing]==0.2.3; extra == "testing"
54
53
  Requires-Dist: pytest-json-report~=1.5; extra == "testing"
55
- Requires-Dist: flake8==7.3.0; extra == "testing"
54
+ Requires-Dist: pytest-cov<7; extra == "testing"
56
55
  Requires-Dist: coverage~=7.6; extra == "testing"
57
56
  Provides-Extra: documentation
58
57
  Requires-Dist: MartinPdeS-docs<0.5,>=0.4.0; extra == "documentation"
58
+ Provides-Extra: dev
59
+ Requires-Dist: martinpdes-maintenance[quality]==0.2.3; extra == "dev"
60
+ Requires-Dist: flake8==7.3.0; extra == "dev"
59
61
  Dynamic: license-file
60
62
 
61
63
  TypedUnit
@@ -7,9 +7,11 @@ TypedUnit/directories.py
7
7
  TypedUnit/helper.py
8
8
  TypedUnit/units.py
9
9
  TypedUnit/utils.py
10
+ TypedUnit/validation.py
10
11
  TypedUnit.egg-info/PKG-INFO
11
12
  TypedUnit.egg-info/SOURCES.txt
12
13
  TypedUnit.egg-info/dependency_links.txt
13
14
  TypedUnit.egg-info/requires.txt
14
15
  TypedUnit.egg-info/top_level.txt
15
- tests/test_0.py
16
+ tests/test_0.py
17
+ tests/test_validate_quantity.py
@@ -3,15 +3,18 @@ numpy
3
3
  matplotlib
4
4
  pint_pandas
5
5
 
6
+ [dev]
7
+ martinpdes-maintenance[quality]==0.2.3
8
+ flake8==7.3.0
9
+
6
10
  [documentation]
7
11
  MartinPdeS-docs<0.5,>=0.4.0
8
12
 
9
13
  [release]
10
- martinpdes-maintenance==0.2.2
14
+ martinpdes-maintenance==0.2.3
11
15
 
12
16
  [testing]
13
- pytest<10.0,>=7.4
14
- pytest-cov<7,>=2
17
+ martinpdes-maintenance[testing]==0.2.3
15
18
  pytest-json-report~=1.5
16
- flake8==7.3.0
19
+ pytest-cov<7
17
20
  coverage~=7.6
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "TypedUnit"
7
- version = "0.2.17"
7
+ version = "0.3.0"
8
8
  description = "A package wrapping pint for handling typed units in Python."
9
9
  license = {file = "LICENSE"}
10
10
  readme = "README.rst"
@@ -42,18 +42,13 @@ Documentation = "https://martinpdes.github.io/TypedUnit/"
42
42
  Repository = "https://github.com/MartinPdeS/TypedUnit.git"
43
43
 
44
44
  [project.optional-dependencies]
45
- release = ["martinpdes-maintenance==0.2.2"]
46
- testing = [
47
- "pytest >=7.4,<10.0",
48
- "pytest-cov >=2,<7",
49
- "pytest-json-report ~=1.5",
50
- "flake8 ==7.3.0",
51
- "coverage ~=7.6"
52
- ]
45
+ release = ["martinpdes-maintenance==0.2.3"]
46
+ testing = ["martinpdes-maintenance[testing]==0.2.3", "pytest-json-report ~=1.5", "pytest-cov<7", "coverage ~=7.6"]
53
47
 
54
48
  documentation = [
55
49
  "MartinPdeS-docs>=0.4.0,<0.5",
56
50
  ]
51
+ dev = ["martinpdes-maintenance[quality]==0.2.3", "flake8 ==7.3.0"]
57
52
 
58
53
  [tool.ruff]
59
54
  target-version = "py310"
@@ -0,0 +1,94 @@
1
+ """Direct quantity validation across registries and array shapes."""
2
+
3
+ # ruff: noqa: I001
4
+ import numpy as np
5
+ import pint
6
+ import pytest
7
+ from TypedUnit import Length, validate_quantity, validate_units
8
+
9
+
10
+ def test_valid_values_are_not_replaced_or_mutated() -> None:
11
+ """Compatible alternate units and array shapes preserve the input."""
12
+ registry = pint.UnitRegistry()
13
+ value = np.array([1.0, 2.0]) * registry.mm
14
+ assert (
15
+ validate_quantity(
16
+ value=value,
17
+ unit="m",
18
+ name="lengths",
19
+ registry=registry,
20
+ shape=(2,),
21
+ positive=True,
22
+ )
23
+ is None
24
+ )
25
+ np.testing.assert_array_equal(value.magnitude, [1.0, 2.0])
26
+ assert value.units == registry.mm
27
+ validate_quantity(value=2 * registry.cm, unit="m", name="length", shape=())
28
+
29
+
30
+ def test_registry_is_optional_but_explicit_registry_is_enforced() -> None:
31
+ """Foreign registries are accepted by default and rejected when specified."""
32
+ first = pint.UnitRegistry()
33
+ second = pint.UnitRegistry()
34
+ validate_quantity(value=1 * first.mm, unit="m", name="length")
35
+ with pytest.raises(TypeError, match="specified unit registry"):
36
+ validate_quantity(
37
+ value=1 * first.mm,
38
+ unit="m",
39
+ name="length",
40
+ registry=second,
41
+ )
42
+ with pytest.raises(TypeError, match="length"):
43
+ validate_quantity(value=1, unit="m", name="length")
44
+
45
+
46
+ def test_units_and_shape_are_checked() -> None:
47
+ """Wrong dimensions propagate Pint errors and scalar shape is explicit."""
48
+ registry = pint.UnitRegistry()
49
+ with pytest.raises(pint.DimensionalityError):
50
+ validate_quantity(value=1 * registry.second, unit="m", name="length")
51
+ with pytest.raises(ValueError, match="shape"):
52
+ validate_quantity(
53
+ value=np.array([1]) * registry.mm,
54
+ unit="m",
55
+ name="length",
56
+ shape=(),
57
+ )
58
+
59
+
60
+ @pytest.mark.parametrize("value", [np.nan, np.inf, -np.inf])
61
+ def test_nonfinite_values_rejected(value: float) -> None:
62
+ """Nonfinite values are rejected independently of sign requirements."""
63
+ registry = pint.UnitRegistry()
64
+ with pytest.raises(ValueError, match="finite"):
65
+ validate_quantity(value=value * registry.mm, unit="m", name="length")
66
+
67
+
68
+ def test_sign_requirements_apply_to_every_element() -> None:
69
+ """Strict positivity rejects zero while nonnegativity permits it."""
70
+ registry = pint.UnitRegistry()
71
+ zero = np.array([0, 1]) * registry.mm
72
+ validate_quantity(value=zero, unit="m", name="length", nonnegative=True)
73
+ with pytest.raises(ValueError, match="positive"):
74
+ validate_quantity(value=zero, unit="m", name="length", positive=True)
75
+ with pytest.raises(ValueError, match="nonnegative"):
76
+ validate_quantity(
77
+ value=np.array([1, -1]) * registry.mm,
78
+ unit="m",
79
+ name="length",
80
+ nonnegative=True,
81
+ )
82
+
83
+
84
+ def test_existing_validate_units_decorator_remains_available() -> None:
85
+ """The separate direct validator does not replace the decorator API."""
86
+ registry = pint.UnitRegistry()
87
+
88
+ @validate_units
89
+ def identity(length: Length) -> Length:
90
+ return length
91
+
92
+ assert identity(2 * registry.mm) == 2 * registry.mm
93
+ with pytest.raises(ValueError):
94
+ identity(2 * registry.second)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes