Source code for rule_engine.ast.literal

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
#  rule_engine/ast/literal.py
#
#  Redistribution and use in source and binary forms, with or without
#  modification, are permitted provided that the following conditions are
#  met:
#
#  * Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
#  * Redistributions in binary form must reproduce the above
#    copyright notice, this list of conditions and the following disclaimer
#    in the documentation and/or other materials provided with the
#    distribution.
#  * Neither the name of the project nor the names of its
#    contributors may be used to endorse or promote products derived from
#    this software without specific prior written permission.
#
#  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
#  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
#  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
#  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
#  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
#  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
#  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
#  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
#  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
#  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
#  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#

import collections
from typing import TYPE_CHECKING, Any, Iterable

from .. import errors
from ..parser.utilities import parse_datetime, parse_float, parse_timedelta
from ..types import DataType, coerce_value
from ..types import _DataTypeDef

from .base import ExpressionBase, LiteralExpressionBase, _is_reduced, _iterable_member_value_type

if TYPE_CHECKING:
    from ..engine.context import Context

################################################################################
# Literal Expressions
################################################################################
class _CollectionMixin(object):
    # these attributes are provided by the LiteralExpressionBase half of the subclasses
    result_type: _DataTypeDef
    value: Any
    def evaluate(self, thing: Any) -> Any:
        return self.result_type.python_type(member.evaluate(thing) for member in self.value)

    @property
    def is_reduced(self) -> bool:  # type: ignore[override]
        return _is_reduced(*self.value)

    def to_graphviz(self, digraph: Any, *args: Any, **kwargs: Any) -> None:
        super(_CollectionMixin, self).to_graphviz(digraph, *args, **kwargs)  # type: ignore[misc]
        for member in self.value:
            member.to_graphviz(digraph, *args, **kwargs)
            digraph.edge(str(id(self)), str(id(member)))

[docs] class ArrayExpression(_CollectionMixin, LiteralExpressionBase): # type: ignore[override] """Literal array expressions containing 0 or more sub-expressions.""" result_type: _DataTypeDef = DataType.ARRAY def __init__(self, *args: Any, **kwargs: Any) -> None: super(ArrayExpression, self).__init__(*args, **kwargs) self.result_type = DataType.ARRAY(value_type=_iterable_member_value_type(self.value)) @classmethod def build(cls, context: 'Context', value: Iterable[ExpressionBase]) -> 'ArrayExpression': # type: ignore[override] return cls(context, [member.build() for member in value])
[docs] class BooleanExpression(LiteralExpressionBase): """Literal boolean expressions representing True or False.""" result_type: _DataTypeDef = DataType.BOOLEAN
[docs] class BytesExpression(LiteralExpressionBase): """ Literal bytes expressions representing a binary string. This expression type always evaluates to true when not empty. """ result_type: _DataTypeDef = DataType.BYTES
[docs] class DatetimeExpression(LiteralExpressionBase): """ Literal datetime expressions representing a specific point in time. This expression type always evaluates to true. """ result_type: _DataTypeDef = DataType.DATETIME @classmethod def from_string(cls, context: 'Context', string: str) -> 'DatetimeExpression': dt = parse_datetime(string, default_timezone=context.default_timezone) return cls(context, dt)
[docs] class TimedeltaExpression(LiteralExpressionBase): """ Literal timedelta expressions representing an offset from a specific point in time. .. versionadded:: 3.5.0 """ result_type: _DataTypeDef = DataType.TIMEDELTA @classmethod def from_string(cls, context: 'Context', string: str) -> 'TimedeltaExpression': return cls(context, parse_timedelta(string))
[docs] class FloatExpression(LiteralExpressionBase): """Literal float expressions representing numerical values.""" result_type: _DataTypeDef = DataType.FLOAT def __init__(self, context: 'Context', value: Any, **kwargs: Any) -> None: value = coerce_value(value) super(FloatExpression, self).__init__(context, value, **kwargs) @classmethod def from_string(cls, context: 'Context', string: str) -> 'FloatExpression': return cls(context, parse_float(string))
[docs] class FunctionExpression(LiteralExpressionBase): """Literal mapping expression representing a function.""" # there's no syntax for defining functions, but this is required by the parser when a method is referred to on a # literal value, e.g. `b"41".decode('utf-8')` result_type: _DataTypeDef = DataType.FUNCTION
[docs] class MappingExpression(LiteralExpressionBase): """Literal mapping expression representing a set of associations between keys and values.""" result_type: _DataTypeDef = DataType.MAPPING def __init__(self, context: 'Context', value: Any, **kwargs: Any) -> None: if isinstance(value, dict): value = tuple(value.items()) super(MappingExpression, self).__init__(context, value, **kwargs) self.result_type = DataType.MAPPING( key_type=_iterable_member_value_type(key for key, _ in self.value), value_type=_iterable_member_value_type(value for _, value in self.value) ) @classmethod def build(cls, context: 'Context', value: Any) -> 'MappingExpression': # type: ignore[override] value = collections.OrderedDict(value) value = collections.OrderedDict((k.build(), v.build()) for k, v in value.items()) return cls(context, value) def evaluate(self, thing: Any) -> Any: mapping: 'collections.OrderedDict[Any, Any]' = collections.OrderedDict() for key, value in self.value: key = key.evaluate(thing) key_type = DataType.from_value(key) if key_type.is_compound and not DataType.is_type(key_type, DataType.ARRAY): raise errors.EngineError("the {} data type may not be used for mapping keys".format(key_type.name)) mapping[key] = value # defer value evaluation to avoid evaluating values of duplicate keys for key, value in mapping.items(): mapping[key] = value.evaluate(thing) return mapping @property def is_reduced(self) -> bool: # type: ignore[override] return all(_is_reduced(key, value) for key, value in self.value)
[docs] class NullExpression(LiteralExpressionBase): """Literal null expressions representing null values. This expression type always evaluates to false.""" result_type: _DataTypeDef = DataType.NULL def __init__(self, context: 'Context', value: Any = None) -> None: # all of the literal expressions take a value if value is not None: raise TypeError('value must be None') super(NullExpression, self).__init__(context, value=None)
[docs] class SetExpression(_CollectionMixin, LiteralExpressionBase): # type: ignore[override] """Literal set expressions containing 0 or more sub-expressions.""" result_type: _DataTypeDef = DataType.SET def __init__(self, *args: Any, **kwargs: Any) -> None: super(SetExpression, self).__init__(*args, **kwargs) self.result_type = DataType.SET(value_type=_iterable_member_value_type(self.value)) @classmethod def build(cls, context: 'Context', value: Iterable[ExpressionBase]) -> 'SetExpression': # type: ignore[override] built = set(member.build() for member in value) return cls(context, built)
[docs] class StringExpression(LiteralExpressionBase): """Literal string expressions representing an array of characters.""" result_type: _DataTypeDef = DataType.STRING