expression-py 0.1.0__py3-none-any.whl
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.
- expression/__init__.py +350 -0
- expression/grammar.peg +191 -0
- expression/helpers.py +169 -0
- expression/parser.py +968 -0
- expression/tokenizer.py +171 -0
- expression_py-0.1.0.dist-info/METADATA +350 -0
- expression_py-0.1.0.dist-info/RECORD +10 -0
- expression_py-0.1.0.dist-info/WHEEL +5 -0
- expression_py-0.1.0.dist-info/licenses/LICENSE +674 -0
- expression_py-0.1.0.dist-info/top_level.txt +1 -0
expression/tokenizer.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# Copyright (C) 2026 Jakub T. Jankiewicz <https://jcu.bi/>
|
|
2
|
+
#
|
|
3
|
+
# This file is part of expression.py.
|
|
4
|
+
#
|
|
5
|
+
# expression.py is free software: you can redistribute it and/or modify
|
|
6
|
+
# it under the terms of the GNU General Public License as published by
|
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
# (at your option) any later version.
|
|
9
|
+
#
|
|
10
|
+
# expression.py is distributed in the hope that it will be useful,
|
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
# GNU General Public License for more details.
|
|
14
|
+
#
|
|
15
|
+
# You should have received a copy of the GNU General Public License
|
|
16
|
+
# along with expression.py. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
|
|
18
|
+
import re
|
|
19
|
+
import token
|
|
20
|
+
import tokenize
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ExpressionTokenizer:
|
|
24
|
+
"""Custom tokenizer for the expression language, compatible with pegen's Parser."""
|
|
25
|
+
|
|
26
|
+
TOKEN_PATTERNS = [
|
|
27
|
+
('WHITESPACE', r'[ \t]+'),
|
|
28
|
+
('HEX', r'0x[0-9A-Fa-f]+'),
|
|
29
|
+
('BIN', r'0b[01]+'),
|
|
30
|
+
('FLOAT', r'(?:[0-9]+\.[0-9]*|\.[0-9]+)(?:e[+-]?[0-9]+)?|[0-9]+e[+-]?[0-9]+'),
|
|
31
|
+
('INT', r'[0-9]+'),
|
|
32
|
+
('DSTRING', r'"[^"\\]*(?:\\[\S\s][^"\\]*)*"'),
|
|
33
|
+
('SSTRING', r"'[^'\\]*(?:\\[\S\s][^'\\]*)*'"),
|
|
34
|
+
('REGEX', r'/(?:[^/\\]|\\.)+/[imsxUXJ]*'),
|
|
35
|
+
('STRICT_EQ', r'==='),
|
|
36
|
+
('STRICT_NE', r'!=='),
|
|
37
|
+
('EQ', r'=='),
|
|
38
|
+
('MATCH', r'=~'),
|
|
39
|
+
('NE', r'!='),
|
|
40
|
+
('NOT', r'!'),
|
|
41
|
+
('ASSIGN', r'='),
|
|
42
|
+
('AND', r'&&'),
|
|
43
|
+
('OR', r'\|\|'),
|
|
44
|
+
('POWER', r'\*\*'),
|
|
45
|
+
('SPACESHIP', r'<=>'),
|
|
46
|
+
('LSHIFT', r'<<'),
|
|
47
|
+
('RSHIFT', r'>>'),
|
|
48
|
+
('GE', r'>='),
|
|
49
|
+
('LE', r'<='),
|
|
50
|
+
('GT', r'>'),
|
|
51
|
+
('LT', r'<'),
|
|
52
|
+
('PLUS', r'\+'),
|
|
53
|
+
('MINUS', r'-'),
|
|
54
|
+
('TIMES', r'\*'),
|
|
55
|
+
('DIV', r'/'),
|
|
56
|
+
('MOD', r'%'),
|
|
57
|
+
('AMP', r'&'),
|
|
58
|
+
('PIPE', r'\|'),
|
|
59
|
+
('QUESTION', r'\?'),
|
|
60
|
+
('CARET', r'\^'),
|
|
61
|
+
('LPAREN', r'\('),
|
|
62
|
+
('RPAREN', r'\)'),
|
|
63
|
+
('LBRACKET', r'\['),
|
|
64
|
+
('RBRACKET', r'\]'),
|
|
65
|
+
('LBRACE', r'\{'),
|
|
66
|
+
('RBRACE', r'\}'),
|
|
67
|
+
('COMMA', r','),
|
|
68
|
+
('COLON', r':'),
|
|
69
|
+
('SEMICOLON', r';'),
|
|
70
|
+
('DOLLAR_NAME', r'\$[0-9A-Za-z_]+'),
|
|
71
|
+
('NAME', r'[A-Za-z_][A-Za-z_0-9]*'),
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
_compiled = re.compile('|'.join(f'(?P<{name}>{pattern})' for name, pattern in TOKEN_PATTERNS))
|
|
75
|
+
|
|
76
|
+
OP_TOKENS = {
|
|
77
|
+
'STRICT_EQ', 'STRICT_NE', 'EQ', 'MATCH', 'NE', 'NOT', 'ASSIGN',
|
|
78
|
+
'AND', 'OR', 'POWER', 'SPACESHIP', 'LSHIFT', 'RSHIFT', 'GE', 'LE', 'GT', 'LT',
|
|
79
|
+
'PLUS', 'MINUS', 'TIMES', 'DIV', 'MOD', 'AMP', 'PIPE', 'QUESTION', 'CARET',
|
|
80
|
+
'LPAREN', 'RPAREN', 'LBRACKET', 'RBRACKET', 'LBRACE', 'RBRACE',
|
|
81
|
+
'COMMA', 'COLON', 'SEMICOLON',
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
def __init__(self, text):
|
|
85
|
+
self._text = text
|
|
86
|
+
self._tokens = []
|
|
87
|
+
self._index = 0
|
|
88
|
+
self._tokenize(text)
|
|
89
|
+
|
|
90
|
+
REGEX_TOKEN_TYPE = 100
|
|
91
|
+
|
|
92
|
+
def _can_be_regex(self):
|
|
93
|
+
if not self._tokens:
|
|
94
|
+
return True
|
|
95
|
+
last = self._tokens[-1]
|
|
96
|
+
if last.type == token.NUMBER:
|
|
97
|
+
return False
|
|
98
|
+
if last.type == token.NAME:
|
|
99
|
+
return False
|
|
100
|
+
if last.type == token.STRING:
|
|
101
|
+
return False
|
|
102
|
+
if last.string in (')', ']'):
|
|
103
|
+
return False
|
|
104
|
+
return True
|
|
105
|
+
|
|
106
|
+
def _tokenize(self, text):
|
|
107
|
+
pos = 0
|
|
108
|
+
line = text
|
|
109
|
+
while pos < len(text):
|
|
110
|
+
m = self._compiled.match(text, pos)
|
|
111
|
+
if m is None:
|
|
112
|
+
raise SyntaxError(f"Unexpected character: {text[pos]!r} at position {pos}")
|
|
113
|
+
kind = m.lastgroup
|
|
114
|
+
value = m.group()
|
|
115
|
+
start = (1, pos)
|
|
116
|
+
end = (1, pos + len(value))
|
|
117
|
+
pos = m.end()
|
|
118
|
+
if kind == 'WHITESPACE':
|
|
119
|
+
continue
|
|
120
|
+
if kind == 'REGEX':
|
|
121
|
+
if self._can_be_regex():
|
|
122
|
+
tok = tokenize.TokenInfo(self.REGEX_TOKEN_TYPE, value, start, end, line)
|
|
123
|
+
else:
|
|
124
|
+
div_end = (1, start[1] + 1)
|
|
125
|
+
tok = tokenize.TokenInfo(token.OP, '/', start, div_end, line)
|
|
126
|
+
self._tokens.append(tok)
|
|
127
|
+
pos = start[1] + 1
|
|
128
|
+
continue
|
|
129
|
+
elif kind in ('HEX', 'BIN', 'FLOAT', 'INT'):
|
|
130
|
+
tok = tokenize.TokenInfo(token.NUMBER, value, start, end, line)
|
|
131
|
+
elif kind in ('DSTRING', 'SSTRING'):
|
|
132
|
+
tok = tokenize.TokenInfo(token.STRING, value, start, end, line)
|
|
133
|
+
elif kind in ('NAME', 'DOLLAR_NAME'):
|
|
134
|
+
tok = tokenize.TokenInfo(token.NAME, value, start, end, line)
|
|
135
|
+
elif kind in self.OP_TOKENS:
|
|
136
|
+
tok = tokenize.TokenInfo(token.OP, value, start, end, line)
|
|
137
|
+
else:
|
|
138
|
+
tok = tokenize.TokenInfo(token.OP, value, start, end, line)
|
|
139
|
+
self._tokens.append(tok)
|
|
140
|
+
self._tokens.append(
|
|
141
|
+
tokenize.TokenInfo(token.ENDMARKER, '', (1, pos), (1, pos), line)
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def peek(self):
|
|
145
|
+
if self._index < len(self._tokens):
|
|
146
|
+
return self._tokens[self._index]
|
|
147
|
+
return self._tokens[-1]
|
|
148
|
+
|
|
149
|
+
def getnext(self):
|
|
150
|
+
tok = self.peek()
|
|
151
|
+
if self._index < len(self._tokens):
|
|
152
|
+
self._index += 1
|
|
153
|
+
return tok
|
|
154
|
+
|
|
155
|
+
def mark(self):
|
|
156
|
+
return self._index
|
|
157
|
+
|
|
158
|
+
def reset(self, index):
|
|
159
|
+
self._index = index
|
|
160
|
+
|
|
161
|
+
def diagnose(self):
|
|
162
|
+
if not self._tokens:
|
|
163
|
+
return tokenize.TokenInfo(token.ENDMARKER, '', (1, 0), (1, 0), '')
|
|
164
|
+
return self._tokens[min(self._index, len(self._tokens) - 1)]
|
|
165
|
+
|
|
166
|
+
def get_last_non_whitespace_token(self):
|
|
167
|
+
for i in range(self._index - 1, -1, -1):
|
|
168
|
+
tok = self._tokens[i]
|
|
169
|
+
if tok.type not in (token.ENDMARKER, token.NEWLINE, token.INDENT, token.DEDENT):
|
|
170
|
+
return tok
|
|
171
|
+
return self._tokens[0] if self._tokens else None
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: expression-py
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Mathematical expression evaluation library with PEG parser
|
|
5
|
+
Author-email: "Jakub T. Jankiewicz" <jcubic.jj@gmail.com>
|
|
6
|
+
License-Expression: GPL-3.0-or-later
|
|
7
|
+
Project-URL: Homepage, https://github.com/jcubic/expression.py
|
|
8
|
+
Project-URL: Repository, https://github.com/jcubic/expression.py
|
|
9
|
+
Project-URL: Issues, https://github.com/jcubic/expression.py/issues
|
|
10
|
+
Keywords: expression,evaluator,parser,peg,math
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: pegen>=0.3.0
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# expression.py
|
|
28
|
+
|
|
29
|
+
[](https://pypi.org/project/expression-py/)
|
|
30
|
+
[](https://github.com/jcubic/expression.py/actions/workflows/ci.yml)
|
|
31
|
+
[](https://pepy.tech/projects/expression-py)
|
|
32
|
+
[](https://github.com/jcubic/expression.py)
|
|
33
|
+
[](https://coveralls.io/github/jcubic/expression.py?branch=master)
|
|
34
|
+
[](https://github.com/jcubic/expression.py/blob/master/LICENSE)
|
|
35
|
+
|
|
36
|
+
Mathematical expression evaluation library for Python, built with a PEG parser generated by
|
|
37
|
+
[pegen](https://github.com/we-like-parsers/pegen). An alternative to
|
|
38
|
+
[safeeval](https://pypi.org/project/safeeval/) that supports more Ruby operators like `=~` and array
|
|
39
|
+
operators.
|
|
40
|
+
|
|
41
|
+
This is a Python port of [jcubic/expression.php](https://github.com/jcubic/expression.php).
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install expression-py
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Usage
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from expression import Expression
|
|
53
|
+
|
|
54
|
+
expr = Expression()
|
|
55
|
+
|
|
56
|
+
# Basic arithmetic
|
|
57
|
+
expr.evaluate("2 + 3 * 4") # 14
|
|
58
|
+
expr.evaluate("(2 + 3) * 4") # 20
|
|
59
|
+
expr.evaluate("2 ** 10") # 1024
|
|
60
|
+
expr.evaluate("10 % 3") # 1
|
|
61
|
+
|
|
62
|
+
# Floating point
|
|
63
|
+
expr.evaluate("0.1 + 0.2") # 0.30000000000000004
|
|
64
|
+
expr.evaluate("1.5e2") # 150.0
|
|
65
|
+
|
|
66
|
+
# Hex and binary literals
|
|
67
|
+
expr.evaluate("0xFF") # 255
|
|
68
|
+
expr.evaluate("0b1010") # 10
|
|
69
|
+
|
|
70
|
+
# Bitshift operators
|
|
71
|
+
expr.evaluate("100 >> 2") # 25
|
|
72
|
+
expr.evaluate("100 << 2") # 400
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Variables
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
expr = Expression()
|
|
79
|
+
|
|
80
|
+
# Assignment
|
|
81
|
+
expr.evaluate("x = 10")
|
|
82
|
+
expr.evaluate("x + 2") # 12
|
|
83
|
+
|
|
84
|
+
# Pre-set variables
|
|
85
|
+
expr.variables = {"width": 100, "height": 50}
|
|
86
|
+
expr.evaluate("width * height") # 5000
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Built-in Constants and Functions
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
expr = Expression()
|
|
93
|
+
|
|
94
|
+
# Constants
|
|
95
|
+
expr.evaluate("pi") # 3.141592653589793
|
|
96
|
+
expr.evaluate("e") # 2.718281828459045
|
|
97
|
+
|
|
98
|
+
# Math functions
|
|
99
|
+
expr.evaluate("sqrt(16)") # 4.0
|
|
100
|
+
expr.evaluate("sin(pi / 2)") # 1.0
|
|
101
|
+
expr.evaluate("abs(-42)") # 42
|
|
102
|
+
expr.evaluate("log(e)") # 1.0
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Available built-in functions: `sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `sinh`, `cosh`, `tanh`, `asinh`, `acosh`, `atanh`, `sqrt`, `abs`, `log`, `ln`.
|
|
106
|
+
|
|
107
|
+
Aliases: `arcsin`/`arccos`/`arctan`/`arcsinh`/`arccosh`/`arctanh` map to their `a`-prefixed equivalents. `ln` is an alias for `log`.
|
|
108
|
+
|
|
109
|
+
### Custom Functions
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
expr = Expression()
|
|
113
|
+
|
|
114
|
+
# Define functions in the expression language
|
|
115
|
+
expr.evaluate("f(x, y) = x + y")
|
|
116
|
+
expr.evaluate("f(10, 20)") # 30
|
|
117
|
+
|
|
118
|
+
expr.evaluate("square(x) = x * x")
|
|
119
|
+
expr.evaluate("square(5)") # 25
|
|
120
|
+
|
|
121
|
+
# Register Python functions
|
|
122
|
+
expr.functions["even"] = lambda x: x % 2 == 0
|
|
123
|
+
expr.evaluate("even(4)") # True
|
|
124
|
+
|
|
125
|
+
# Access defined functions from Python
|
|
126
|
+
expr.evaluate("add(a, b) = a + b")
|
|
127
|
+
expr.functions["add"](3, 4) # 7
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Implicit Multiplication
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
expr = Expression()
|
|
134
|
+
|
|
135
|
+
expr.evaluate("f(x) = 2x")
|
|
136
|
+
expr.evaluate("f(5)") # 10
|
|
137
|
+
expr.evaluate("x = 3")
|
|
138
|
+
expr.evaluate("2x") # 6
|
|
139
|
+
expr.evaluate("2(3 + 4)") # 14
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Boolean and Comparison Operators
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
expr = Expression()
|
|
146
|
+
|
|
147
|
+
# Comparisons
|
|
148
|
+
expr.evaluate("10 == 10") # True
|
|
149
|
+
expr.evaluate("10 != 20") # True
|
|
150
|
+
expr.evaluate("10 > 5") # True
|
|
151
|
+
expr.evaluate("10 <= 10") # True
|
|
152
|
+
|
|
153
|
+
# Strict equality (type-aware)
|
|
154
|
+
expr.evaluate("2 === 2") # True
|
|
155
|
+
expr.evaluate("'2' !== 2") # True
|
|
156
|
+
|
|
157
|
+
# Boolean operators
|
|
158
|
+
expr.evaluate("10 > 5 && 20 > 10") # True
|
|
159
|
+
expr.evaluate("10 > 50 || 20 > 10") # True
|
|
160
|
+
expr.evaluate("!0") # True
|
|
161
|
+
|
|
162
|
+
# Keywords
|
|
163
|
+
expr.evaluate("true == true") # True
|
|
164
|
+
expr.evaluate("null == null") # True
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Strings
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
expr = Expression()
|
|
171
|
+
|
|
172
|
+
# String literals (single or double quotes)
|
|
173
|
+
expr.evaluate('"hello" + " " + "world"') # "hello world"
|
|
174
|
+
expr.evaluate("'foo' == 'foo'") # True
|
|
175
|
+
|
|
176
|
+
# Ruby-style string operators
|
|
177
|
+
expr.evaluate('"ab" * 3') # "ababab" (repeat)
|
|
178
|
+
expr.evaluate('"a" << "b"') # "ab" (append; mutates a variable)
|
|
179
|
+
expr.evaluate('"a" <=> "b"') # -1 (lexicographic compare)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Regular Expressions
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
expr = Expression()
|
|
186
|
+
|
|
187
|
+
# Match operator
|
|
188
|
+
expr.evaluate('"foobar" =~ /([fo]+)/i') # True
|
|
189
|
+
expr.evaluate("$1") # "Foo" (capture group)
|
|
190
|
+
|
|
191
|
+
expr.evaluate('"hello" =~ /([0-9]+)/') # False
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### JSON Literals
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
expr = Expression()
|
|
198
|
+
|
|
199
|
+
# Objects and arrays
|
|
200
|
+
expr.evaluate('{"name": "Alice"}') # {"name": "Alice"}
|
|
201
|
+
expr.evaluate('[10, 20, 30]') # [10, 20, 30]
|
|
202
|
+
|
|
203
|
+
# Property access
|
|
204
|
+
expr.evaluate('{"foo": "bar"}["foo"]') # "bar"
|
|
205
|
+
expr.evaluate('[10, 20][0]') # 10
|
|
206
|
+
|
|
207
|
+
# Comparison
|
|
208
|
+
expr.evaluate('{"a": 1} == {"a": 1}') # True
|
|
209
|
+
expr.evaluate('[1, 2] != [3, 4]') # True
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Array Operators
|
|
213
|
+
|
|
214
|
+
Ruby-inspired operators for concise list manipulation. When one operand is an
|
|
215
|
+
array and the other is a scalar, the scalar is coerced to a single-element array.
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
expr = Expression()
|
|
219
|
+
|
|
220
|
+
# Intersection (&) — common elements, deduped, left order preserved
|
|
221
|
+
expr.evaluate("[1, 1, 2, 3] & [3, 4]") # [3]
|
|
222
|
+
|
|
223
|
+
# Union (|) — combined elements, deduped
|
|
224
|
+
expr.evaluate("[1, 2] | [2, 3]") # [1, 2, 3]
|
|
225
|
+
|
|
226
|
+
# Difference (-) — left elements not in right
|
|
227
|
+
expr.evaluate("[1, 2, 2, 3] - [2]") # [1, 3]
|
|
228
|
+
|
|
229
|
+
# Concatenation (+) — keeps duplicates
|
|
230
|
+
expr.evaluate("[1, 2] + [2, 3]") # [1, 2, 2, 3]
|
|
231
|
+
|
|
232
|
+
# Append (<<) — mutates the left operand when it is a variable
|
|
233
|
+
expr.evaluate("[1, 2] << 3") # [1, 2, 3]
|
|
234
|
+
|
|
235
|
+
# Multiplication (*) — repeat with an integer
|
|
236
|
+
expr.evaluate("[1, 2] * 3") # [1, 2, 1, 2, 1, 2]
|
|
237
|
+
# Join (*) — with a string separator
|
|
238
|
+
expr.evaluate('["a", "b"] * "-"') # "a-b"
|
|
239
|
+
|
|
240
|
+
# Deep equality (==) and spaceship (<=>)
|
|
241
|
+
expr.evaluate("[1, 2] == [1, 2]") # True
|
|
242
|
+
expr.evaluate("[1, 2] <=> [1, 3]") # -1
|
|
243
|
+
|
|
244
|
+
# Membership (in) — array element, or substring of a string
|
|
245
|
+
expr.evaluate("2 in [1, 2, 3]") # True
|
|
246
|
+
expr.evaluate('"py" in "python"') # True
|
|
247
|
+
|
|
248
|
+
# Scalar coercion
|
|
249
|
+
expr.evaluate("[1, 2, 3] & 2") # [2]
|
|
250
|
+
expr.evaluate("1 + [2, 3]") # [1, 2, 3]
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
When neither operand is an array, these operators fall back to scalar
|
|
254
|
+
semantics: `&` and `|` are bitwise AND/OR, `<<`/`>>` are bitshifts, `<=>`
|
|
255
|
+
compares numbers or strings, and `+`/`-`/`*` are arithmetic.
|
|
256
|
+
|
|
257
|
+
```python
|
|
258
|
+
expr.evaluate("6 & 3") # 2 (bitwise AND)
|
|
259
|
+
expr.evaluate("6 | 1") # 7 (bitwise OR)
|
|
260
|
+
expr.evaluate("5 <=> 3") # 1
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Empty arrays are falsy (unlike JavaScript), so they work directly in boolean
|
|
264
|
+
contexts:
|
|
265
|
+
|
|
266
|
+
```python
|
|
267
|
+
expr.evaluate("!![]") # False
|
|
268
|
+
expr.evaluate('[] || "default"') # "default"
|
|
269
|
+
expr.evaluate("[] ? 'yes' : 'no'") # "no"
|
|
270
|
+
|
|
271
|
+
# Validator pattern: true only when at least one skill matches
|
|
272
|
+
expr.variables = {"skills": ["Python", "AI"]}
|
|
273
|
+
bool(expr.evaluate('skills & ["AI", "ML"]')) # True
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Conditional (Ternary) Operator
|
|
277
|
+
|
|
278
|
+
```python
|
|
279
|
+
expr = Expression()
|
|
280
|
+
|
|
281
|
+
expr.evaluate("1 > 0 ? 'yes' : 'no'") # "yes"
|
|
282
|
+
expr.evaluate("[] ? 'yes' : 'no'") # "no"
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Error Handling
|
|
286
|
+
|
|
287
|
+
```python
|
|
288
|
+
expr = Expression()
|
|
289
|
+
|
|
290
|
+
# Errors raise exceptions by default
|
|
291
|
+
try:
|
|
292
|
+
expr.evaluate("10 / 0")
|
|
293
|
+
except ZeroDivisionError:
|
|
294
|
+
print("Division by zero!")
|
|
295
|
+
|
|
296
|
+
# Suppress errors (returns None on error)
|
|
297
|
+
expr.suppress_errors = True
|
|
298
|
+
expr.evaluate("unknown_var") # None
|
|
299
|
+
print(expr.last_error) # error message
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Semicolons
|
|
303
|
+
|
|
304
|
+
Trailing semicolons are optional and ignored:
|
|
305
|
+
|
|
306
|
+
```python
|
|
307
|
+
expr.evaluate("10 + 10;") # 20
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## Operator Precedence (lowest to highest)
|
|
311
|
+
|
|
312
|
+
| Precedence | Operators |
|
|
313
|
+
|-----------|-----------|
|
|
314
|
+
| 0 | `? :` (ternary) |
|
|
315
|
+
| 1 | `\|\|` |
|
|
316
|
+
| 2 | `&&` |
|
|
317
|
+
| 3 | `==`, `!=`, `===`, `!==`, `=~`, `<=>`, `>`, `<`, `>=`, `<=`, `in` |
|
|
318
|
+
| 4 | `<<`, `>>` |
|
|
319
|
+
| 5 | `+`, `-`, `\|` |
|
|
320
|
+
| 6 | `*`, `/`, `%`, `&`, `[]`, implicit multiplication |
|
|
321
|
+
| 7 | `!`, unary `-`, unary `+` |
|
|
322
|
+
| 8 | `**`, `^` |
|
|
323
|
+
|
|
324
|
+
Operators `&` (intersection), `\|` (union), `-` (difference), `+`
|
|
325
|
+
(concatenation), `<<` (append), `*` (repeat/join), `==` (deep equality), `<=>`
|
|
326
|
+
(spaceship), and `in` (membership) are type-dispatched: they apply array
|
|
327
|
+
semantics when either operand is an array, otherwise scalar semantics.
|
|
328
|
+
|
|
329
|
+
## Development
|
|
330
|
+
|
|
331
|
+
```bash
|
|
332
|
+
# Install in development mode
|
|
333
|
+
pip install -e ".[dev]"
|
|
334
|
+
|
|
335
|
+
# Run tests
|
|
336
|
+
pytest
|
|
337
|
+
|
|
338
|
+
# Regenerate parser from grammar
|
|
339
|
+
python -m pegen expression/grammar.peg -o expression/parser.py
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
## License
|
|
343
|
+
|
|
344
|
+
Copyright (c) 2026 [Jakub T. Jankiewicz](https://jakub.jankiewicz.org/)
|
|
345
|
+
|
|
346
|
+
This program is free software: you can redistribute it and/or modify it under the terms of the GNU
|
|
347
|
+
General Public License as published by the Free Software Foundation, either version 3 of the
|
|
348
|
+
License, or (at your option) any later version.
|
|
349
|
+
|
|
350
|
+
See [LICENSE](https://github.com/jcubic/expression.py/blob/master/LICENSE) for the full text.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
expression/__init__.py,sha256=WsgYZ6L0lcV_4xlZFPeFcJR4QkvzfiQCY9_GPT_IBEQ,12147
|
|
2
|
+
expression/grammar.peg,sha256=aQ6Xttkp5ZYvOdoPLDIo5HsQ5DjUiLpvaXiQGwasQQM,6254
|
|
3
|
+
expression/helpers.py,sha256=KJgoiJganC3u_0l-F4YgFJ3yuFAd9OzwjLbvlg6ObhY,4681
|
|
4
|
+
expression/parser.py,sha256=rXeAC8498WMBEaXE7su8V8o4rIN7BF1RmpktwPLpVJg,26024
|
|
5
|
+
expression/tokenizer.py,sha256=pb3HAzk7Qvd4ue7hA4QQu5SeCON3Ma7tVJpZeNK5XzU,5893
|
|
6
|
+
expression_py-0.1.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
7
|
+
expression_py-0.1.0.dist-info/METADATA,sha256=DOQXXwRCJgAKtUlo3bwf9LJavBN3OAU05xgj7xhEtgI,10527
|
|
8
|
+
expression_py-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
9
|
+
expression_py-0.1.0.dist-info/top_level.txt,sha256=C_mXdwowx0RYeeMT6UIE9x297SZHlvVgtCIjaf24L8k,11
|
|
10
|
+
expression_py-0.1.0.dist-info/RECORD,,
|