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 ADDED
@@ -0,0 +1,350 @@
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 math
19
+
20
+ from expression.parser import ExpressionParser as _GeneratedParser
21
+ from expression.tokenizer import ExpressionTokenizer
22
+ from expression.helpers import (
23
+ with_type, is_typed, is_string_type, is_array_type,
24
+ validate_number, validate_types, maybe_regex, to_array,
25
+ )
26
+
27
+
28
+ class ExpressionParserExt(_GeneratedParser):
29
+ KEYWORDS = ('true', 'false', 'null', 'in')
30
+ SOFT_KEYWORDS = ()
31
+
32
+ def __init__(self, tokenizer, variables, constants, functions, source_text):
33
+ super().__init__(tokenizer, verbose=False)
34
+ self.variables = variables
35
+ self.constants = constants
36
+ self.functions = functions
37
+ self._source_text = source_text
38
+
39
+ def regex_literal(self):
40
+ tok = self._tokenizer.peek()
41
+ if tok.type == ExpressionTokenizer.REGEX_TOKEN_TYPE:
42
+ return self._tokenizer.getnext()
43
+ return None
44
+
45
+ def function_assignment(self):
46
+ mark = self._mark()
47
+ name_tok = self.name()
48
+ if name_tok is None:
49
+ self._reset(mark)
50
+ return None
51
+ if not self.expect('('):
52
+ self._reset(mark)
53
+ return None
54
+ params = []
55
+ rparen = self.expect(')')
56
+ if not rparen:
57
+ p = self.name()
58
+ if p is None:
59
+ self._reset(mark)
60
+ return None
61
+ params.append(p.string)
62
+ while self.expect(','):
63
+ p = self.name()
64
+ if p is None:
65
+ self._reset(mark)
66
+ return None
67
+ params.append(p.string)
68
+ rparen = self.expect(')')
69
+ if not rparen:
70
+ self._reset(mark)
71
+ return None
72
+ eq = self.expect('=')
73
+ if not eq:
74
+ self._reset(mark)
75
+ return None
76
+ next_tok = self._tokenizer.peek()
77
+ if next_tok.string in ('=', '~'):
78
+ self._reset(mark)
79
+ return None
80
+ body_start = next_tok.start[1]
81
+ body = self._source_text[body_start:].rstrip(';').strip()
82
+ while self._tokenizer.peek().string != '' and self._tokenizer.peek().type != 0:
83
+ self._tokenizer.getnext()
84
+ return self._func_assign(name_tok.string, params, body)
85
+
86
+ def _func_assign(self, name, params, body=None):
87
+ if body is None:
88
+ body_start = self._tokenizer.peek().start[1]
89
+ body = self._source_text[body_start:].rstrip(';').strip()
90
+
91
+ captured_params = list(params)
92
+ captured_body = body
93
+
94
+ def func(*args):
95
+ expr = Expression()
96
+ for i, p in enumerate(captured_params):
97
+ expr.variables[p] = args[i]
98
+ return expr.evaluate(captured_body)
99
+
100
+ self.functions[name] = func
101
+ return with_type(True)
102
+
103
+ def _var_assign(self, name, value):
104
+ if name in self.constants:
105
+ raise Exception(f"Can't assign value to constant '{name}'")
106
+ self.variables[name] = value
107
+ return value
108
+
109
+ def _resolve_var(self, name):
110
+ if name in ('true', 'false', 'null'):
111
+ return None
112
+ if name in self.constants:
113
+ return with_type(self.constants[name])
114
+ if name in self.variables:
115
+ return with_type(self.variables[name])
116
+ raise Exception(f"Variable '{name}' not found")
117
+
118
+ def _parse_string(self, tok):
119
+ raw = tok.string
120
+ if raw.startswith('"'):
121
+ value = raw[1:-1].replace('\\"', '"').replace('\\\\', '\\')
122
+ else:
123
+ value = raw[1:-1].replace("\\'", "'").replace('\\\\', '\\')
124
+ return maybe_regex(value)
125
+
126
+ def _parse_number(self, tok):
127
+ s = tok.string
128
+ if s.startswith('0x') or s.startswith('0X'):
129
+ return with_type(int(s, 16))
130
+ if s.startswith('0b') or s.startswith('0B'):
131
+ return with_type(int(s, 2))
132
+ if '.' in s or 'e' in s.lower():
133
+ return with_type(float(s))
134
+ return with_type(int(s))
135
+
136
+ def _compare_op(self, op, a, b, fn):
137
+ validate_types(['integer', 'double', 'boolean'], op, a)
138
+ validate_types(['integer', 'double', 'boolean'], op, b)
139
+ return with_type(fn(a['value'], b['value']))
140
+
141
+ def _shift_op(self, op, a, b, fn):
142
+ validate_number(op, a)
143
+ validate_number(op, b)
144
+ return with_type(fn(int(a['value']), int(b['value'])))
145
+
146
+ def _plus(self, a, b):
147
+ if is_array_type(a) or is_array_type(b):
148
+ return with_type(to_array(a) + to_array(b), 'array')
149
+ if is_string_type(b):
150
+ return with_type(str(a['value']) + b['value'])
151
+ validate_number('+', b)
152
+ validate_number('+', a)
153
+ return with_type(a['value'] + b['value'])
154
+
155
+ def _minus(self, a, b):
156
+ if is_array_type(a) or is_array_type(b):
157
+ right = to_array(b)
158
+ return with_type([x for x in to_array(a) if x not in right], 'array')
159
+ validate_number('-', b)
160
+ validate_number('-', a)
161
+ return with_type(a['value'] - b['value'])
162
+
163
+ def _mul(self, a, b):
164
+ if is_array_type(a) or is_array_type(b):
165
+ if is_array_type(a) and is_string_type(b):
166
+ return with_type(b['value'].join(str(x) for x in a['value']))
167
+ if is_array_type(b) and is_string_type(a):
168
+ return with_type(a['value'].join(str(x) for x in b['value']))
169
+ arr = a if is_array_type(a) else b
170
+ num = b if is_array_type(a) else a
171
+ validate_number('*', num)
172
+ return with_type(list(arr['value']) * int(num['value']), 'array')
173
+ if is_string_type(a) or is_string_type(b):
174
+ s = a if is_string_type(a) else b
175
+ num = b if is_string_type(a) else a
176
+ validate_number('*', num)
177
+ return with_type(s['value'] * int(num['value']))
178
+ validate_number('*', a)
179
+ validate_number('*', b)
180
+ return with_type(a['value'] * b['value'])
181
+
182
+ def _union(self, a, b):
183
+ if not is_array_type(a) and not is_array_type(b):
184
+ validate_number('|', a)
185
+ validate_number('|', b)
186
+ return with_type(int(a['value']) | int(b['value']))
187
+ result = []
188
+ for x in to_array(a) + to_array(b):
189
+ if x not in result:
190
+ result.append(x)
191
+ return with_type(result, 'array')
192
+
193
+ def _intersect(self, a, b):
194
+ if not is_array_type(a) and not is_array_type(b):
195
+ validate_number('&', a)
196
+ validate_number('&', b)
197
+ return with_type(int(a['value']) & int(b['value']))
198
+ right = to_array(b)
199
+ result = []
200
+ for x in to_array(a):
201
+ if x in right and x not in result:
202
+ result.append(x)
203
+ return with_type(result, 'array')
204
+
205
+ def _lshift(self, a, b):
206
+ if is_array_type(a):
207
+ a['value'].append(b['value'])
208
+ return a
209
+ if is_string_type(a):
210
+ a['value'] = a['value'] + str(b['value'])
211
+ return a
212
+ return self._shift_op('<<', a, b, lambda x, y: x << y)
213
+
214
+ def _in(self, a, b):
215
+ if is_array_type(b):
216
+ return with_type(a['value'] in b['value'])
217
+ if is_string_type(b):
218
+ return with_type(str(a['value']) in b['value'])
219
+ return with_type(a['value'] in [b['value']])
220
+
221
+ def _spaceship(self, a, b):
222
+ x = a['value']
223
+ y = b['value']
224
+ if x < y:
225
+ return with_type(-1)
226
+ if x > y:
227
+ return with_type(1)
228
+ return with_type(0)
229
+
230
+ def _div(self, a, b):
231
+ validate_number('/', a)
232
+ validate_number('/', b)
233
+ if b['value'] == 0:
234
+ raise ZeroDivisionError("Division by zero")
235
+ return with_type(a['value'] / b['value'])
236
+
237
+ def _mod(self, a, b):
238
+ validate_number('%', a)
239
+ validate_number('%', b)
240
+ return with_type(a['value'] % b['value'])
241
+
242
+ def _property_access(self, obj, prop):
243
+ validate_types(['array'], '[', obj)
244
+ validate_types(['string', 'double', 'integer', 'boolean'], '[', prop)
245
+ return with_type(obj['value'][prop['value']])
246
+
247
+ def _implicit_mul(self, a, b):
248
+ validate_number('[*]', a)
249
+ validate_number('[*]', b)
250
+ return with_type(a['value'] * b['value'])
251
+
252
+ def _unary_minus(self, a):
253
+ validate_number('-', a)
254
+ return with_type(a['value'] * -1)
255
+
256
+ def _unary_plus(self, a):
257
+ if is_string_type(a):
258
+ return with_type(float(a['value']))
259
+ return a
260
+
261
+ def _call_function(self, name, args):
262
+ BUILTIN_MAP = {
263
+ 'arcsin': 'asin', 'arcsinh': 'asinh',
264
+ 'arccos': 'acos', 'arccosh': 'acosh',
265
+ 'arctan': 'atan', 'arctanh': 'atanh',
266
+ 'ln': 'log',
267
+ }
268
+ BUILTIN_FUNCTIONS = [
269
+ 'sin', 'sinh', 'asin', 'asinh',
270
+ 'cos', 'cosh', 'acos', 'acosh',
271
+ 'tan', 'tanh', 'atan', 'atanh',
272
+ 'sqrt', 'abs', 'ln', 'log',
273
+ ]
274
+ resolved = BUILTIN_MAP.get(name, name)
275
+ is_builtin = resolved in BUILTIN_FUNCTIONS
276
+ is_custom = name in self.functions
277
+ if not is_builtin and not is_custom:
278
+ raise Exception(f"function '{name}' doesn't exist")
279
+ arg_values = [a['value'] for a in args]
280
+ if is_builtin:
281
+ if resolved == 'abs':
282
+ func = abs
283
+ else:
284
+ func = getattr(math, resolved)
285
+ result = func(*arg_values)
286
+ else:
287
+ result = self.functions[name](*arg_values)
288
+ return with_type(result)
289
+
290
+ def _build_json_obj(self, key_tok, value, rest):
291
+ key = self._json_string_val(key_tok)
292
+ d = {key: value}
293
+ d.update(rest)
294
+ return d
295
+
296
+ def _make_obj(self, key_tok, value):
297
+ key = self._json_string_val(key_tok)
298
+ return {key: value}
299
+
300
+ def _json_string_val(self, tok):
301
+ raw = tok.string
302
+ if raw.startswith('"'):
303
+ return raw[1:-1].replace('\\"', '"').replace('\\\\', '\\')
304
+ return raw[1:-1].replace("\\'", "'").replace('\\\\', '\\')
305
+
306
+ def _json_number_val(self, tok):
307
+ s = tok.string
308
+ if '.' in s or 'e' in s.lower():
309
+ return float(s)
310
+ return int(s)
311
+
312
+ def _merge_obj(self, key_tok, value, rest):
313
+ key = self._json_string_val(key_tok)
314
+ d = {key: value}
315
+ d.update(rest)
316
+ return d
317
+
318
+
319
+ class Expression:
320
+ def __init__(self):
321
+ self.constants = {"e": math.e, "pi": math.pi}
322
+ self.variables = {}
323
+ self.functions = {}
324
+ self.suppress_errors = False
325
+ self.last_error = ""
326
+
327
+ def evaluate(self, expr):
328
+ if not expr or not expr.strip():
329
+ return None
330
+ text = expr.strip()
331
+ tokenizer = ExpressionTokenizer(text)
332
+ parser = ExpressionParserExt(
333
+ tokenizer, self.variables, self.constants, self.functions, text
334
+ )
335
+ try:
336
+ result = parser.start()
337
+ if result is None:
338
+ raise Exception(f"invalid syntax: {expr}")
339
+ self.variables = parser.variables
340
+ self.functions = parser.functions
341
+ if is_typed(result):
342
+ return result['value']
343
+ return result
344
+ except ZeroDivisionError:
345
+ raise
346
+ except Exception as e:
347
+ self.last_error = str(e) + f" in expression: {expr}"
348
+ if not self.suppress_errors:
349
+ raise
350
+ return None
expression/grammar.peg ADDED
@@ -0,0 +1,191 @@
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
+ # PEG grammar for the expression evaluation language, in pegen format.
19
+
20
+ @class ExpressionParser
21
+ @subheader """\
22
+ from expression.helpers import (
23
+ with_type, is_typed, is_number, is_string_type, is_array_type,
24
+ validate_number, validate_types, maybe_regex, do_check_equal,
25
+ do_match, do_power, loose_equal, to_array,
26
+ )
27
+ """
28
+
29
+ start[object]: a=start_rule ENDMARKER { a }
30
+
31
+ start_rule[object]:
32
+ | function_assignment
33
+ | variable_assignment
34
+ | a=expr ';' { a }
35
+ | expr
36
+
37
+ function_assignment[object]:
38
+ | name=NAME '(' params=param_list_inner ')' '=' func_rest { self._func_assign(name.string, params) }
39
+ | name=NAME '(' ')' '=' func_rest { self._func_assign(name.string, []) }
40
+
41
+ param_list_inner[list]:
42
+ | a=NAME ',' b=param_list_inner { [a.string] + b }
43
+ | a=NAME { [a.string] }
44
+
45
+ func_rest[object]:
46
+ | a=expr ';' { a }
47
+ | a=expr { a }
48
+
49
+ variable_assignment[object]:
50
+ | name=NAME '=' !'=' !'~' value=expr ';' { self._var_assign(name.string, value) }
51
+ | name=NAME '=' !'=' !'~' value=expr { self._var_assign(name.string, value) }
52
+
53
+ expr[object]: ternary
54
+
55
+ ternary[object]:
56
+ | a=boolean '?' b=expr ':' c=ternary { b if a['value'] else c }
57
+ | boolean
58
+
59
+ boolean[object]:
60
+ | a=boolean '&&' b=compare { with_type(b['value'] if a['value'] else a['value']) }
61
+ | a=boolean '||' b=compare { with_type(a['value'] if a['value'] else b['value']) }
62
+ | compare
63
+
64
+ compare[object]:
65
+ | a=compare '===' b=bitshift { with_type(type(a['value']) == type(b['value']) and a['value'] == b['value']) }
66
+ | a=compare '!==' b=bitshift { with_type(not (type(a['value']) == type(b['value']) and a['value'] == b['value'])) }
67
+ | a=compare '<=>' b=bitshift { self._spaceship(a, b) }
68
+ | a=compare '==' b=bitshift { do_check_equal(a, b, lambda x, y: loose_equal(x, y)) }
69
+ | a=compare '=~' b=bitshift { do_match(a, b, self.variables) }
70
+ | a=compare '!=' b=bitshift { do_check_equal(a, b, lambda x, y: not loose_equal(x, y)) }
71
+ | a=compare '>=' b=bitshift { self._compare_op('>=', a, b, lambda x, y: x >= y) }
72
+ | a=compare '<=' b=bitshift { self._compare_op('<=', a, b, lambda x, y: x <= y) }
73
+ | a=compare '>' b=bitshift { self._compare_op('>', a, b, lambda x, y: x > y) }
74
+ | a=compare '<' b=bitshift { self._compare_op('<', a, b, lambda x, y: x < y) }
75
+ | a=compare 'in' b=bitshift { self._in(a, b) }
76
+ | bitshift
77
+
78
+ bitshift[object]:
79
+ | a=bitshift '<<' b=sum_expr { self._lshift(a, b) }
80
+ | a=bitshift '>>' b=sum_expr { self._shift_op('>>', a, b, lambda x, y: x >> y) }
81
+ | sum_expr
82
+
83
+ sum_expr[object]:
84
+ | a=sum_expr '+' b=product { self._plus(a, b) }
85
+ | a=sum_expr '-' b=product { self._minus(a, b) }
86
+ | a=sum_expr '|' b=product { self._union(a, b) }
87
+ | product
88
+
89
+ product[object]:
90
+ | a=product '*' !'*' b=unary { self._mul(a, b) }
91
+ | a=product '/' b=unary { self._div(a, b) }
92
+ | a=product '%' b=unary { self._mod(a, b) }
93
+ | a=product '&' b=unary { self._intersect(a, b) }
94
+ | a=product '[' b=expr ']' { self._property_access(a, b) }
95
+ | a=product b=implicit_mul { self._implicit_mul(a, b) }
96
+ | unary
97
+
98
+ implicit_mul[object]:
99
+ | paren_expr_pow
100
+ | func_call_pow
101
+ | var_ref_pow
102
+
103
+ paren_expr_pow[object]:
104
+ | a=paren_expr '**' b=value { do_power(a, b) }
105
+ | a=paren_expr '^' b=value { do_power(a, b) }
106
+ | paren_expr
107
+
108
+ func_call_pow[object]:
109
+ | a=function_call '**' b=value { do_power(a, b) }
110
+ | a=function_call '^' b=value { do_power(a, b) }
111
+ | function_call
112
+
113
+ var_ref_pow[object]:
114
+ | a=var_ref '**' b=value { do_power(a, b) }
115
+ | a=var_ref '^' b=value { do_power(a, b) }
116
+ | var_ref
117
+
118
+ unary[object]:
119
+ | '!' a=unary { with_type(not a['value']) }
120
+ | '-' a=unary { self._unary_minus(a) }
121
+ | '+' a=unary { self._unary_plus(a) }
122
+ | power
123
+
124
+ power[object]:
125
+ | a=value '**' b=power { do_power(a, b) }
126
+ | a=value '^' b=power { do_power(a, b) }
127
+ | value
128
+
129
+ value[object]:
130
+ | json_value
131
+ | const_value
132
+ | regex_value
133
+ | string_value
134
+ | number_value
135
+ | function_call
136
+ | var_ref
137
+ | paren_expr
138
+
139
+ paren_expr[object]:
140
+ | '(' a=expr ')' { a }
141
+
142
+ json_value[object]:
143
+ | '{' a=json_obj_body '}' { with_type(a, 'array') }
144
+ | '{' '}' { with_type({}, 'array') }
145
+ | '[' a=json_arr_body ']' { with_type(a, 'array') }
146
+ | '[' ']' { with_type([], 'array') }
147
+
148
+ json_obj_body[dict]:
149
+ | a=STRING ':' b=json_element ',' c=json_obj_body { self._merge_obj(a, b, c) }
150
+ | a=STRING ':' b=json_element { self._make_obj(a, b) }
151
+
152
+ json_arr_body[list]:
153
+ | a=json_element ',' b=json_arr_body { [a] + b }
154
+ | a=json_element { [a] }
155
+
156
+ json_element[object]:
157
+ | '{' a=json_obj_body '}' { dict(a) }
158
+ | '[' a=json_arr_body ']' { list(a) }
159
+ | a=STRING { self._json_string_val(a) }
160
+ | a=NUMBER { self._json_number_val(a) }
161
+ | 'true' { True }
162
+ | 'false' { False }
163
+ | 'null' { None }
164
+
165
+ const_value[object]:
166
+ | 'true' { with_type(True) }
167
+ | 'false' { with_type(False) }
168
+ | 'null' { with_type(None) }
169
+
170
+ regex_value[object]:
171
+ | a=regex_literal { with_type(a.string, 'regex') }
172
+
173
+ regex_literal[object]:
174
+ | a=OP { a }
175
+
176
+ string_value[object]:
177
+ | a=STRING { self._parse_string(a) }
178
+
179
+ number_value[object]:
180
+ | a=NUMBER { self._parse_number(a) }
181
+
182
+ function_call[object]:
183
+ | name=NAME '(' args=arg_list_inner ')' { self._call_function(name.string, args) }
184
+ | name=NAME '(' ')' { self._call_function(name.string, []) }
185
+
186
+ arg_list_inner[list]:
187
+ | a=expr ',' b=arg_list_inner { [a] + b }
188
+ | a=expr { [a] }
189
+
190
+ var_ref[object]:
191
+ | a=NAME { self._resolve_var(a.string) }