python-units 0.2.0__py3-none-any.whl → 0.3.0__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.
api/public.py CHANGED
@@ -53,6 +53,7 @@ from core.quantity import (
53
53
  from core.unit_definitions import BaseUnit, CustomUnitBase, DerivedUnit, SIUnit
54
54
  from models.dimension import Dimension, DimensionSystem
55
55
 
56
+
56
57
  Unit = Quantity
57
58
 
58
59
  __all__ = [
core/deprecations.py ADDED
@@ -0,0 +1,62 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Deprecation helpers for compatibility APIs."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import warnings
7
+ from collections.abc import Callable
8
+ from typing import TypeVar
9
+
10
+
11
+ _T = TypeVar("_T")
12
+
13
+
14
+ def warn_legacy_api(name: str, replacement: str, removal_version: str) -> None:
15
+ """
16
+ Emit a deprecation warning for a legacy public API.
17
+
18
+ Args:
19
+ name: Deprecated API name.
20
+ replacement: Preferred API name or usage pattern.
21
+ removal_version: Planned removal release.
22
+
23
+ Returns:
24
+ None.
25
+
26
+ Raises:
27
+ None.
28
+ """
29
+ warnings.warn(
30
+ "{} is deprecated; use {} instead. It is scheduled for removal in {}.".format(
31
+ name,
32
+ replacement,
33
+ removal_version,
34
+ ),
35
+ DeprecationWarning,
36
+ stacklevel=4,
37
+ )
38
+
39
+
40
+ def deprecated_call(
41
+ name: str,
42
+ replacement: str,
43
+ removal_version: str,
44
+ callback: Callable[[], _T],
45
+ ) -> _T:
46
+ """
47
+ Warn for a deprecated API call and return the callback result.
48
+
49
+ Args:
50
+ name: Deprecated API name.
51
+ replacement: Preferred API name or usage pattern.
52
+ removal_version: Planned removal release.
53
+ callback: Zero-argument callable that performs the actual work.
54
+
55
+ Returns:
56
+ The callback result.
57
+
58
+ Raises:
59
+ Any exception raised by ``callback``.
60
+ """
61
+ warn_legacy_api(name, replacement, removal_version)
62
+ return callback()
core/quantity.py CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  from __future__ import annotations
5
5
 
6
+ from core.deprecations import deprecated_call
6
7
  from core.errors import InvalidValueError, UnitCompatibilityError, UnitOperandError
7
8
  from core.unit_definitions import BaseUnit, SIUnit, clone_unit
8
9
  from models.dimension import SI_DIMENSION_SYSTEM
@@ -200,8 +201,12 @@ def float_quantity(quantity: Quantity) -> Quantity:
200
201
 
201
202
  def long_quantity(quantity: Quantity) -> Quantity:
202
203
  """Legacy compatibility helper equivalent to ``int_quantity``."""
203
- require_quantity_operand(quantity, "long conversion")
204
- return Quantity(int(quantity.value), quantity.unit)
204
+ return deprecated_call(
205
+ "long_quantity",
206
+ "int_quantity",
207
+ "1.0.0",
208
+ lambda: int_quantity(quantity),
209
+ )
205
210
 
206
211
 
207
212
  def complex_quantity(quantity: Quantity) -> Quantity:
@@ -210,7 +215,41 @@ def complex_quantity(quantity: Quantity) -> Quantity:
210
215
  return Quantity(complex(quantity.value), quantity.unit)
211
216
 
212
217
 
213
- int_unit = int_quantity
214
- float_unit = float_quantity
215
- long_unit = long_quantity
216
- complex_unit = complex_quantity
218
+ def int_unit(quantity: Quantity) -> Quantity:
219
+ """Legacy compatibility helper equivalent to ``int_quantity``."""
220
+ return deprecated_call(
221
+ "int_unit",
222
+ "int_quantity",
223
+ "1.0.0",
224
+ lambda: int_quantity(quantity),
225
+ )
226
+
227
+
228
+ def float_unit(quantity: Quantity) -> Quantity:
229
+ """Legacy compatibility helper equivalent to ``float_quantity``."""
230
+ return deprecated_call(
231
+ "float_unit",
232
+ "float_quantity",
233
+ "1.0.0",
234
+ lambda: float_quantity(quantity),
235
+ )
236
+
237
+
238
+ def long_unit(quantity: Quantity) -> Quantity:
239
+ """Legacy compatibility helper equivalent to ``int_quantity``."""
240
+ return deprecated_call(
241
+ "long_unit",
242
+ "int_quantity",
243
+ "1.0.0",
244
+ lambda: int_quantity(quantity),
245
+ )
246
+
247
+
248
+ def complex_unit(quantity: Quantity) -> Quantity:
249
+ """Legacy compatibility helper equivalent to ``complex_quantity``."""
250
+ return deprecated_call(
251
+ "complex_unit",
252
+ "complex_quantity",
253
+ "1.0.0",
254
+ lambda: complex_quantity(quantity),
255
+ )
core/unit_definitions.py CHANGED
@@ -4,7 +4,8 @@
4
4
  from __future__ import annotations
5
5
 
6
6
  from numbers import Number
7
- from typing import Dict
7
+ from types import MappingProxyType
8
+ from typing import Dict, Mapping
8
9
 
9
10
  from core.errors import (
10
11
  InvalidUnitError,
@@ -14,12 +15,17 @@ from core.errors import (
14
15
  )
15
16
  from models.dimension import Dimension, DimensionSystem, SI_DIMENSION_SYSTEM
16
17
 
17
- _CANONICAL_UNITS: dict[Dimension, "BaseUnit"] = {}
18
+ _CANONICAL_UNITS: Mapping[Dimension, "BaseUnit"] = MappingProxyType({})
19
+ _REGISTERED_CANONICAL_UNITS: tuple["BaseUnit", ...] = ()
18
20
 
19
21
 
20
22
  def register_canonical_unit(unit: "BaseUnit") -> None:
21
- """Register a preferred unit for a canonical dimension."""
22
- _CANONICAL_UNITS[unit.dimension] = unit
23
+ """Validate a preferred unit against the static canonical SI registry."""
24
+ require_unit_instance(unit)
25
+ if not any(registered_unit == unit for registered_unit in _REGISTERED_CANONICAL_UNITS):
26
+ raise InvalidUnitError(
27
+ "canonical units are statically defined and cannot be registered"
28
+ )
23
29
 
24
30
 
25
31
  def require_unit_instance(unit: object) -> None:
@@ -80,7 +86,17 @@ class BaseUnit:
80
86
  self._dimension = dimension
81
87
 
82
88
  def __eq__(self, unit2: object) -> bool:
83
- return isinstance(unit2, BaseUnit) and self.dimension == unit2.dimension
89
+ if not isinstance(unit2, BaseUnit):
90
+ return False
91
+ if self.dimension != unit2.dimension:
92
+ return False
93
+ if isinstance(self, DerivedUnit) or isinstance(unit2, DerivedUnit):
94
+ return (
95
+ isinstance(self, DerivedUnit)
96
+ and isinstance(unit2, DerivedUnit)
97
+ and self.name == unit2.name
98
+ )
99
+ return True
84
100
 
85
101
  def _combine(self, unit2: "BaseUnit", operator_name: str) -> "BaseUnit":
86
102
  require_unit_instance(unit2)
@@ -213,3 +229,79 @@ class DerivedUnit(BaseUnit):
213
229
  if self.name:
214
230
  return self.name
215
231
  return self.full_units
232
+
233
+
234
+ def _build_canonical_units() -> tuple[Mapping[Dimension, BaseUnit], tuple[BaseUnit, ...]]:
235
+ ampere = SIUnit.define("A")
236
+ candela = SIUnit.define("cd")
237
+ kelvin = SIUnit.define("K")
238
+ kilogram = SIUnit.define("kg")
239
+ metre = SIUnit.define("m")
240
+ mole = SIUnit.define("mol")
241
+ second = SIUnit.define("s")
242
+
243
+ newton = DerivedUnit.define("N", kilogram * metre / second / second)
244
+ pascal = DerivedUnit.define("Pa", newton / metre / metre)
245
+ joule = DerivedUnit.define("J", newton * metre)
246
+ watt = DerivedUnit.define("W", joule / second)
247
+ coulomb = DerivedUnit.define("C", second * ampere)
248
+ volt = DerivedUnit.define("V", watt / ampere)
249
+ farad = DerivedUnit.define("F", coulomb / volt)
250
+ ohm = DerivedUnit.define("Ω", volt / ampere)
251
+ siemens = DerivedUnit.define("S", ampere / volt)
252
+ weber = DerivedUnit.define("Wb", volt * second)
253
+ tesla = DerivedUnit.define("T", weber / metre / metre)
254
+ henry = DerivedUnit.define("H", weber / ampere)
255
+ steradian = DerivedUnit.define("sr", metre * metre / metre / metre)
256
+ lumen = DerivedUnit.define("lm", candela * steradian)
257
+ lux = DerivedUnit.define("lx", lumen / metre / metre)
258
+
259
+ registered_units = (
260
+ ampere,
261
+ candela,
262
+ kelvin,
263
+ kilogram,
264
+ metre,
265
+ mole,
266
+ second,
267
+ newton,
268
+ pascal,
269
+ joule,
270
+ watt,
271
+ coulomb,
272
+ volt,
273
+ farad,
274
+ ohm,
275
+ siemens,
276
+ weber,
277
+ tesla,
278
+ henry,
279
+ lumen,
280
+ lux,
281
+ )
282
+ preferred_units = (
283
+ ampere,
284
+ kelvin,
285
+ kilogram,
286
+ metre,
287
+ mole,
288
+ second,
289
+ newton,
290
+ pascal,
291
+ joule,
292
+ watt,
293
+ coulomb,
294
+ volt,
295
+ farad,
296
+ ohm,
297
+ siemens,
298
+ weber,
299
+ tesla,
300
+ henry,
301
+ lumen,
302
+ lux,
303
+ )
304
+ return MappingProxyType({unit.dimension: unit for unit in preferred_units}), registered_units
305
+
306
+
307
+ _CANONICAL_UNITS, _REGISTERED_CANONICAL_UNITS = _build_canonical_units()
models/dimension.py CHANGED
@@ -31,6 +31,13 @@ class Dimension:
31
31
  raise ValueError(
32
32
  "dimension must define {} exponents".format(len(self.system.symbols))
33
33
  )
34
+ for exponent in self.exponents:
35
+ if not isinstance(exponent, int) or isinstance(exponent, bool):
36
+ raise ValueError(
37
+ "dimension exponents must be integers, got {}".format(
38
+ type(exponent).__name__
39
+ )
40
+ )
34
41
 
35
42
  @classmethod
36
43
  def from_mapping(
@@ -40,11 +47,30 @@ class Dimension:
40
47
  ) -> "Dimension":
41
48
  """Construct a dimension from a base-symbol mapping."""
42
49
  dimension_system = system or cls.default_system
50
+ unknown_symbols = set(mapping) - set(dimension_system.symbols)
51
+ if unknown_symbols:
52
+ raise ValueError(
53
+ "unknown dimension symbols: {}".format(", ".join(sorted(unknown_symbols)))
54
+ )
43
55
  return cls(
44
56
  system=dimension_system,
45
- exponents=tuple(int(mapping.get(symbol, 0)) for symbol in dimension_system.symbols),
57
+ exponents=tuple(
58
+ cls._validate_exponent(mapping.get(symbol, 0))
59
+ for symbol in dimension_system.symbols
60
+ ),
46
61
  )
47
62
 
63
+ @staticmethod
64
+ def _validate_exponent(exponent: object) -> int:
65
+ """Return a valid exponent or raise for invalid exponent input."""
66
+ if not isinstance(exponent, int) or isinstance(exponent, bool):
67
+ raise ValueError(
68
+ "dimension exponents must be integers, got {}".format(
69
+ type(exponent).__name__
70
+ )
71
+ )
72
+ return exponent
73
+
48
74
  def to_mapping(self) -> dict[str, int]:
49
75
  """Return a base-symbol mapping for compatibility with public APIs."""
50
76
  return dict(zip(self.system.symbols, self.exponents))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-units
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Python library to represent numbers with units
5
5
  Author-email: "Paul K. Korir, PhD" <paul.korir@gmail.com>
6
6
  License-Expression: GPL-3.0-or-later
@@ -92,8 +92,10 @@ print(u.Unit(1, u.metre))
92
92
  ```
93
93
 
94
94
  The legacy `Unit` constructor remains available as a compatibility alias for
95
- `Quantity` during the migration period, but new code should prefer
96
- `from units import Quantity` and `from units.si import ...`.
95
+ `Quantity` during the migration period. It is deprecated and scheduled for
96
+ removal in `1.0.0`, but it remains a true alias until then so existing type
97
+ checks keep working. New code should prefer `from units import Quantity` and
98
+ `from units.si import ...`.
97
99
 
98
100
  The package is Python 3-only. Python 2 compatibility behavior is not part of the
99
101
  supported interface.
@@ -146,11 +148,19 @@ Canonical unit imports:
146
148
 
147
149
  Legacy compatibility helpers:
148
150
 
151
+ * `Unit`
149
152
  * `long_quantity`
153
+ * `int_unit`
154
+ * `float_unit`
150
155
  * `long_unit`
151
-
152
- These names remain available as compatibility aliases for integer conversion in
153
- Python 3, but new code should prefer `int_quantity`.
156
+ * `complex_unit`
157
+
158
+ These names remain available during the migration period and emit
159
+ `DeprecationWarning` when called. `Unit` remains a true alias for `Quantity` and
160
+ does not emit a call-time warning, because preserving `Unit is Quantity` is part
161
+ of the pre-`1.0.0` compatibility contract. New code should prefer `Quantity`,
162
+ scalar-by-unit construction, and the `*_quantity` conversion helpers. The
163
+ deprecated compatibility paths are scheduled for removal in `1.0.0`.
154
164
 
155
165
  # Notes on semantics
156
166
 
@@ -1,14 +1,15 @@
1
1
  adapters/__init__.py,sha256=CSemt7d9mX9of0YQijrkQJ9FXwzXqN0TcnCeWGhXXC4,64
2
2
  api/__init__.py,sha256=EVoYM25GfHSWZnMigUDRsAnnyMnMUoeVZnlxlgQRfUI,72
3
- api/public.py,sha256=fFQgVFfBlrvavn2XXdh4z1Co1lGgj46JiEvqV4b-_LY,1758
3
+ api/public.py,sha256=9iHQ3hy6S2NBAQYF9_vqLUrXlaVhSN_ibFVEZKmXjls,1759
4
4
  api/si.py,sha256=Qx-WMSia7wxd6hnuhadTtpB8jyeVH7eK-4s98rMnlyM,2242
5
5
  core/__init__.py,sha256=OjXKNa-EMmUZbVVyG4rLhoKfDl6ifZihCSmv27_XIMQ,811
6
+ core/deprecations.py,sha256=SQf13eA5dJt78CvIKuXi3vI1bBknwEPHT3FkDWh0Xmc,1436
6
7
  core/errors.py,sha256=iwA73z53DPMPmdoGg-T_PYpyfETqbxwGpxcDYOrsxLY,609
7
- core/quantity.py,sha256=5A8o3qDiWp2Hq8z5hRTVjS6j9woCSdu5XfzgXY_LIdM,9054
8
- core/unit_definitions.py,sha256=a0-2Ntz9RSLfww7GmCuR0D4fCl5adf4lNFwL48WnpfA,7611
8
+ core/quantity.py,sha256=KbZowTitSwjudPDgx3cw-nnu4hnnLECxDG6T7vTjE9s,10058
9
+ core/unit_definitions.py,sha256=c3vbjM5-bm-wRNG6z9qJY3b54W2U5JsQM-Vpt2QnaW4,10312
9
10
  models/__init__.py,sha256=8-39jReNyDmbbih6K5IHeqwoWfagjXzjy7nIfJBpPMI,189
10
- models/dimension.py,sha256=iGIZCT5_AZySAhcH8RYHh3tYUmdmseJ70KlbbsctzZY,2757
11
- python_units-0.2.0.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ models/dimension.py,sha256=UNlb234AaUim7iXWzc_2RZ5aZCUefZTvOGTNizoQiPc,3789
12
+ python_units-0.3.0.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
13
  services/__init__.py,sha256=tSot9S3F7fHezy3Lj4DNRMQK1ATptRaN0efKRJSgrKs,65
13
14
  units/__init__.py,sha256=Tyj9-XgWDco8J7s_wx2jaEP3fylc2Tc6VZIbsZ1ron4,251
14
15
  units/dimension.py,sha256=v-aiG3bDV7tJUV_xuxZSAw8UHQe0GnbybI2UkiEwuQM,221
@@ -18,7 +19,7 @@ units/si.py,sha256=2wqvff8wxQP1w-fWPYolWuIeBGl942jyX8N6dQyGwDU,118
18
19
  units/unit.py,sha256=5g_azobEX4LPiO7W3n5RmpYCmZuVpZDX77Ipy9ydILk,450
19
20
  utils/__init__.py,sha256=zTkrwGlYAf6IRcS8e_9nRyACmwJO5Ni-Lwoy55K3qCw,191
20
21
  utils/numbers.py,sha256=_wzMOCoU2hOybqVcT-x__7eY6WkwHa97Ahhr9L5reQE,884
21
- python_units-0.2.0.dist-info/METADATA,sha256=u0daWTTz-YbT8Ox1KB6iUpTn3HjndViAV-2hx47K-r4,6879
22
- python_units-0.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
23
- python_units-0.2.0.dist-info/top_level.txt,sha256=xVEgUUcetmpTHsYk3A-xD9qAxU5S2yD0n8YW1r08tn0,46
24
- python_units-0.2.0.dist-info/RECORD,,
22
+ python_units-0.3.0.dist-info/METADATA,sha256=2brF79U2LZZXfM_7o-YhOdH7i8RQmqNA4LGMHpBHEJM,7377
23
+ python_units-0.3.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
24
+ python_units-0.3.0.dist-info/top_level.txt,sha256=xVEgUUcetmpTHsYk3A-xD9qAxU5S2yD0n8YW1r08tn0,46
25
+ python_units-0.3.0.dist-info/RECORD,,