unitx 0.3.0__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. unitx-0.5.0/.github/workflows/ci.yml +50 -0
  2. unitx-0.5.0/.github/workflows/release.yml +113 -0
  3. {unitx-0.3.0 → unitx-0.5.0}/.gitignore +3 -3
  4. unitx-0.5.0/CHANGELOG.md +103 -0
  5. unitx-0.5.0/PKG-INFO +338 -0
  6. unitx-0.5.0/README.md +316 -0
  7. unitx-0.5.0/conftest.py +21 -0
  8. unitx-0.5.0/pyproject.toml +47 -0
  9. unitx-0.5.0/pyrightconfig.json +16 -0
  10. unitx-0.5.0/src/unitx/__init__.py +1 -0
  11. unitx-0.5.0/src/unitx/_numerics.py +289 -0
  12. unitx-0.5.0/src/unitx/_unicode.py +42 -0
  13. unitx-0.5.0/src/unitx/_unit_data.py +173 -0
  14. unitx-0.5.0/src/unitx/dimension.py +175 -0
  15. unitx-0.5.0/src/unitx/exceptions.py +56 -0
  16. unitx-0.5.0/src/unitx/factor.py +531 -0
  17. unitx-0.5.0/src/unitx/monomial.py +204 -0
  18. unitx-0.5.0/src/unitx/quantity.py +379 -0
  19. unitx-0.5.0/src/unitx/singleunit.py +204 -0
  20. unitx-0.5.0/src/unitx/unit.py +376 -0
  21. unitx-0.5.0/src/unitx/unitparser.py +348 -0
  22. unitx-0.5.0/tests/__init__.py +0 -0
  23. unitx-0.5.0/tests/test_dimension.py +286 -0
  24. unitx-0.5.0/tests/test_factor.py +1059 -0
  25. unitx-0.5.0/tests/test_import.py +5 -0
  26. unitx-0.5.0/tests/test_monomial.py +223 -0
  27. unitx-0.5.0/tests/test_numeric.py +244 -0
  28. unitx-0.5.0/tests/test_quantity.py +452 -0
  29. unitx-0.5.0/tests/test_singleunit.py +297 -0
  30. unitx-0.5.0/tests/test_typing.py +31 -0
  31. unitx-0.5.0/tests/test_unicode.py +38 -0
  32. unitx-0.5.0/tests/test_unit.py +537 -0
  33. unitx-0.5.0/tests/test_unit_data.py +90 -0
  34. unitx-0.5.0/tests/test_unit_definition.py +432 -0
  35. unitx-0.5.0/tests/test_unitparser.py +587 -0
  36. unitx-0.5.0/tests/test_warnings.py +182 -0
  37. unitx-0.5.0/tests/typing/check_quantity.py +182 -0
  38. unitx-0.3.0/PKG-INFO +0 -46
  39. unitx-0.3.0/README.md +0 -2
  40. unitx-0.3.0/pyproject.toml +0 -45
  41. unitx-0.3.0/src/unitx/__init__.py +0 -7
  42. unitx-0.3.0/src/unitx/_data/prefix.py +0 -65
  43. unitx-0.3.0/src/unitx/_data/unit.py +0 -44
  44. unitx-0.3.0/src/unitx/dimension.py +0 -132
  45. unitx-0.3.0/src/unitx/exceptions.py +0 -30
  46. unitx-0.3.0/src/unitx/factor.py +0 -298
  47. unitx-0.3.0/src/unitx/monomial.py +0 -251
  48. unitx-0.3.0/src/unitx/quantity.py +0 -128
  49. unitx-0.3.0/src/unitx/singleunit.py +0 -116
  50. unitx-0.3.0/src/unitx/unit.py +0 -86
  51. unitx-0.3.0/src/unitx/unitparser.py +0 -184
  52. unitx-0.3.0/src/unitx/utils/number.py +0 -121
  53. unitx-0.3.0/src/unitx/utils/special_char.py +0 -41
  54. unitx-0.3.0/src/unitx/utils/valuetype.py +0 -15
  55. unitx-0.3.0/tests/test_dimension.py +0 -81
  56. unitx-0.3.0/tests/test_factor.py +0 -113
  57. unitx-0.3.0/tests/test_monomial.py +0 -113
  58. unitx-0.3.0/tests/test_quantity.py +0 -105
  59. unitx-0.3.0/tests/test_singleunit.py +0 -72
  60. unitx-0.3.0/tests/test_unit.py +0 -50
  61. unitx-0.3.0/tests/test_unitparser.py +0 -123
  62. {unitx-0.3.0 → unitx-0.5.0}/LICENSE +0 -0
  63. /unitx-0.3.0/src/unitx/utils/__init__.py → /unitx-0.5.0/src/unitx/py.typed +0 -0
@@ -0,0 +1,50 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+ workflow_dispatch:
9
+
10
+ permissions:
11
+ contents: read
12
+
13
+ jobs:
14
+ test:
15
+ name: Test Python ${{ matrix.python-version }} (${{ matrix.backend }})
16
+ runs-on: ubuntu-latest
17
+
18
+ strategy:
19
+ fail-fast: false
20
+ matrix:
21
+ python-version: ["3.12", "3.13", "3.14", "3.15"]
22
+ backend: ["builtin", "sympy"]
23
+
24
+ steps:
25
+ - name: Checkout repository
26
+ uses: actions/checkout@v6
27
+
28
+ - name: Set up Python
29
+ uses: actions/setup-python@v7
30
+ with:
31
+ python-version: ${{ matrix.python-version }}
32
+ cache: pip
33
+ cache-dependency-path: pyproject.toml
34
+
35
+ - name: Install package and test dependencies
36
+ run: |
37
+ python -m pip install --upgrade pip
38
+ python -m pip install -e ".[test]"
39
+
40
+ - name: Install SymPy
41
+ if: matrix.backend == 'sympy'
42
+ run: python -m pip install sympy
43
+
44
+ - name: Run Ruff
45
+ run: |
46
+ ruff check .
47
+ ruff format --check .
48
+
49
+ - name: Run tests
50
+ run: pytest -q
@@ -0,0 +1,113 @@
1
+ name: Release
2
+
3
+ # Configure a PyPI Trusted Publisher for DaitTHU/unitx, release.yml, and the
4
+ # GitHub environment named pypi.
5
+ # Manual runs validate distributions; publishing a GitHub release uploads them.
6
+ on:
7
+ release:
8
+ types: [published]
9
+ workflow_dispatch:
10
+
11
+ permissions:
12
+ contents: read
13
+
14
+ jobs:
15
+ build:
16
+ name: Validate and build distributions
17
+ runs-on: ubuntu-latest
18
+
19
+ steps:
20
+ - name: Checkout repository
21
+ uses: actions/checkout@v6
22
+ with:
23
+ persist-credentials: false
24
+
25
+ - name: Set up Python
26
+ uses: actions/setup-python@v7
27
+ with:
28
+ python-version: "3.12"
29
+
30
+ - name: Check release version
31
+ id: version
32
+ env:
33
+ RELEASE_TAG: ${{ github.event.release.tag_name }}
34
+ run: |
35
+ python - <<'PY'
36
+ import os
37
+ import tomllib
38
+ from pathlib import Path
39
+
40
+ version = tomllib.loads(Path('pyproject.toml').read_text())['project']['version']
41
+ tag = os.environ.get('RELEASE_TAG', '')
42
+ if tag and tag != f'v{version}':
43
+ raise SystemExit(f'Release tag {tag!r} does not match project version {version!r}.')
44
+ with Path(os.environ['GITHUB_OUTPUT']).open('a') as output:
45
+ output.write(f'version={version}\n')
46
+ print(f'Building unitx {version}')
47
+ PY
48
+
49
+ - name: Install build and validation dependencies
50
+ run: python -m pip install -e ".[test]" build twine
51
+
52
+ - name: Validate source
53
+ run: |
54
+ python -m ruff check .
55
+ python -m ruff format --check .
56
+ python -m pytest -q
57
+
58
+ - name: Build wheel and source distribution
59
+ run: python -m build
60
+
61
+ - name: Check distribution metadata
62
+ run: python -m twine check --strict dist/*
63
+
64
+ - name: Install built wheel
65
+ run: python -m pip install --force-reinstall --no-deps dist/*.whl
66
+
67
+ - name: Check installed package
68
+ working-directory: ${{ runner.temp }}
69
+ env:
70
+ EXPECTED_VERSION: ${{ steps.version.outputs.version }}
71
+ run: |
72
+ python - <<'PY'
73
+ import os
74
+ from fractions import Fraction
75
+ from importlib.metadata import version
76
+ from importlib.resources import files
77
+
78
+ from unitx.quantity import Quantity
79
+ from unitx.unit import Unit
80
+
81
+ assert version('unitx') == os.environ['EXPECTED_VERSION']
82
+ assert files('unitx').joinpath('py.typed').is_file()
83
+ assert Quantity(1, 'cm').to('m').value == Fraction(1, 100)
84
+ assert 2 * Unit('m') == Quantity(200, 'cm')
85
+ PY
86
+
87
+ - name: Store distributions
88
+ uses: actions/upload-artifact@v5
89
+ with:
90
+ name: distributions
91
+ path: dist/*
92
+ if-no-files-found: error
93
+
94
+ publish:
95
+ name: Publish to PyPI
96
+ if: github.event_name == 'release'
97
+ needs: build
98
+ runs-on: ubuntu-latest
99
+ environment:
100
+ name: pypi
101
+ url: https://pypi.org/p/unitx
102
+ permissions:
103
+ id-token: write
104
+
105
+ steps:
106
+ - name: Download distributions
107
+ uses: actions/download-artifact@v6
108
+ with:
109
+ name: distributions
110
+ path: dist/
111
+
112
+ - name: Publish distributions
113
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -199,7 +199,7 @@ cython_debug/
199
199
  # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
200
200
  # and can be added to the global gitignore or merged into this file. However, if you prefer,
201
201
  # you could uncomment the following to ignore the entire vscode folder
202
- # .vscode/
202
+ .vscode/
203
203
  # Temporary file for partial code execution
204
204
  tempCodeRunnerFile.py
205
205
 
@@ -217,5 +217,5 @@ __marimo__/
217
217
  # Streamlit
218
218
  .streamlit/secrets.toml
219
219
 
220
- # vscode settings
221
- .vscode/
220
+ # macOS
221
+ .DS_Store
@@ -0,0 +1,103 @@
1
+ # Changelog
2
+
3
+ ## [0.5.0] - 2026-10-17
4
+
5
+ ### Added
6
+
7
+ - Add `ScaledUnit` in `unitx.unit` for formal unit products with an additional exact positive scale. Multiplying or dividing a `Factor` or `SymbolicConstant` by a unit constructs a `ScaledUnit`.
8
+ - Preserve explicit scales through unit multiplication, division, rational powers, quantity conversion, readable display, reconstructible representations, copying, and pickling. A scale of one compares and hashes equal to the corresponding ordinary `Unit`.
9
+ - Add `Unit.expression()` to return the formal unit product in parser syntax, without any additional `ScaledUnit` scale.
10
+ - Add `.define()` on `Unit`, `ScaledUnit`, and `Quantity` objects to register units in the current process. Definitions return an ordinary `Unit` with its own formal identity and the source's physical dimension, incorporating quantity magnitudes into the registered SI factor without changing the source object.
11
+ - Require a nonempty primary symbol and name, with optional symbol aliases, name aliases, and configurable SI prefix support. All spellings must be single printable unit tokens; empty aliases are ignored.
12
+ - Add `UnitRegistryError` for conflicts with existing unit spellings or ambiguous prefix meanings. Validate symbols, names, aliases, and generated prefixed forms before changing the registry.
13
+ - Accept positive finite int, Fraction, Decimal, and float magnitudes for quantity definitions. Fraction and Decimal scales stay exact; float scales follow Factor's denominator limit and can warn or round to zero and fail. Restoring custom units in another process requires their definitions to be registered there.
14
+
15
+ ### Changed
16
+
17
+ - Resolve dynamically registered unit symbols, names, aliases, and prefixed forms without relying on import-time unit-length limits.
18
+ - Display quantities with an explicit scale other than one using parentheses around the scaled unit, without numerically evaluating symbolic scales.
19
+ - Escape symbols in `SingleUnit.__repr__` so registered symbols containing quotes or backslashes can be reconstructed.
20
+
21
+ ## [0.4.0] - 2026-10-16
22
+
23
+ Replace the implementation previously published on PyPI as 0.3.0. The 0.0.x
24
+ entries below retain this repository's development milestones.
25
+
26
+ ### Changed
27
+
28
+ - Require Python 3.12 or later; Python 3.11 is no longer supported.
29
+ - Use module imports for `Quantity`, `Unit`, `Dimension`, and `DIMENSIONLESS`; the former top-level exports and `unitx.utils` namespace are no longer provided.
30
+ - Remove `Unit` length and special truth-value behavior. Check formal identity with `unit == Unit('')`, or physical dimensions with `unit.is_dimensionless()`.
31
+ - Compare and hash scalar quantities using exact rational coefficients and remaining irrational unit factors. Finite floats retain their stored binary rational values for equality, without a tolerance.
32
+ - Convert finite Decimal magnitudes exactly to Fraction for scaling and arithmetic, independent of the Decimal precision context; numerical results can change type.
33
+ - Normalize quantity exponents once for both the magnitude and unit, with approximate native evaluation for nonintegral magnitude powers.
34
+
35
+ ### Fixed
36
+
37
+ - Interpret plain numbers as unscaled values when adding to or subtracting from scaled dimensionless quantities.
38
+ - Preserve the original magnitude and type when multiplying or dividing a quantity by a Unit.
39
+ - Cancel common irrational factors before evaluating quantity ordering, retaining exact ordering when the ratio is rational.
40
+
41
+ ### Added
42
+
43
+ - Support Fraction and Decimal scalar multiplication and division alongside int and float.
44
+ - Distribute inline type information with `py.typed` and check numerical and custom scalar contracts with Pyright.
45
+ - Add a release workflow that validates version tags, checks distributions and the installed wheel, and publishes GitHub releases through PyPI Trusted Publishing.
46
+ - Document construction, conversion, unit syntax, numerical rules, supported units, and migration from 0.3.0. NumPy integration remains planned and is not supported in this release.
47
+
48
+ ## [0.0.7] - 2026-10-15
49
+
50
+ - Add `Unit` with string construction for compound unit expressions, including registered symbols, names, prefixes, and aliases.
51
+ - Support unit multiplication and division, rational powers, and cancellation while preserving exact rational exponents.
52
+ - Expose unit symbols, coherent SI scaling factors, physical dimensions, and dimensionless checks; dimensionless expressions can retain nontrivial scaling factors.
53
+ - Define equality and hashing by formal unit factors regardless of insertion order, preserving distinct prefixed unit identities.
54
+ - Provide readable Unicode display and reconstructible `repr` strings for compound units and rational powers.
55
+ - Add executable usage examples and regression tests for alias normalization, equality and hashing, exact and approximate factors, dimensions, power cancellation, conversion round trips, state preservation, and invalid inputs.
56
+
57
+ ## [0.0.6] - 2026-10-15
58
+
59
+ - Add `UnitParser` to parse unit expressions into `Monomial[SingleUnit]`, resolving symbols, names, prefixes, and aliases through `SingleUnit`.
60
+ - Support explicit and implicit multiplication, division, parentheses, and exact rational powers using adjacent numbers, `^`, or `**`, including decimal exponents and parenthesized fractions.
61
+ - Support empty and dimensionless expressions, the literal identity `1` in products such as `m 1/s`, and single-factor leading reciprocals such as `/s` and `/(m s)`.
62
+ - Cache successful parse results so repeated calls return the same object, while allowing failed attempts to be retried.
63
+ - Add `UnitSyntaxError` with diagnostics for malformed expressions, invalid numeric bases, zero fraction denominators, and redundant suffixes.
64
+ - Use spaces for multiplication in ambiguous prefix warning suggestions, matching the supported expression syntax.
65
+ - Add grammar documentation, executable examples, and regression tests for exact parsing, whitespace, diagnostics, caching, retries, and warning behavior and attribution.
66
+
67
+ ## [0.0.5] - 2026-10-15
68
+
69
+ - Add built-in SI prefix data and initial unit definitions for `s`, `m`, `g`, `A`, `K`, `mol`, and `cd`, plus the dimensionless unit, with exact factors, dimensions, and prefix restrictions.
70
+ - Add immutable, hashable `SingleUnit` with symbol and name parsing, canonical identity, dimension and factor queries, prefix inspection and removal, copying, and serialization.
71
+ - Support symbol and name aliases, including `meter`, micro prefix variants `u` and `μ`, and the uppercase `K` alias for kilo, while preserving canonical symbols and names.
72
+ - Add `UnitSymbolError` for invalid unit inputs and `UnitSymbolWarning` for potentially misleading prefix aliases, with warnings attributed to user call sites.
73
+ - Add `Factor` hashing consistent with equality to rational numbers and individual `SymbolicConstant` instances.
74
+ - Add regression tests for unit definitions, alias normalization, exact factors, immutability, serialization, and warning behavior, filtering, and attribution.
75
+
76
+ ## [0.0.4] - 2026-10-15
77
+
78
+ - Add `Factor` for exact positive scaling expressions with rational exponents, multiplicative arithmetic, and exact rational or approximate float evaluation, along with predefined `ONE`, `TWO`, and `TEN` instances.
79
+ - Add immutable `SymbolicConstant` with symbolic identity and float approximations, along with the predefined `PI` constant.
80
+ - Support factor decomposition, compact formatting, and magnitude scaling that converts finite `Decimal` magnitudes exactly before multiplication.
81
+ - Provide built-in bounded integer factorization and primality testing, optional SymPy support, and `IncompleteFactorWarning` for unresolved factorization or unproven primality.
82
+ - Display negative `Monomial` powers as denominator terms, with parentheses around multiple denominator factors.
83
+ - Attribute numerical warnings to user call sites without manually passing warning stack levels.
84
+ - Raise the minimum supported Python version from 3.11 to 3.12.
85
+ - Extend CI to test both built-in and SymPy backends on Python 3.12–3.15.
86
+
87
+ ## [0.0.3] - 2026-10-14
88
+
89
+ - Add immutable, hashable `Monomial` for formal products with rational exponents.
90
+ - Support multiplication, division, rational powers, inverses, and numerator/denominator decomposition.
91
+ - Provide explicit exponent queries, component views, Unicode formatting, and exact representation round trips.
92
+ - Add tests for monomial arithmetic, normalization, immutability, hashing, copying, and serialization.
93
+
94
+ ## [0.0.2] - 2026-10-13
95
+
96
+ - Add immutable physical dimensions with rational exponents and dimensionless normalization.
97
+ - Support dimension arithmetic, exponent aliases, and exact representation round trips.
98
+ - Add numerical conversion utilities with inexact float warnings and Unicode exponent formatting.
99
+ - Add tests for dimensions, numerical conversion, formatting, copying, and serialization.
100
+
101
+ ## [0.0.1] - 2026-10-13
102
+
103
+ - Project initialization and setup.
unitx-0.5.0/PKG-INFO ADDED
@@ -0,0 +1,338 @@
1
+ Metadata-Version: 2.5
2
+ Name: unitx
3
+ Version: 0.5.0
4
+ Summary: A modern, intuitive Python library for units, quantities, and dimensional analysis.
5
+ Project-URL: Homepage, https://github.com/DaitTHU/unitx
6
+ Project-URL: Issues, https://github.com/DaitTHU/unitx/issues
7
+ Author: Dait
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Programming Language :: Python :: 3.14
14
+ Classifier: Programming Language :: Python :: 3.15
15
+ Classifier: Topic :: Scientific/Engineering :: Physics
16
+ Requires-Python: >=3.12
17
+ Provides-Extra: test
18
+ Requires-Dist: pyright>=1.1.414; extra == 'test'
19
+ Requires-Dist: pytest>=8; extra == 'test'
20
+ Requires-Dist: ruff>=0.8; extra == 'test'
21
+ Description-Content-Type: text/markdown
22
+
23
+ # unitx
24
+
25
+ Physical units, scalar quantities, and dimensional analysis for Python, with
26
+ exact rational unit scales and exponents.
27
+
28
+ unitx is in early development. Public APIs may change during the 0.x series.
29
+
30
+ ## Installation
31
+
32
+ Requires **Python 3.12 or later**, with no mandatory runtime dependencies.
33
+
34
+ Install a published release:
35
+
36
+ ```sh
37
+ python -m pip install unitx
38
+ ```
39
+
40
+ To use the current development version, run this from the repository root:
41
+
42
+ ```sh
43
+ python -m pip install -e .
44
+ ```
45
+
46
+ This README describes the current source tree, including unreleased changes.
47
+ For a published release, consult the README at its matching Git tag.
48
+
49
+ ## Upgrading from 0.3.0
50
+
51
+ Version 0.4.0 replaces the implementation previously published on PyPI as 0.3.0.
52
+ It includes incompatible changes:
53
+
54
+ - Python 3.12 or later is required; Python 3.11 is no longer supported.
55
+ - Import classes from their modules. The former top-level exports and
56
+ `unitx.utils` namespace are no longer provided:
57
+
58
+ ```python
59
+ from unitx.dimension import DIMENSIONLESS, Dimension
60
+ from unitx.quantity import Quantity
61
+ from unitx.unit import Unit
62
+ ```
63
+
64
+ - `Unit` no longer defines `len()` or special truth-value behavior. Use
65
+ `unit == Unit('')` to check the formal identity, or `unit.is_dimensionless()`
66
+ to check physical dimensions; these are different questions.
67
+ - Conversion and arithmetic can produce different magnitude types, especially
68
+ for Decimal inputs. Review the [numerical behavior](#numerical-behavior)
69
+ before relying on a result's concrete type or approximate equality.
70
+
71
+ ## Quick start
72
+
73
+ Import the classes from their modules:
74
+
75
+ ```pycon
76
+ >>> from unitx.quantity import Quantity
77
+ >>> from unitx.unit import Unit
78
+ >>> distance = 3 * Unit('m')
79
+ >>> elapsed = Quantity(2, 's')
80
+ >>> speed = distance / elapsed
81
+ >>> print(speed)
82
+ 1.5 m/s
83
+ >>> print(speed.to('cm/s'))
84
+ 150.0 cm/s
85
+
86
+ ```
87
+
88
+ `Quantity(value, unit)` accepts a unit expression or an existing `Unit` object.
89
+ Multiplying a scalar by a unit constructs a quantity; dividing a scalar by a
90
+ unit constructs a quantity with the reciprocal unit. Construction preserves the
91
+ original magnitude and its type.
92
+
93
+ These are directional construction operators: `value * unit` constructs a
94
+ `Quantity`, and `factor * unit` constructs a `ScaledUnit` when the factor is a
95
+ `Factor` or `SymbolicConstant`. The reverse forms `unit * value` and
96
+ `unit * factor` are unsupported and raise `TypeError`.
97
+
98
+ ## Conversion and arithmetic
99
+
100
+ Use `.to()` to convert between units with the same physical dimensions.
101
+ Addition and subtraction convert the right operand to the left operand's unit:
102
+
103
+ ```pycon
104
+ >>> length = Quantity(1, 'm')
105
+ >>> print(length + Quantity(50, 'cm'))
106
+ 3/2 m
107
+ >>> length.to('cm').value
108
+ 100
109
+ >>> Quantity(1, 'cm').to('m').value
110
+ Fraction(1, 100)
111
+
112
+ ```
113
+
114
+ Multiplication and division combine units. Powers apply to both the magnitude
115
+ and the unit. Multiplication or division by a `Unit` alone preserves the
116
+ magnitude unchanged.
117
+
118
+ Conversion, addition, subtraction, and ordering across incompatible dimensions
119
+ raise `unitx.exceptions.DimensionError`. Equality across incompatible dimensions
120
+ returns `False`.
121
+
122
+ For repeated conversions, reuse a target `Unit` object to avoid parsing the
123
+ expression on every call.
124
+
125
+ ## Unit expressions
126
+
127
+ Expressions support registered symbols, singular English names, SI prefixes,
128
+ multiplication, division, parentheses, and rational powers:
129
+
130
+ ```pycon
131
+ >>> Unit('kg m/s2') == Unit('kg*m/s**2')
132
+ True
133
+ >>> Unit('m^(1/2)') ** 2 == Unit('m')
134
+ True
135
+ >>> Unit('micrometre') == Unit('um')
136
+ True
137
+ >>> Unit('ms') == Unit('m s')
138
+ False
139
+
140
+ ```
141
+
142
+ - Multiplication can use `*` or whitespace. Adjacent letters form one unit
143
+ token: `ms` is a millisecond, while `m s` is metre times second.
144
+ - Powers can use adjacent numbers, `^`, or `**`. Exponents written as ratios
145
+ require parentheses, as in `m^(1/2)`; decimal exponents such as `m^0.5` are exact.
146
+ - Multiplication and division associate left to right. Use `m/(s A)` to put
147
+ both `s` and `A` in the denominator; `m/s A` means `(m/s)*A`.
148
+ - Unit symbols and names are case-sensitive. `u`, `µ`, and `μ` are accepted
149
+ micro prefixes. Use lowercase `k` for kilo; `K` denotes kelvin.
150
+
151
+ The current unit table contains `s`, `m`, `g`, `A`, `K`, `mol`, and `cd`, plus
152
+ the dimensionless identity. Mass scales are referenced to kilograms: `g` has
153
+ factor `1/1000`, and `kg` has factor `1`. Named derived units such as `N` are
154
+ not pre-registered; use expressions such as `kg*m/s2` or define them explicitly.
155
+
156
+ ## Defining units
157
+
158
+ Call `.define()` on a `Unit`, `ScaledUnit`, or positive real `Quantity` to
159
+ register a new unit and return it:
160
+
161
+ ```pycon
162
+ >>> newton = Unit('kg*m/s2').define('N', name='newton')
163
+ >>> minute = (60 * Unit('s')).define('min', name='minute', prefixable=False)
164
+ >>> Quantity(2, minute).to('s').value
165
+ 120
166
+ >>> from unitx.factor import PI
167
+ >>> degree = ((PI / 180) * Unit()).define('deg', name='degree', prefixable=False)
168
+ >>> degree.factor == PI / 180
169
+ True
170
+ >>> Quantity(1, 'kN').to(newton).value
171
+ 1000
172
+
173
+ ```
174
+
175
+ Multiplying a `Factor` or `SymbolicConstant` by a unit creates a `ScaledUnit`,
176
+ which preserves an exact additional scale through unit arithmetic. Its `scale`
177
+ is relative to its formal unit product; its `factor` is the total scale to the
178
+ coherent SI unit. An ordinary `Unit` has an implicit additional scale of one;
179
+ only `ScaledUnit` stores that scale. Unit multiplication, division, and powers
180
+ involving a `ScaledUnit` keep that type even when the scale cancels to one.
181
+ A scale of one compares equal to the corresponding `Unit` and has the same
182
+ hash. Calling `.define()` returns an ordinary `Unit` with its own formal
183
+ identity, sharing the definition's dimension and exact SI factor.
184
+
185
+ `ScaledUnit` supports shallow copying, deep copying, and pickling while
186
+ preserving its exact scale and formal units. Restoring custom units requires
187
+ their definitions to be registered in the receiving process.
188
+
189
+ Definitions affect the current process and leave the source object unchanged.
190
+ `symbol_aliases` and `name_aliases` each accept a string or tuple of strings.
191
+ The symbol, names, and aliases must each be a single printable unit token,
192
+ without whitespace, decimal digits, or parser operators. The primary symbol
193
+ and `name` are required and must be nonempty; empty aliases are ignored.
194
+
195
+ SI prefixes are enabled by default; set `prefixable=False` to disable them.
196
+ Registration raises `UnitRegistryError` if a new spelling, including a generated
197
+ prefixed form, already resolves to a unit or conflicts with another spelling
198
+ in the same definition. Validation finishes before the registry changes.
199
+
200
+ Quantity definitions require positive finite `int`, `Fraction`, `Decimal`, or
201
+ `float` magnitudes. Prefer `Fraction` or `Decimal` for exact scales. Floats use
202
+ Factor's denominator limit and can warn or round to zero and fail, for example
203
+ with `1e-10`. Defining a unit does not evaluate symbolic scales numerically.
204
+
205
+ ## Units, dimensions, and equality
206
+
207
+ A `Unit` retains its formal factors. A `Quantity` compares physical values
208
+ across compatible units:
209
+
210
+ ```pycon
211
+ >>> Unit('m') == Unit('cm')
212
+ False
213
+ >>> Quantity(1, 'm') == Quantity(100, 'cm')
214
+ True
215
+ >>> len({Quantity(1, 'm'), Quantity(100, 'cm')})
216
+ 1
217
+ >>> from unitx.dimension import Dimension
218
+ >>> speed.dimension == Dimension(L=1, T=-1)
219
+ True
220
+
221
+ ```
222
+
223
+ Equal quantities have equal hashes when their magnitudes are hashable. Different
224
+ formal unit expressions can have the same physical dimension and scale without
225
+ being equal as `Unit` objects.
226
+
227
+ An expression can be dimensionless and still carry a scale. Plain numbers added
228
+ to or subtracted from dimensionless quantities represent unscaled values:
229
+
230
+ ```pycon
231
+ >>> scaled = Quantity(1, 'km/m')
232
+ >>> scaled.is_dimensionless()
233
+ True
234
+ >>> scaled == 1000
235
+ True
236
+ >>> (scaled + 1).to('').value
237
+ Fraction(1001, 1)
238
+
239
+ ```
240
+
241
+ ## Numerical behavior
242
+
243
+ Exact unit algebra and numerical magnitude evaluation have different rules:
244
+
245
+ - Rational unit scales and exponents remain exact. Prefer `Fraction` or
246
+ `Decimal` for exact exponents; float exponents may be approximated with an
247
+ `InexactFloatWarning`.
248
+ - Conversion and arithmetic can change the magnitude's type. Finite `Decimal`
249
+ operands are converted exactly to `Fraction`, without Decimal context
250
+ rounding.
251
+ - Other magnitudes use native numerical arithmetic. In particular, integer
252
+ division produces a float, and nonintegral magnitude powers can round or
253
+ produce complex values.
254
+ - Equality is exact, without a tolerance. Finite floats are compared as their
255
+ stored binary rational values. Floating-point conversions and irrational
256
+ powers may round; compare their values approximately in a common unit when
257
+ appropriate.
258
+
259
+ ```pycon
260
+ >>> from decimal import Decimal
261
+ >>> from fractions import Fraction
262
+ >>> Quantity(Decimal('0.1'), 'm').to('cm').value
263
+ 10
264
+ >>> (Quantity(Fraction(1), 'm') / Quantity(3, 's')).value
265
+ Fraction(1, 3)
266
+ >>> Quantity(0.1, '') == Quantity(Fraction(1, 10), '')
267
+ False
268
+
269
+ ```
270
+
271
+ The built-in integer factorization backend is bounded. Unresolved factors emit
272
+ `IncompleteFactorWarning` and can limit simplification and comparisons. SymPy
273
+ is an optional, more capable factorization backend:
274
+
275
+ ```sh
276
+ python -m pip install sympy
277
+ ```
278
+
279
+ ## Current scope
280
+
281
+ - **NumPy scalars, arrays, and NumPy operations are not supported yet. NumPy
282
+ integration is planned.** Structural compatibility with a magnitude protocol
283
+ does not imply NumPy support.
284
+ - Custom magnitudes must support scalar arithmetic, return a Python `bool`
285
+ from equality, and defer raw operations with `Unit` operands to its reflected
286
+ operators.
287
+ - Scalar multiplication and division of quantities currently accept `int`,
288
+ `float`, `Fraction`, and `Decimal`. Complex magnitudes are supported, including
289
+ results of fractional powers; ordering follows the magnitude's native rules.
290
+ - Unit conversion is multiplicative. Offset temperature scales such as Celsius
291
+ and Fahrenheit are not supported.
292
+
293
+ ## Building blocks
294
+
295
+ | Class | Import module | Purpose |
296
+ | --- | --- | --- |
297
+ | `Quantity` | `unitx.quantity` | Scalar values paired with units, conversion, and arithmetic |
298
+ | `Unit` | `unitx.unit` | Compound units, scaling factors, and physical dimensions |
299
+ | `ScaledUnit` | `unitx.unit` | Formal unit products with an additional exact scale |
300
+ | `Dimension` | `unitx.dimension` | Seven physical dimensions with rational exponents |
301
+ | `SingleUnit` | `unitx.singleunit` | One registered unit with an optional prefix |
302
+ | `Factor` | `unitx.factor` | Positive scaling expressions and magnitude scaling |
303
+ | `UnitParser` | `unitx.unitparser` | Parsing unit expressions into formal products |
304
+ | `Monomial` | `unitx.monomial` | Immutable formal products with rational exponents |
305
+
306
+ The package includes inline type annotations and a `py.typed` marker. Construction
307
+ and operations with `Unit` preserve the magnitude type in type hints; conversion
308
+ and numerical arithmetic use result unions where the concrete type depends on
309
+ runtime values or units. Modules whose names start with `_` are internal.
310
+
311
+ ## Development
312
+
313
+ Install the development version and test dependencies:
314
+
315
+ ```sh
316
+ python -m pip install -e ".[test]"
317
+ python -m pytest -q
318
+ python -m ruff check .
319
+ python -m ruff format --check .
320
+ ```
321
+
322
+ The pytest suite includes doctests and Pyright checks for the typed API. CI runs
323
+ the suite with both the built-in and SymPy factorization backends.
324
+
325
+ ## Releases
326
+
327
+ The [release workflow](.github/workflows/release.yml) validates the source,
328
+ builds a wheel and source distribution, checks their metadata, and tests the
329
+ installed wheel. Manual workflow runs store the distributions without uploading
330
+ them to PyPI.
331
+
332
+ Publishing a GitHub release runs the same checks and uploads the distributions
333
+ to PyPI. The release tag must match the project version, for example `v0.4.0`
334
+ for version `0.4.0`. Configure a PyPI Trusted Publisher for the repository,
335
+ workflow `release.yml`, and GitHub environment `pypi` before publishing.
336
+
337
+ See [CHANGELOG.md](CHANGELOG.md) for release history. Licensed under the
338
+ [MIT License](LICENSE).