unit-syntax 0.3.1__tar.gz → 0.4.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.
- unit_syntax-0.4.0/CHANGELOG.md +47 -0
- unit_syntax-0.4.0/PKG-INFO +271 -0
- unit_syntax-0.3.1/PKG-INFO → unit_syntax-0.4.0/README.md +89 -24
- unit_syntax-0.4.0/pyproject.toml +47 -0
- unit_syntax-0.4.0/unit_syntax/__main__.py +27 -0
- unit_syntax-0.4.0/unit_syntax/_parser_support.py +176 -0
- unit_syntax-0.4.0/unit_syntax/check.py +294 -0
- unit_syntax-0.4.0/unit_syntax/import_hook.py +79 -0
- {unit_syntax-0.3.1 → unit_syntax-0.4.0}/unit_syntax/parser.py +4314 -2330
- unit_syntax-0.4.0/unit_syntax/py.typed +0 -0
- {unit_syntax-0.3.1 → unit_syntax-0.4.0}/unit_syntax/transform.py +8 -6
- unit_syntax-0.4.0/unit_syntax/typing.py +70 -0
- unit_syntax-0.4.0/unit_syntax/typing.pyi +321 -0
- unit_syntax-0.3.1/README.md +0 -159
- unit_syntax-0.3.1/pyproject.toml +0 -29
- unit_syntax-0.3.1/unit_syntax/__main__.py +0 -9
- unit_syntax-0.3.1/unit_syntax/import_hook.py +0 -22
- {unit_syntax-0.3.1 → unit_syntax-0.4.0}/unit_syntax/__init__.py +0 -0
- {unit_syntax-0.3.1 → unit_syntax-0.4.0}/unit_syntax/ipython.py +0 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Optional static dimensional checking with `unit-syntax check` and
|
|
8
|
+
`python -m unit_syntax check`. Install it with `unit-syntax[check]`.
|
|
9
|
+
The checker uses ty, checks project imports without executing user code,
|
|
10
|
+
and reports diagnostics at original filenames, lines, and columns.
|
|
11
|
+
- Dimension-aware `Quantity[D]` annotations and named physical dimensions in
|
|
12
|
+
`unit_syntax.typing`, generated arithmetic overloads, and a `py.typed` marker.
|
|
13
|
+
Common dimensional products and quotients are normalized; other combinations
|
|
14
|
+
retain symbolic dimensions and may be conservatively rejected.
|
|
15
|
+
- Static checks for incompatible arithmetic and unit conversions, dimensional
|
|
16
|
+
function arguments, and numeric conversion of physical quantities. Only
|
|
17
|
+
dimensionless quantities support implicit numeric conversion in the typing
|
|
18
|
+
contract; angles and solid angles remain distinct dimensions. Runtime values
|
|
19
|
+
remain ordinary Pint quantities.
|
|
20
|
+
- Grammar and dimensional-checking regression tests, a typed-units example,
|
|
21
|
+
grammar provenance and regeneration instructions, and validation notes.
|
|
22
|
+
- CI coverage for Python 3.11, 3.14, and 3.15, including parser and typing-stub
|
|
23
|
+
regeneration checks and distribution builds.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- Updated the parser to target Python 3.15 syntax, including generic declarations,
|
|
28
|
+
type parameter defaults, modern f-strings, template strings, unparenthesized
|
|
29
|
+
exception lists, lazy imports, and unpacking comprehensions. Executing newer
|
|
30
|
+
syntax requires an interpreter that supports it.
|
|
31
|
+
- Replaced `import-transforms` with a loader using Python's public `importlib`
|
|
32
|
+
interfaces. Imported modules and standalone scripts compile the transformed
|
|
33
|
+
AST directly, with unit-construction globals supplied by unit-syntax.
|
|
34
|
+
- Raised the minimum Python version from 3.8 to 3.10 and updated Pint, IPython,
|
|
35
|
+
and pegen requirements, development dependencies, and the dependency lockfile.
|
|
36
|
+
- Updated release tooling to build and test with the optional checker enabled.
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- Preserved CPython UTF-8 byte-column locations and the separate source spans
|
|
41
|
+
of unit values, suffixes, and complete applications, including parentheses
|
|
42
|
+
in nested conversions such as `(1 inch) mm`.
|
|
43
|
+
- Preserved original source locations when lowering unit expressions to calls.
|
|
44
|
+
- Prevented Unicode separators inside strings from introducing false source
|
|
45
|
+
lines in parser locations or dimensional-checking diagnostics.
|
|
46
|
+
- Preserved template interpolation expression text across Python 3.14 and 3.15
|
|
47
|
+
differences in trailing whitespace handling.
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: unit-syntax
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Physical unit literals for Jupyter and IPython
|
|
5
|
+
Author: Adam Hupp
|
|
6
|
+
Author-email: adam@hupp.org
|
|
7
|
+
Requires-Python: >=3.10,<4
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
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
|
+
Provides-Extra: check
|
|
16
|
+
Requires-Dist: Pint (>=0.24.4) ; python_version < "3.11"
|
|
17
|
+
Requires-Dist: Pint (>=0.25.3) ; python_version == "3.11"
|
|
18
|
+
Requires-Dist: Pint (>=0.26.2) ; python_version >= "3.12"
|
|
19
|
+
Requires-Dist: ipython (>=8) ; python_version < "3.11"
|
|
20
|
+
Requires-Dist: ipython (>=9) ; python_version >= "3.11"
|
|
21
|
+
Requires-Dist: pegen (>=0.3,<0.4)
|
|
22
|
+
Requires-Dist: ty (>=0.0.95) ; extra == "check"
|
|
23
|
+
Project-URL: Repository, https://github.com/ahupp/unit-syntax
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
`unit-syntax` adds support for physical units to the Python language:
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
>>> speed = 5 meters/second
|
|
30
|
+
>>> (2 seconds) * speed
|
|
31
|
+
10 meter
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Why? I like to use Python as a calculator for physical problems and wished it had the type safety of explicit units along with the readability of normal notation.
|
|
35
|
+
|
|
36
|
+
`unit-syntax` works in Jupyter notebooks, standalone Python scripts, and Python packages.
|
|
37
|
+
|
|
38
|
+
[How does it work?](https://github.com/ahupp/unit-syntax#how-does-it-work)
|
|
39
|
+
|
|
40
|
+
## Getting Started
|
|
41
|
+
|
|
42
|
+
Install the package:
|
|
43
|
+
|
|
44
|
+
```shell
|
|
45
|
+
$ pip install unit-syntax
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### ... with Jupyter/IPython
|
|
49
|
+
|
|
50
|
+
To enable unit-syntax in a Jupyter/IPython session run:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
%load_ext unit_syntax
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Tip: In Jupyter this must be run in its own cell before any units expressions are evaluated.
|
|
57
|
+
|
|
58
|
+
### ... with standalone scripts
|
|
59
|
+
|
|
60
|
+
To run a standalone script with units:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
$ python -m unit_syntax <path_to_script.py>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Note that this installs a custom import hook that affects all imports performed by the script.
|
|
67
|
+
|
|
68
|
+
### ... with Python packages
|
|
69
|
+
|
|
70
|
+
To use/distribute a package with unit-syntax, add this in your `__init__.py`:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from unit_syntax.import_hook import enable_units_for_package
|
|
74
|
+
enable_units_for_package(__name__)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
This applies the transform only to sub-modules of your package.
|
|
78
|
+
|
|
79
|
+
## Usage
|
|
80
|
+
|
|
81
|
+
[An interactive notebook to play around with units](https://colab.research.google.com/drive/1PInyLGZHnUzEuUVgMsLrUUNdCurXK7v1#scrollTo=JszzXmATY0TV)
|
|
82
|
+
|
|
83
|
+
Units can be applied to any "simple" expression:
|
|
84
|
+
|
|
85
|
+
- number: `1 meter`
|
|
86
|
+
- variables: `x parsec`, `y.z watts`, `area[id] meters**2`
|
|
87
|
+
- lists and tuples: `[1., 37.] newton meters`
|
|
88
|
+
- unary operators: `-x dBm`
|
|
89
|
+
- power: `x**2 meters`
|
|
90
|
+
|
|
91
|
+
In expressions mixing units and binary operators, parenthesize:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
one_lux = (1 lumen)/(1 meter**2)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Units can be used in any place where Python allows expressions, e.g:
|
|
98
|
+
|
|
99
|
+
- function arguments: `area_of_circle(radius=1 meter)`
|
|
100
|
+
- list comprehensions: `[x meters for x in range(10)]`
|
|
101
|
+
|
|
102
|
+
Quantities can be converted to another measurement system:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
>>> (88 miles / hour) furlongs / fortnight
|
|
106
|
+
236543.5269120001 furlong / fortnight
|
|
107
|
+
>>> (0 degC) degF
|
|
108
|
+
31.999999999999936 degree_Fahrenheit
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Compound units (e.g. `newtons/meter**2`) are supported and follow the usual precedence rules.
|
|
112
|
+
|
|
113
|
+
Units _may not_ begin with parentheses (consider the possible
|
|
114
|
+
interpretations of `x (meters)`). Parentheses are allowed anywhere else:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
# parsed as a function call, will result in a runtime error
|
|
118
|
+
x (newton meters)/(second*kg)
|
|
119
|
+
# a-ok
|
|
120
|
+
x newton meters/(second*kg)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Using unknown units produces a syntax error at import time:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
>>> 1 smoot
|
|
127
|
+
...
|
|
128
|
+
SyntaxError: 'smoot' is not defined in the unit registry
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## How does it work?
|
|
132
|
+
|
|
133
|
+
`unit-syntax` transforms python-with-units into standard python that calls the excellent [pint](https://pint.readthedocs.io/en/stable/) units handling library.
|
|
134
|
+
|
|
135
|
+
The parser is [pegen](https://we-like-parsers.github.io/pegen/), which is a standalone version of the same parser generator used by Python itself. The grammar is a [lightly modified](https://github.com/ahupp/unit-syntax/compare/base-grammar..main#diff-7405fdc26614e4d2e7f8f37c9b559ccb3a7f7c619d41e207dda28afdfae20f83) version the official Python grammar shipped with pegen.
|
|
136
|
+
|
|
137
|
+
The grammar now targets **Python 3.15**, including generic declarations, type
|
|
138
|
+
parameter defaults, f-strings and template strings, unparenthesized exception
|
|
139
|
+
types, unpacking comprehensions, and lazy imports. Execution of newer syntax
|
|
140
|
+
requires a Python interpreter that supports it. Python 3.10 is the minimum
|
|
141
|
+
supported interpreter. See [GRAMMAR.md](GRAMMAR.md) for the pinned upstream
|
|
142
|
+
sources and regeneration instructions.
|
|
143
|
+
|
|
144
|
+
Syntax transformation in IPython/Jupyter uses [IPython custom input transformers](https://ipython.readthedocs.io/en/stable/config/inputtransforms.html).
|
|
145
|
+
|
|
146
|
+
Syntax transformation of arbitrary Python modules uses Python's public
|
|
147
|
+
`importlib` finder and loader interfaces; see `unit_syntax.import_hook`.
|
|
148
|
+
|
|
149
|
+
## Static dimension checking
|
|
150
|
+
|
|
151
|
+
Install the optional checker, then check a script or package before executing it:
|
|
152
|
+
|
|
153
|
+
```sh
|
|
154
|
+
pip install 'unit-syntax[check]'
|
|
155
|
+
python -m unit_syntax check model.py
|
|
156
|
+
# Equivalent console command:
|
|
157
|
+
unit-syntax check my_package/
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
The checker uses **ty**. It creates a temporary Python view of the source, gives
|
|
161
|
+
every unit literal its physical dimension, checks project imports, and maps
|
|
162
|
+
diagnostics back to the original filenames and positions. It never executes the
|
|
163
|
+
checked code or writes transformed code into the project. Running `ty check`
|
|
164
|
+
directly on a file containing unit suffixes will still produce syntax errors;
|
|
165
|
+
use the `unit-syntax check` entry point. `--project PATH` explicitly selects the
|
|
166
|
+
root for imports. Syntax-only errors in unrelated, unselected files do not
|
|
167
|
+
prevent checking the selected files.
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
from math import sqrt
|
|
171
|
+
|
|
172
|
+
width = 18 mm
|
|
173
|
+
height = 27 mm
|
|
174
|
+
ratio = width / height
|
|
175
|
+
ear = width * sqrt(1 - ratio * ratio) # accepted: ratio is dimensionless
|
|
176
|
+
|
|
177
|
+
sqrt(width) # error: Quantity[Length] is not dimensionless
|
|
178
|
+
width + (2 seconds) # error: Length and Time are incompatible
|
|
179
|
+
converted = (2 seconds) mm # error: conversion requires compatible dimensions
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Function annotations can express the same contract:
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
from unit_syntax.typing import Quantity, Length, Area
|
|
186
|
+
|
|
187
|
+
def surface_area(radius: Quantity[Length]) -> Quantity[Area]:
|
|
188
|
+
return radius * radius
|
|
189
|
+
|
|
190
|
+
surface_area(5 mm) # accepted
|
|
191
|
+
surface_area(5 seconds) # rejected by the checker
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
In `unit_syntax.typing`, `Quantity[D]` carries a **dimension**. This differs
|
|
195
|
+
from Pint's `pint.Quantity[T]`, whose type parameter describes the **magnitude**
|
|
196
|
+
(such as `float` or an array), not whether the value is a length or time. Pint's
|
|
197
|
+
existing annotations do not perform this dimensional checking. At runtime
|
|
198
|
+
`unit_syntax.typing.Quantity` is an alias for Pint's real `Quantity`; values,
|
|
199
|
+
registries, formatting, and arithmetic continue to use Pint.
|
|
200
|
+
|
|
201
|
+
Only `Quantity[Dimensionless]` supports `__float__` in the typing contract.
|
|
202
|
+
Pint already performs dimensionless numeric conversion at runtime, including
|
|
203
|
+
scale normalization: `float((1 m) / (1 mm))` is `1000`. Angles remain a distinct
|
|
204
|
+
static dimension, even though Pint treats radians as dimensionless. Convert an
|
|
205
|
+
angle to radians and explicitly extract its magnitude when calling Python's
|
|
206
|
+
plain numeric trigonometric functions.
|
|
207
|
+
|
|
208
|
+
The operator annotations normalize common dimensions, including area, volume,
|
|
209
|
+
velocity, acceleration, force, pressure, energy, power, and density. Matching
|
|
210
|
+
dimensions divide to a dimensionless quantity, including arbitrary compound
|
|
211
|
+
units written as literals. Other products and quotients retain symbolic
|
|
212
|
+
dimension types. Python typing cannot generally simplify arbitrary exponent
|
|
213
|
+
algebra, so an equivalent expression outside the normalized set can be
|
|
214
|
+
conservatively rejected. Physical powers currently require a supported literal
|
|
215
|
+
integer exponent (`-3` through `4`); dimensionless powers accept ordinary
|
|
216
|
+
numbers. Magnitude dtype and shape constraints, runtime-selected units,
|
|
217
|
+
arbitrary `.to()` strings, and Pint contexts remain runtime checks. Untyped
|
|
218
|
+
Python values retain the usual limits of gradual type checking.
|
|
219
|
+
|
|
220
|
+
The checker resolves literal unit names with its supplied registry (the Pint
|
|
221
|
+
application registry by default). Python API callers can pass their own registry
|
|
222
|
+
to `unit_syntax.check.checking_source`; checking does not execute package
|
|
223
|
+
initializers to discover custom registry definitions.
|
|
224
|
+
|
|
225
|
+
## Why only allow units on simple expressions?
|
|
226
|
+
|
|
227
|
+
Imagine units were instead parsed as operator with high precedence and you wrote this reasonable looking expression:
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
ppi = 300 pixels/inch
|
|
231
|
+
y = x inches * ppi
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
`inches * ppi` would be parsed as the unit, leading to (at best) a runtime error sometime later and at worst an incorrect calculation. This could be avoided by parenthesizing the expression (e.g. `(x inches) * ppi`, but if that's optional it's easy to forget. So the intent of this restriction is to make these risky forms uncommon and thus more obvious. This is not a hypothetical concern, I hit this within 10 minutes of first using the initial syntax.
|
|
235
|
+
|
|
236
|
+
## Prior Art
|
|
237
|
+
|
|
238
|
+
The immediate inspriration of `unit-syntax` is a language called [Fortress](https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.180.6323&rep=rep1&type=pdf) from Sun Microsystems. Fortress was intended as a modern Fortran, and had first-class support for units in both the syntax and type system.
|
|
239
|
+
|
|
240
|
+
F# (an OCaml derivative from Microsoft) also [has first class support for units](https://en.wikibooks.org/wiki/F_Sharp_Programming/Units_of_Measure).
|
|
241
|
+
|
|
242
|
+
The Julia package [Unitful.jl](http://painterqubits.github.io/Unitful.jl/stable/)
|
|
243
|
+
|
|
244
|
+
A [long discussion on the python-ideas mailing list](https://lwn.net/Articles/900739/) about literal units in Python.
|
|
245
|
+
|
|
246
|
+
## Development
|
|
247
|
+
|
|
248
|
+
To regenerate the parser:
|
|
249
|
+
|
|
250
|
+
`python -m pegen python_units.gram -o unit_syntax/parser.py`
|
|
251
|
+
|
|
252
|
+
Running tests:
|
|
253
|
+
|
|
254
|
+
```
|
|
255
|
+
$ poetry install --with dev --extras check
|
|
256
|
+
$ poetry run pytest
|
|
257
|
+
$ poetry run python scripts/generate_typing.py
|
|
258
|
+
$ poetry run ty check unit_syntax/typing.pyi
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Future work and open questions
|
|
262
|
+
|
|
263
|
+
- Test against various ipython and python versions
|
|
264
|
+
- Ensure bytecode caching still works
|
|
265
|
+
- Test with wider range of source files with the wildcard loader
|
|
266
|
+
- Extend static normalization to more dimensional products and exponent expressions.
|
|
267
|
+
- Typography of output
|
|
268
|
+
- pre-parse units
|
|
269
|
+
- talk to pint about interop between UnitRegistries
|
|
270
|
+
- Fix reported location of SyntaxError when paren missing
|
|
271
|
+
|
|
@@ -1,23 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: unit-syntax
|
|
3
|
-
Version: 0.3.1
|
|
4
|
-
Summary: Physical unit literals for Jupyter and IPython
|
|
5
|
-
Home-page: https://github.com/ahupp/unit-syntax
|
|
6
|
-
Author: Adam Hupp
|
|
7
|
-
Author-email: adam@hupp.org
|
|
8
|
-
Requires-Python: >=3.8,<4
|
|
9
|
-
Classifier: Programming Language :: Python :: 3
|
|
10
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
-
Requires-Dist: Pint (>0.20)
|
|
15
|
-
Requires-Dist: import-transforms (>=0.6,<0.7)
|
|
16
|
-
Requires-Dist: ipython (>=7)
|
|
17
|
-
Requires-Dist: pegen (>=0.2,<0.3)
|
|
18
|
-
Project-URL: Repository, https://github.com/ahupp/unit-syntax
|
|
19
|
-
Description-Content-Type: text/markdown
|
|
20
|
-
|
|
21
1
|
`unit-syntax` adds support for physical units to the Python language:
|
|
22
2
|
|
|
23
3
|
```python
|
|
@@ -129,9 +109,93 @@ SyntaxError: 'smoot' is not defined in the unit registry
|
|
|
129
109
|
|
|
130
110
|
The parser is [pegen](https://we-like-parsers.github.io/pegen/), which is a standalone version of the same parser generator used by Python itself. The grammar is a [lightly modified](https://github.com/ahupp/unit-syntax/compare/base-grammar..main#diff-7405fdc26614e4d2e7f8f37c9b559ccb3a7f7c619d41e207dda28afdfae20f83) version the official Python grammar shipped with pegen.
|
|
131
111
|
|
|
112
|
+
The grammar now targets **Python 3.15**, including generic declarations, type
|
|
113
|
+
parameter defaults, f-strings and template strings, unparenthesized exception
|
|
114
|
+
types, unpacking comprehensions, and lazy imports. Execution of newer syntax
|
|
115
|
+
requires a Python interpreter that supports it. Python 3.10 is the minimum
|
|
116
|
+
supported interpreter. See [GRAMMAR.md](GRAMMAR.md) for the pinned upstream
|
|
117
|
+
sources and regeneration instructions.
|
|
118
|
+
|
|
132
119
|
Syntax transformation in IPython/Jupyter uses [IPython custom input transformers](https://ipython.readthedocs.io/en/stable/config/inputtransforms.html).
|
|
133
120
|
|
|
134
|
-
Syntax transformation of arbitrary Python modules uses
|
|
121
|
+
Syntax transformation of arbitrary Python modules uses Python's public
|
|
122
|
+
`importlib` finder and loader interfaces; see `unit_syntax.import_hook`.
|
|
123
|
+
|
|
124
|
+
## Static dimension checking
|
|
125
|
+
|
|
126
|
+
Install the optional checker, then check a script or package before executing it:
|
|
127
|
+
|
|
128
|
+
```sh
|
|
129
|
+
pip install 'unit-syntax[check]'
|
|
130
|
+
python -m unit_syntax check model.py
|
|
131
|
+
# Equivalent console command:
|
|
132
|
+
unit-syntax check my_package/
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The checker uses **ty**. It creates a temporary Python view of the source, gives
|
|
136
|
+
every unit literal its physical dimension, checks project imports, and maps
|
|
137
|
+
diagnostics back to the original filenames and positions. It never executes the
|
|
138
|
+
checked code or writes transformed code into the project. Running `ty check`
|
|
139
|
+
directly on a file containing unit suffixes will still produce syntax errors;
|
|
140
|
+
use the `unit-syntax check` entry point. `--project PATH` explicitly selects the
|
|
141
|
+
root for imports. Syntax-only errors in unrelated, unselected files do not
|
|
142
|
+
prevent checking the selected files.
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
from math import sqrt
|
|
146
|
+
|
|
147
|
+
width = 18 mm
|
|
148
|
+
height = 27 mm
|
|
149
|
+
ratio = width / height
|
|
150
|
+
ear = width * sqrt(1 - ratio * ratio) # accepted: ratio is dimensionless
|
|
151
|
+
|
|
152
|
+
sqrt(width) # error: Quantity[Length] is not dimensionless
|
|
153
|
+
width + (2 seconds) # error: Length and Time are incompatible
|
|
154
|
+
converted = (2 seconds) mm # error: conversion requires compatible dimensions
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Function annotations can express the same contract:
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
from unit_syntax.typing import Quantity, Length, Area
|
|
161
|
+
|
|
162
|
+
def surface_area(radius: Quantity[Length]) -> Quantity[Area]:
|
|
163
|
+
return radius * radius
|
|
164
|
+
|
|
165
|
+
surface_area(5 mm) # accepted
|
|
166
|
+
surface_area(5 seconds) # rejected by the checker
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
In `unit_syntax.typing`, `Quantity[D]` carries a **dimension**. This differs
|
|
170
|
+
from Pint's `pint.Quantity[T]`, whose type parameter describes the **magnitude**
|
|
171
|
+
(such as `float` or an array), not whether the value is a length or time. Pint's
|
|
172
|
+
existing annotations do not perform this dimensional checking. At runtime
|
|
173
|
+
`unit_syntax.typing.Quantity` is an alias for Pint's real `Quantity`; values,
|
|
174
|
+
registries, formatting, and arithmetic continue to use Pint.
|
|
175
|
+
|
|
176
|
+
Only `Quantity[Dimensionless]` supports `__float__` in the typing contract.
|
|
177
|
+
Pint already performs dimensionless numeric conversion at runtime, including
|
|
178
|
+
scale normalization: `float((1 m) / (1 mm))` is `1000`. Angles remain a distinct
|
|
179
|
+
static dimension, even though Pint treats radians as dimensionless. Convert an
|
|
180
|
+
angle to radians and explicitly extract its magnitude when calling Python's
|
|
181
|
+
plain numeric trigonometric functions.
|
|
182
|
+
|
|
183
|
+
The operator annotations normalize common dimensions, including area, volume,
|
|
184
|
+
velocity, acceleration, force, pressure, energy, power, and density. Matching
|
|
185
|
+
dimensions divide to a dimensionless quantity, including arbitrary compound
|
|
186
|
+
units written as literals. Other products and quotients retain symbolic
|
|
187
|
+
dimension types. Python typing cannot generally simplify arbitrary exponent
|
|
188
|
+
algebra, so an equivalent expression outside the normalized set can be
|
|
189
|
+
conservatively rejected. Physical powers currently require a supported literal
|
|
190
|
+
integer exponent (`-3` through `4`); dimensionless powers accept ordinary
|
|
191
|
+
numbers. Magnitude dtype and shape constraints, runtime-selected units,
|
|
192
|
+
arbitrary `.to()` strings, and Pint contexts remain runtime checks. Untyped
|
|
193
|
+
Python values retain the usual limits of gradual type checking.
|
|
194
|
+
|
|
195
|
+
The checker resolves literal unit names with its supplied registry (the Pint
|
|
196
|
+
application registry by default). Python API callers can pass their own registry
|
|
197
|
+
to `unit_syntax.check.checking_source`; checking does not execute package
|
|
198
|
+
initializers to discover custom registry definitions.
|
|
135
199
|
|
|
136
200
|
## Why only allow units on simple expressions?
|
|
137
201
|
|
|
@@ -163,8 +227,10 @@ To regenerate the parser:
|
|
|
163
227
|
Running tests:
|
|
164
228
|
|
|
165
229
|
```
|
|
166
|
-
$ poetry install --with dev
|
|
230
|
+
$ poetry install --with dev --extras check
|
|
167
231
|
$ poetry run pytest
|
|
232
|
+
$ poetry run python scripts/generate_typing.py
|
|
233
|
+
$ poetry run ty check unit_syntax/typing.pyi
|
|
168
234
|
```
|
|
169
235
|
|
|
170
236
|
## Future work and open questions
|
|
@@ -172,9 +238,8 @@ $ poetry run pytest
|
|
|
172
238
|
- Test against various ipython and python versions
|
|
173
239
|
- Ensure bytecode caching still works
|
|
174
240
|
- Test with wider range of source files with the wildcard loader
|
|
175
|
-
-
|
|
241
|
+
- Extend static normalization to more dimensional products and exponent expressions.
|
|
176
242
|
- Typography of output
|
|
177
243
|
- pre-parse units
|
|
178
244
|
- talk to pint about interop between UnitRegistries
|
|
179
245
|
- Fix reported location of SyntaxError when paren missing
|
|
180
|
-
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "unit-syntax"
|
|
3
|
+
version = "0.4.0"
|
|
4
|
+
authors = ["Adam Hupp <adam@hupp.org>"]
|
|
5
|
+
packages = [{ include = "unit_syntax" }]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
include = [{ path = "CHANGELOG.md", format = "sdist" }]
|
|
8
|
+
repository = "https://github.com/ahupp/unit-syntax"
|
|
9
|
+
description = "Physical unit literals for Jupyter and IPython"
|
|
10
|
+
|
|
11
|
+
[tool.poetry.dependencies]
|
|
12
|
+
python = ">=3.10, < 4"
|
|
13
|
+
Pint = [
|
|
14
|
+
{version = ">=0.24.4", python = "<3.11"},
|
|
15
|
+
{version = ">=0.25.3", python = ">=3.11,<3.12"},
|
|
16
|
+
{version = ">=0.26.2", python = ">=3.12"},
|
|
17
|
+
]
|
|
18
|
+
ipython = [
|
|
19
|
+
{version = ">=8", python = "<3.11"},
|
|
20
|
+
{version = ">=9", python = ">=3.11"},
|
|
21
|
+
]
|
|
22
|
+
pegen = "^0.3"
|
|
23
|
+
ty = {version = ">=0.0.95", optional = true}
|
|
24
|
+
|
|
25
|
+
[tool.poetry.extras]
|
|
26
|
+
check = ["ty"]
|
|
27
|
+
|
|
28
|
+
[tool.poetry.scripts]
|
|
29
|
+
unit-syntax = "unit_syntax.check:main"
|
|
30
|
+
|
|
31
|
+
[tool.poetry.group.dev.dependencies]
|
|
32
|
+
pytest = ">=8"
|
|
33
|
+
black = ">=25"
|
|
34
|
+
numpy = [
|
|
35
|
+
{version = ">=1.24.3", python = "<3.11"},
|
|
36
|
+
{version = ">=2.4", python = ">=3.11,<3.12"},
|
|
37
|
+
{version = ">=2.5.4", python = ">=3.12"},
|
|
38
|
+
]
|
|
39
|
+
ty = ">=0.0.95"
|
|
40
|
+
|
|
41
|
+
[build-system]
|
|
42
|
+
requires = ["poetry-core>=1.0.0"]
|
|
43
|
+
build-backend = "poetry.core.masonry.api"
|
|
44
|
+
|
|
45
|
+
[tool.black]
|
|
46
|
+
include = '\.pyi?$'
|
|
47
|
+
extend-exclude = 'parser.py$'
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from .transform import UnitSourceTransform
|
|
3
|
+
|
|
4
|
+
if len(sys.argv) < 2:
|
|
5
|
+
sys.exit("usage: python -m unit_syntax <script.py>")
|
|
6
|
+
|
|
7
|
+
if sys.argv[1] == "check":
|
|
8
|
+
from .check import main
|
|
9
|
+
|
|
10
|
+
sys.exit(main(sys.argv[1:]))
|
|
11
|
+
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from .import_hook import enable_units_everywhere
|
|
14
|
+
|
|
15
|
+
filename = sys.argv[1]
|
|
16
|
+
sys.argv = sys.argv[1:]
|
|
17
|
+
sys.path.insert(0, str(Path(filename).resolve().parent))
|
|
18
|
+
transform = UnitSourceTransform(None)
|
|
19
|
+
enable_units_everywhere()
|
|
20
|
+
namespace = {
|
|
21
|
+
"__name__": "__main__",
|
|
22
|
+
"__file__": filename,
|
|
23
|
+
"__package__": None,
|
|
24
|
+
**transform.injected_globals(),
|
|
25
|
+
}
|
|
26
|
+
source = Path(filename).read_text(encoding="utf-8")
|
|
27
|
+
exec(compile(transform.transform(source), filename, "exec"), namespace)
|