Skip to content

Commit 4d354c9

Browse files
authored
Merge pull request #2134 from hgrecco/develop
refactor: reorganize and add typing to pint/pint_eval.py
2 parents 2e04c4c + f05be7d commit 4d354c9

File tree

3 files changed

+83
-80
lines changed

3 files changed

+83
-80
lines changed

pint/pint_eval.py

+81-76
Original file line numberDiff line numberDiff line change
@@ -12,40 +12,24 @@
1212
import operator
1313
import token as tokenlib
1414
import tokenize
15+
from collections.abc import Iterable
1516
from io import BytesIO
1617
from tokenize import TokenInfo
17-
from typing import Any
18-
19-
try:
20-
from uncertainties import ufloat
21-
22-
HAS_UNCERTAINTIES = True
23-
except ImportError:
24-
HAS_UNCERTAINTIES = False
25-
ufloat = None
18+
from typing import Any, Callable, Generator, Generic, Iterator, TypeVar
2619

20+
from .compat import HAS_UNCERTAINTIES, ufloat
2721
from .errors import DefinitionSyntaxError
2822

29-
# For controlling order of operations
30-
_OP_PRIORITY = {
31-
"+/-": 4,
32-
"**": 3,
33-
"^": 3,
34-
"unary": 2,
35-
"*": 1,
36-
"": 1, # operator for implicit ops
37-
"//": 1,
38-
"/": 1,
39-
"%": 1,
40-
"+": 0,
41-
"-": 0,
42-
}
23+
S = TypeVar("S")
4324

25+
if HAS_UNCERTAINTIES:
26+
_ufloat = ufloat # type: ignore
27+
else:
4428

45-
def _ufloat(left, right):
46-
if HAS_UNCERTAINTIES:
47-
return ufloat(left, right)
48-
raise TypeError("Could not import support for uncertainties")
29+
def _ufloat(*args: Any, **kwargs: Any):
30+
raise TypeError(
31+
"Please install the uncertainties package to be able to parse quantities with uncertainty."
32+
)
4933

5034

5135
def _power(left: Any, right: Any) -> Any:
@@ -63,46 +47,93 @@ def _power(left: Any, right: Any) -> Any:
6347
return operator.pow(left, right)
6448

6549

66-
# https://stackoverflow.com/a/1517965/1291237
67-
class tokens_with_lookahead:
68-
def __init__(self, iter):
50+
UnaryOpT = Callable[
51+
[
52+
Any,
53+
],
54+
Any,
55+
]
56+
BinaryOpT = Callable[[Any, Any], Any]
57+
58+
_UNARY_OPERATOR_MAP: dict[str, UnaryOpT] = {"+": lambda x: x, "-": lambda x: x * -1}
59+
60+
_BINARY_OPERATOR_MAP: dict[str, BinaryOpT] = {
61+
"+/-": _ufloat,
62+
"**": _power,
63+
"*": operator.mul,
64+
"": operator.mul, # operator for implicit ops
65+
"/": operator.truediv,
66+
"+": operator.add,
67+
"-": operator.sub,
68+
"%": operator.mod,
69+
"//": operator.floordiv,
70+
}
71+
72+
# For controlling order of operations
73+
_OP_PRIORITY = {
74+
"+/-": 4,
75+
"**": 3,
76+
"^": 3,
77+
"unary": 2,
78+
"*": 1,
79+
"": 1, # operator for implicit ops
80+
"//": 1,
81+
"/": 1,
82+
"%": 1,
83+
"+": 0,
84+
"-": 0,
85+
}
86+
87+
88+
class IteratorLookAhead(Generic[S]):
89+
"""An iterator with lookahead buffer.
90+
91+
Adapted: https://stackoverflow.com/a/1517965/1291237
92+
"""
93+
94+
def __init__(self, iter: Iterator[S]):
6995
self.iter = iter
70-
self.buffer = []
96+
self.buffer: list[S] = []
7197

7298
def __iter__(self):
7399
return self
74100

75-
def __next__(self):
101+
def __next__(self) -> S:
76102
if self.buffer:
77103
return self.buffer.pop(0)
78104
else:
79105
return self.iter.__next__()
80106

81-
def lookahead(self, n):
107+
def lookahead(self, n: int) -> S:
82108
"""Return an item n entries ahead in the iteration."""
83109
while n >= len(self.buffer):
84110
try:
85111
self.buffer.append(self.iter.__next__())
86112
except StopIteration:
87-
return None
113+
raise ValueError("Cannot look ahead, out of range")
88114
return self.buffer[n]
89115

90116

91-
def _plain_tokenizer(input_string):
117+
def plain_tokenizer(input_string: str) -> Generator[TokenInfo, None, None]:
118+
"""Standard python tokenizer"""
92119
for tokinfo in tokenize.tokenize(BytesIO(input_string.encode("utf-8")).readline):
93120
if tokinfo.type != tokenlib.ENCODING:
94121
yield tokinfo
95122

96123

97-
def uncertainty_tokenizer(input_string):
98-
def _number_or_nan(token):
124+
def uncertainty_tokenizer(input_string: str) -> Generator[TokenInfo, None, None]:
125+
"""Tokenizer capable of parsing uncertainties as v+/-u and v±u"""
126+
127+
def _number_or_nan(token: TokenInfo) -> bool:
99128
if token.type == tokenlib.NUMBER or (
100129
token.type == tokenlib.NAME and token.string == "nan"
101130
):
102131
return True
103132
return False
104133

105-
def _get_possible_e(toklist, e_index):
134+
def _get_possible_e(
135+
toklist: IteratorLookAhead[TokenInfo], e_index: int
136+
) -> TokenInfo | None:
106137
possible_e_token = toklist.lookahead(e_index)
107138
if (
108139
possible_e_token.string[0] == "e"
@@ -143,7 +174,7 @@ def _get_possible_e(toklist, e_index):
143174
possible_e = None
144175
return possible_e
145176

146-
def _apply_e_notation(mantissa, exponent):
177+
def _apply_e_notation(mantissa: TokenInfo, exponent: TokenInfo) -> TokenInfo:
147178
if mantissa.string == "nan":
148179
return mantissa
149180
if float(mantissa.string) == 0.0:
@@ -156,7 +187,12 @@ def _apply_e_notation(mantissa, exponent):
156187
line=exponent.line,
157188
)
158189

159-
def _finalize_e(nominal_value, std_dev, toklist, possible_e):
190+
def _finalize_e(
191+
nominal_value: TokenInfo,
192+
std_dev: TokenInfo,
193+
toklist: IteratorLookAhead[TokenInfo],
194+
possible_e: TokenInfo,
195+
) -> tuple[TokenInfo, TokenInfo]:
160196
nominal_value = _apply_e_notation(nominal_value, possible_e)
161197
std_dev = _apply_e_notation(std_dev, possible_e)
162198
next(toklist) # consume 'e' and positive exponent value
@@ -178,8 +214,9 @@ def _finalize_e(nominal_value, std_dev, toklist, possible_e):
178214
# wading through all that vomit, just eliminate the problem
179215
# in the input by rewriting ± as +/-.
180216
input_string = input_string.replace("±", "+/-")
181-
toklist = tokens_with_lookahead(_plain_tokenizer(input_string))
217+
toklist = IteratorLookAhead(plain_tokenizer(input_string))
182218
for tokinfo in toklist:
219+
assert tokinfo is not None
183220
line = tokinfo.line
184221
start = tokinfo.start
185222
if (
@@ -194,7 +231,7 @@ def _finalize_e(nominal_value, std_dev, toklist, possible_e):
194231
end=toklist.lookahead(1).end,
195232
line=line,
196233
)
197-
for i in range(-1, 1):
234+
for _ in range(-1, 1):
198235
next(toklist)
199236
yield plus_minus_op
200237
elif (
@@ -280,31 +317,7 @@ def _finalize_e(nominal_value, std_dev, toklist, possible_e):
280317
if HAS_UNCERTAINTIES:
281318
tokenizer = uncertainty_tokenizer
282319
else:
283-
tokenizer = _plain_tokenizer
284-
285-
import typing
286-
287-
UnaryOpT = typing.Callable[
288-
[
289-
Any,
290-
],
291-
Any,
292-
]
293-
BinaryOpT = typing.Callable[[Any, Any], Any]
294-
295-
_UNARY_OPERATOR_MAP: dict[str, UnaryOpT] = {"+": lambda x: x, "-": lambda x: x * -1}
296-
297-
_BINARY_OPERATOR_MAP: dict[str, BinaryOpT] = {
298-
"+/-": _ufloat,
299-
"**": _power,
300-
"*": operator.mul,
301-
"": operator.mul, # operator for implicit ops
302-
"/": operator.truediv,
303-
"+": operator.add,
304-
"-": operator.sub,
305-
"%": operator.mod,
306-
"//": operator.floordiv,
307-
}
320+
tokenizer = plain_tokenizer
308321

309322

310323
class EvalTreeNode:
@@ -344,12 +357,7 @@ def to_string(self) -> str:
344357

345358
def evaluate(
346359
self,
347-
define_op: typing.Callable[
348-
[
349-
Any,
350-
],
351-
Any,
352-
],
360+
define_op: UnaryOpT,
353361
bin_op: dict[str, BinaryOpT] | None = None,
354362
un_op: dict[str, UnaryOpT] | None = None,
355363
):
@@ -395,9 +403,6 @@ def evaluate(
395403
return define_op(self.left)
396404

397405

398-
from collections.abc import Iterable
399-
400-
401406
def _build_eval_tree(
402407
tokens: list[TokenInfo],
403408
op_priority: dict[str, int],

pint/testsuite/benchmarks/test_01_eval.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22

33
import pytest
44

5-
from pint.pint_eval import _plain_tokenizer as plain_tokenizer
6-
from pint.pint_eval import uncertainty_tokenizer
5+
from pint.pint_eval import plain_tokenizer, uncertainty_tokenizer
76

87
VALUES = [
98
"1",

pint/testsuite/test_pint_eval.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22

33
import pytest
44

5-
from pint.pint_eval import _plain_tokenizer as plain_tokenizer
6-
from pint.pint_eval import build_eval_tree, uncertainty_tokenizer
5+
from pint.pint_eval import build_eval_tree, plain_tokenizer, uncertainty_tokenizer
76
from pint.util import string_preprocessor
87

98
TOKENIZERS = (plain_tokenizer, uncertainty_tokenizer)

0 commit comments

Comments
 (0)