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.
Files changed (41) hide show
  1. rule_engine/__init__.py +65 -0
  2. rule_engine/_vendor/__init__.py +4 -0
  3. rule_engine/_vendor/ply/LICENSE +32 -0
  4. rule_engine/_vendor/ply/__init__.py +5 -0
  5. rule_engine/_vendor/ply/lex.py +1098 -0
  6. rule_engine/_vendor/ply/yacc.py +3433 -0
  7. rule_engine/ast/__init__.py +92 -0
  8. rule_engine/ast/base.py +343 -0
  9. rule_engine/ast/binary/__init__.py +103 -0
  10. rule_engine/ast/binary/arithmetic.py +211 -0
  11. rule_engine/ast/binary/base.py +112 -0
  12. rule_engine/ast/binary/comparison.py +154 -0
  13. rule_engine/ast/expression/__init__.py +35 -0
  14. rule_engine/ast/expression/access.py +434 -0
  15. rule_engine/ast/expression/control.py +256 -0
  16. rule_engine/ast/expression/resolution.py +220 -0
  17. rule_engine/ast/literal.py +183 -0
  18. rule_engine/builtins.py +230 -0
  19. rule_engine/debug_ast.py +65 -0
  20. rule_engine/debug_repl.py +159 -0
  21. rule_engine/engine/__init__.py +34 -0
  22. rule_engine/engine/_attribute_resolver.py +353 -0
  23. rule_engine/engine/context.py +438 -0
  24. rule_engine/engine/rule.py +148 -0
  25. rule_engine/errors.py +359 -0
  26. rule_engine/parser/__init__.py +614 -0
  27. rule_engine/parser/base.py +86 -0
  28. rule_engine/parser/utilities.py +114 -0
  29. rule_engine/suggestions.py +105 -0
  30. rule_engine/types/__init__.py +61 -0
  31. rule_engine/types/_object.py +318 -0
  32. rule_engine/types/coercion.py +141 -0
  33. rule_engine/types/dataclass.py +149 -0
  34. rule_engine/types/datatype.py +398 -0
  35. rule_engine/types/definitions.py +454 -0
  36. rule_engine/types/sqlalchemy.py +171 -0
  37. rule_engine-5.0.0.dist-info/METADATA +126 -0
  38. rule_engine-5.0.0.dist-info/RECORD +41 -0
  39. rule_engine-5.0.0.dist-info/WHEEL +5 -0
  40. rule_engine-5.0.0.dist-info/licenses/LICENSE +28 -0
  41. 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
@@ -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')