xpint 0.1.0b1__py3-none-any.whl

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.
xpint/__init__.py ADDED
@@ -0,0 +1,32 @@
1
+ """xpint — extended semantic quantities for Pint."""
2
+
3
+ from importlib.metadata import version
4
+
5
+ from .registry import (
6
+ AffineRole,
7
+ DesignationSignature,
8
+ DesignationTerm,
9
+ KindDefinition,
10
+ SemanticQuantity,
11
+ SemanticRegistry,
12
+ SemanticUnit,
13
+ get_application_registry,
14
+ get_default_registry,
15
+ set_application_registry,
16
+ )
17
+
18
+ __version__ = version("xpint")
19
+
20
+ __all__ = [
21
+ "AffineRole",
22
+ "DesignationSignature",
23
+ "DesignationTerm",
24
+ "KindDefinition",
25
+ "SemanticQuantity",
26
+ "SemanticRegistry",
27
+ "SemanticUnit",
28
+ "__version__",
29
+ "get_application_registry",
30
+ "get_default_registry",
31
+ "set_application_registry",
32
+ ]
xpint/_adapters.py ADDED
@@ -0,0 +1,155 @@
1
+ """Guard Pint's numerical adapters before they strip semantic objects."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from functools import wraps
7
+ from inspect import Parameter, signature
8
+ from typing import Any
9
+
10
+ from pint import registry_helpers
11
+ from pint.compat import np # type: ignore[attr-defined]
12
+ from pint.util import UnitsContainer, pi_theorem
13
+
14
+
15
+ def _require_plain(registry: Any, value: Any, operation: str) -> None:
16
+ registry._require_owned(value, name=f"{operation} input")
17
+ if isinstance(value, (registry.Quantity, registry.Unit)):
18
+ if registry.Quantity._operands_have_semantics(value):
19
+ raise TypeError(
20
+ f"{operation} cannot represent classified quantity metadata; "
21
+ "explicitly remove it or operate on magnitude first"
22
+ )
23
+ elif isinstance(value, dict):
24
+ for item in value.values():
25
+ _require_plain(registry, item, operation)
26
+ elif isinstance(value, (tuple, list)):
27
+ for item in value:
28
+ _require_plain(registry, item, operation)
29
+ elif np is not None and isinstance(value, np.ndarray) and value.dtype == np.dtype(object):
30
+ for item in value.flat:
31
+ _require_plain(registry, item, operation)
32
+
33
+
34
+ def _require_plain_unit_spec(registry: Any, value: Any, operation: str) -> None:
35
+ if value is None:
36
+ return
37
+ if isinstance(value, str) and "=" not in value and "[" not in value:
38
+ value = registry.Unit(value)
39
+ _require_plain(registry, value, operation)
40
+
41
+
42
+ def _require_plain_wraps_input(registry: Any, value: Any, strict: bool) -> None:
43
+ if strict and isinstance(value, str):
44
+ value = registry.parse_expression(value)
45
+ registry._require_owned(value, name="wraps input")
46
+ if isinstance(value, (registry.Quantity, registry.Unit)):
47
+ _require_plain(registry, value, "wraps")
48
+
49
+
50
+ def _wraps_preserves_variadic_passthrough(registry: Any) -> bool:
51
+ """Check Pint's binding behavior without calling the user's function."""
52
+ probe = registry.Quantity(1, "")
53
+
54
+ def preserves_probe(*values: Any, scale: Any) -> bool:
55
+ return values[1] is probe
56
+
57
+ wrapped = registry_helpers.wraps(registry, None, (None, ""))(preserves_probe)
58
+ return bool(wrapped(None, probe, scale=probe))
59
+
60
+
61
+ def _wraps(
62
+ registry: Any, ret: Any, args: Any, strict: bool
63
+ ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
64
+ argument_specs = args if isinstance(args, (list, tuple)) else (args,)
65
+ return_specs = ret if isinstance(ret, (list, tuple)) else (ret,)
66
+ for spec in argument_specs:
67
+ _require_plain_unit_spec(registry, spec, "wraps")
68
+ for spec in return_specs:
69
+ registry._require_owned(spec, name="wraps return unit")
70
+ if isinstance(spec, registry.Unit) and spec.has_designation:
71
+ _require_plain(registry, spec, "wraps")
72
+ pint_decorator = registry_helpers.wraps(registry, ret, args, strict)
73
+
74
+ def decorate(function: Callable[..., Any]) -> Callable[..., Any]:
75
+ wrapped = pint_decorator(function)
76
+ sig = signature(function)
77
+ # Pint validates declaration counts; an omitted **kwargs specification
78
+ # means pass-through in converters that support omitting it.
79
+ declarations = tuple(zip(sig.parameters.values(), argument_specs, strict=False))
80
+ # Every value bound to a declared unit is a conversion candidate: a converter
81
+ # that binds by parameter name strips each *args element and each **kwargs
82
+ # value under that parameter's declaration, not just the first one.
83
+ declared = tuple(
84
+ (parameter.name, parameter.kind) for parameter, spec in declarations if spec is not None
85
+ )
86
+ # Pint 0.25.3 addresses declarations by positional slot instead, so a declared
87
+ # parameter after *args strips the variadic element occupying its index. Guard
88
+ # those slots unless the declaration covers them or Pint binds them correctly.
89
+ misbound_kinds = (Parameter.KEYWORD_ONLY, Parameter.VAR_KEYWORD)
90
+ misbound_slots = tuple(
91
+ index
92
+ for index, (parameter, spec) in enumerate(declarations)
93
+ if spec is not None and parameter.kind in misbound_kinds
94
+ )
95
+ if any(kind is Parameter.VAR_POSITIONAL for _name, kind in declared) or (
96
+ misbound_slots
97
+ and any(
98
+ parameter.kind is Parameter.VAR_POSITIONAL for parameter in sig.parameters.values()
99
+ )
100
+ and _wraps_preserves_variadic_passthrough(registry)
101
+ ):
102
+ misbound_slots = ()
103
+
104
+ @wraps(function)
105
+ def guarded(*values: Any, **kwargs: Any) -> Any:
106
+ bound = sig.bind(*values, **kwargs)
107
+ bound.apply_defaults()
108
+ for name, kind in declared:
109
+ value = bound.arguments[name]
110
+ if kind is Parameter.VAR_POSITIONAL:
111
+ for item in value:
112
+ _require_plain_wraps_input(registry, item, strict)
113
+ elif kind is Parameter.VAR_KEYWORD:
114
+ for item in value.values():
115
+ _require_plain_wraps_input(registry, item, strict)
116
+ else:
117
+ _require_plain_wraps_input(registry, value, strict)
118
+ for index in misbound_slots:
119
+ if index < len(values):
120
+ _require_plain_wraps_input(registry, values[index], strict)
121
+ return wrapped(*values, **kwargs)
122
+
123
+ return guarded
124
+
125
+ return decorate
126
+
127
+
128
+ def _check(
129
+ registry: Any, dimensions: tuple[Any, ...]
130
+ ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
131
+ for dimension in dimensions:
132
+ registry._require_owned(dimension, name="check dimension")
133
+ return registry_helpers.check(registry, *dimensions)
134
+
135
+
136
+ def _pi_theorem(registry: Any, quantities: dict[str, Any]) -> Any:
137
+ for value in quantities.values():
138
+ if isinstance(value, (dict, UnitsContainer)):
139
+ value = registry.Unit(registry.UnitsContainer(value))
140
+ _require_plain_unit_spec(registry, value, "pi_theorem")
141
+ return pi_theorem(quantities, registry)
142
+
143
+
144
+ def _setup_matplotlib(registry: Any, enable: bool) -> None:
145
+ try:
146
+ import matplotlib.units
147
+ from pint.matplotlib import setup_matplotlib_handlers
148
+
149
+ from ._matplotlib import _SemanticConverter
150
+ except ImportError as exc:
151
+ raise ImportError("setup_matplotlib requires matplotlib; install matplotlib first") from exc
152
+ setup_matplotlib_handlers(registry, enable) # type: ignore[no-untyped-call]
153
+ if enable:
154
+ converters = matplotlib.units.registry
155
+ converters[registry.Quantity] = _SemanticConverter(registry) # type: ignore[no-untyped-call]
xpint/_formatter.py ADDED
@@ -0,0 +1,38 @@
1
+ """Guard registry formatter delegates that cannot represent designations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any, override
7
+
8
+ from pint.delegates import Formatter
9
+
10
+
11
+ def _require_undesignated(value: Any) -> None:
12
+ if getattr(value, "has_designation", False):
13
+ raise TypeError(
14
+ "Pint formatter APIs cannot represent designated objects; "
15
+ "use str() or format() on the xpint quantity or unit instead"
16
+ )
17
+
18
+
19
+ @dataclass
20
+ class _GuardedFormatter:
21
+ delegate: Any
22
+
23
+ def format_quantity(self, quantity: Any, *args: Any, **kwargs: Any) -> Any:
24
+ _require_undesignated(quantity)
25
+ return self.delegate.format_quantity(quantity, *args, **kwargs)
26
+
27
+ def format_unit(self, unit: Any, *args: Any, **kwargs: Any) -> Any:
28
+ _require_undesignated(unit)
29
+ return self.delegate.format_unit(unit, *args, **kwargs)
30
+
31
+ def __getattr__(self, name: str) -> Any:
32
+ return getattr(self.delegate, name)
33
+
34
+
35
+ class _SemanticFormatter(Formatter):
36
+ @override
37
+ def get_formatter(self, spec: str) -> Any:
38
+ return _GuardedFormatter(super().get_formatter(spec))
xpint/_matplotlib.py ADDED
@@ -0,0 +1,27 @@
1
+ """Optional Pint plotting adapter that rejects semantic metadata loss."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, override
6
+
7
+ from pint.matplotlib import PintConverter
8
+
9
+ from ._adapters import _require_plain, _require_plain_unit_spec
10
+
11
+
12
+ class _SemanticConverter(PintConverter):
13
+ @override
14
+ def convert(self, value: Any, unit: Any, axis: Any) -> Any: # type: ignore[override]
15
+ _require_plain(self._reg, value, "matplotlib")
16
+ _require_plain_unit_spec(self._reg, unit, "matplotlib")
17
+ return super().convert(value, unit, axis) # type: ignore[no-untyped-call]
18
+
19
+ @override
20
+ def _convert_value(self, value: Any, unit: Any, axis: Any) -> Any:
21
+ _require_plain(self._reg, value, "matplotlib")
22
+ return super()._convert_value(value, unit, axis) # type: ignore[no-untyped-call]
23
+
24
+ @override
25
+ def axisinfo(self, unit: Any, axis: Any) -> Any: # type: ignore[override]
26
+ _require_plain_unit_spec(self._reg, unit, "matplotlib")
27
+ return super().axisinfo(unit, axis) # type: ignore[no-untyped-call]
xpint/_measurement.py ADDED
@@ -0,0 +1,73 @@
1
+ """Keep Pint measurements within the unclassified quantity surface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Self, override
6
+
7
+ from pint.facets.measurement import Measurement
8
+ from pint.facets.measurement.objects import MISSING
9
+ from pint.util import UnitsContainer
10
+
11
+ from ._adapters import _require_plain
12
+
13
+
14
+ def _require_measurement_units(registry: Any, units: Any) -> None:
15
+ if isinstance(units, (str, UnitsContainer)):
16
+ units = registry.Unit(units)
17
+ _require_plain(registry, units, "Measurement")
18
+
19
+
20
+ class _SemanticMeasurement(Measurement):
21
+ """Reject semantic inputs before Pint extracts magnitudes and physical units."""
22
+
23
+ __hash__ = Measurement.__hash__
24
+
25
+ def __new__(cls, value: Any, error: Any = MISSING, units: Any = MISSING) -> Self:
26
+ registry = cls._REGISTRY
27
+ _require_plain(registry, value, "Measurement")
28
+ _require_plain(registry, error, "Measurement")
29
+ # Pint accepts Measurement(ufloat, units) as well as (value, error, units).
30
+ if units is MISSING and hasattr(value, "nominal_value"):
31
+ _require_measurement_units(registry, error)
32
+ else:
33
+ _require_measurement_units(registry, units)
34
+ return super().__new__(cls, value, error, units) # type: ignore[no-untyped-call,no-any-return]
35
+
36
+ @override
37
+ def _check(self, other: Any) -> bool:
38
+ _require_plain(self._REGISTRY, other, "Measurement arithmetic")
39
+ return super()._check(other)
40
+
41
+ @override
42
+ def __pow__(self, other: Any) -> Any:
43
+ _require_plain(self._REGISTRY, other, "Measurement exponent")
44
+ return super().__pow__(other)
45
+
46
+ @override
47
+ def __ipow__(self, other: Any) -> Any:
48
+ _require_plain(self._REGISTRY, other, "Measurement exponent")
49
+ return super().__ipow__(other)
50
+
51
+ @override
52
+ def to(self, other: Any = None, *contexts: Any, **ctx_kwargs: Any) -> Any:
53
+ _require_measurement_units(self._REGISTRY, other)
54
+ return super().to(other, *contexts, **ctx_kwargs)
55
+
56
+ @override
57
+ def ito(self, other: Any = None, *contexts: Any, **ctx_kwargs: Any) -> None:
58
+ _require_measurement_units(self._REGISTRY, other)
59
+ super().ito(other, *contexts, **ctx_kwargs)
60
+
61
+ @override
62
+ def compare(self, other: Any, op: Any) -> Any:
63
+ _require_plain(self._REGISTRY, other, "Measurement comparison")
64
+ return super().compare(other, op)
65
+
66
+ @override
67
+ def __eq__(self, other: Any) -> Any:
68
+ registry: Any = self._REGISTRY
69
+ if getattr(other, "_REGISTRY", registry) is not registry:
70
+ return False
71
+ if registry.Quantity._operands_have_semantics(other):
72
+ return False
73
+ return super().__eq__(other)
xpint/_policies.py ADDED
@@ -0,0 +1,160 @@
1
+ """Immutable semantic dispatch tables and built-in registry catalogs."""
2
+
3
+ from types import MappingProxyType
4
+
5
+ _NUMPY_METHOD_PRESERVING = frozenset(
6
+ {
7
+ "astype",
8
+ "compress",
9
+ "conj",
10
+ "conjugate",
11
+ "copy",
12
+ "diagonal",
13
+ "flatten",
14
+ "item",
15
+ "max",
16
+ "mean",
17
+ "min",
18
+ "ravel",
19
+ "repeat",
20
+ "reshape",
21
+ "round",
22
+ "squeeze",
23
+ "swapaxes",
24
+ "take",
25
+ "transpose",
26
+ }
27
+ )
28
+ _NUMPY_METHOD_ADDITIVE = frozenset({"sum", "cumsum"})
29
+ # These methods share argument names with the corresponding NumPy functions.
30
+ _NUMPY_METHOD_REDUCTIONS = frozenset({"sum", "max", "min", "mean", "std", "var", "cumsum"})
31
+ _NUMPY_METHOD_SUPPORTED = (
32
+ _NUMPY_METHOD_PRESERVING | _NUMPY_METHOD_ADDITIVE | frozenset({"std", "var"})
33
+ )
34
+
35
+ _NUMPY_UFUNC_BINARY_METHODS = MappingProxyType(
36
+ {
37
+ "add": ("__add__", "__radd__"),
38
+ "subtract": ("__sub__", "__rsub__"),
39
+ "multiply": ("__mul__", "__rmul__"),
40
+ "divide": ("__truediv__", "__rtruediv__"),
41
+ "true_divide": ("__truediv__", "__rtruediv__"),
42
+ "power": ("__pow__", "__rpow__"),
43
+ "equal": ("__eq__", "__eq__"),
44
+ "not_equal": ("__ne__", "__ne__"),
45
+ "less": ("__lt__", "__gt__"),
46
+ "less_equal": ("__le__", "__ge__"),
47
+ "greater": ("__gt__", "__lt__"),
48
+ "greater_equal": ("__ge__", "__le__"),
49
+ }
50
+ )
51
+ _NUMPY_UFUNC_MAGNITUDE_PREDICATES = frozenset({"isfinite", "isinf", "isnan", "signbit"})
52
+
53
+ # Trigonometry consumes or produces an angle, so an input kind never describes the
54
+ # output. Pint converts the argument to radian and rejects a non-angle; xpint only has
55
+ # to prevent its own metadata from crossing the boundary silently.
56
+ _ANGLE_UFUNCS = frozenset(
57
+ {
58
+ "sin",
59
+ "cos",
60
+ "tan",
61
+ "arcsin",
62
+ "arccos",
63
+ "arctan",
64
+ "arctan2",
65
+ "degrees",
66
+ "radians",
67
+ "deg2rad",
68
+ "rad2deg",
69
+ }
70
+ )
71
+
72
+ _NUMPY_FUNCTION_PRESERVING = frozenset({"mean", "nanmean", "max", "min", "amax", "amin"})
73
+ _NUMPY_FUNCTION_ADDITIVE = frozenset({"sum", "nansum", "cumsum", "nancumsum"})
74
+ _NUMPY_STATISTICS = frozenset({"std", "nanstd", "var", "nanvar"})
75
+ _NUMPY_VARIANCES = frozenset({"var", "nanvar"})
76
+ _NUMPY_MAGNITUDE_INDICES = frozenset({"argmax", "argmin", "argsort", "nonzero"})
77
+ _NUMPY_UFUNC_POWERS = MappingProxyType({"sqrt": 0.5, "square": 2, "cbrt": 1 / 3, "reciprocal": -1})
78
+ _NUMPY_FUNCTION_REDUCTIONS = (
79
+ _NUMPY_FUNCTION_PRESERVING | _NUMPY_FUNCTION_ADDITIVE | _NUMPY_STATISTICS
80
+ )
81
+ _NUMPY_FUNCTION_LIKE_CONSTRUCTORS = frozenset({"zeros_like"})
82
+ _NUMPY_FUNCTION_COMPOSITION = frozenset({"concatenate", "stack", "where"})
83
+
84
+ # Canonical unit per angular dimension, used to tell a caller what a rejected value
85
+ # actually carries and which factor clears it.
86
+ _ANGULAR_UNITS = MappingProxyType({"[angle]": "radian"})
87
+
88
+ # Pint declares [luminous_flux] and [illuminance] independently of the units carrying
89
+ # them, so redefining steradian alone leaves those named dimensions inconsistent.
90
+ _ANGLE_DEFINITIONS = (
91
+ "radian = [angle] = rad",
92
+ # A small patch on a sphere is a rectangle whose sides are arcs, so its solid angle
93
+ # is a product of two plane angles with the radius and every constant cancelling.
94
+ # Leonard (Metrologia 58 052001, 2021) and Kalinin (Metrologia 56 065009, 2019) both
95
+ # reach the same place: the steradian is a coherent derived unit, sr = rad**2.
96
+ "steradian = radian ** 2 = sr",
97
+ "[luminous_flux] = [luminosity] * [angle] ** 2",
98
+ "[illuminance] = [luminous_flux] / [length] ** 2",
99
+ )
100
+
101
+ # Conjugate forms. Generalized force is energy per coordinate and conjugate momentum is
102
+ # action per coordinate, so once angle is a coordinate the dimension appears on its own;
103
+ # newton * meter carries no angle and would leave torque indistinguishable from energy.
104
+ _ANGLE_KIND_OVERRIDES = MappingProxyType(
105
+ {
106
+ "Dimensionless": "dimensionless",
107
+ "Torque": "joule / radian",
108
+ "AngularMomentum": "joule * second / radian",
109
+ "MomentOfInertia": "kilogram * meter ** 2 / radian ** 2",
110
+ }
111
+ )
112
+
113
+ _DEFAULT_KINDS = MappingProxyType(
114
+ {
115
+ "Dimensionless": "radian",
116
+ "Energy": "joule",
117
+ "Torque": "newton * meter",
118
+ "PlaneAngle": "radian",
119
+ "SolidAngle": "steradian",
120
+ "Frequency": "hertz",
121
+ "AngularVelocity": "radian / second",
122
+ "Action": "joule * second",
123
+ "AngularMomentum": "kilogram * meter ** 2 / second",
124
+ "Temperature": "kelvin",
125
+ "TemperatureDifference": "kelvin",
126
+ "Length": "meter",
127
+ "Time": "second",
128
+ "Mass": "kilogram",
129
+ "ElectricCurrent": "ampere",
130
+ "AmountOfSubstance": "mole",
131
+ "LuminousIntensity": "candela",
132
+ "Force": "newton",
133
+ "Pressure": "pascal",
134
+ "EnergyDensity": "joule / meter ** 3",
135
+ "Power": "watt",
136
+ "ElectricCharge": "coulomb",
137
+ "ElectricPotential": "volt",
138
+ "Capacitance": "farad",
139
+ "Resistance": "ohm",
140
+ "Conductance": "siemens",
141
+ "MagneticFlux": "weber",
142
+ "MagneticFluxDensity": "tesla",
143
+ "Inductance": "henry",
144
+ "LuminousFlux": "lumen",
145
+ "Illuminance": "lux",
146
+ "Radioactivity": "becquerel",
147
+ "AbsorbedDose": "gray",
148
+ "EquivalentDose": "sievert",
149
+ "CatalyticActivity": "katal",
150
+ "Velocity": "meter / second",
151
+ "Acceleration": "meter / second ** 2",
152
+ "Area": "meter ** 2",
153
+ "Volume": "meter ** 3",
154
+ "Density": "kilogram / meter ** 3",
155
+ "Entropy": "joule / kelvin",
156
+ "HeatCapacity": "joule / kelvin",
157
+ "KinematicViscosity": "meter ** 2 / second",
158
+ "Diffusivity": "meter ** 2 / second",
159
+ }
160
+ )