unitx 0.7.0__tar.gz → 0.7.1__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.
Files changed (48) hide show
  1. {unitx-0.7.0 → unitx-0.7.1}/CHANGELOG.md +24 -0
  2. {unitx-0.7.0 → unitx-0.7.1}/PKG-INFO +35 -14
  3. {unitx-0.7.0 → unitx-0.7.1}/README.md +34 -13
  4. {unitx-0.7.0 → unitx-0.7.1}/pyproject.toml +1 -1
  5. unitx-0.7.1/src/unitx/_constnamespace.py +33 -0
  6. {unitx-0.7.0 → unitx-0.7.1}/src/unitx/si/bases.py +9 -7
  7. {unitx-0.7.0 → unitx-0.7.1}/src/unitx/si/constants.py +14 -22
  8. {unitx-0.7.0 → unitx-0.7.1}/src/unitx/si/units.py +125 -70
  9. {unitx-0.7.0 → unitx-0.7.1}/src/unitx/unit.py +4 -3
  10. {unitx-0.7.0 → unitx-0.7.1}/src/unitx/unitregistry.py +4 -4
  11. {unitx-0.7.0 → unitx-0.7.1}/tests/test_si.py +62 -1
  12. {unitx-0.7.0 → unitx-0.7.1}/tests/test_singleunit.py +6 -2
  13. {unitx-0.7.0 → unitx-0.7.1}/tests/test_unitregistry.py +28 -4
  14. {unitx-0.7.0 → unitx-0.7.1}/tests/typing/check_quantity.py +6 -0
  15. {unitx-0.7.0 → unitx-0.7.1}/tests/typing/check_si.py +9 -1
  16. unitx-0.7.0/src/unitx/_constclass.py +0 -34
  17. {unitx-0.7.0 → unitx-0.7.1}/.github/workflows/ci.yml +0 -0
  18. {unitx-0.7.0 → unitx-0.7.1}/.github/workflows/release.yml +0 -0
  19. {unitx-0.7.0 → unitx-0.7.1}/.gitignore +0 -0
  20. {unitx-0.7.0 → unitx-0.7.1}/LICENSE +0 -0
  21. {unitx-0.7.0 → unitx-0.7.1}/conftest.py +0 -0
  22. {unitx-0.7.0 → unitx-0.7.1}/pyrightconfig.json +0 -0
  23. {unitx-0.7.0 → unitx-0.7.1}/src/unitx/__init__.py +0 -0
  24. {unitx-0.7.0 → unitx-0.7.1}/src/unitx/_numerics.py +0 -0
  25. {unitx-0.7.0 → unitx-0.7.1}/src/unitx/_unicode.py +0 -0
  26. {unitx-0.7.0 → unitx-0.7.1}/src/unitx/dimension.py +0 -0
  27. {unitx-0.7.0 → unitx-0.7.1}/src/unitx/exceptions.py +0 -0
  28. {unitx-0.7.0 → unitx-0.7.1}/src/unitx/factor.py +0 -0
  29. {unitx-0.7.0 → unitx-0.7.1}/src/unitx/monomial.py +0 -0
  30. {unitx-0.7.0 → unitx-0.7.1}/src/unitx/py.typed +0 -0
  31. {unitx-0.7.0 → unitx-0.7.1}/src/unitx/quantity.py +0 -0
  32. {unitx-0.7.0 → unitx-0.7.1}/src/unitx/si/__init__.py +0 -0
  33. {unitx-0.7.0 → unitx-0.7.1}/src/unitx/singleunit.py +0 -0
  34. {unitx-0.7.0 → unitx-0.7.1}/src/unitx/unitparser.py +0 -0
  35. {unitx-0.7.0 → unitx-0.7.1}/tests/__init__.py +0 -0
  36. {unitx-0.7.0 → unitx-0.7.1}/tests/test_builtin_definitions.py +0 -0
  37. {unitx-0.7.0 → unitx-0.7.1}/tests/test_dimension.py +0 -0
  38. {unitx-0.7.0 → unitx-0.7.1}/tests/test_factor.py +0 -0
  39. {unitx-0.7.0 → unitx-0.7.1}/tests/test_import.py +0 -0
  40. {unitx-0.7.0 → unitx-0.7.1}/tests/test_monomial.py +0 -0
  41. {unitx-0.7.0 → unitx-0.7.1}/tests/test_numeric.py +0 -0
  42. {unitx-0.7.0 → unitx-0.7.1}/tests/test_quantity.py +0 -0
  43. {unitx-0.7.0 → unitx-0.7.1}/tests/test_typing.py +0 -0
  44. {unitx-0.7.0 → unitx-0.7.1}/tests/test_unicode.py +0 -0
  45. {unitx-0.7.0 → unitx-0.7.1}/tests/test_unit.py +0 -0
  46. {unitx-0.7.0 → unitx-0.7.1}/tests/test_unit_definition.py +0 -0
  47. {unitx-0.7.0 → unitx-0.7.1}/tests/test_unitparser.py +0 -0
  48. {unitx-0.7.0 → unitx-0.7.1}/tests/test_warnings.py +0 -0
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.7.1] - 2026-10-19
4
+
5
+ ### Added
6
+
7
+ - Accept a single exact spelling string for `prefixable`, equivalent to a
8
+ one-element tuple, with the same spelling validation before registration.
9
+ - Expand the `si` namespace's prefixed shortcuts and common units, including
10
+ percent, permille, permyriad, rotational frequency (`rpm`), speed of light
11
+ (`c`), gram-force, volt-ampere, ampere-hour, mho, and nit. Enable Torr prefixes.
12
+ - Add `SI.kappa` and the wavelength and frequency Wien displacement constants,
13
+ `SI.b` and `SI.b_`.
14
+
15
+ ### Changed
16
+
17
+ - Use fixed Decimal approximations with about 50 significant digits for pi and
18
+ the positive roots in the Wien displacement laws. Derived constants still use
19
+ the Decimal context active at import.
20
+ - Treat `u` and `Da` as spellings of one formal unit, with prefixes enabled only
21
+ on `Da`. Rebuild stored units when migrating from the separate `u` identity;
22
+ the mass conversion scale is unchanged. Use `kDa` rather than `kilodalton`.
23
+ - Keep ASCII micro spellings such as `um` and `us` convenient to type without
24
+ warnings. The explicit product `u m` retains its separate mass-times-length
25
+ meaning.
26
+
3
27
  ## [0.7.0] - 2026-10-19
4
28
 
5
29
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: unitx
3
- Version: 0.7.0
3
+ Version: 0.7.1
4
4
  Summary: A modern, intuitive Python library for units, quantities, and dimensional analysis.
5
5
  Project-URL: Homepage, https://github.com/DaitTHU/unitx
6
6
  Project-URL: Issues, https://github.com/DaitTHU/unitx/issues
@@ -22,11 +22,15 @@ Description-Content-Type: text/markdown
22
22
 
23
23
  # unitx
24
24
 
25
- Write Python formulas with ready-to-use units and physical constants. unitx
25
+ [![PyPI version](https://img.shields.io/pypi/v/unitx)](https://pypi.org/project/unitx/)
26
+ [![Python versions](https://img.shields.io/pypi/pyversions/unitx)](https://pypi.org/project/unitx/)
27
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
28
+
29
+ Write Python formulas with ready-to-use units and physical constants. `unitx`
26
30
  handles scalar quantities, unit conversion, and dimensional analysis, with
27
31
  exact rational unit scales and exponents.
28
32
 
29
- unitx is in early development. Public APIs may change during the 0.x series.
33
+ `unitx` is in early development. Public APIs may change during the 0.x series.
30
34
 
31
35
  ## Installation
32
36
 
@@ -101,15 +105,17 @@ astronomical units are included. Units and constants can be combined directly:
101
105
 
102
106
  Both namespaces are read-only and do not need to be instantiated. `si` exposes
103
107
  a selection of convenient attributes; other registered prefix combinations
104
- remain available through `Unit`, for example `Unit('Tm')`. Importing unitx
108
+ remain available through `Unit`, for example `Unit('Tm')`. Importing `unitx`
105
109
  initializes the predefined unit catalogue. Defining a custom unit registers its
106
110
  text spellings without adding an attribute to `si`.
107
111
 
108
112
  SI defining constants retain their exact decimal values. Measured constants
109
113
  are stored nominal values; measurement uncertainties are not propagated.
110
114
  Derived Decimal constants are evaluated in the Decimal context active at
111
- import. Formulas involving pi use `Decimal(str(math.pi))` as an approximation;
112
- additional Decimal precision does not add precision to pi.
115
+ import. Pi and the positive roots used by the Wien displacement constants
116
+ (`SI.b` for wavelength and `SI.b_` for frequency) use fixed Decimal
117
+ approximations with about 50 significant digits. Raising the context precision
118
+ does not add accuracy beyond those stored approximations.
113
119
 
114
120
  ## Constructing units and quantities
115
121
 
@@ -267,7 +273,7 @@ meaning rather than acting as string delimiters. Bare compound names are
267
273
  not guessed from adjacent words, and ordinary parentheses still group products.
268
274
 
269
275
  The unit table contains `s`, `m`, `g`, `A`, `K`, `mol`, and `cd`, plus the
270
- dimensionless identity. Importing unitx also initializes the common units in
276
+ dimensionless identity. Importing `unitx` also initializes the common units in
271
277
  `unitx.si`, including named derived units such as `N`. Mass scales are referenced
272
278
  to kilograms: `g` has factor `1/1000`, and `kg` has factor `1`.
273
279
 
@@ -337,8 +343,9 @@ Bare multiword names are recognized only as complete inputs. For example,
337
343
  as a factor; use `[astronomical unit] / arcsec`, symbols, or Unit objects instead.
338
344
 
339
345
  SI prefixes are enabled by default. `prefixable: PrefixPolicy = True` accepts
340
- `True` for all spellings, `False` for none, or a tuple of exact symbols, names,
341
- and aliases supplied in that definition. With a tuple, only the listed
346
+ `True` for all spellings, `False` for none, a single spelling string, or a tuple
347
+ of exact symbols, names, and aliases supplied in that definition. A string such
348
+ as `prefixable='Da'` is equivalent to `prefixable=('Da',)`. Only the selected
342
349
  spellings accept prefixes; an empty tuple disables prefixes. Unknown spellings
343
350
  and non-string entries are errors. For example,
344
351
  `prefixable=('arcsec', 'arcsecond')` enables all SI prefixes on those two
@@ -347,7 +354,7 @@ spellings, while other spellings of that unit remain unprefixable.
347
354
  The tuple selects **unit spellings**, not prefix symbols. Prefix names attach
348
355
  directly to the first word of a multiword name, as in `kiloastronomical unit`;
349
356
  a separate `kilo astronomical unit` is not accepted. `PrefixPolicy` is available
350
- from `unitx.unitregistry` for annotations and means `bool | tuple[str, ...]`.
357
+ from `unitx.unitregistry` for annotations and means `bool | str | tuple[str, ...]`.
351
358
 
352
359
  ```pycon
353
360
  >>> tick = si.s.define(
@@ -390,10 +397,24 @@ form; `760 * si.mmHg` converts to `si.atm` with value 1 under the library's
390
397
  existing pressure convention. Prefixes do not stack: `mmmHg` and `kmmHg` are
391
398
  invalid. `kt` retains its meaning as a knot alias; tonne remains unprefixable.
392
399
 
393
- The newly registered `u` denotes the unified atomic mass unit. ASCII micro
394
- spellings such as `um` still mean micrometres, but now issue `UnitSymbolWarning`
395
- because `u m` has a separate meaning. Use `µm`, `μm`, or `micrometre` to avoid
396
- this warning.
400
+ `si.mas` and `si.uas` provide milli- and microarcseconds as namespace shortcuts;
401
+ their registered expressions are `marcsec` and `µarcsec`. The strings `mas`
402
+ and `uas` are not registered aliases. `si.Da` and `si.u` now share one formal
403
+ unit identity; only the spelling `Da` accepts prefixes, such as `kDa`.
404
+ `si.mTorr` is available, and Torr accepts SI prefixes. Tonne uses `t` or
405
+ `tonne`; the ambiguous spelling `ton` is not registered.
406
+
407
+ `si.rpm` represents rotational frequency: `60 * si.rpm` converts to `si.Hz`
408
+ with value 1. For angular velocity, multiply that frequency by `2 * PI` and
409
+ convert to `si.rad / si.s`; directly converting frequency to `rad/s` does
410
+ not insert the physical factor of 2π. `si.c` is a speed unit equal to the
411
+ speed of light, while `SI.c` is the corresponding physical quantity.
412
+
413
+ The spelling `u` denotes the unified atomic mass unit when used on its own.
414
+ ASCII micro spellings such as `um` and `us` remain supported without warnings,
415
+ so entering non-ASCII characters is optional. `um`, `µm`, `μm`, and
416
+ `micrometre` all mean micrometres; the explicit product `u m` instead means
417
+ unified atomic mass unit times metre.
397
418
 
398
419
  Registration raises `UnitRegistryError` if a new spelling, including a generated
399
420
  prefixed form, already resolves to a unit or conflicts with another spelling
@@ -1,10 +1,14 @@
1
1
  # unitx
2
2
 
3
- Write Python formulas with ready-to-use units and physical constants. unitx
3
+ [![PyPI version](https://img.shields.io/pypi/v/unitx)](https://pypi.org/project/unitx/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/unitx)](https://pypi.org/project/unitx/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
6
+
7
+ Write Python formulas with ready-to-use units and physical constants. `unitx`
4
8
  handles scalar quantities, unit conversion, and dimensional analysis, with
5
9
  exact rational unit scales and exponents.
6
10
 
7
- unitx is in early development. Public APIs may change during the 0.x series.
11
+ `unitx` is in early development. Public APIs may change during the 0.x series.
8
12
 
9
13
  ## Installation
10
14
 
@@ -79,15 +83,17 @@ astronomical units are included. Units and constants can be combined directly:
79
83
 
80
84
  Both namespaces are read-only and do not need to be instantiated. `si` exposes
81
85
  a selection of convenient attributes; other registered prefix combinations
82
- remain available through `Unit`, for example `Unit('Tm')`. Importing unitx
86
+ remain available through `Unit`, for example `Unit('Tm')`. Importing `unitx`
83
87
  initializes the predefined unit catalogue. Defining a custom unit registers its
84
88
  text spellings without adding an attribute to `si`.
85
89
 
86
90
  SI defining constants retain their exact decimal values. Measured constants
87
91
  are stored nominal values; measurement uncertainties are not propagated.
88
92
  Derived Decimal constants are evaluated in the Decimal context active at
89
- import. Formulas involving pi use `Decimal(str(math.pi))` as an approximation;
90
- additional Decimal precision does not add precision to pi.
93
+ import. Pi and the positive roots used by the Wien displacement constants
94
+ (`SI.b` for wavelength and `SI.b_` for frequency) use fixed Decimal
95
+ approximations with about 50 significant digits. Raising the context precision
96
+ does not add accuracy beyond those stored approximations.
91
97
 
92
98
  ## Constructing units and quantities
93
99
 
@@ -245,7 +251,7 @@ meaning rather than acting as string delimiters. Bare compound names are
245
251
  not guessed from adjacent words, and ordinary parentheses still group products.
246
252
 
247
253
  The unit table contains `s`, `m`, `g`, `A`, `K`, `mol`, and `cd`, plus the
248
- dimensionless identity. Importing unitx also initializes the common units in
254
+ dimensionless identity. Importing `unitx` also initializes the common units in
249
255
  `unitx.si`, including named derived units such as `N`. Mass scales are referenced
250
256
  to kilograms: `g` has factor `1/1000`, and `kg` has factor `1`.
251
257
 
@@ -315,8 +321,9 @@ Bare multiword names are recognized only as complete inputs. For example,
315
321
  as a factor; use `[astronomical unit] / arcsec`, symbols, or Unit objects instead.
316
322
 
317
323
  SI prefixes are enabled by default. `prefixable: PrefixPolicy = True` accepts
318
- `True` for all spellings, `False` for none, or a tuple of exact symbols, names,
319
- and aliases supplied in that definition. With a tuple, only the listed
324
+ `True` for all spellings, `False` for none, a single spelling string, or a tuple
325
+ of exact symbols, names, and aliases supplied in that definition. A string such
326
+ as `prefixable='Da'` is equivalent to `prefixable=('Da',)`. Only the selected
320
327
  spellings accept prefixes; an empty tuple disables prefixes. Unknown spellings
321
328
  and non-string entries are errors. For example,
322
329
  `prefixable=('arcsec', 'arcsecond')` enables all SI prefixes on those two
@@ -325,7 +332,7 @@ spellings, while other spellings of that unit remain unprefixable.
325
332
  The tuple selects **unit spellings**, not prefix symbols. Prefix names attach
326
333
  directly to the first word of a multiword name, as in `kiloastronomical unit`;
327
334
  a separate `kilo astronomical unit` is not accepted. `PrefixPolicy` is available
328
- from `unitx.unitregistry` for annotations and means `bool | tuple[str, ...]`.
335
+ from `unitx.unitregistry` for annotations and means `bool | str | tuple[str, ...]`.
329
336
 
330
337
  ```pycon
331
338
  >>> tick = si.s.define(
@@ -368,10 +375,24 @@ form; `760 * si.mmHg` converts to `si.atm` with value 1 under the library's
368
375
  existing pressure convention. Prefixes do not stack: `mmmHg` and `kmmHg` are
369
376
  invalid. `kt` retains its meaning as a knot alias; tonne remains unprefixable.
370
377
 
371
- The newly registered `u` denotes the unified atomic mass unit. ASCII micro
372
- spellings such as `um` still mean micrometres, but now issue `UnitSymbolWarning`
373
- because `u m` has a separate meaning. Use `µm`, `μm`, or `micrometre` to avoid
374
- this warning.
378
+ `si.mas` and `si.uas` provide milli- and microarcseconds as namespace shortcuts;
379
+ their registered expressions are `marcsec` and `µarcsec`. The strings `mas`
380
+ and `uas` are not registered aliases. `si.Da` and `si.u` now share one formal
381
+ unit identity; only the spelling `Da` accepts prefixes, such as `kDa`.
382
+ `si.mTorr` is available, and Torr accepts SI prefixes. Tonne uses `t` or
383
+ `tonne`; the ambiguous spelling `ton` is not registered.
384
+
385
+ `si.rpm` represents rotational frequency: `60 * si.rpm` converts to `si.Hz`
386
+ with value 1. For angular velocity, multiply that frequency by `2 * PI` and
387
+ convert to `si.rad / si.s`; directly converting frequency to `rad/s` does
388
+ not insert the physical factor of 2π. `si.c` is a speed unit equal to the
389
+ speed of light, while `SI.c` is the corresponding physical quantity.
390
+
391
+ The spelling `u` denotes the unified atomic mass unit when used on its own.
392
+ ASCII micro spellings such as `um` and `us` remain supported without warnings,
393
+ so entering non-ASCII characters is optional. `um`, `µm`, `μm`, and
394
+ `micrometre` all mean micrometres; the explicit product `u m` instead means
395
+ unified atomic mass unit times metre.
375
396
 
376
397
  Registration raises `UnitRegistryError` if a new spelling, including a generated
377
398
  prefixed form, already resolves to a unit or conflicts with another spelling
@@ -4,7 +4,7 @@ build-backend = 'hatchling.build'
4
4
 
5
5
  [project]
6
6
  name = 'unitx'
7
- version = '0.7.0'
7
+ version = '0.7.1'
8
8
  description = 'A modern, intuitive Python library for units, quantities, and dimensional analysis.'
9
9
  readme = 'README.md'
10
10
  requires-python = '>=3.12'
@@ -0,0 +1,33 @@
1
+ """Class-based namespaces for constants accessed directly as class attributes.
2
+
3
+ Namespaces cannot be instantiated, and their attributes cannot be rebound or
4
+ deleted. Objects referenced by those attributes are not recursively frozen.
5
+ """
6
+
7
+ from typing import Any, NoReturn
8
+
9
+ __all__ = ['ConstNamespace']
10
+
11
+
12
+ class _ConstNamespaceMeta(type):
13
+ def __call__(metacls, *args: Any, **kwds: Any) -> NoReturn:
14
+ """
15
+ This namespace cannot be instantiated; access constants via class attributes.
16
+ """
17
+ raise TypeError(metacls.__call__.__doc__)
18
+
19
+ def __setattr__(cls, name, value) -> NoReturn:
20
+ """
21
+ Constant namespace attributes cannot be assigned.
22
+ """
23
+ raise AttributeError(cls.__class__.__setattr__.__doc__)
24
+
25
+ def __delattr__(cls, name) -> NoReturn:
26
+ """
27
+ Constant namespace attributes cannot be deleted.
28
+ """
29
+ raise AttributeError(cls.__class__.__delattr__.__doc__)
30
+
31
+
32
+ class ConstNamespace(metaclass=_ConstNamespaceMeta):
33
+ """Base for constant namespaces with protected class attribute bindings."""
@@ -2,13 +2,12 @@
2
2
 
3
3
  from typing import Final
4
4
 
5
- from .._constclass import ConstClass
6
- from ..factor import ONE
5
+ from .._constnamespace import ConstNamespace
7
6
  from ..quantity import Quantity, quantity
8
7
  from ..unit import Unit
9
8
 
10
9
 
11
- class _si(ConstClass):
10
+ class _si(ConstNamespace):
12
11
  # SI base unit
13
12
 
14
13
  one: Final = Unit()
@@ -34,7 +33,7 @@ class _si(ConstClass):
34
33
  """radian, plane angle"""
35
34
  sr: Final = one.define('sr', name='steradian')
36
35
  """steradian, solid angle"""
37
- Hz: Final = (ONE / s).define('Hz', name='hertz')
36
+ Hz: Final = (s**-1).define('Hz', name='hertz')
38
37
  """hertz, frequency"""
39
38
  N: Final = (kg * m / s**2).define('N', name='newton')
40
39
  """newton, force/weight"""
@@ -64,7 +63,7 @@ class _si(ConstClass):
64
63
  """lumen, luminous flux"""
65
64
  lx: Final = (lm / m**2).define('lx', name='lux')
66
65
  """lux, illuminance"""
67
- Bq: Final = (ONE / s).define('Bq', name='becquerel')
66
+ Bq: Final = (s**-1).define('Bq', name='becquerel')
68
67
  """becquerel, activity referred to a radionuclide"""
69
68
  Gy: Final = (J / kg).define('Gy', name='gray')
70
69
  """gray, absorbed dose"""
@@ -74,9 +73,9 @@ class _si(ConstClass):
74
73
  """katal, catalytic activity"""
75
74
 
76
75
 
77
- class _SI(ConstClass):
76
+ class _SI(ConstNamespace):
78
77
  DeltanuCs: Final = Quantity(9_192_631_770, _si.Hz)
79
- """hyperfine transition frequency of Cs-133, definition of second."""
78
+ """hyperfine transition frequency ∆ν of Cs-133, definition of second."""
80
79
  c: Final = Quantity(299_792_458, _si.m / _si.s)
81
80
  """speed of light in vacuum"""
82
81
  h: Final = quantity(6.626_070_15e-34, _si.J * _si.s)
@@ -89,3 +88,6 @@ class _SI(ConstClass):
89
88
  """Avogadro constant"""
90
89
  Kcd: Final = Quantity(683, _si.lm / _si.W)
91
90
  """luminous efficacy of 540 THz radiation, definition of candela"""
91
+
92
+ g: Final = quantity(9.80665, _si.m / _si.s**2) # defined here for si.kgf
93
+ """standard acceleration of gravity"""
@@ -1,16 +1,19 @@
1
- import math
2
1
  from decimal import Decimal
3
2
  from typing import Final
4
3
 
5
- from .._constclass import ConstClass
4
+ from .._constnamespace import ConstNamespace
6
5
  from ..quantity import quantity
7
6
  from .bases import _SI
8
7
  from .units import si
9
8
 
10
9
  __all__ = ['SI', 'Particle']
11
10
 
12
- # Match the constants' Decimal magnitudes when using the approximate value of pi.
13
- _PI = Decimal(str(math.pi))
11
+ _PI = Decimal('3.1415926535897932384626433832795028841971693993751')
12
+ """the ratio of the circumference of a circle to its diameter"""
13
+ _WIEN_W = Decimal('4.9651142317442763036987591313228939440555849867973')
14
+ """Approximate positive root of x = 5*(1 - exp(-x))."""
15
+ _WIEN_F = Decimal('2.8214393721220788934031913302944851953458817440731')
16
+ """Approximate positive root of x = 3*(1 - exp(-x))."""
14
17
 
15
18
 
16
19
  class SI(_SI):
@@ -31,8 +34,6 @@ class SI(_SI):
31
34
 
32
35
  hbar: Final = _SI.h / (2 * _PI)
33
36
  """reduced Planck constant, ħ: Final = h / 2π"""
34
- g: Final = quantity(9.80665, 'm/s2')
35
- """standard acceleration of gravity"""
36
37
  T0: Final = quantity(273.15, si.K)
37
38
  """standard temperature"""
38
39
 
@@ -40,12 +41,8 @@ class SI(_SI):
40
41
 
41
42
  G: Final = quantity(6.674_30e-11, 'm3/(kg*s2)')
42
43
  """Newtonian constant of gravitation"""
43
- # kappa: Final = 8 * _PI * G / c**4 # simplify=True
44
- # """Einstein gravitational constant"""
45
- # H0: Final = Quantity(70, 'km/s*Mpc')
46
- # """Hubble constant, approx"""
47
- # Lambda: Final = quantity(1.089e-52, 'm-2')
48
- # """cosmological constant, approx"""
44
+ kappa: Final = (8 * _PI * G / _SI.c**4).to(si.N**-1)
45
+ """Einstein gravitational constant"""
49
46
 
50
47
  # electromagnetic constants
51
48
 
@@ -104,7 +101,6 @@ class SI(_SI):
104
101
  """molar gas constant"""
105
102
  F: Final = _SI.NA * _SI.e
106
103
  """Faraday constant"""
107
-
108
104
  sigma: Final = (_PI**2 * _SI.kB**4 / (60 * hbar**3 * _SI.c**2)).to('W/(m2*K4)')
109
105
  """Stefan-Boltzmann constant"""
110
106
  c1L: Final = (2 * _SI.h * _SI.c**2).to('W*m2/sr')
@@ -113,22 +109,18 @@ class SI(_SI):
113
109
  """first radiation constant"""
114
110
  c2: Final = _SI.h * _SI.c / _SI.kB
115
111
  """second radiation constant"""
116
- # b: Final = c2 / Math.WEIN_W
117
- # """Wien wavelength displacement law constant"""
118
- # b_: Final = (Math.WEIN_F * c / c2).to('Hz/K')
119
- # """Wien frequency displacement law constant"""
112
+ b: Final = c2 / _WIEN_W
113
+ """Wien wavelength displacement law constant"""
114
+ b_: Final = (_WIEN_F * _SI.c / c2).to('Hz/K')
115
+ """Wien frequency displacement law constant"""
120
116
 
121
117
  Vm: Final = (R * T0 / si.bar).to('m3/mol')
122
118
  """molar volume of ideal gas (273.15 K, 100 kPa)"""
123
- # Vmatm: Final = (R * T0 / si.atm).to(Vm.unit)
124
- # """molar volume of ideal gas (273.15 K, 101.325 kPa)"""
125
119
  n0: Final = _SI.NA / Vm
126
120
  """Loschmidt constant (273.15 K, 100 kPa)"""
127
- # n0atm: Final = _SI.NA / Vmatm
128
- # """Loschmidt constant (273.15 K, 101.32 kPa)"""
129
121
 
130
122
 
131
- class Particle(ConstClass):
123
+ class Particle(ConstNamespace):
132
124
  """
133
125
  Common particle data, including mass and magnetic moment of
134
126
  electron (e), muon (μ), tau (τ), proton (p), neutron (n),
@@ -20,7 +20,7 @@ def _prefixed_units(unit: str, prefixes: str | list[str]) -> Iterator[Unit]:
20
20
 
21
21
 
22
22
  class si(_si):
23
- """The `si` constclass offers a comprehensive set of common units
23
+ """The `si` namespace offers a comprehensive set of common units
24
24
  such as meters (m), kilograms (kg), and more.
25
25
 
26
26
  These units can be directly used in formulas or code.
@@ -32,72 +32,67 @@ class si(_si):
32
32
 
33
33
  # SI base unit
34
34
 
35
- as_, fs, ps, ns, us, ms = _prefixed_units('s', 'afpnum')
35
+ ys, zs, as_, fs, ps, ns, us, ms = _prefixed_units('s', 'yzafpnum')
36
36
  """second, time"""
37
- fm, pm, nm, um, mm, cm, dm, km = _prefixed_units('m', 'fpnumcdk')
37
+ am, fm, pm, nm, um, mm, cm, dm, km, Mm, Gm = _prefixed_units('m', 'afpnumcdkMG')
38
38
  """metre/meter, length"""
39
- pg, ng, ug, mg, g = _prefixed_units('g', 'pnum ')
39
+ fg, pg, ng, ug, mg, g, Mg, Gg = _prefixed_units('g', 'fpnum MG')
40
40
  """gram, mass"""
41
- pA, nA, uA, mA, kA = _prefixed_units('A', 'pnumk')
41
+ fA, pA, nA, uA, mA, kA, MA = _prefixed_units('A', 'fpnumkM')
42
42
  """ampere, electric current"""
43
43
  nK, uK, mK = _prefixed_units('K', 'num')
44
44
  """kelvin, thermodynamic temperature"""
45
- pmol, nmol, umol, mmol, kmol = _prefixed_units('mol', 'pnumk')
45
+ fmol, pmol, nmol, umol, mmol, kmol = _prefixed_units('mol', 'fpnumk')
46
46
  """mole, amount of substance"""
47
- mcd: Final = Unit('mcd')
47
+ ucd, mcd, kcd = _prefixed_units('cd', 'umk')
48
48
  """candela, luminous intensity"""
49
49
 
50
50
  # SI derived unit
51
51
 
52
- urad, mrad = _prefixed_units('rad', 'um')
52
+ nrad, urad, mrad = _prefixed_units('rad', 'num')
53
53
  """radian, plane angle"""
54
- kHz, MHz, GHz, THz, PHz = _prefixed_units('Hz', 'kMGTP')
54
+ nHz, uHz, mHz, kHz, MHz, GHz, THz, PHz, EHz = _prefixed_units('Hz', 'numkMGTPE')
55
55
  """hertz, frequency"""
56
- pN, nN, uN, mN, kN, MN = _prefixed_units('N', 'pnumkM')
56
+ fN, pN, nN, uN, mN, kN, MN, GN = _prefixed_units('N', 'fpnumkMG')
57
57
  """newton, force/weight"""
58
- mPa, hPa, kPa, MPa, GPa = _prefixed_units('Pa', 'mhkMG')
58
+ fPa, nPa, uPa, mPa, hPa, kPa, MPa, GPa, TPa = _prefixed_units('Pa', 'fnumhkMGT')
59
59
  """pascal, pressure/stress"""
60
- nJ, uJ, mJ, kJ, MJ, GJ = _prefixed_units('J', 'numkMG')
60
+ fJ, pJ, nJ, uJ, mJ, kJ, MJ, GJ, TJ, PJ = _prefixed_units('J', 'fpnumkMGTP')
61
61
  """joule, energy/work/heat"""
62
- pW, nW, uW, mW, kW, MW, GW, TW = _prefixed_units('W', 'pnumkMGT')
62
+ fW, pW, nW, uW, mW, kW, MW, GW, TW, PW = _prefixed_units('W', 'fpnumkMGTP')
63
63
  """watt, power/radiant flux"""
64
- fC, pC, nC, uC, mC = _prefixed_units('C', 'fpnum')
64
+ aC, fC, pC, nC, uC, mC, kC = _prefixed_units('C', 'afpnumk')
65
65
  """coulomb, electric charge"""
66
- nV, uV, mV, kV, MV = _prefixed_units('V', 'numkM')
66
+ pV, nV, uV, mV, kV, MV, GV = _prefixed_units('V', 'pnumkMG')
67
67
  """volt, electric potential/voltage/emf"""
68
- fF, pF, nF, uF, mF = _prefixed_units('F', 'fpnum')
68
+ aF, fF, pF, nF, uF, mF, kF = _prefixed_units('F', 'afpnumk')
69
69
  """farad, capacitance"""
70
- uohm, mohm, kohm, Mohm, Gohm = _prefixed_units('Ω', 'umkMG')
70
+ nohm, uohm, mohm, kohm, Mohm, Gohm, Tohm = _prefixed_units('Ω', 'numkMGT')
71
71
  """ohm, resistance/impedance/reactance"""
72
- uS, mS = _prefixed_units('S', 'um')
72
+ pS, nS, uS, mS, kS = _prefixed_units('S', 'pnumk')
73
73
  """siemens, electrical conductance"""
74
- uWb, mWb = _prefixed_units('Wb', 'um')
74
+ nWb, uWb, mWb, kWb = _prefixed_units('Wb', 'numk')
75
75
  """weber, magnetic flux"""
76
- nT, uT, mT = _prefixed_units('T', 'num')
76
+ fT, pT, nT, uT, mT, kT = _prefixed_units('T', 'fpnumk')
77
77
  """tesla, magnetic flux density"""
78
- nH, uH, mH = _prefixed_units('H', 'num')
78
+ pH, nH, uH, mH, kH = _prefixed_units('H', 'pnumk')
79
79
  """henry, inductance"""
80
- kBq, MBq, GBq, TBq = _prefixed_units('Bq', 'kMGT')
80
+ ulm, mlm, klm, Mlm = _prefixed_units('lm', 'umkM')
81
+ """lumen, luminous flux"""
82
+ mlx, klx = _prefixed_units('lx', 'mk')
83
+ """lux, illuminance"""
84
+ mBq, kBq, MBq, GBq, TBq, PBq = _prefixed_units('Bq', 'mkMGTP')
81
85
  """becquerel, activity referred to a radionuclide"""
82
- uGy, mGy = _prefixed_units('Gy', 'um')
86
+ nGy, uGy, mGy, kGy, MGy = _prefixed_units('Gy', 'numkM')
83
87
  """gray, absorbed dose"""
84
- nSv, uSv, mSv = _prefixed_units('Sv', 'num')
88
+ pSv, nSv, uSv, mSv, kSv = _prefixed_units('Sv', 'pnumk')
85
89
  """sievert, equivalent dose"""
90
+ nkat, ukat, mkat = _prefixed_units('kat', 'num')
91
+ """katal, catalytic activity"""
86
92
 
87
- # non-SI units accepted for use with SI
93
+ # other common units
88
94
 
89
- min: Final = (60 * _si.s).define('min', name='minute', prefixable=False)
90
- """minute, 1 min = 60 s, time"""
91
- h: Final = (60 * min).define('h', name='hour', prefixable=False)
92
- """hour, 1 h = 60 min, time"""
93
- d: Final = (24 * h).define('d', name='day', prefixable=False)
94
- """day, 1 d = 24 h, time"""
95
- yr: Final = (365 * d).define('yr', name='year', prefixable=False)
96
- """simple year, 1 yr = 365 d, time"""
97
- a_j: Final = (365.25 * d).define('a_j', name='Julian year')
98
- """Julian year, 1 a_j = 365.25 d, time"""
99
- mo: Final = (a_j / 12).define('mo', name='month', prefixable=False)
100
- """month, 1 mo = 1/12 a_j, time"""
95
+ # dimensionless
101
96
  deg: Final = (PI / 180 * _si.rad).define(
102
97
  '°', name='degree', symbol_aliases='deg', prefixable=False
103
98
  )
@@ -113,66 +108,126 @@ class si(_si):
113
108
  prefixable=('arcsec', 'arcsecond'),
114
109
  )
115
110
  """arcsecond, plane angle"""
111
+ mas, uas = _prefixed_units('arcsec', 'mu')
112
+ """arcsecond, plane angle"""
113
+ percent: Final = (_si.one / 100).define(
114
+ '%', name='percent', symbol_aliases='٪', prefixable=False
115
+ )
116
+ """percent, 1/100, dimensionless"""
117
+ permille: Final = (_si.one / 1000).define('‰', name='permille', prefixable=False)
118
+ """permille, 1/1000, dimensionless"""
119
+ permyriad: Final = (_si.one / 10000).define(
120
+ '‱', 'permyriad', 'per ten thousand', prefixable=False
121
+ )
122
+ """permyriad, 1/10000, dimensionless"""
123
+ # time/frequency related
124
+ min: Final = (60 * _si.s).define('min', name='minute', prefixable=False)
125
+ """minute, 1 min = 60 s, time"""
126
+ h: Final = (60 * min).define('h', name='hour', prefixable=False)
127
+ """hour, 1 h = 60 min, time"""
128
+ d: Final = (24 * h).define('d', name='day', prefixable=False)
129
+ """day, 1 d = 24 h, time"""
130
+ yr: Final = (365 * d).define('yr', name='year', prefixable=False)
131
+ """simple year, 1 yr = 365 d, time"""
132
+ a_j: Final = (365.25 * d).define('a_j', 'Julian year', 'annum')
133
+ """Julian year, 1 a_j = 365.25 d, time"""
134
+ mo: Final = (a_j / 12).define('mo', name='month', prefixable=False)
135
+ """month, 1 mo = 1/12 a_j, time"""
136
+ rpm: Final = (1 / min).define('rpm', name='revolutions per minute', prefixable=False)
137
+ """revoltions per minute, 60 rpm = 1 Hz, rotational speed"""
138
+ # length/area/volume related
139
+ au: Final = (149_597_870_700 * _si.m).define(
140
+ 'au', name='astronomical unit', symbol_aliases='AU'
141
+ )
142
+ """astronomical unit, length"""
143
+ pc: Final = (au / arcsec).define('pc', name='parsec')
144
+ """parsec, length"""
145
+ kpc, Mpc, Gpc, Tpc = _prefixed_units('pc', 'kMGT')
146
+ """parsec, length"""
147
+ ly: Final = (_SI.c * a_j).define('ly', name='light year', prefixable=False)
148
+ """light year, length"""
149
+ nmile: Final = (1852 * _si.m).define('nmi', 'nautical mile', symbol_aliases='NM')
150
+ """nautical mile, length in marine and aerial navigation"""
151
+ angstrom: Final = (_si.m / TEN**10).define(
152
+ 'Å', 'ångström', 'angstrom', symbol_aliases=('Å', 'Å')
153
+ ) # symbol: chr(0xC5), chr(0x212B), 'A'+chr(0x30A)
154
+ """ångström, 1 Å = 10⁻¹⁰ m,
155
+ lengths of chemical bonds, sizes of atoms and molecules, and wavelengths.
156
+ """
116
157
  a: Final = (100 * _si.m**2).define('a', name='are', prefixable=False)
117
158
  """are, 1 a = 100 m², land area"""
118
159
  ha: Final = (100 * a).define('ha', name='hectare', prefixable=False)
119
160
  """hectare, 1 ha = 100 a = 10000 m², land area"""
120
161
  b: Final = (_si.m**2 / TEN**28).define('b', name='barn')
121
162
  """barn, 1 b = 10⁻²⁸ m², effective cross sections."""
122
- L: Final = (dm**3).define('L', 'liter', 'litre', symbol_aliases='l')
123
- """liter/litre, 1 L = 1 dm³, volume"""
124
- nL, uL, mL, cL, dL = _prefixed_units('L', 'numcd')
125
- """liter/litre, 1 L = 1 dm³, volume"""
163
+ L: Final = (dm**3).define('L', 'litre', 'liter', symbol_aliases='l')
164
+ """litre/liter, 1 L = 1 dm³, volume"""
165
+ fL, pL, nL, uL, mL, cL, dL, kL, ML = _prefixed_units('L', 'fpnumcdkM')
166
+ """litre/liter, 1 L = 1 dm³, volume"""
167
+ # mass related
126
168
  t: Final = (1000 * _si.kg).define('t', name='tonne', prefixable=False)
127
169
  """tonne, 1 t = 1000 kg, mass"""
128
- # symbol aliases: chr(0xC5), chr(0x212B), 'A'+chr(0x30A)
129
- angstrom: Final = (_si.m / TEN**10).define(
130
- 'Å', 'ångström', 'angstrom', symbol_aliases=('Å', 'Å')
170
+ Da: Final = (Decimal('1.660_539_068_92e-27') * _si.kg).define(
171
+ 'Da', 'dalton', 'unified atomic mass unit', symbol_aliases='u', prefixable='Da'
131
172
  )
132
- """ångström, 1 Å = 10⁻¹⁰ m,
133
- lengths of chemical bonds, sizes of atoms and molecules, and wavelengths.
134
- """
135
- Gal: Final = (cm / _si.s**2).define('Gal', name='gal')
136
- """gal, acceleration"""
137
- bar: Final = (TEN**5 * _si.Pa).define('bar', name='bar')
138
- """bar, standard-state pressure"""
139
- mbar: Final = Unit('mbar')
140
- """millibar, standard-state pressure"""
141
- Da: Final = (Decimal('1.660_539_068_92e-27') * _si.kg).define('Da', name='dalton')
142
173
  """dalton, unified atomic mass unit, 1 Da = m(¹²C) / 12, mass"""
143
- u: Final = Da.define('u', name='unified atomic mass unit', prefixable=False)
174
+ kDa, MDa = _prefixed_units('Da', 'kM')
175
+ """dalton, unified atomic mass unit, 1 Da = m(¹²C) / 12, mass"""
176
+ u: Final = Unit('u')
144
177
  """unified atomic mass unit, 1 u = m(¹²C) / 12, mass"""
145
- au: Final = (149_597_870_700 * _si.m).define('au', name='astronomical unit')
146
- """astronomical unit, length"""
147
- pc: Final = (au / arcsec).define('pc', name='parsec')
148
- """parsec, length"""
149
- ly: Final = (_SI.c * a_j).define('ly', name='light year', prefixable=False)
150
- """light year, length"""
151
- nmile: Final = (1852 * _si.m).define('nmi', 'nautical mile', symbol_aliases='NM')
152
- """nautical mile, length in marine and aerial navigation"""
178
+ # kinetic related
179
+ c: Final = _SI.c.define('c', name='speed of light', prefixable=False)
180
+ """speed of light in vacuum, 299_792_458 m/s"""
181
+ Gal: Final = (cm / _si.s**2).define('Gal', 'gal', 'galileo')
182
+ """gal, acceleration"""
153
183
  knot: Final = (nmile / h).define('kn', 'knot', symbol_aliases='kt')
154
184
  """knot, speed in marine and aerial navigation"""
185
+ # dynamic related
186
+ gf: Final = (_SI.g * g).define('gf', 'gram-force', 'gram force')
187
+ """gram-force, 1 gf = 1 g * 9.80665 m/s², force"""
188
+ kgf: Final = Unit('kgf')
189
+ """kilogram-force, 1 kgf = 1 kg * 9.80665 m/s², force"""
190
+ bar: Final = (TEN**5 * _si.Pa).define('bar', name='bar')
191
+ """bar, standard-state pressure"""
192
+ ubar, mbar = _prefixed_units('bar', 'um')
193
+ """bar, standard-state pressure"""
155
194
  atm: Final = (101_325 * _si.Pa).define('atm', name='atmosphere')
156
195
  """standard atmosphere"""
157
196
  mHg: Final = (1000 * (atm / 760)).define('mHg', name='metre of mercury')
158
197
  """metre of mercury, 0.760 mHg = 1 atm, pressure"""
159
198
  mmHg: Final = Unit('mmHg')
160
199
  """millimetre of mercury, 760 mmHg = 1 atm, pressure"""
161
- Torr: Final = mmHg.define('Torr', 'torr', 'torricelli', prefixable=False)
200
+ Torr: Final = mmHg.define('Torr', 'torr', 'torricelli')
162
201
  """torr, 760 Torr = 1 atm, pressure"""
202
+ mTorr: Final = Unit('mTorr')
203
+ """millitorr, pressure"""
163
204
  Wh: Final = (_si.W * h).define('Wh', name='watthour')
164
205
  """watthour, energy"""
165
- mWh, kWh, MWh, GWh, TWh = _prefixed_units('Wh', 'mkMGT')
206
+ uWh, mWh, kWh, MWh, GWh, TWh, PWh = _prefixed_units('Wh', 'umkMGTP')
166
207
  """watthour, energy"""
167
208
  eV: Final = (_SI.e * _si.V).define('eV', name='electronvolt')
168
209
  """electron volt, energy"""
169
- meV, keV, MeV, GeV, TeV, PeV = _prefixed_units('eV', 'mkMGTP')
210
+ ueV, meV, keV, MeV, GeV, TeV, PeV, EeV = _prefixed_units('eV', 'umkMGTPE')
170
211
  """electron volt, energy"""
171
- cal: Final = (Decimal('4.184') * _si.J).define('cal', name='calorie')
212
+ cal: Final = (4184 * mJ).define('cal', name='calorie')
172
213
  """calorie, energy"""
173
214
  kcal: Final = Unit('kcal')
174
215
  """kilocalorie, energy"""
175
- var: Final = (_si.V * _si.A).define('var', name='volt-ampere reactive')
216
+ VA: Final = (_si.V * _si.A).define('VA', name='volt-ampere')
217
+ """volt-ampere, apparent power"""
218
+ kVA, MVA = _prefixed_units('VA', 'kM')
219
+ """volt-ampere, apparent power"""
220
+ var: Final = (_si.V * _si.A).define('var', name='volt-ampere reactive', symbol_aliases='VAr')
221
+ """volt-ampere reactive, reactive power"""
222
+ kvar, Mvar = _prefixed_units('var', 'kM')
176
223
  """volt-ampere reactive, reactive power"""
177
- kvar: Final = Unit('kvar')
178
- """kilovolt-ampere reactive, reactive power"""
224
+ # electromagnetic related
225
+ Ah: Final = (_si.A * h).define('Ah', name='ampere-hour')
226
+ """ampere-hour, electric charge"""
227
+ mAh, kAh, MAh = _prefixed_units('Ah', 'mkM')
228
+ """ampere-hour, electric charge"""
229
+ mho: Final = _si.S.define('mho', name='mho', symbol_aliases='℧')
230
+ """mho, electrical conductance"""
231
+ # others
232
+ nt: Final = (_si.cd / _si.m**2).define('nt', name='nit')
233
+ """nit, luminance"""
@@ -285,10 +285,11 @@ class Unit:
285
285
  All new texts, including prefixed forms, must be unambiguous and must
286
286
  not already resolve to registered units or implicit unit products.
287
287
  Complex names work standalone or in ``[name]`` references in expressions.
288
- ``prefixable`` accepts True for all spellings, False for none, or a
289
- tuple of exact symbols, names, or aliases from this definition, such as
288
+ ``prefixable`` accepts True for all spellings, False for none, a single
289
+ spelling string, or a tuple of exact symbols, names, or aliases, such as
290
290
  ``('arcsec', 'arcsecond')``. Only listed spellings accept prefixes;
291
- an empty tuple disables all prefixes. Unknown spellings are errors.
291
+ ``'arcsec'`` is equivalent to ``('arcsec',)``. An empty tuple disables
292
+ all prefixes. Unknown or empty spellings are errors.
292
293
  Invalid definitions raise TypeError, ValueError, or UnitRegistryError
293
294
  before changing the registry.
294
295
  """
@@ -22,11 +22,11 @@ from .factor import ONE, TEN, Factor
22
22
 
23
23
  __all__ = ['PrefixPolicy']
24
24
 
25
- type PrefixPolicy = bool | tuple[str, ...]
25
+ type PrefixPolicy = bool | str | tuple[str, ...]
26
26
 
27
27
  _UNIT_SYMBOL_PATTERN = r'[^\s\d' + re.escape('*^()/+-[]' + SUPERSCRIPTS) + ']+'
28
28
  _UNIT_SYMBOL_RE = re.compile(_UNIT_SYMBOL_PATTERN)
29
- _UNIT_NAME_RE = re.compile(_UNIT_SYMBOL_PATTERN + '(?:[ -]' + _UNIT_SYMBOL_PATTERN + ')*')
29
+ _UNIT_NAME_RE = re.compile(f'{_UNIT_SYMBOL_PATTERN}(?:[ -]{_UNIT_SYMBOL_PATTERN})*')
30
30
  _PREFIXED_NAME_EXCEPTIONS = {('M', 'ohm'): 'megohm', ('k', 'ohm'): 'kilohm'}
31
31
 
32
32
 
@@ -113,7 +113,7 @@ class UnitDefinition:
113
113
  self.factor = factor
114
114
  self.names = (name, *name_aliases)
115
115
  self.dimension = dimension
116
- self.prefixable = prefixable
116
+ self.prefixable = (prefixable,) if isinstance(prefixable, str) else prefixable
117
117
  self.symbols: tuple[str, ...] = ()
118
118
 
119
119
  def allows_prefix(self, spelling: str) -> bool:
@@ -229,7 +229,7 @@ def _preferred_unit_spelling(
229
229
  def _register_unit(symbol: str, definition: UnitDefinition, /, *symbol_aliases: str) -> None:
230
230
  """Validate every binding and generated spelling before updating indexes."""
231
231
  if not isinstance(definition.prefixable, (bool, tuple)):
232
- raise TypeError('Prefixable must be bool or tuple of spellings.')
232
+ raise TypeError('Prefixable must be bool, str or tuple of spellings.')
233
233
  for kind, texts in (
234
234
  ('symbol', (symbol,)),
235
235
  ('name', (definition.name,)),
@@ -1,8 +1,11 @@
1
1
  """Public SI namespaces, conversions, and constant values."""
2
2
 
3
+ import copy
3
4
  import os
5
+ import pickle
4
6
  import subprocess
5
7
  import sys
8
+ import warnings
6
9
  from decimal import Decimal, localcontext
7
10
  from fractions import Fraction
8
11
  from pathlib import Path
@@ -79,6 +82,18 @@ def test_si_coherent_units(attribute: str, expression: str) -> None:
79
82
  ('Wh', 'J', 3600),
80
83
  ('eV', 'J', Decimal('1.602176634e-19')),
81
84
  ('cal', 'J', Decimal('4.184')),
85
+ ('percent', '', Fraction(1, 100)),
86
+ ('permille', '', Fraction(1, 1000)),
87
+ ('permyriad', '', Fraction(1, 10000)),
88
+ ('rpm', 'Hz', Fraction(1, 60)),
89
+ ('c', 'm/s', 299792458),
90
+ ('gf', 'N', Decimal('0.00980665')),
91
+ ('kgf', 'N', Decimal('9.80665')),
92
+ ('Ah', 'C', 3600),
93
+ ('VA', 'W', 1),
94
+ ('mho', 'S', 1),
95
+ ('nt', 'cd/m2', 1),
96
+ ('mTorr', 'Pa', Fraction(4053, 30400)),
82
97
  ],
83
98
  )
84
99
  def test_si_unit_conversions(attribute: str, target: str, value: int | Fraction | Decimal) -> None:
@@ -151,6 +166,20 @@ def test_si_hbar_value() -> None:
151
166
  assert float(SI.hbar.value) == pytest.approx(1.0545718176461565e-34, rel=1e-15, abs=0)
152
167
 
153
168
 
169
+ @pytest.mark.parametrize(
170
+ ('attribute', 'target', 'value'),
171
+ [
172
+ ('kappa', '1/N', 2.076647442844972e-43),
173
+ ('b', 'm*K', 2.8977719551851727e-3),
174
+ ('b_', 'Hz/K', 5.878925757646825e10),
175
+ ],
176
+ )
177
+ def test_si_derived_constants(attribute: str, target: str, value: float) -> None:
178
+ constant = getattr(SI, attribute)
179
+ assert constant.unit.dimension == Unit(target).dimension
180
+ assert float(constant.value_in(target)) == pytest.approx(value, rel=1e-14, abs=0)
181
+
182
+
154
183
  @pytest.mark.parametrize(
155
184
  ('attribute', 'target'),
156
185
  [('Phi0', si.Wb), ('G0', si.S), ('RK', si.ohm), ('a0', si.m), ('lambdaC', si.m), ('Eh', si.J)],
@@ -247,7 +276,8 @@ def test_si_mercury_prefixes() -> None:
247
276
  assert Unit('kt') == si.knot
248
277
  for unit in (si.mHg, si.mmHg, si.Torr):
249
278
  assert Unit(unit.expression()) == unit
250
- for text in ('mmmHg', 'kmmHg', 'millimillimetre of mercury', 'kTorr'):
279
+ assert Quantity(1, 'kTorr').to(si.Torr).value == 1000
280
+ for text in ('mmmHg', 'kmmHg', 'millimillimetre of mercury'):
251
281
  with pytest.raises(UnitSymbolError):
252
282
  Unit(text)
253
283
 
@@ -257,6 +287,37 @@ def test_si_light_year() -> None:
257
287
  assert 1 * si.eV == SI.e * si.V
258
288
 
259
289
 
290
+ def test_si_dalton_aliases() -> None:
291
+ assert si.u == si.Da == Unit('unified atomic mass unit')
292
+ assert hash(si.u) == hash(si.Da)
293
+ assert {si.Da: 'found'}[si.u] == 'found'
294
+ assert Quantity(1, si.kDa).to(si.Da).value == 1000
295
+ for text in ('mu', 'kilodalton', 'kilounified atomic mass unit', 'ton'):
296
+ with pytest.raises(UnitSymbolError):
297
+ Unit(text)
298
+
299
+
300
+ def test_si_ascii_micro() -> None:
301
+ with warnings.catch_warnings(record=True) as recorded:
302
+ warnings.simplefilter('always')
303
+ assert Unit('um') == si.um
304
+ assert Unit('us') == si.us
305
+ assert Unit('u m') == si.u * si.m
306
+ assert Unit('u m') != Unit('um')
307
+ assert not recorded
308
+
309
+
310
+ @pytest.mark.parametrize('attribute', ['mas', 'uas', 'u', 'Da', 'mTorr', 'c', 'gf', 'VA', 'Ah'])
311
+ def test_si_unit_roundtrip(attribute: str) -> None:
312
+ unit = getattr(si, attribute)
313
+ assert Unit(unit.expression()) == unit
314
+ assert eval(repr(unit), {'Unit': Unit}) == unit
315
+ assert copy.copy(unit) == copy.deepcopy(unit) == unit
316
+ restored = pickle.loads(pickle.dumps(unit))
317
+ assert restored == unit
318
+ assert hash(restored) == hash(unit)
319
+
320
+
260
321
  @pytest.mark.parametrize('name', ['volt-ampere reactive', 'kilovolt-ampere reactive'])
261
322
  def test_si_reactive_power_name(name: str) -> None:
262
323
  assert Unit('[' + name + '] / s') == Unit(name) / si.s
@@ -53,6 +53,9 @@ def test_single_unit_combinations() -> None:
53
53
  ('M', 'Ω'): 'megohm',
54
54
  ('k', 'Ω'): 'kilohm',
55
55
  }.get((prefix, symbol), prefix_definition.name + unit_definition.name)
56
+ if prefix and not unit_definition.allows_prefix(unit_definition.name):
57
+ # Da permits prefixed symbols but no prefixed names.
58
+ expected_name = unit.symbol
56
59
  assert unit.name == expected_name
57
60
  assert SingleUnit(unit.name) == unit
58
61
  assert unit.dimension == unit_definition.dimension
@@ -96,7 +99,6 @@ def test_single_unit_name_alias(prefix: str) -> None:
96
99
  ('KK', 'kK', 'kilokelvin', Dimension(Theta=1), 1000),
97
100
  ('Kmol', 'kmol', 'kilomole', Dimension(N=1), 1000),
98
101
  ('Kcd', 'kcd', 'kilocandela', Dimension(J=1), 1000),
99
- ('um', 'µm', 'micrometre', Dimension(L=1), Fraction(1, 10**6)),
100
102
  ],
101
103
  )
102
104
  def test_single_unit_symbol_warning(
@@ -120,7 +122,9 @@ def test_single_unit_symbol_warning(
120
122
  assert type(unit.factor.value) is type(factor)
121
123
 
122
124
 
123
- @pytest.mark.parametrize('text', ['', 'K', 'kelvin', 'kK', 'km', 'ms', 'Ms', 'µm', 'μm'])
125
+ @pytest.mark.parametrize(
126
+ 'text', ['', 'K', 'kelvin', 'kK', 'km', 'ms', 'Ms', 'um', 'us', 'µm', 'μm']
127
+ )
124
128
  def test_single_unit_no_warning(text: str) -> None:
125
129
  with warnings.catch_warnings(record=True) as recorded:
126
130
  warnings.simplefilter('always')
@@ -116,9 +116,30 @@ def test_unit_define_duplicate_spellings() -> None:
116
116
  Unit('millireview policy')
117
117
 
118
118
 
119
- @pytest.mark.parametrize('policy', [None, [], ['rvpolicy'], {}, {'rvpolicy': True}, 'rvpolicy', 1])
119
+ @pytest.mark.parametrize('source', [Unit('m'), PI * Unit('m'), Quantity(Fraction(3, 7), 'm')])
120
+ @pytest.mark.parametrize('spelling', ['rvselect', 'rvselected', 'review selected', 'review alias'])
121
+ def test_unit_define_string_policy(source, spelling: str) -> None:
122
+ unit = source.define(
123
+ 'rvselect',
124
+ 'review selected',
125
+ 'review alias',
126
+ symbol_aliases='rvselected',
127
+ prefixable=spelling,
128
+ )
129
+ for text in ('rvselect', 'rvselected', 'review selected', 'review alias'):
130
+ assert Unit(text) == unit
131
+ prefixed = ('kilo' if ' ' in text else 'k') + text
132
+ if text == spelling:
133
+ assert Unit(prefixed).factor == 1000 * unit.factor
134
+ assert Unit(Unit(prefixed).expression()) == Unit(prefixed)
135
+ else:
136
+ with pytest.raises(UnitSymbolError):
137
+ Unit(prefixed)
138
+
139
+
140
+ @pytest.mark.parametrize('policy', [None, [], ['rvpolicy'], {}, {'rvpolicy': True}, 1])
120
141
  def test_unit_define_invalid_policy(policy, unit_registry) -> None:
121
- with pytest.raises(TypeError, match='Prefixable must be bool or tuple of spellings'):
142
+ with pytest.raises(TypeError, match='Prefixable must be bool, str or tuple of spellings'):
122
143
  Unit('m').define('rvpolicy', 'review policy', prefixable=policy)
123
144
  assert (UNIT_DEFINITIONS, UNIT_NAME_TO_SYMBOL, UNIT_SYMBOL_ALIAS_TO_SYMBOL) == unit_registry
124
145
 
@@ -131,9 +152,12 @@ def test_unit_define_invalid_policy_type(text, unit_registry) -> None:
131
152
 
132
153
 
133
154
  @pytest.mark.parametrize('text', ['unknown', '', 'mrvpolicy', 'm', 'µ'])
134
- def test_unit_define_invalid_policy_spelling(text: str, unit_registry) -> None:
155
+ @pytest.mark.parametrize('as_tuple', [False, True])
156
+ def test_unit_define_invalid_policy_spelling(text: str, as_tuple: bool, unit_registry) -> None:
135
157
  with pytest.raises(ValueError, match='is not a registered spelling'):
136
- Unit('m').define('rvpolicy', 'review policy', prefixable=('rvpolicy', text))
158
+ Unit('m').define(
159
+ 'rvpolicy', 'review policy', prefixable=('rvpolicy', text) if as_tuple else text
160
+ )
137
161
  assert (UNIT_DEFINITIONS, UNIT_NAME_TO_SYMBOL, UNIT_SYMBOL_ALIAS_TO_SYMBOL) == unit_registry
138
162
 
139
163
 
@@ -124,6 +124,12 @@ def test_unit_numeric_scaling(unit: Unit, scaled: ScaledUnit) -> None:
124
124
  def test_unit_define() -> None:
125
125
  policy: PrefixPolicy = ('rvpolicy',)
126
126
  assert_type(Unit('m').define('rvpolicy', 'review policy', prefixable=policy), Unit)
127
+ single_policy: PrefixPolicy = 'rvpolicy'
128
+ assert_type(Unit('m').define('rvpolicy', 'review policy', prefixable=single_policy), Unit)
129
+ assert_type((PI * Unit('m')).define('rvpolicy', 'review policy', prefixable='rvpolicy'), Unit)
130
+ assert_type(
131
+ Quantity(2, 'm').define('rvpolicy', 'review policy', prefixable='review policy'), Unit
132
+ )
127
133
  assert_type(
128
134
  Unit('m').define(
129
135
  'rvbound',
@@ -3,7 +3,7 @@
3
3
  from decimal import Decimal
4
4
  from typing import assert_type
5
5
 
6
- from unitx.quantity import Quantity
6
+ from unitx.quantity import Magnitude, Quantity
7
7
  from unitx.si import SI, si
8
8
  from unitx.unit import Unit
9
9
 
@@ -16,6 +16,10 @@ def test_si_unit_types() -> None:
16
16
  assert_type(si.mHg, Unit)
17
17
  assert_type(si.mmHg, Unit)
18
18
  assert_type(si.cd, Unit)
19
+ assert_type(si.mas, Unit)
20
+ assert_type(si.kDa, Unit)
21
+ assert_type(si.c, Unit)
22
+ assert_type(si.kgf, Unit)
19
23
  assert_type(2 * si.m, Quantity[int])
20
24
  assert_type(Decimal('0.5') * si.s, Quantity[Decimal])
21
25
 
@@ -24,5 +28,9 @@ def test_si_constant_types() -> None:
24
28
  assert_type(SI.c, Quantity[int])
25
29
  assert_type(SI.h, Quantity[Decimal])
26
30
  assert_type(SI.hbar, Quantity[Decimal])
31
+ # Integer powers currently have a conservative numeric return type.
32
+ assert_type(SI.kappa, Quantity[Magnitude])
33
+ assert_type(SI.b, Quantity[Decimal])
34
+ assert_type(SI.b_, Quantity[Decimal])
27
35
  assert_type(SI.e, Quantity[Decimal])
28
36
  assert_type(SI.h.to(si.J * si.s), Quantity[Decimal])
@@ -1,34 +0,0 @@
1
- """
2
- ConstClass is a class storing static constants.
3
- It's similar to Enum, but members of Enum need an extra value attribute `value`
4
- to access its value, and I think it's unnecessary, so I use ConstClass.
5
- """
6
-
7
- from typing import Any, NoReturn
8
-
9
- __all__ = ['ConstMeta', 'ConstClass']
10
-
11
-
12
- class ConstMeta(type):
13
- def __call__(metacls, *args: Any, **kwds: Any) -> NoReturn:
14
- """
15
- No, this class restores constants, you can NOT instantiate objects.
16
- please access constants via class attributes.
17
- """
18
- raise TypeError(metacls.__call__.__doc__)
19
-
20
- def __setattr__(cls, name, value) -> NoReturn:
21
- """
22
- No, this class restores constants, you can NOT modify them.
23
- """
24
- raise AttributeError(cls.__class__.__setattr__.__doc__)
25
-
26
- def __delattr__(cls, name) -> NoReturn:
27
- """
28
- No, this class restores constants, you can NOT delete them.
29
- """
30
- raise AttributeError(cls.__class__.__delattr__.__doc__)
31
-
32
-
33
- class ConstClass(metaclass=ConstMeta):
34
- pass
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes