unit-syntax 0.2.1__tar.gz → 0.2.2__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.2.1 → unit_syntax-0.2.2}/PKG-INFO +8 -19
- {unit_syntax-0.2.1 → unit_syntax-0.2.2}/README.md +7 -18
- {unit_syntax-0.2.1 → unit_syntax-0.2.2}/pyproject.toml +1 -1
- unit_syntax-0.2.2/unit_syntax/__init__.py +14 -0
- unit_syntax-0.2.2/unit_syntax/ipython.py +71 -0
- {unit_syntax-0.2.1 → unit_syntax-0.2.2}/unit_syntax/transform.py +6 -15
- unit_syntax-0.2.1/unit_syntax/__init__.py +0 -68
- unit_syntax-0.2.1/unit_syntax/ipython/__init__.py +0 -3
- {unit_syntax-0.2.1 → unit_syntax-0.2.2}/unit_syntax/parser.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: unit-syntax
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Physical unit literals for Jupyter and IPython
|
|
5
5
|
Home-page: https://github.com/ahupp/unit-syntax
|
|
6
6
|
Author: Adam Hupp
|
|
@@ -25,7 +25,7 @@ Description-Content-Type: text/markdown
|
|
|
25
25
|
10 meter
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
Why? I often use Python as an interactive calculator for physical problems
|
|
28
|
+
Why? I often use Python as an interactive calculator for physical problems and wished it had the type safety of explicit units along with the readability of normal notation.
|
|
29
29
|
|
|
30
30
|
`unit-syntax` currently supports Jupyter notebooks and the IPython interpreter; maybe someday support for standalone scripts.
|
|
31
31
|
|
|
@@ -42,7 +42,7 @@ $ pip install unit-syntax
|
|
|
42
42
|
To enable unit-syntax in a Jupyter/IPython session run:
|
|
43
43
|
|
|
44
44
|
```python
|
|
45
|
-
|
|
45
|
+
%load_ext unit_syntax
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
Tip: In Jupyter this must be run in its own cell before any units expressions are evaluated.
|
|
@@ -105,30 +105,18 @@ The parser is [pegen](https://we-like-parsers.github.io/pegen/), which is the sa
|
|
|
105
105
|
|
|
106
106
|
Syntax transformation in IPython/Jupyter uses [IPython custom input transformers](https://ipython.readthedocs.io/en/stable/config/inputtransforms.html).
|
|
107
107
|
|
|
108
|
-
````python
|
|
109
|
-
|
|
110
|
-
An IPython [custom input transformer](https://ipython.readthedocs.io/en/stable/config/inputtransforms.html) parses the new syntax using a modified version of the official Python grammar, and translates it into calls to the excellent [Pint](https://pint.readthedocs.io/) units library.
|
|
111
|
-
|
|
112
108
|
## Why only allow units on simple expressions?
|
|
113
109
|
|
|
114
|
-
|
|
110
|
+
Imagine units were instead parsed as operator with high precedence and you wrote this reasonable looking expression:
|
|
115
111
|
|
|
116
112
|
```python
|
|
117
113
|
ppi = 300 pixels/inch
|
|
118
114
|
y = x inches * ppi
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
`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 in general it's too error prone to allow free intermixing of operators and units. (Note: This is not a hypoethical concern, I hit this within 10 minutes of first trying out the idea)
|
|
122
|
-
|
|
123
|
-
## Help!
|
|
124
|
-
|
|
125
|
-
If you're getting an unexpected result, try using `unit_syntax.enable_ipython(debug_transform=True)`. This will log the transformed Python code to the console.
|
|
126
|
-
|
|
127
|
-
If you're stuck, feel free to open an issue.
|
|
115
|
+
```
|
|
128
116
|
|
|
129
|
-
|
|
117
|
+
`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 in general it's too error prone to allow free intermixing of operators and units. (Note: This is not a hypothical concern, I hit this within 10 minutes of first trying out the idea)
|
|
130
118
|
|
|
131
|
-
|
|
119
|
+
It's easy to detect units used when they aren't allowed (that syntax isn't valid anywhere else), but not generally possible to determine if you forgot parens or meant to write a unit.
|
|
132
120
|
|
|
133
121
|
## Prior Art
|
|
134
122
|
|
|
@@ -162,4 +150,5 @@ $ poetry run pytest
|
|
|
162
150
|
- Typography of output
|
|
163
151
|
- make it work with numba
|
|
164
152
|
- understand how numpy interop works
|
|
153
|
+
- pre-parse units
|
|
165
154
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
10 meter
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
Why? I often use Python as an interactive calculator for physical problems
|
|
9
|
+
Why? I often use Python as an interactive calculator for physical problems and wished it had the type safety of explicit units along with the readability of normal notation.
|
|
10
10
|
|
|
11
11
|
`unit-syntax` currently supports Jupyter notebooks and the IPython interpreter; maybe someday support for standalone scripts.
|
|
12
12
|
|
|
@@ -23,7 +23,7 @@ $ pip install unit-syntax
|
|
|
23
23
|
To enable unit-syntax in a Jupyter/IPython session run:
|
|
24
24
|
|
|
25
25
|
```python
|
|
26
|
-
|
|
26
|
+
%load_ext unit_syntax
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
Tip: In Jupyter this must be run in its own cell before any units expressions are evaluated.
|
|
@@ -86,30 +86,18 @@ The parser is [pegen](https://we-like-parsers.github.io/pegen/), which is the sa
|
|
|
86
86
|
|
|
87
87
|
Syntax transformation in IPython/Jupyter uses [IPython custom input transformers](https://ipython.readthedocs.io/en/stable/config/inputtransforms.html).
|
|
88
88
|
|
|
89
|
-
````python
|
|
90
|
-
|
|
91
|
-
An IPython [custom input transformer](https://ipython.readthedocs.io/en/stable/config/inputtransforms.html) parses the new syntax using a modified version of the official Python grammar, and translates it into calls to the excellent [Pint](https://pint.readthedocs.io/) units library.
|
|
92
|
-
|
|
93
89
|
## Why only allow units on simple expressions?
|
|
94
90
|
|
|
95
|
-
|
|
91
|
+
Imagine units were instead parsed as operator with high precedence and you wrote this reasonable looking expression:
|
|
96
92
|
|
|
97
93
|
```python
|
|
98
94
|
ppi = 300 pixels/inch
|
|
99
95
|
y = x inches * ppi
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
`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 in general it's too error prone to allow free intermixing of operators and units. (Note: This is not a hypoethical concern, I hit this within 10 minutes of first trying out the idea)
|
|
103
|
-
|
|
104
|
-
## Help!
|
|
105
|
-
|
|
106
|
-
If you're getting an unexpected result, try using `unit_syntax.enable_ipython(debug_transform=True)`. This will log the transformed Python code to the console.
|
|
107
|
-
|
|
108
|
-
If you're stuck, feel free to open an issue.
|
|
96
|
+
```
|
|
109
97
|
|
|
110
|
-
|
|
98
|
+
`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 in general it's too error prone to allow free intermixing of operators and units. (Note: This is not a hypothical concern, I hit this within 10 minutes of first trying out the idea)
|
|
111
99
|
|
|
112
|
-
|
|
100
|
+
It's easy to detect units used when they aren't allowed (that syntax isn't valid anywhere else), but not generally possible to determine if you forgot parens or meant to write a unit.
|
|
113
101
|
|
|
114
102
|
## Prior Art
|
|
115
103
|
|
|
@@ -143,3 +131,4 @@ $ poetry run pytest
|
|
|
143
131
|
- Typography of output
|
|
144
132
|
- make it work with numba
|
|
145
133
|
- understand how numpy interop works
|
|
134
|
+
- pre-parse units
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import pint
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
# Use the _DEFAULT_REGISTRY so we can interoperate with other pint-using libraries
|
|
5
|
+
ureg = pint._DEFAULT_REGISTRY
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
BOOTSTRAP = """
|
|
9
|
+
import unit_syntax
|
|
10
|
+
_unit_syntax_q = unit_syntax.ureg.Quantity
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
from .ipython import load_ipython_extension, unload_ipython_extension
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from .transform import transform
|
|
2
|
+
import pint
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def _add_formatters(ipython):
|
|
6
|
+
# Overide default formatters to use reduced units. This lets us keep using the
|
|
7
|
+
# default registry for interop with other pint-using libraries, but gives more
|
|
8
|
+
# sensible output in the notebook.
|
|
9
|
+
def add_formatter(mime, display_fn_name):
|
|
10
|
+
fn = getattr(pint.Quantity, display_fn_name, None)
|
|
11
|
+
if fn is None:
|
|
12
|
+
return
|
|
13
|
+
|
|
14
|
+
def fmt_reduced(q, *args):
|
|
15
|
+
return fn(q.to_reduced_units(), *args)
|
|
16
|
+
|
|
17
|
+
formatter = ipython.display_formatter.formatters[mime]
|
|
18
|
+
formatter.for_type(pint.Quantity, fmt_reduced)
|
|
19
|
+
|
|
20
|
+
add_formatter("text/markdown", "_repr_markdown_")
|
|
21
|
+
add_formatter("text/html", "_repr_html_")
|
|
22
|
+
add_formatter("text/plain", "_repr_pretty_")
|
|
23
|
+
add_formatter("text/latex", "_repr_latex_")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_has_init_log = False
|
|
27
|
+
_debug = False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def transform_lines(lines: list[str]) -> list[str]:
|
|
31
|
+
"""IPython transforms provide a list of strings in the current cell, but to parse correctly we
|
|
32
|
+
need to parse them as a single string"""
|
|
33
|
+
return transform("".join(lines)).splitlines(keepends=True)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def ipython_transform(lines):
|
|
37
|
+
ret = transform_lines(lines)
|
|
38
|
+
|
|
39
|
+
if _debug:
|
|
40
|
+
import logging
|
|
41
|
+
|
|
42
|
+
global _has_init_log
|
|
43
|
+
if not _has_init_log:
|
|
44
|
+
logging.basicConfig(level=logging.DEBUG, force=True)
|
|
45
|
+
_has_init_log = True
|
|
46
|
+
logging.debug("unit_syntax: %s -> %s", "\n".join(lines), "\n".join(ret))
|
|
47
|
+
return ret
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def load_ipython_extension(ipython):
|
|
51
|
+
if not hasattr(ipython, "input_transformers_post"):
|
|
52
|
+
raise ImportError("Unsupported IPython version, version >=7 is required")
|
|
53
|
+
|
|
54
|
+
ipython.input_transformers_post.append(ipython_transform)
|
|
55
|
+
|
|
56
|
+
_add_formatters(ipython)
|
|
57
|
+
|
|
58
|
+
from . import BOOTSTRAP
|
|
59
|
+
|
|
60
|
+
ipython.run_cell(BOOTSTRAP)
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
import matplotlib
|
|
64
|
+
|
|
65
|
+
pint.setup_matplotlib(True)
|
|
66
|
+
except ImportError:
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def unload_ipython_extension(ipython):
|
|
71
|
+
ipython.input_transformers_post.remove(ipython_transform)
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import ast
|
|
2
|
-
from io import StringIO
|
|
3
|
-
from typing import Iterator, Optional, Generator
|
|
4
2
|
from .parser import parse_string, UnitsExpr
|
|
5
3
|
import pint
|
|
6
|
-
from pprint import pprint
|
|
7
4
|
|
|
8
5
|
|
|
9
6
|
class UnitExprTransformer(ast.NodeTransformer):
|
|
7
|
+
"""
|
|
8
|
+
AST transformer to turn python-with-units into standard python
|
|
9
|
+
"""
|
|
10
|
+
|
|
10
11
|
ureg: pint.UnitRegistry
|
|
11
12
|
|
|
12
13
|
def __init__(self, ureg: pint.UnitRegistry):
|
|
@@ -22,11 +23,7 @@ class UnitExprTransformer(ast.NodeTransformer):
|
|
|
22
23
|
|
|
23
24
|
value = self.generic_visit(node.value)
|
|
24
25
|
return ast.Call(
|
|
25
|
-
|
|
26
|
-
ast.Name(id="unit_syntax", ctx=ast.Load()),
|
|
27
|
-
"Quantity",
|
|
28
|
-
ctx=ast.Load(),
|
|
29
|
-
),
|
|
26
|
+
ast.Name(id="_unit_syntax_q", ctx=ast.Load()),
|
|
30
27
|
args=[value, node.units],
|
|
31
28
|
keywords=[],
|
|
32
29
|
)
|
|
@@ -38,13 +35,7 @@ def transform(code: str) -> str:
|
|
|
38
35
|
"""Transform a string of python-with-units into a standard python string"""
|
|
39
36
|
|
|
40
37
|
tree = parse_string(code, mode="file")
|
|
41
|
-
ureg = pint.
|
|
38
|
+
ureg = pint._DEFAULT_REGISTRY
|
|
42
39
|
tree_std = UnitExprTransformer(ureg).visit(tree)
|
|
43
40
|
tree_std = ast.fix_missing_locations(tree_std)
|
|
44
41
|
return ast.unparse(tree_std)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def transform_lines(lines: list[str]) -> list[str]:
|
|
48
|
-
"""IPython transforms provide a list of strings in the current cell, but to parse correctly we
|
|
49
|
-
need to parse them as a single string"""
|
|
50
|
-
return transform("".join(lines)).splitlines(keepends=True)
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import pint
|
|
2
|
-
|
|
3
|
-
# Use the _DEFAULT_REGISTRY so we can interoperate with other pint-using libraries
|
|
4
|
-
ureg = pint._DEFAULT_REGISTRY
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def Quantity(value, units):
|
|
8
|
-
if isinstance(value, ureg.Quantity):
|
|
9
|
-
return value.to(units)
|
|
10
|
-
else:
|
|
11
|
-
return ureg.Quantity(value, units)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def enable_ipython(debug_transform=False):
|
|
15
|
-
"""
|
|
16
|
-
debug_transform -- if True, log the input and output of each transform
|
|
17
|
-
"""
|
|
18
|
-
import IPython
|
|
19
|
-
from .transform import transform_lines
|
|
20
|
-
|
|
21
|
-
ip = IPython.get_ipython()
|
|
22
|
-
if not hasattr(ip, "input_transformers_post"):
|
|
23
|
-
raise ImportError("Unsupported IPython version, version >=7 is required")
|
|
24
|
-
|
|
25
|
-
if debug_transform:
|
|
26
|
-
import logging
|
|
27
|
-
|
|
28
|
-
logging.basicConfig(level=logging.DEBUG, force=True)
|
|
29
|
-
|
|
30
|
-
def transform_lines_dbg(lines):
|
|
31
|
-
ret = transform_lines(lines)
|
|
32
|
-
logging.debug("unit_syntax: %s -> %s", "\n".join(lines), "\n".join(ret))
|
|
33
|
-
return ret
|
|
34
|
-
|
|
35
|
-
do_transform = transform_lines_dbg
|
|
36
|
-
else:
|
|
37
|
-
do_transform = transform_lines
|
|
38
|
-
ip.input_transformers_post.append(do_transform)
|
|
39
|
-
|
|
40
|
-
# Overide default formatters to use reduced units. This lets us keep using the
|
|
41
|
-
# default registry for interop with other pint-using libraries, but gives more
|
|
42
|
-
# sensible output in the notebook.
|
|
43
|
-
def add_formatter(mime, display_fn_name):
|
|
44
|
-
fn = getattr(pint.Quantity, display_fn_name, None)
|
|
45
|
-
if fn is None:
|
|
46
|
-
return
|
|
47
|
-
|
|
48
|
-
def fmt_reduced(q, *args):
|
|
49
|
-
return fn(q.to_reduced_units(), *args)
|
|
50
|
-
|
|
51
|
-
formatter = ip.display_formatter.formatters[mime]
|
|
52
|
-
formatter.for_type(pint.Quantity, fmt_reduced)
|
|
53
|
-
|
|
54
|
-
add_formatter("text/markdown", "_repr_markdown_")
|
|
55
|
-
add_formatter("text/html", "_repr_html_")
|
|
56
|
-
add_formatter("text/plain", "_repr_pretty_")
|
|
57
|
-
add_formatter("text/latex", "_repr_latex_")
|
|
58
|
-
|
|
59
|
-
# ensure the module is still visible if imported via
|
|
60
|
-
# `from unit_syntax import ipython` for some reason
|
|
61
|
-
ip.run_cell("import unit_syntax")
|
|
62
|
-
|
|
63
|
-
try:
|
|
64
|
-
import matplotlib
|
|
65
|
-
|
|
66
|
-
pint.setup_matplotlib(True)
|
|
67
|
-
except ImportError:
|
|
68
|
-
pass
|
|
File without changes
|