istr-python 1.1.43__tar.gz → 1.1.44__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: istr-python
3
- Version: 1.1.43
3
+ Version: 1.1.44
4
4
  Summary: istr - strings you can count on
5
5
  Author-email: Ruud van der Ham <rt.van.der.ham@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/salabim/istr
@@ -1041,6 +1041,41 @@ Note that `str(istr(":="))` is ":=" and does not assign any value.
1041
1041
 
1042
1042
  Usually, composing and decomposing uses the globals namespace, but this can be overridden with the namespace parameter. See the test suite for details.
1043
1043
 
1044
+ #### Evaluating expressions with one letter variables
1045
+ The `istr.eval` method is essentially the same as `istr.compose`, but (safely) evaluates the result.
1046
+ E.g.
1047
+
1048
+ ```
1049
+ a = 5
1050
+ b = 2
1051
+ print(istr.eval('aab + ba')
1052
+ ```
1053
+ will print `577`
1054
+
1055
+ The operators +, -, * and / are supported.
1056
+ Comparisons (==, <=, <, >, >= are also supported. In that case, the result is also istr-ed, so istr('0') for False and istr('1') for True. E.g.
1057
+
1058
+ ```
1059
+ a = 5
1060
+ b = 2
1061
+ print(istr.eval('a > b')
1062
+ ```
1063
+ will print 1 (meaning True)
1064
+
1065
+ Note that leading zeroes are accepted. So, `istr.eval('03 + 7')` is `10`.
1066
+ Instead of ==, = can be used. So, `istr.eval('12 = 5 + 7')` is `1` (meaning True)
1067
+
1068
+ ##### Alternative method
1069
+
1070
+ If istr is called with a string starting with a `?`, the string without the leading `?` will be evaluated using the new `istr.eval` method. So, the above examples read:
1071
+ ```
1072
+ a = 5
1073
+ b = 2
1074
+ print(istr("?aab + ba':")
1075
+ print(istr("?a > b")
1076
+ ```
1077
+ with output `577` and `1` (meaning True)
1078
+
1044
1079
  #### Subclassing istr
1045
1080
 
1046
1081
  When a class is derived from istr, all methods will return that newly derived class.
@@ -1028,6 +1028,41 @@ Note that `str(istr(":="))` is ":=" and does not assign any value.
1028
1028
 
1029
1029
  Usually, composing and decomposing uses the globals namespace, but this can be overridden with the namespace parameter. See the test suite for details.
1030
1030
 
1031
+ #### Evaluating expressions with one letter variables
1032
+ The `istr.eval` method is essentially the same as `istr.compose`, but (safely) evaluates the result.
1033
+ E.g.
1034
+
1035
+ ```
1036
+ a = 5
1037
+ b = 2
1038
+ print(istr.eval('aab + ba')
1039
+ ```
1040
+ will print `577`
1041
+
1042
+ The operators +, -, * and / are supported.
1043
+ Comparisons (==, <=, <, >, >= are also supported. In that case, the result is also istr-ed, so istr('0') for False and istr('1') for True. E.g.
1044
+
1045
+ ```
1046
+ a = 5
1047
+ b = 2
1048
+ print(istr.eval('a > b')
1049
+ ```
1050
+ will print 1 (meaning True)
1051
+
1052
+ Note that leading zeroes are accepted. So, `istr.eval('03 + 7')` is `10`.
1053
+ Instead of ==, = can be used. So, `istr.eval('12 = 5 + 7')` is `1` (meaning True)
1054
+
1055
+ ##### Alternative method
1056
+
1057
+ If istr is called with a string starting with a `?`, the string without the leading `?` will be evaluated using the new `istr.eval` method. So, the above examples read:
1058
+ ```
1059
+ a = 5
1060
+ b = 2
1061
+ print(istr("?aab + ba':")
1062
+ print(istr("?a > b")
1063
+ ```
1064
+ with output `577` and `1` (meaning True)
1065
+
1031
1066
  #### Subclassing istr
1032
1067
 
1033
1068
  When a class is derived from istr, all methods will return that newly derived class.
@@ -5,7 +5,7 @@
5
5
  # |_||___/ \__||_|
6
6
  # strings you can count on
7
7
 
8
- __version__ = "1.1.43"
8
+ __version__ = "1.1.44"
9
9
  import functools
10
10
  import itertools
11
11
  import types
@@ -17,6 +17,8 @@ import copy
17
17
  import bisect
18
18
  import collections
19
19
  import numbers
20
+ import re
21
+ import ast
20
22
 
21
23
  """
22
24
  Note: the changelog is in changelog.md
@@ -282,15 +284,18 @@ class istr(str):
282
284
  return map(lambda v: cls(v, base=base, int_format=int_format, repr_mode=repr_mode, namespace=get_namespace(namespace)), value)
283
285
  return type(value)(map(lambda v: cls(v, base=base, int_format=int_format, repr_mode=repr_mode, namespace=get_namespace(namespace)), value))
284
286
 
285
- if isinstance(value, str) and (any(value.startswith(s) and value != s for s in ("=", ":="))):
286
- if value[0] == "=":
287
- value = str(cls.compose(value[1:], namespace=get_namespace(namespace)))
288
- else: # it's :=
289
- var_name = value[2:]
290
- if not var_name.isidentifier():
291
- raise ValueError(f"{var_name!r} is not a valid identifier")
292
- value = str(cls.compose(var_name, namespace=get_namespace(namespace)))
293
- get_namespace(namespace)[var_name] = cls(value)
287
+ if isinstance(value, str) and (any(value.startswith(s) and value != s for s in ("=", ":=",'?'))):
288
+ match value[0]:
289
+ case '?':
290
+ value = str(cls.eval(value[1:], namespace=get_namespace(namespace)))
291
+ case '=':
292
+ value = str(cls.compose(value[1:], namespace=get_namespace(namespace)))
293
+ case ':':
294
+ var_name = value[2:]
295
+ if not var_name.isidentifier():
296
+ raise ValueError(f"{var_name!r} is not a valid identifier")
297
+ value = str(cls.compose(var_name, namespace=get_namespace(namespace)))
298
+ get_namespace(namespace)[var_name] = cls(value)
294
299
  as_int = cls._to_int(value, base)
295
300
  if as_int is cls._nan or isinstance(value, str):
296
301
  as_str = value
@@ -674,7 +679,20 @@ class istr(str):
674
679
  result.append(str(namespace[letter]))
675
680
  else:
676
681
  result.append(letter)
677
- return cls("".join(result))
682
+
683
+ result_str = "".join(result)
684
+ return cls(result_str)
685
+
686
+ @classmethod
687
+ def eval(cls, letters, namespace=None):
688
+ """
689
+ evaluates an istr from individual letter variables
690
+ """
691
+ expr = cls.compose(letters, namespace=namespace)
692
+ expr = re.sub(r"\b0+(\d+)\b", r"\1", expr) # delete leading zeroes
693
+ expr=re.sub(r'(?<![<>=!])=(?!=)', '==', expr) # support =
694
+ result = safe_eval(expr)
695
+ return cls(result)
678
696
 
679
697
  def __or__(self, other):
680
698
  if isinstance(other, str):
@@ -1330,6 +1348,54 @@ def real_caller_frame():
1330
1348
  return frame
1331
1349
 
1332
1350
 
1351
+ OPS = {ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul, ast.Div: operator.truediv, ast.Pow: operator.pow}
1352
+
1353
+ COMPARE_OPS = {
1354
+ ast.Eq: operator.eq, # ==
1355
+ ast.NotEq: operator.ne, # !=
1356
+ ast.Lt: operator.lt, # <
1357
+ ast.LtE: operator.le, # <=
1358
+ ast.Gt: operator.gt, # >
1359
+ ast.GtE: operator.ge, # >=
1360
+ }
1361
+
1362
+
1363
+ def safe_eval(expr):
1364
+ def evaluate(node):
1365
+ if isinstance(node, ast.Constant) and isinstance(node.value, (int, float, str, bool)):
1366
+ return node.value
1367
+
1368
+ if isinstance(node, ast.BinOp) and type(node.op) in OPS:
1369
+ return OPS[type(node.op)](evaluate(node.left), evaluate(node.right))
1370
+
1371
+ if isinstance(node, ast.UnaryOp) and isinstance(node.op, (ast.UAdd, ast.USub)):
1372
+ value = evaluate(node.operand)
1373
+ return value if isinstance(node.op, ast.UAdd) else -value
1374
+
1375
+ if isinstance(node, ast.Compare):
1376
+ left = evaluate(node.left)
1377
+
1378
+ # Supports chained comparisons, e.g. 1 < 2 < 3
1379
+ for op, comparator in zip(node.ops, node.comparators):
1380
+ op_func = COMPARE_OPS.get(type(op))
1381
+ if op_func is None:
1382
+ raise ValueError("Unsupported comparison")
1383
+
1384
+ right = evaluate(comparator)
1385
+
1386
+ if not op_func(left, right):
1387
+ return False
1388
+
1389
+ left = right
1390
+
1391
+ return True
1392
+
1393
+ raise ValueError("Unsupported expression")
1394
+
1395
+ tree = ast.parse(expr, mode="eval")
1396
+ return evaluate(tree.body)
1397
+
1398
+
1333
1399
  istr.type = type(istr(0))
1334
1400
 
1335
1401
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: istr-python
3
- Version: 1.1.43
3
+ Version: 1.1.44
4
4
  Summary: istr - strings you can count on
5
5
  Author-email: Ruud van der Ham <rt.van.der.ham@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/salabim/istr
@@ -1041,6 +1041,41 @@ Note that `str(istr(":="))` is ":=" and does not assign any value.
1041
1041
 
1042
1042
  Usually, composing and decomposing uses the globals namespace, but this can be overridden with the namespace parameter. See the test suite for details.
1043
1043
 
1044
+ #### Evaluating expressions with one letter variables
1045
+ The `istr.eval` method is essentially the same as `istr.compose`, but (safely) evaluates the result.
1046
+ E.g.
1047
+
1048
+ ```
1049
+ a = 5
1050
+ b = 2
1051
+ print(istr.eval('aab + ba')
1052
+ ```
1053
+ will print `577`
1054
+
1055
+ The operators +, -, * and / are supported.
1056
+ Comparisons (==, <=, <, >, >= are also supported. In that case, the result is also istr-ed, so istr('0') for False and istr('1') for True. E.g.
1057
+
1058
+ ```
1059
+ a = 5
1060
+ b = 2
1061
+ print(istr.eval('a > b')
1062
+ ```
1063
+ will print 1 (meaning True)
1064
+
1065
+ Note that leading zeroes are accepted. So, `istr.eval('03 + 7')` is `10`.
1066
+ Instead of ==, = can be used. So, `istr.eval('12 = 5 + 7')` is `1` (meaning True)
1067
+
1068
+ ##### Alternative method
1069
+
1070
+ If istr is called with a string starting with a `?`, the string without the leading `?` will be evaluated using the new `istr.eval` method. So, the above examples read:
1071
+ ```
1072
+ a = 5
1073
+ b = 2
1074
+ print(istr("?aab + ba':")
1075
+ print(istr("?a > b")
1076
+ ```
1077
+ with output `577` and `1` (meaning True)
1078
+
1044
1079
  #### Subclassing istr
1045
1080
 
1046
1081
  When a class is derived from istr, all methods will return that newly derived class.
@@ -10,7 +10,7 @@ authors = [
10
10
  { name = "Ruud van der Ham", email = "rt.van.der.ham@gmail.com" },
11
11
  ]
12
12
  description = "istr - strings you can count on"
13
- version = "1.1.43"
13
+ version = "1.1.44"
14
14
  readme = "README.md"
15
15
  requires-python = ">=3.10"
16
16
  dependencies = []
File without changes