unitx 0.13.2__tar.gz → 0.13.3__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 (105) hide show
  1. {unitx-0.13.2 → unitx-0.13.3}/CHANGELOG.md +13 -0
  2. {unitx-0.13.2 → unitx-0.13.3}/PKG-INFO +2 -2
  3. {unitx-0.13.2 → unitx-0.13.3}/README.md +1 -1
  4. {unitx-0.13.2 → unitx-0.13.3}/docs/README.md +1 -0
  5. unitx-0.13.3/docs/serialization.md +225 -0
  6. {unitx-0.13.2 → unitx-0.13.3}/docs/typing.md +5 -0
  7. {unitx-0.13.2 → unitx-0.13.3}/pyproject.toml +1 -1
  8. {unitx-0.13.2 → unitx-0.13.3}/pyrightconfig.json +2 -0
  9. {unitx-0.13.2 → unitx-0.13.3}/src/unitx/exceptions.py +10 -0
  10. unitx-0.13.3/src/unitx/serialization.py +354 -0
  11. unitx-0.13.3/tests/test_serialization.py +548 -0
  12. unitx-0.13.3/tests/typing/check_serialization.py +32 -0
  13. {unitx-0.13.2 → unitx-0.13.3}/.github/workflows/ci.yml +0 -0
  14. {unitx-0.13.2 → unitx-0.13.3}/.github/workflows/release.yml +0 -0
  15. {unitx-0.13.2 → unitx-0.13.3}/.gitignore +0 -0
  16. {unitx-0.13.2 → unitx-0.13.3}/CONTRIBUTING.md +0 -0
  17. {unitx-0.13.2 → unitx-0.13.3}/LICENSE +0 -0
  18. {unitx-0.13.2 → unitx-0.13.3}/benchmarks/numpy_writes.py +0 -0
  19. {unitx-0.13.2 → unitx-0.13.3}/conftest.py +0 -0
  20. {unitx-0.13.2 → unitx-0.13.3}/docs/affine.md +0 -0
  21. {unitx-0.13.2 → unitx-0.13.3}/docs/catalogue.md +0 -0
  22. {unitx-0.13.2 → unitx-0.13.3}/docs/custom-units.md +0 -0
  23. {unitx-0.13.2 → unitx-0.13.3}/docs/display.md +0 -0
  24. {unitx-0.13.2 → unitx-0.13.3}/docs/images/blackbody.svg +0 -0
  25. {unitx-0.13.2 → unitx-0.13.3}/docs/migration.md +0 -0
  26. {unitx-0.13.2 → unitx-0.13.3}/docs/numpy.md +0 -0
  27. {unitx-0.13.2 → unitx-0.13.3}/docs/scalars.md +0 -0
  28. {unitx-0.13.2 → unitx-0.13.3}/docs/stdlib.md +0 -0
  29. {unitx-0.13.2 → unitx-0.13.3}/docs/units.md +0 -0
  30. {unitx-0.13.2 → unitx-0.13.3}/examples/README.md +0 -0
  31. {unitx-0.13.2 → unitx-0.13.3}/examples/blackbody.ipynb +0 -0
  32. {unitx-0.13.2 → unitx-0.13.3}/examples/damped_oscillator.ipynb +0 -0
  33. {unitx-0.13.2 → unitx-0.13.3}/examples/gaussian_wavepacket.ipynb +0 -0
  34. {unitx-0.13.2 → unitx-0.13.3}/examples/optical_attenuation.ipynb +0 -0
  35. {unitx-0.13.2 → unitx-0.13.3}/examples/pendulum_uncertainty.ipynb +0 -0
  36. {unitx-0.13.2 → unitx-0.13.3}/src/unitx/__init__.py +0 -0
  37. {unitx-0.13.2 → unitx-0.13.3}/src/unitx/_constnamespace.py +0 -0
  38. {unitx-0.13.2 → unitx-0.13.3}/src/unitx/_conversion.py +0 -0
  39. {unitx-0.13.2 → unitx-0.13.3}/src/unitx/_display.py +0 -0
  40. {unitx-0.13.2 → unitx-0.13.3}/src/unitx/_immutable.py +0 -0
  41. {unitx-0.13.2 → unitx-0.13.3}/src/unitx/_numerics.py +0 -0
  42. {unitx-0.13.2 → unitx-0.13.3}/src/unitx/_numpy.py +0 -0
  43. {unitx-0.13.2 → unitx-0.13.3}/src/unitx/_numpy_typing.py +0 -0
  44. {unitx-0.13.2 → unitx-0.13.3}/src/unitx/_unicode.py +0 -0
  45. {unitx-0.13.2 → unitx-0.13.3}/src/unitx/affinequantity.py +0 -0
  46. {unitx-0.13.2 → unitx-0.13.3}/src/unitx/affineunit.py +0 -0
  47. {unitx-0.13.2 → unitx-0.13.3}/src/unitx/dimension.py +0 -0
  48. {unitx-0.13.2 → unitx-0.13.3}/src/unitx/factor.py +0 -0
  49. {unitx-0.13.2 → unitx-0.13.3}/src/unitx/math.py +0 -0
  50. {unitx-0.13.2 → unitx-0.13.3}/src/unitx/monomial.py +0 -0
  51. {unitx-0.13.2 → unitx-0.13.3}/src/unitx/py.typed +0 -0
  52. {unitx-0.13.2 → unitx-0.13.3}/src/unitx/quantity.py +0 -0
  53. {unitx-0.13.2 → unitx-0.13.3}/src/unitx/si/__init__.py +0 -0
  54. {unitx-0.13.2 → unitx-0.13.3}/src/unitx/si/bases.py +0 -0
  55. {unitx-0.13.2 → unitx-0.13.3}/src/unitx/si/constants.py +0 -0
  56. {unitx-0.13.2 → unitx-0.13.3}/src/unitx/si/units.py +0 -0
  57. {unitx-0.13.2 → unitx-0.13.3}/src/unitx/singleunit.py +0 -0
  58. {unitx-0.13.2 → unitx-0.13.3}/src/unitx/time.py +0 -0
  59. {unitx-0.13.2 → unitx-0.13.3}/src/unitx/unit.py +0 -0
  60. {unitx-0.13.2 → unitx-0.13.3}/src/unitx/unitparser.py +0 -0
  61. {unitx-0.13.2 → unitx-0.13.3}/src/unitx/unitregistry.py +0 -0
  62. {unitx-0.13.2 → unitx-0.13.3}/tests/__init__.py +0 -0
  63. {unitx-0.13.2 → unitx-0.13.3}/tests/_notebooks.py +0 -0
  64. {unitx-0.13.2 → unitx-0.13.3}/tests/example_typing/check_blackbody.py +0 -0
  65. {unitx-0.13.2 → unitx-0.13.3}/tests/numpy_typing/check_math.py +0 -0
  66. {unitx-0.13.2 → unitx-0.13.3}/tests/numpy_typing/check_numpy.py +0 -0
  67. {unitx-0.13.2 → unitx-0.13.3}/tests/test_affine.py +0 -0
  68. {unitx-0.13.2 → unitx-0.13.3}/tests/test_affinequantity.py +0 -0
  69. {unitx-0.13.2 → unitx-0.13.3}/tests/test_builtin_definitions.py +0 -0
  70. {unitx-0.13.2 → unitx-0.13.3}/tests/test_dimension.py +0 -0
  71. {unitx-0.13.2 → unitx-0.13.3}/tests/test_display.py +0 -0
  72. {unitx-0.13.2 → unitx-0.13.3}/tests/test_examples.py +0 -0
  73. {unitx-0.13.2 → unitx-0.13.3}/tests/test_factor.py +0 -0
  74. {unitx-0.13.2 → unitx-0.13.3}/tests/test_immutable.py +0 -0
  75. {unitx-0.13.2 → unitx-0.13.3}/tests/test_import.py +0 -0
  76. {unitx-0.13.2 → unitx-0.13.3}/tests/test_math.py +0 -0
  77. {unitx-0.13.2 → unitx-0.13.3}/tests/test_math_numpy.py +0 -0
  78. {unitx-0.13.2 → unitx-0.13.3}/tests/test_monomial.py +0 -0
  79. {unitx-0.13.2 → unitx-0.13.3}/tests/test_numeric.py +0 -0
  80. {unitx-0.13.2 → unitx-0.13.3}/tests/test_numpy.py +0 -0
  81. {unitx-0.13.2 → unitx-0.13.3}/tests/test_numpy_affine.py +0 -0
  82. {unitx-0.13.2 → unitx-0.13.3}/tests/test_numpy_differentiation.py +0 -0
  83. {unitx-0.13.2 → unitx-0.13.3}/tests/test_numpy_integration.py +0 -0
  84. {unitx-0.13.2 → unitx-0.13.3}/tests/test_numpy_multi_outputs.py +0 -0
  85. {unitx-0.13.2 → unitx-0.13.3}/tests/test_numpy_mutation.py +0 -0
  86. {unitx-0.13.2 → unitx-0.13.3}/tests/test_numpy_outputs.py +0 -0
  87. {unitx-0.13.2 → unitx-0.13.3}/tests/test_numpy_products.py +0 -0
  88. {unitx-0.13.2 → unitx-0.13.3}/tests/test_numpy_statistics.py +0 -0
  89. {unitx-0.13.2 → unitx-0.13.3}/tests/test_quantity.py +0 -0
  90. {unitx-0.13.2 → unitx-0.13.3}/tests/test_si.py +0 -0
  91. {unitx-0.13.2 → unitx-0.13.3}/tests/test_singleunit.py +0 -0
  92. {unitx-0.13.2 → unitx-0.13.3}/tests/test_siunitx_display.py +0 -0
  93. {unitx-0.13.2 → unitx-0.13.3}/tests/test_time.py +0 -0
  94. {unitx-0.13.2 → unitx-0.13.3}/tests/test_typing.py +0 -0
  95. {unitx-0.13.2 → unitx-0.13.3}/tests/test_unicode.py +0 -0
  96. {unitx-0.13.2 → unitx-0.13.3}/tests/test_unicode_units.py +0 -0
  97. {unitx-0.13.2 → unitx-0.13.3}/tests/test_unit.py +0 -0
  98. {unitx-0.13.2 → unitx-0.13.3}/tests/test_unit_definition.py +0 -0
  99. {unitx-0.13.2 → unitx-0.13.3}/tests/test_unitparser.py +0 -0
  100. {unitx-0.13.2 → unitx-0.13.3}/tests/test_unitregistry.py +0 -0
  101. {unitx-0.13.2 → unitx-0.13.3}/tests/test_warnings.py +0 -0
  102. {unitx-0.13.2 → unitx-0.13.3}/tests/typing/check_affine.py +0 -0
  103. {unitx-0.13.2 → unitx-0.13.3}/tests/typing/check_math_time.py +0 -0
  104. {unitx-0.13.2 → unitx-0.13.3}/tests/typing/check_quantity.py +0 -0
  105. {unitx-0.13.2 → unitx-0.13.3}/tests/typing/check_si.py +0 -0
@@ -2,6 +2,19 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.13.3] - 2026-10-22
6
+
7
+ ### Added
8
+
9
+ - `unitx.serialization.to_dict()` / `from_dict()` exchange scalar Quantities,
10
+ affine points and their units as versioned JSON-compatible records. Preserve
11
+ int/float/Fraction/Decimal magnitudes, exact extra scales, formal unit identity
12
+ and coordinate references; validate existing registry meanings without
13
+ registering units. NumPy, complex magnitudes and subclasses are not supported.
14
+ - Add `SerializationError` for invalid records and failed restoration, and an
15
+ independent `VersionError` for unsupported explicit versions. Both derive
16
+ from `UnitxError` and `ValueError`; decoding preserves underlying causes.
17
+
5
18
  ## [0.13.2] - 2026-10-22
6
19
 
7
20
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: unitx
3
- Version: 0.13.2
3
+ Version: 0.13.3
4
4
  Summary: Physical units, quantities, and unit conversion with exact unit algebra, type hints, and NumPy integration.
5
5
  Project-URL: Homepage, https://github.com/DaitTHU/unitx
6
6
  Project-URL: Documentation, https://github.com/DaitTHU/unitx/blob/main/docs/README.md
@@ -213,7 +213,7 @@ from matplotlib import pyplot as plt
213
213
  from unitx.si import SI, si
214
214
 
215
215
  wavelength = np.linspace(0.01, 3, 100)[:, None] * si.um
216
- temperature = np.array([3000, 4000, 5000]) * si.K
216
+ temperature = np.asarray([3000, 4000, 5000]) * si.K
217
217
  exponent = SI.h * SI.c / (wavelength * SI.kB * temperature)
218
218
  radiance = 2 * SI.h * SI.c**2 / (wavelength**5 * np.expm1(exponent))
219
219
  radiance = radiance.to('kW/sr/m2/nm')
@@ -185,7 +185,7 @@ from matplotlib import pyplot as plt
185
185
  from unitx.si import SI, si
186
186
 
187
187
  wavelength = np.linspace(0.01, 3, 100)[:, None] * si.um
188
- temperature = np.array([3000, 4000, 5000]) * si.K
188
+ temperature = np.asarray([3000, 4000, 5000]) * si.K
189
189
  exponent = SI.h * SI.c / (wavelength * SI.kB * temperature)
190
190
  radiance = 2 * SI.h * SI.c**2 / (wavelength**5 * np.expm1(exponent))
191
191
  radiance = radiance.to('kW/sr/m2/nm')
@@ -16,6 +16,7 @@ reading about an older version.
16
16
  | Call unit-aware mathematical functions or read a clock | [Standard-library enhancements](stdlib.md) |
17
17
  | Calculate with arrays, masks, reductions and output buffers | [NumPy support](numpy.md) |
18
18
  | Format text or export LaTeX, HTML and siunitx | [Display and formatting](display.md) |
19
+ | Save and restore scalar quantities as JSON-compatible records | [Scalar data exchange](serialization.md) |
19
20
  | Understand annotations and NumPy inference limits | [Typing](typing.md) |
20
21
  | Update code written for an earlier release | [Migration notes](migration.md) |
21
22
  | Run scientific examples | [Physics notebooks](../examples/README.md) |
@@ -0,0 +1,225 @@
1
+ # Scalar data exchange
2
+
3
+ `unitx.serialization.to_dict()` returns a versioned record made of dictionaries,
4
+ lists, strings, null and an integer format version. `from_dict()` restores it.
5
+ Use Python's JSON functions, or another transport for the same data structure:
6
+
7
+ ```pycon
8
+ >>> import json
9
+ >>> from decimal import Decimal
10
+ >>> from fractions import Fraction
11
+ >>> from unitx import AffineQuantity, AffineUnit, Quantity, Unit
12
+ >>> from unitx.serialization import from_dict, to_dict
13
+ >>> original = Quantity(Fraction(1, 3), 'cm')
14
+ >>> text = json.dumps(to_dict(original), allow_nan=False)
15
+ >>> restored = from_dict(json.loads(text))
16
+ >>> type(restored.value) is Fraction
17
+ True
18
+ >>> restored.value, restored.unit
19
+ (Fraction(1, 3), Unit('cm'))
20
+ >>> restored == original
21
+ True
22
+
23
+ ```
24
+
25
+ The module supports `Unit`, `ScaledUnit`, `AffineUnit`, scalar `Quantity` and
26
+ scalar `AffineQuantity`. Import the two functions from `unitx.serialization`;
27
+ they are not package-root exports. No optional dependency is required.
28
+
29
+ ## Stored numbers
30
+
31
+ Magnitudes must be exact instances of Python `int`, `float`, `Fraction` or
32
+ `Decimal`. Booleans, complex numbers, numeric subclasses, custom magnitudes,
33
+ NumPy scalars and arrays are rejected with TypeError. A round trip preserves
34
+ the stored magnitude's type and value without converting its unit.
35
+
36
+ | Type tag | `value` field | Meaning |
37
+ | --- | --- | --- |
38
+ | `int` | A canonical decimal integer string | Retains integers beyond the exact range of a JSON consumer's floating-point numbers |
39
+ | `fraction` | Two integer strings, `[numerator, denominator]` | Exact rational, with positive denominator |
40
+ | `float` | `float.hex()` text | Exact finite binary value, including subnormals and signed zero; `inf`, `-inf` and `nan` are also supported |
41
+ | `decimal` | Decimal text | Retains sign, coefficient, trailing zeros and exponent; infinities and NaNs are supported |
42
+
43
+ Float NaNs preserve their category; NaN payload bits and sign are not part of
44
+ the format. Decimal NaN payloads and signaling status are retained. Export
45
+ and import do not round a Decimal magnitude in the active context. Invalid
46
+ Decimal strings are rejected even when the caller disables InvalidOperation
47
+ traps, without setting flags in the caller's context.
48
+
49
+ All numerical data except the format version use strings. Consequently strict
50
+ JSON encoding with `allow_nan=False` works even for nonfinite magnitudes.
51
+ Python's normal integer-string conversion and Decimal representation limits
52
+ still apply.
53
+
54
+ ## Units and points
55
+
56
+ The format preserves the formal product, including prefixes and rational
57
+ exponents. Equal physical scale does not merge distinct units such as Hz and
58
+ Bq, and `km/m` retains its nontrivial dimensionless scale. A ScaledUnit stores
59
+ its additional Factor separately, including when that Factor is one:
60
+
61
+ ```pycon
62
+ >>> from unitx import pi
63
+ >>> from unitx.unit import ScaledUnit
64
+ >>> scaled = ScaledUnit('m', pi ** Fraction(1, 3))
65
+ >>> rebuilt = from_dict(json.loads(json.dumps(to_dict(scaled))))
66
+ >>> type(rebuilt) is ScaledUnit and rebuilt.scale == scaled.scale
67
+ True
68
+ >>> rebuilt**3 == scaled**3
69
+ True
70
+
71
+ ```
72
+
73
+ An AffineUnit stores its physical origin, difference unit and logarithmic kind.
74
+ Registered coordinates also retain their canonical registered symbol;
75
+ anonymous coordinates remain anonymous. Points stay AffineQuantity objects:
76
+
77
+ ```pycon
78
+ >>> point = AffineQuantity(Decimal('20.00'), 'degC')
79
+ >>> rebuilt = from_dict(json.loads(json.dumps(to_dict(point))))
80
+ >>> type(rebuilt) is AffineQuantity and rebuilt.unit == point.unit
81
+ True
82
+ >>> rebuilt.value.as_tuple() == point.value.as_tuple()
83
+ True
84
+ >>> epoch = AffineUnit(Quantity(946684800, 's'), delta_unit='s')
85
+ >>> custom = AffineQuantity(Fraction(1, 2), epoch)
86
+ >>> from_dict(to_dict(custom)).unit == epoch
87
+ True
88
+
89
+ ```
90
+
91
+ This preserves the existing model's information. It does not add clock identity,
92
+ timezone or source labels to time points. Point arithmetic and the existing
93
+ hashability rules are unchanged.
94
+
95
+ ## Registry requirements
96
+
97
+ Import resolves spellings against the receiving process's existing registry.
98
+ Register custom units explicitly during application initialization before
99
+ decoding records that refer to them. Unknown spellings raise SerializationError
100
+ with the original UnitSymbolError retained as `__cause__`.
101
+ Decoding never registers or replaces definitions, and neither function changes
102
+ the input object or record.
103
+
104
+ A linear unit record contains its canonical formal identity and its combined
105
+ dimension, SI Factor and logarithmic degree. Import checks these against the
106
+ resolved unit. A registered coordinate must also match the saved physical
107
+ origin, its formal reference and difference units, and logarithmic kind.
108
+ Conflicts raise SerializationError rather than silently changing the result.
109
+
110
+ These checks describe the resulting unit, not a complete registry snapshot.
111
+ They do not compare names, aliases, prefix policies or each individual unit's
112
+ conversion metadata when changes cancel within a product. The receiving
113
+ application remains responsible for compatible unit definitions. Factor checks
114
+ compare exact formal state: a changed symbolic approximation or a different
115
+ unresolved-integer decomposition can cause rejection even if physical values
116
+ are approximately equal. No floating-point tolerance is used to infer equality.
117
+
118
+ ## Version 1 record format
119
+
120
+ Only the outermost object has `"version": 1`; every object record has a `type`.
121
+ For example, an integer length is represented as:
122
+
123
+ ```json
124
+ {
125
+ "version": 1,
126
+ "type": "quantity",
127
+ "value": {"type": "int", "value": "3"},
128
+ "unit": {
129
+ "type": "unit",
130
+ "expression": "m",
131
+ "identity": [["", "m", "1", "1"]],
132
+ "dimension": [["0", "1"], ["1", "1"], ["0", "1"], ["0", "1"], ["0", "1"], ["0", "1"], ["0", "1"]],
133
+ "factor": [],
134
+ "log_degree": ["0", "1"]
135
+ }
136
+ }
137
+ ```
138
+
139
+ The object records have these exact fields:
140
+
141
+ | `type` | Fields in addition to `type` |
142
+ | --- | --- |
143
+ | `quantity`, `affine_quantity` | `value` scalar record, `unit` linear/affine unit record respectively |
144
+ | `unit` | `expression`, `identity`, `dimension`, `factor`, `log_degree` |
145
+ | `scaled_unit` | The unit fields plus `scale`, a Factor component list |
146
+ | `affine_unit` | `origin` linear Quantity record, `delta_unit` linear unit record, `kind` null/`power`/`amplitude`, `symbol` canonical registered string or null |
147
+
148
+ `identity` is a sorted list of `[prefix_symbol, unprefixed_symbol, numerator,
149
+ denominator]` terms; the last two strings encode the nonzero exponent.
150
+ `dimension` lists seven rational pairs in T, L, M, I, Theta, N, J order.
151
+ `log_degree` is another rational pair.
152
+
153
+ Factor component lists preserve the formal integer/symbolic bases and rational
154
+ exponents. The empty list represents one. Each component has an `exponent`
155
+ pair and one of these base descriptions:
156
+
157
+ | Component `type` | Other fields |
158
+ | --- | --- |
159
+ | `prime` | `value`: prime integer string |
160
+ | `symbolic` | `symbol`: literal label, `value`: positive finite float.hex() approximation |
161
+ | `unfactored` | `value`: unresolved integer string, `limit`: stored factorization-limit integer string |
162
+
163
+ Symbolic labels are reconstructed as SymbolicConstant objects, never evaluated
164
+ as code. Their mathematical construction requirements remain the caller's
165
+ responsibility. Unresolved bases keep the existing incomplete-factorization
166
+ limitations and emit IncompleteFactorWarning on reconstruction.
167
+
168
+ Unknown versions, tags, missing or extra fields and invalid nested records are
169
+ rejected. The version is independent of the package release number; future
170
+ incompatible record changes require a new format version. This is a data
171
+ format, not a Python class/subclass reconstruction protocol. No `eval`, dynamic
172
+ class imports, JSON encoder subclasses, file I/O or configurable codec registry
173
+ are involved. Applications parsing external input should apply their own size
174
+ and resource limits.
175
+
176
+ ## Errors
177
+
178
+ Import the error classes from `unitx.exceptions`. The public boundaries are:
179
+
180
+ | Operation and failure | Exception |
181
+ | --- | --- |
182
+ | `to_dict()` receives an unsupported object or magnitude | TypeError |
183
+ | `from_dict()` receives a non-dictionary top-level input | TypeError |
184
+ | A record specifies an unsupported integer format version | VersionError |
185
+ | A record has no version or the version is not an integer | SerializationError |
186
+ | Invalid fields, missing definitions, conflicting unit meanings or failed reconstruction | SerializationError |
187
+
188
+ VersionError and SerializationError both inherit UnitxError and ValueError.
189
+ They are independent classes: VersionError is available for explicit version
190
+ checks elsewhere in unitx as well. It currently describes the record format
191
+ version, not the installed package version. Catch the two classes separately
192
+ for different recovery actions, or catch UnitxError for both:
193
+
194
+ ```pycon
195
+ >>> from unitx.exceptions import SerializationError, UnitxError, VersionError
196
+ >>> try:
197
+ ... from_dict({'version': 2})
198
+ ... except VersionError:
199
+ ... print('Unsupported record version')
200
+ Unsupported record version
201
+ >>> try:
202
+ ... from_dict({'version': 1, 'type': 'unknown'})
203
+ ... except SerializationError:
204
+ ... print('Invalid record')
205
+ Invalid record
206
+
207
+ ```
208
+
209
+ Underlying numeric, parsing and coordinate exceptions, including
210
+ UnitSymbolError, UnitSyntaxError and DimensionError, are retained as
211
+ `error.__cause__` when wrapped. Explicit record-validation errors use
212
+ SerializationError directly. Exceptions outside the expected validation
213
+ families, such as RuntimeError, are not wrapped.
214
+
215
+ The return type of `from_dict()` is a union of the supported object classes;
216
+ use `isinstance` when code requires a specific class. Its scalar magnitude type
217
+ is the supported union, since arbitrary input data cannot statically determine
218
+ the original numerical type.
219
+
220
+ Numerical encodings follow the Python documentation for
221
+ [float hexadecimal conversion](https://docs.python.org/3/library/stdtypes.html#float.hex),
222
+ [Decimal construction](https://docs.python.org/3/library/decimal.html#decimal.Decimal)
223
+ and [JSON data exchange](https://docs.python.org/3/library/json.html).
224
+
225
+ [Documentation index](README.md) · [Scalar behavior](scalars.md) · [Custom units](custom-units.md)
@@ -26,6 +26,11 @@ for its array overloads. `unitx.time` clocks return `AffineQuantity[int]`;
26
26
  duration adapters use ordinary Quantity objects. `time.from_datetime()` returns
27
27
  `AffineQuantity[int]`; `time.to_datetime()` returns an aware datetime.
28
28
 
29
+ `unitx.serialization.from_dict()` returns a union of the supported unit and
30
+ scalar quantity classes. Use `isinstance` to narrow it. The magnitude type is
31
+ `int | float | Fraction | Decimal`; the input record determines its runtime type.
32
+ See [scalar data exchange](serialization.md).
33
+
29
34
  A signature such as `Quantity[float]` does not require metres rather than
30
35
  seconds. An `Annotated` label alone does not teach a type checker dimensional
31
36
  algebra. unitx does not currently supply dimension-parameterized quantities
@@ -4,7 +4,7 @@ build-backend = 'hatchling.build'
4
4
 
5
5
  [project]
6
6
  name = 'unitx'
7
- version = '0.13.2'
7
+ version = '0.13.3'
8
8
  description = 'Physical units, quantities, and unit conversion with exact unit algebra, type hints, and NumPy integration.'
9
9
  readme = 'README.md'
10
10
  requires-python = '>=3.12'
@@ -3,6 +3,7 @@
3
3
  "src/unitx/_display.py",
4
4
  "src/unitx/math.py",
5
5
  "src/unitx/time.py",
6
+ "src/unitx/serialization.py",
6
7
  "src/unitx/affineunit.py",
7
8
  "src/unitx/affinequantity.py",
8
9
  "src/unitx/_conversion.py",
@@ -18,6 +19,7 @@
18
19
  "tests/typing"
19
20
  ],
20
21
  "strict": [
22
+ "src/unitx/serialization.py",
21
23
  "src/unitx/quantity.py",
22
24
  "tests/typing",
23
25
  "tests/numpy_typing",
@@ -7,12 +7,14 @@ __all__ = [
7
7
  'DimensionError',
8
8
  'IncompleteFactorWarning',
9
9
  'InexactFloatWarning',
10
+ 'SerializationError',
10
11
  'UnitRegistryError',
11
12
  'UnitSymbolError',
12
13
  'UnitSymbolWarning',
13
14
  'UnitSyntaxError',
14
15
  'UnitxError',
15
16
  'UnitxWarning',
17
+ 'VersionError',
16
18
  ]
17
19
 
18
20
 
@@ -36,6 +38,14 @@ class UnitRegistryError(UnitxError):
36
38
  """Exception raised for errors in the unit registry."""
37
39
 
38
40
 
41
+ class SerializationError(UnitxError, ValueError):
42
+ """A serialized record has an invalid format or cannot be restored."""
43
+
44
+
45
+ class VersionError(UnitxError, ValueError):
46
+ """An explicitly specified version is unsupported by the operation."""
47
+
48
+
39
49
  # Python 3.12 warnings.warn() new API
40
50
  _WARNING_SKIP_PREFIXES: Final = (os.path.dirname(__file__) + os.sep,)
41
51