rule-engine 5.0.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.
- rule_engine/__init__.py +65 -0
- rule_engine/_vendor/__init__.py +4 -0
- rule_engine/_vendor/ply/LICENSE +32 -0
- rule_engine/_vendor/ply/__init__.py +5 -0
- rule_engine/_vendor/ply/lex.py +1098 -0
- rule_engine/_vendor/ply/yacc.py +3433 -0
- rule_engine/ast/__init__.py +92 -0
- rule_engine/ast/base.py +343 -0
- rule_engine/ast/binary/__init__.py +103 -0
- rule_engine/ast/binary/arithmetic.py +211 -0
- rule_engine/ast/binary/base.py +112 -0
- rule_engine/ast/binary/comparison.py +154 -0
- rule_engine/ast/expression/__init__.py +35 -0
- rule_engine/ast/expression/access.py +434 -0
- rule_engine/ast/expression/control.py +256 -0
- rule_engine/ast/expression/resolution.py +220 -0
- rule_engine/ast/literal.py +183 -0
- rule_engine/builtins.py +230 -0
- rule_engine/debug_ast.py +65 -0
- rule_engine/debug_repl.py +159 -0
- rule_engine/engine/__init__.py +34 -0
- rule_engine/engine/_attribute_resolver.py +353 -0
- rule_engine/engine/context.py +438 -0
- rule_engine/engine/rule.py +148 -0
- rule_engine/errors.py +359 -0
- rule_engine/parser/__init__.py +614 -0
- rule_engine/parser/base.py +86 -0
- rule_engine/parser/utilities.py +114 -0
- rule_engine/suggestions.py +105 -0
- rule_engine/types/__init__.py +61 -0
- rule_engine/types/_object.py +318 -0
- rule_engine/types/coercion.py +141 -0
- rule_engine/types/dataclass.py +149 -0
- rule_engine/types/datatype.py +398 -0
- rule_engine/types/definitions.py +454 -0
- rule_engine/types/sqlalchemy.py +171 -0
- rule_engine-5.0.0.dist-info/METADATA +126 -0
- rule_engine-5.0.0.dist-info/RECORD +41 -0
- rule_engine-5.0.0.dist-info/WHEEL +5 -0
- rule_engine-5.0.0.dist-info/licenses/LICENSE +28 -0
- rule_engine-5.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
#
|
|
4
|
+
# rule_engine/ast/__init__.py
|
|
5
|
+
#
|
|
6
|
+
# Redistribution and use in source and binary forms, with or without
|
|
7
|
+
# modification, are permitted provided that the following conditions are
|
|
8
|
+
# met:
|
|
9
|
+
#
|
|
10
|
+
# * Redistributions of source code must retain the above copyright
|
|
11
|
+
# notice, this list of conditions and the following disclaimer.
|
|
12
|
+
# * Redistributions in binary form must reproduce the above
|
|
13
|
+
# copyright notice, this list of conditions and the following disclaimer
|
|
14
|
+
# in the documentation and/or other materials provided with the
|
|
15
|
+
# distribution.
|
|
16
|
+
# * Neither the name of the project nor the names of its
|
|
17
|
+
# contributors may be used to endorse or promote products derived from
|
|
18
|
+
# this software without specific prior written permission.
|
|
19
|
+
#
|
|
20
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
21
|
+
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
22
|
+
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
23
|
+
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
24
|
+
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
25
|
+
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
26
|
+
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
27
|
+
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
28
|
+
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
29
|
+
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
30
|
+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
31
|
+
#
|
|
32
|
+
|
|
33
|
+
from .base import (
|
|
34
|
+
ASTNodeBase,
|
|
35
|
+
Assignment,
|
|
36
|
+
Comment,
|
|
37
|
+
ExpressionBase,
|
|
38
|
+
LiteralExpressionBase,
|
|
39
|
+
Statement,
|
|
40
|
+
_assert_is_bytes,
|
|
41
|
+
_assert_is_integer_number,
|
|
42
|
+
_assert_is_natural_number,
|
|
43
|
+
_assert_is_numeric,
|
|
44
|
+
_assert_is_string,
|
|
45
|
+
_assert_not_nullable,
|
|
46
|
+
_is_reduced,
|
|
47
|
+
_iterable_member_value_type,
|
|
48
|
+
_resolve_type,
|
|
49
|
+
)
|
|
50
|
+
from .literal import (
|
|
51
|
+
ArrayExpression,
|
|
52
|
+
BooleanExpression,
|
|
53
|
+
BytesExpression,
|
|
54
|
+
DatetimeExpression,
|
|
55
|
+
FloatExpression,
|
|
56
|
+
FunctionExpression,
|
|
57
|
+
MappingExpression,
|
|
58
|
+
NullExpression,
|
|
59
|
+
SetExpression,
|
|
60
|
+
StringExpression,
|
|
61
|
+
TimedeltaExpression,
|
|
62
|
+
_CollectionMixin,
|
|
63
|
+
)
|
|
64
|
+
from .binary import (
|
|
65
|
+
AddExpression,
|
|
66
|
+
ArithmeticComparisonExpression,
|
|
67
|
+
ArithmeticExpression,
|
|
68
|
+
BinaryExpressionBase,
|
|
69
|
+
BitwiseExpression,
|
|
70
|
+
BitwiseShiftExpression,
|
|
71
|
+
CoalesceExpression,
|
|
72
|
+
ComparisonExpression,
|
|
73
|
+
FuzzyComparisonExpression,
|
|
74
|
+
LogicExpression,
|
|
75
|
+
SubtractExpression,
|
|
76
|
+
)
|
|
77
|
+
from .expression import (
|
|
78
|
+
ComprehensionExpression,
|
|
79
|
+
ContainsExpression,
|
|
80
|
+
FunctionCallExpression,
|
|
81
|
+
GetAttributeExpression,
|
|
82
|
+
GetItemExpression,
|
|
83
|
+
GetSliceExpression,
|
|
84
|
+
SymbolExpression,
|
|
85
|
+
TernaryExpression,
|
|
86
|
+
UnaryExpression,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# deprecated alias — use BinaryExpressionBase instead
|
|
90
|
+
LeftOperatorRightExpressionBase = BinaryExpressionBase
|
|
91
|
+
|
|
92
|
+
from ..types import _ObjectDataTypeDef, _ReferenceDataTypeDef
|
rule_engine/ast/base.py
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
#
|
|
4
|
+
# rule_engine/ast/base.py
|
|
5
|
+
#
|
|
6
|
+
# Redistribution and use in source and binary forms, with or without
|
|
7
|
+
# modification, are permitted provided that the following conditions are
|
|
8
|
+
# met:
|
|
9
|
+
#
|
|
10
|
+
# * Redistributions of source code must retain the above copyright
|
|
11
|
+
# notice, this list of conditions and the following disclaimer.
|
|
12
|
+
# * Redistributions in binary form must reproduce the above
|
|
13
|
+
# copyright notice, this list of conditions and the following disclaimer
|
|
14
|
+
# in the documentation and/or other materials provided with the
|
|
15
|
+
# distribution.
|
|
16
|
+
# * Neither the name of the project nor the names of its
|
|
17
|
+
# contributors may be used to endorse or promote products derived from
|
|
18
|
+
# this software without specific prior written permission.
|
|
19
|
+
#
|
|
20
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
21
|
+
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
22
|
+
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
23
|
+
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
24
|
+
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
25
|
+
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
26
|
+
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
27
|
+
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
28
|
+
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
29
|
+
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
30
|
+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
31
|
+
#
|
|
32
|
+
|
|
33
|
+
import datetime
|
|
34
|
+
from typing import TYPE_CHECKING, Any, Iterable
|
|
35
|
+
|
|
36
|
+
from .. import errors
|
|
37
|
+
from ..types import *
|
|
38
|
+
from ..types import _CollectionDataTypeDef, _DataTypeDef, _ReferenceDataTypeDef
|
|
39
|
+
|
|
40
|
+
if TYPE_CHECKING:
|
|
41
|
+
from ..engine.context import Context
|
|
42
|
+
|
|
43
|
+
def _assert_is_bytes(*values: Any) -> None:
|
|
44
|
+
if not all(map(isinstance, values, [bytes])):
|
|
45
|
+
raise errors.EvaluationError('data type mismatch (not a bytes value)')
|
|
46
|
+
|
|
47
|
+
def _assert_is_integer_number(*values: Any) -> None:
|
|
48
|
+
if not all(map(is_integer_number, values)):
|
|
49
|
+
raise errors.EvaluationError('data type mismatch (not an integer number)')
|
|
50
|
+
|
|
51
|
+
def _assert_is_natural_number(*values: Any) -> None:
|
|
52
|
+
if not all(map(is_natural_number, values)):
|
|
53
|
+
raise errors.EvaluationError('data type mismatch (not a natural number)')
|
|
54
|
+
|
|
55
|
+
def _assert_is_numeric(*values: Any) -> None:
|
|
56
|
+
if not all(map(is_numeric, values)):
|
|
57
|
+
raise errors.EvaluationError('data type mismatch (not a numeric value)')
|
|
58
|
+
|
|
59
|
+
def _assert_is_string(*values: Any) -> None:
|
|
60
|
+
if not all(map(isinstance, values, [str])):
|
|
61
|
+
raise errors.EvaluationError('data type mismatch (not a string value)')
|
|
62
|
+
|
|
63
|
+
def _is_reduced(*values: Any) -> bool:
|
|
64
|
+
"""
|
|
65
|
+
Check if the ast expression *value* is a literal expression and if it is a compound datatype, that all of its
|
|
66
|
+
members are reduced literals. A value that causes this to evaluate to True for is able to be evaluated without a
|
|
67
|
+
*thing*.
|
|
68
|
+
"""
|
|
69
|
+
return all((isinstance(value, LiteralExpressionBase) and value.is_reduced) for value in values)
|
|
70
|
+
|
|
71
|
+
def _iterable_member_value_type(value: Iterable[Any]) -> _DataTypeDef:
|
|
72
|
+
value = (
|
|
73
|
+
member.result_type if isinstance(member, ExpressionBase) else member for member in value
|
|
74
|
+
)
|
|
75
|
+
return iterable_member_value_type(value)
|
|
76
|
+
|
|
77
|
+
def _propagate_nullable(result_type: _DataTypeDef, *operand_types: _DataTypeDef) -> _DataTypeDef:
|
|
78
|
+
"""
|
|
79
|
+
Sticky nullability: if any operand's static type is :py:class:`_NullableDataTypeDef`, wrap *result_type* in
|
|
80
|
+
:py:class:`_NullableDataTypeDef`; otherwise return *result_type* unchanged. Only :py:class:`.TernaryExpression`
|
|
81
|
+
uses this — its branches are alternatives rather than operands, so an operand-position strictness check does
|
|
82
|
+
not apply.
|
|
83
|
+
"""
|
|
84
|
+
if any(DataType.is_type(t, DataType.NULLABLE) for t in operand_types):
|
|
85
|
+
return DataType.NULLABLE.wrap(result_type)
|
|
86
|
+
return result_type
|
|
87
|
+
|
|
88
|
+
def _assert_not_nullable(dt: _DataTypeDef, *, role: str) -> None:
|
|
89
|
+
"""
|
|
90
|
+
Raise :py:exc:`~rule_engine.errors.EvaluationError` if *dt* is :py:class:`_NullableDataTypeDef`. The *role*
|
|
91
|
+
string describes the rejected operand (e.g. ``"left operand of '+'"``, ``"slice target"``) and is embedded
|
|
92
|
+
in the error message along with a pointer at the discharge operators (``??``, ``&.``, ``&[``).
|
|
93
|
+
"""
|
|
94
|
+
if DataType.is_type(dt, DataType.NULLABLE):
|
|
95
|
+
raise errors.EvaluationError(
|
|
96
|
+
"data type mismatch ({0} is nullable; discharge with '??' or use '&.' / '&[' for safe navigation)".format(role)
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def _resolve_type(definition: _DataTypeDef, context: 'Context') -> _DataTypeDef:
|
|
100
|
+
"""
|
|
101
|
+
Resolve any :py:class:`~rule_engine.types._ReferenceDataTypeDef` placeholders inside *definition* via the
|
|
102
|
+
*context*'s :py:meth:`~rule_engine.engine.Context.resolve_type` callback. Returns a new definition with the
|
|
103
|
+
placeholders substituted; the input is never mutated. Already-resolved definitions pass through unchanged.
|
|
104
|
+
|
|
105
|
+
This helper is used at rule parse time to complete the cross-type reference resolution that
|
|
106
|
+
:py:class:`_ObjectDataTypeDef` deliberately leaves for later (self references are already bound at construction
|
|
107
|
+
time).
|
|
108
|
+
"""
|
|
109
|
+
if isinstance(definition, _ReferenceDataTypeDef):
|
|
110
|
+
try:
|
|
111
|
+
resolved = context.resolve_type(definition.name)
|
|
112
|
+
except errors.SymbolResolutionError as error:
|
|
113
|
+
raise errors.EvaluationError(
|
|
114
|
+
"unresolved object reference: {0!r} - add it to the Context type_resolver".format(definition.name)
|
|
115
|
+
) from error
|
|
116
|
+
if not DataType.is_type(resolved, DataType.OBJECT):
|
|
117
|
+
raise errors.EvaluationError(
|
|
118
|
+
"reference {0!r} does not resolve to an OBJECT type".format(definition.name)
|
|
119
|
+
)
|
|
120
|
+
if resolved.name != definition.name:
|
|
121
|
+
raise errors.EvaluationError(
|
|
122
|
+
"reference {0!r} resolves to an OBJECT with mismatched name {1!r}".format(definition.name, resolved.name)
|
|
123
|
+
)
|
|
124
|
+
return resolved
|
|
125
|
+
if DataType.is_type(definition, DataType.OBJECT):
|
|
126
|
+
return definition
|
|
127
|
+
if DataType.is_type(definition, DataType.NULLABLE):
|
|
128
|
+
new_inner = _resolve_type(definition.inner_type, context)
|
|
129
|
+
if new_inner is definition.inner_type:
|
|
130
|
+
return definition
|
|
131
|
+
return definition.__class__(
|
|
132
|
+
definition.name,
|
|
133
|
+
definition.python_type,
|
|
134
|
+
inner_type=new_inner
|
|
135
|
+
)
|
|
136
|
+
if isinstance(definition, _CollectionDataTypeDef):
|
|
137
|
+
new_value_type = _resolve_type(definition.value_type, context)
|
|
138
|
+
if new_value_type is definition.value_type:
|
|
139
|
+
return definition
|
|
140
|
+
return definition.__class__(
|
|
141
|
+
definition.name,
|
|
142
|
+
definition.python_type,
|
|
143
|
+
value_type=new_value_type
|
|
144
|
+
)
|
|
145
|
+
if DataType.is_type(definition, DataType.MAPPING):
|
|
146
|
+
new_key_type = _resolve_type(definition.key_type, context)
|
|
147
|
+
new_value_type = _resolve_type(definition.value_type, context)
|
|
148
|
+
if new_key_type is definition.key_type and new_value_type is definition.value_type:
|
|
149
|
+
return definition
|
|
150
|
+
return definition.__class__(
|
|
151
|
+
definition.name,
|
|
152
|
+
definition.python_type,
|
|
153
|
+
key_type=new_key_type,
|
|
154
|
+
value_type=new_value_type
|
|
155
|
+
)
|
|
156
|
+
if DataType.is_type(definition, DataType.FUNCTION):
|
|
157
|
+
new_return_type = _resolve_type(definition.return_type, context)
|
|
158
|
+
new_argument_types: tuple[_DataTypeDef, ...] | _DataTypeDef
|
|
159
|
+
if definition.argument_types is DataType.UNDEFINED:
|
|
160
|
+
new_argument_types = definition.argument_types
|
|
161
|
+
else:
|
|
162
|
+
assert isinstance(definition.argument_types, tuple)
|
|
163
|
+
new_argument_types = tuple(_resolve_type(arg, context) for arg in definition.argument_types)
|
|
164
|
+
if new_return_type is definition.return_type and new_argument_types is definition.argument_types:
|
|
165
|
+
return definition
|
|
166
|
+
return definition.__class__(
|
|
167
|
+
definition.name,
|
|
168
|
+
definition.python_type,
|
|
169
|
+
value_name=definition.value_name,
|
|
170
|
+
return_type=new_return_type,
|
|
171
|
+
argument_types=new_argument_types,
|
|
172
|
+
minimum_arguments=definition.minimum_arguments
|
|
173
|
+
)
|
|
174
|
+
return definition
|
|
175
|
+
|
|
176
|
+
class Assignment(object):
|
|
177
|
+
"""An internal assignment whereby a symbol is populated with a value of the specified type."""
|
|
178
|
+
__slots__ = ('name', 'value', 'value_type')
|
|
179
|
+
name: str
|
|
180
|
+
value: Any
|
|
181
|
+
value_type: _DataTypeDef | None
|
|
182
|
+
def __init__(self, name: str, *, value: Any = errors.UNDEFINED, value_type: _DataTypeDef | None = None) -> None:
|
|
183
|
+
"""
|
|
184
|
+
:param str name: The symbol name that the assignment is defining.
|
|
185
|
+
:param value: The value of the assignment.
|
|
186
|
+
:param value_type: The data type of the assignment.
|
|
187
|
+
:type value_type: :py:class:`~.DataType`
|
|
188
|
+
"""
|
|
189
|
+
self.name = name
|
|
190
|
+
self.value = value
|
|
191
|
+
if value is not errors.UNDEFINED and value_type is not None:
|
|
192
|
+
value_type = DataType.from_value(value)
|
|
193
|
+
self.value_type = value_type
|
|
194
|
+
|
|
195
|
+
def __repr__(self) -> str:
|
|
196
|
+
return "<{} name={!r} value={!r} value_type={!r} >".format(self.__class__.__name__, self.name, self.value, self.value_type)
|
|
197
|
+
|
|
198
|
+
class ASTNodeBase(object):
|
|
199
|
+
def to_graphviz(self, digraph: Any) -> None:
|
|
200
|
+
digraph.node(str(id(self)), self.__class__.__name__)
|
|
201
|
+
|
|
202
|
+
@classmethod
|
|
203
|
+
def build(cls, *args: Any, **kwargs: Any) -> 'ASTNodeBase':
|
|
204
|
+
return cls(*args, **kwargs).reduce()
|
|
205
|
+
|
|
206
|
+
def evaluate(self, thing: Any) -> Any:
|
|
207
|
+
"""
|
|
208
|
+
Evaluate this AST node and all applicable children nodes.
|
|
209
|
+
|
|
210
|
+
:param thing: The object to use for symbol resolution.
|
|
211
|
+
:return: The result of the evaluation as a native Python type.
|
|
212
|
+
"""
|
|
213
|
+
raise NotImplementedError()
|
|
214
|
+
|
|
215
|
+
def reduce(self) -> 'ASTNodeBase':
|
|
216
|
+
"""
|
|
217
|
+
Reduce this expression into a smaller subset of nodes. If the expression can not be reduced, then return an
|
|
218
|
+
instance of itself, otherwise return a reduced :py:class:`.ExpressionBase` to replace it.
|
|
219
|
+
|
|
220
|
+
:return: Either a reduced version of this node or itself.
|
|
221
|
+
:rtype: :py:class:`.ExpressionBase`
|
|
222
|
+
"""
|
|
223
|
+
return self
|
|
224
|
+
|
|
225
|
+
class Comment(ASTNodeBase):
|
|
226
|
+
__slots__ = ('value',)
|
|
227
|
+
value: Any
|
|
228
|
+
def __init__(self, value: Any) -> None:
|
|
229
|
+
self.value = value
|
|
230
|
+
|
|
231
|
+
def __repr__(self) -> str:
|
|
232
|
+
return "<{0} {1!r}>".format(self.__class__.__name__, self.value)
|
|
233
|
+
|
|
234
|
+
def to_graphviz(self, digraph: Any, *args: Any, **kwargs: Any) -> None:
|
|
235
|
+
digraph.node(str(id(self)), "{}\n{!r}".format(self.__class__.__name__, self.value))
|
|
236
|
+
|
|
237
|
+
################################################################################
|
|
238
|
+
# Base Expression Classes
|
|
239
|
+
################################################################################
|
|
240
|
+
class ExpressionBase(ASTNodeBase):
|
|
241
|
+
__slots__ = ('context',)
|
|
242
|
+
context: 'Context'
|
|
243
|
+
result_type: _DataTypeDef = DataType.UNDEFINED
|
|
244
|
+
"""The data type of the result of successful evaluation."""
|
|
245
|
+
def __repr__(self) -> str:
|
|
246
|
+
return "<{0} >".format(self.__class__.__name__)
|
|
247
|
+
|
|
248
|
+
def _new_value(self, *args: Any, **kwargs: Any) -> Any:
|
|
249
|
+
# perform a context aware load of value
|
|
250
|
+
value = coerce_value(*args, **kwargs)
|
|
251
|
+
if isinstance(value, datetime.datetime) and value.tzinfo is None:
|
|
252
|
+
value = value.replace(tzinfo=self.context.default_timezone)
|
|
253
|
+
return value
|
|
254
|
+
|
|
255
|
+
class LiteralExpressionBase(ExpressionBase):
|
|
256
|
+
"""A base class for representing literal values from the grammar text."""
|
|
257
|
+
__slots__ = ('value',)
|
|
258
|
+
value: Any
|
|
259
|
+
is_reduced: bool = True
|
|
260
|
+
def __init__(self, context: 'Context', value: Any) -> None:
|
|
261
|
+
"""
|
|
262
|
+
:param context: The context to use for evaluating the expression.
|
|
263
|
+
:type context: :py:class:`~rule_engine.engine.Context`
|
|
264
|
+
:param value: The native Python value.
|
|
265
|
+
"""
|
|
266
|
+
self.context = context
|
|
267
|
+
if self.result_type.is_scalar and DataType.from_value(value) != self.result_type:
|
|
268
|
+
raise TypeError("__init__ argument 2 must be {}, not {}".format(self.result_type.python_type.__name__, type(value).__name__))
|
|
269
|
+
self.value = value
|
|
270
|
+
|
|
271
|
+
def __repr__(self) -> str:
|
|
272
|
+
return "<{0} value={1!r} >".format(self.__class__.__name__, self.value)
|
|
273
|
+
|
|
274
|
+
@classmethod
|
|
275
|
+
def from_value(cls, context: 'Context', value: Any) -> 'LiteralExpressionBase':
|
|
276
|
+
"""
|
|
277
|
+
Create a Literal Expression instance to represent the specified *value*.
|
|
278
|
+
|
|
279
|
+
.. versionadded:: 2.0.0
|
|
280
|
+
|
|
281
|
+
:param context: The context to use for evaluating the expression.
|
|
282
|
+
:type context: :py:class:`~rule_engine.engine.Context`
|
|
283
|
+
:param value: The value to represent as a Literal Expression.
|
|
284
|
+
:return: A subclass of :py:class:`~.LiteralExpressionBase` specific to the type of *value*.
|
|
285
|
+
"""
|
|
286
|
+
datatype = DataType.from_value(value)
|
|
287
|
+
for subclass in cls.__subclasses__():
|
|
288
|
+
if DataType.is_compatible(subclass.result_type, datatype):
|
|
289
|
+
break
|
|
290
|
+
else:
|
|
291
|
+
raise errors.EngineError("can not create literal expression from python value: {!r}".format(value))
|
|
292
|
+
if datatype.is_compound:
|
|
293
|
+
if DataType.is_type(datatype, DataType.ARRAY) or DataType.is_type(datatype, DataType.SET):
|
|
294
|
+
value = datatype.python_type(cls.from_value(context, v) for v in value)
|
|
295
|
+
elif DataType.is_type(datatype, DataType.MAPPING):
|
|
296
|
+
value = tuple((cls.from_value(context, k), cls.from_value(context, v)) for k, v in value.items())
|
|
297
|
+
else:
|
|
298
|
+
value = coerce_value(value)
|
|
299
|
+
return subclass(context, value)
|
|
300
|
+
|
|
301
|
+
def evaluate(self, thing: Any) -> Any:
|
|
302
|
+
return self.value
|
|
303
|
+
|
|
304
|
+
def to_graphviz(self, digraph: Any, *args: Any, **kwargs: Any) -> None:
|
|
305
|
+
if self.result_type.is_compound:
|
|
306
|
+
digraph.node(str(id(self)), self.__class__.__name__)
|
|
307
|
+
else:
|
|
308
|
+
digraph.node(str(id(self)), "{}\nvalue={!r}".format(self.__class__.__name__, self.value))
|
|
309
|
+
|
|
310
|
+
class Statement(ASTNodeBase):
|
|
311
|
+
"""A class representing the top level statement of the grammar text."""
|
|
312
|
+
__slots__ = ('context', 'expression', 'comment')
|
|
313
|
+
context: 'Context'
|
|
314
|
+
expression: ExpressionBase
|
|
315
|
+
comment: Comment | None
|
|
316
|
+
def __init__(self, context: 'Context', expression: ExpressionBase, comment: Comment | None = None) -> None:
|
|
317
|
+
"""
|
|
318
|
+
:param context: The context to use for evaluating the statement.
|
|
319
|
+
:type context: :py:class:`~rule_engine.engine.Context`
|
|
320
|
+
:param expression: The top level expression of the statement.
|
|
321
|
+
:type expression: :py:class:`~.ExpressionBase`
|
|
322
|
+
"""
|
|
323
|
+
self.context = context
|
|
324
|
+
self.expression = expression
|
|
325
|
+
self.comment = comment
|
|
326
|
+
|
|
327
|
+
@classmethod
|
|
328
|
+
def build(cls, context: 'Context', expression: ExpressionBase, **kwargs: Any) -> 'Statement': # type: ignore[override]
|
|
329
|
+
built = expression.build()
|
|
330
|
+
assert isinstance(built, ExpressionBase)
|
|
331
|
+
reduced = cls(context, built, **kwargs).reduce()
|
|
332
|
+
assert isinstance(reduced, Statement)
|
|
333
|
+
return reduced
|
|
334
|
+
|
|
335
|
+
def evaluate(self, thing: Any) -> Any:
|
|
336
|
+
return self.expression.evaluate(thing)
|
|
337
|
+
|
|
338
|
+
def to_graphviz(self, digraph: Any, *args: Any, **kwargs: Any) -> None:
|
|
339
|
+
super(Statement, self).to_graphviz(digraph, *args, **kwargs)
|
|
340
|
+
self.expression.to_graphviz(digraph, *args, **kwargs)
|
|
341
|
+
digraph.edge(str(id(self)), str(id(self.expression)))
|
|
342
|
+
if self.comment:
|
|
343
|
+
self.comment.to_graphviz(digraph, *args, **kwargs)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
#
|
|
4
|
+
# rule_engine/ast/binary/__init__.py
|
|
5
|
+
#
|
|
6
|
+
# Redistribution and use in source and binary forms, with or without
|
|
7
|
+
# modification, are permitted provided that the following conditions are
|
|
8
|
+
# met:
|
|
9
|
+
#
|
|
10
|
+
# * Redistributions of source code must retain the above copyright
|
|
11
|
+
# notice, this list of conditions and the following disclaimer.
|
|
12
|
+
# * Redistributions in binary form must reproduce the above
|
|
13
|
+
# copyright notice, this list of conditions and the following disclaimer
|
|
14
|
+
# in the documentation and/or other materials provided with the
|
|
15
|
+
# distribution.
|
|
16
|
+
# * Neither the name of the project nor the names of its
|
|
17
|
+
# contributors may be used to endorse or promote products derived from
|
|
18
|
+
# this software without specific prior written permission.
|
|
19
|
+
#
|
|
20
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
21
|
+
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
22
|
+
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
23
|
+
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
24
|
+
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
25
|
+
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
26
|
+
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
27
|
+
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
28
|
+
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
29
|
+
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
30
|
+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
31
|
+
#
|
|
32
|
+
|
|
33
|
+
from typing import TYPE_CHECKING, Any
|
|
34
|
+
|
|
35
|
+
from ... import errors
|
|
36
|
+
from ...types import DataType
|
|
37
|
+
from ...types import _DataTypeDef
|
|
38
|
+
|
|
39
|
+
from ..base import ExpressionBase, LiteralExpressionBase, _is_reduced
|
|
40
|
+
|
|
41
|
+
from .arithmetic import AddExpression, ArithmeticExpression, BitwiseExpression, BitwiseShiftExpression, SubtractExpression
|
|
42
|
+
from .base import BinaryExpressionBase
|
|
43
|
+
from .comparison import ArithmeticComparisonExpression, ComparisonExpression, FuzzyComparisonExpression, LogicExpression
|
|
44
|
+
|
|
45
|
+
if TYPE_CHECKING:
|
|
46
|
+
from ...engine.context import Context
|
|
47
|
+
|
|
48
|
+
class CoalesceExpression(ExpressionBase):
|
|
49
|
+
"""
|
|
50
|
+
A class representing a null-coalesce (``??``) expression. Evaluates to the left operand when it is not ``None``;
|
|
51
|
+
otherwise evaluates to the right operand. The result type discharges nullability from the left operand when the
|
|
52
|
+
right operand is non-nullable.
|
|
53
|
+
|
|
54
|
+
.. versionadded:: 5.0.0
|
|
55
|
+
"""
|
|
56
|
+
__slots__ = ('left', 'right')
|
|
57
|
+
result_type: _DataTypeDef = DataType.UNDEFINED
|
|
58
|
+
def __init__(self, context: 'Context', left: ExpressionBase, right: ExpressionBase) -> None:
|
|
59
|
+
self.context = context
|
|
60
|
+
self.left = left
|
|
61
|
+
self.right = right
|
|
62
|
+
left_peeled = DataType.NULLABLE.unwrap(left.result_type)
|
|
63
|
+
right_peeled = DataType.NULLABLE.unwrap(right.result_type)
|
|
64
|
+
if left_peeled != DataType.UNDEFINED and right_peeled != DataType.UNDEFINED:
|
|
65
|
+
if left_peeled != DataType.NULL and right_peeled != DataType.NULL:
|
|
66
|
+
if not DataType.is_compatible(left_peeled, right_peeled):
|
|
67
|
+
raise errors.EvaluationError('data type mismatch')
|
|
68
|
+
if left_peeled == DataType.NULL or left_peeled == DataType.UNDEFINED:
|
|
69
|
+
base_type = right_peeled
|
|
70
|
+
else:
|
|
71
|
+
base_type = left_peeled
|
|
72
|
+
if DataType.is_type(right.result_type, DataType.NULLABLE) or right_peeled == DataType.NULL:
|
|
73
|
+
self.result_type = DataType.NULLABLE.wrap(base_type)
|
|
74
|
+
else:
|
|
75
|
+
self.result_type = base_type
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def build(cls, context: 'Context', left: ExpressionBase, right: ExpressionBase) -> ExpressionBase: # type: ignore[override]
|
|
79
|
+
left_built = left.build()
|
|
80
|
+
assert isinstance(left_built, ExpressionBase)
|
|
81
|
+
right_built = right.build()
|
|
82
|
+
assert isinstance(right_built, ExpressionBase)
|
|
83
|
+
reduced = cls(context, left_built, right_built).reduce()
|
|
84
|
+
assert isinstance(reduced, ExpressionBase)
|
|
85
|
+
return reduced
|
|
86
|
+
|
|
87
|
+
def evaluate(self, thing: Any) -> Any:
|
|
88
|
+
left_value = self.left.evaluate(thing)
|
|
89
|
+
if left_value is None:
|
|
90
|
+
return self.right.evaluate(thing)
|
|
91
|
+
return left_value
|
|
92
|
+
|
|
93
|
+
def reduce(self) -> ExpressionBase:
|
|
94
|
+
if not _is_reduced(self.left, self.right):
|
|
95
|
+
return self
|
|
96
|
+
return LiteralExpressionBase.from_value(self.context, self.evaluate(None))
|
|
97
|
+
|
|
98
|
+
def to_graphviz(self, digraph: Any, *args: Any, **kwargs: Any) -> None:
|
|
99
|
+
digraph.node(str(id(self)), self.__class__.__name__)
|
|
100
|
+
self.left.to_graphviz(digraph, *args, **kwargs)
|
|
101
|
+
self.right.to_graphviz(digraph, *args, **kwargs)
|
|
102
|
+
digraph.edge(str(id(self)), str(id(self.left)), label='left')
|
|
103
|
+
digraph.edge(str(id(self)), str(id(self.right)), label='right')
|