From 3c8eaea1e562541a81a5f209b05107b6c23cb1b1 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Mon, 10 Jul 2023 19:55:40 -0400 Subject: [PATCH] Update the docs --- docs/source/change_log.rst | 1 + docs/source/getting_started.rst | 30 +++++++------- docs/source/rule_engine/types.rst | 16 ++++---- docs/source/syntax.rst | 2 + docs/source/types.rst | 34 +++++++++++----- lib/rule_engine/types.py | 67 +++++++++++++++---------------- tests/types.py | 2 + 7 files changed, 83 insertions(+), 69 deletions(-) diff --git a/docs/source/change_log.rst b/docs/source/change_log.rst index a77cf95..4f10af0 100644 --- a/docs/source/change_log.rst +++ b/docs/source/change_log.rst @@ -18,6 +18,7 @@ Version 4.0.0 * **Breaking:** Dropped support for Python versions 3.4 and 3.5 * **Breaking:** Invalid floating point literals now raise :py:exc:`~.errors.FloatSyntaxError` instead of :py:exc:`~.errors.RuleSyntaxError` +* Added the new :py:class:`~rule_engine.types.DataType.FUNCTION` data type Version 3.x.x ------------- diff --git a/docs/source/getting_started.rst b/docs/source/getting_started.rst index 0d6f3eb..fa04d55 100644 --- a/docs/source/getting_started.rst +++ b/docs/source/getting_started.rst @@ -18,13 +18,13 @@ Basic Usage +-----------+-----------------------+-----------------------------------+ | Attribute | Python Type | Rule Engine Type | +-----------+-----------------------+-----------------------------------+ - | title | ``str`` | :py:attr:`~ast.DataType.STRING` | + | title | ``str`` | :py:attr:`~.DataType.STRING` | +-----------+-----------------------+-----------------------------------+ - | publisher | ``str`` | :py:attr:`~ast.DataType.STRING` | + | publisher | ``str`` | :py:attr:`~.DataType.STRING` | +-----------+-----------------------+-----------------------------------+ - | issue | ``int`` | :py:attr:`~ast.DataType.FLOAT` | + | issue | ``int`` | :py:attr:`~.DataType.FLOAT` | +-----------+-----------------------+-----------------------------------+ - | released | ``datetime.date`` | :py:attr:`~ast.DataType.DATETIME` | + | released | ``datetime.date`` | :py:attr:`~.DataType.DATETIME` | +-----------+-----------------------+-----------------------------------+ * An example comic book collection might look like: @@ -180,7 +180,7 @@ Setting A Default Value ^^^^^^^^^^^^^^^^^^^^^^^ By default, :py:class:`engine.Rule` will raise a :py:class:`~errors.SymbolResolutionError` for invalid symbols. In some cases, it may be desirable to change the way in which the language behaves to instead treat unknown symbols with a -default value (most often ``None`` / :py:attr:`ast.DataType.NULL` is used for this purpose, but any value of a supported +default value (most often ``None`` / :py:attr:`~.DataType.NULL` is used for this purpose, but any value of a supported type can be used). To change this behavior, set the *default_value* parameter when initializing the :py:class:`~engine.Context` instance. @@ -249,7 +249,7 @@ type needs to be resolved. The return type should be a member of the :py:class:` context = rule_engine.Context(type_resolver=type_resolver) -:py:attr:`~ast.DataType.UNDEFINED` can be defined as the data type for a valid symbol without specifying explicit type +:py:attr:`~.DataType.UNDEFINED` can be defined as the data type for a valid symbol without specifying explicit type information. In this case, the rule object will know that it is a valid symbol, but will not validate any operations that reference it. @@ -275,18 +275,18 @@ In all cases, when a *type_resolver* is defined, the :py:class:`~engine.Rule` ob Compound Data Types """"""""""""""""""" -Compound data types such as the :py:class:`~ast.DataType.ARRAY` and :py:class:`~ast.DataType.MAPPING` types can -optionally specify member type information by calling their respective type. For example, an array of strings would be -defined as ``DataType.ARRAY(DataType.STRING)`` while a mapping with string keys and float values would be defined as +Compound data types such as the :py:attr:`~.DataType.ARRAY` and :py:attr:`~.DataType.MAPPING` types can optionally +specify member type information by calling their respective type. For example, an array of strings would be defined as +``DataType.ARRAY(DataType.STRING)`` while a mapping with string keys and float values would be defined as ``DataType.MAPPING(DataType.STRING, DataType.FLOAT)``. For more information, see the documentation for the -:py:meth:`~ast.DataType.ARRAY`, :py:meth:`~ast.DataType.MAPPING` functions. +:py:attr:`~.DataType.ARRAY`, :py:attr:`~.DataType.MAPPING` functions. Compound member types can only be a single data type. In some cases the data type can optionally be nullable which means -that the member value can be either the specified type or :py:class:`~ast.DataType.NULL`. For example, a -:py:class:`~ast.DataType.MAPPING` type whose values are all nullable strings may be defined, while a -:py:class:`~ast.DataType.MAPPING` type with one value type of a :py:class:`~ast.DataType.STRING` and another of a -:py:class:`~ast.DataType.BOOLEAN` may not be defined. In this case, the key type may be defined while the value type is -set to :py:class:`~ast.DataType.UNDEFINED` which is the default value. +that the member value can be either the specified type or :py:attr:`~.DataType.NULL`. For example, a +:py:attr:`~.DataType.MAPPING` type whose values are all nullable strings may be defined, while a +:py:attr:`~.DataType.MAPPING` type with one value type of a :py:attr:`~.DataType.STRING` and another of a +:py:attr:`~.DataType.BOOLEAN` may not be defined. In this case, the key type may be defined while the value type is set +to :py:attr:`~.DataType.UNDEFINED` which is the default value. Defining Types From A Dictionary """""""""""""""""""""""""""""""" diff --git a/docs/source/rule_engine/types.rst b/docs/source/rule_engine/types.rst index 27a17a3..debc4fd 100644 --- a/docs/source/rule_engine/types.rst +++ b/docs/source/rule_engine/types.rst @@ -28,11 +28,10 @@ Classes .. autoclass:: DataType :members: - :exclude-members: ARRAY, FUNCTION, MAPPING, SET + :exclude-members: ARRAY, MAPPING, SET :show-inheritance: - .. autoattribute:: ARRAY - :annotation: + .. automethod:: ARRAY .. autoattribute:: BOOLEAN :annotation: @@ -43,20 +42,19 @@ Classes .. autoattribute:: FLOAT :annotation: - .. autoattribute:: FUNCTION - :annotation: + .. automethod:: FUNCTION - .. autoattribute:: MAPPING - :annotation: + .. automethod:: MAPPING .. autoattribute:: NULL :annotation: - .. autoattribute:: SET - :annotation: + .. automethod:: SET .. autoattribute:: STRING :annotation: .. autoattribute:: TIMEDELTA :annotation: + + .. autoattribute:: UNDEFINED diff --git a/docs/source/syntax.rst b/docs/source/syntax.rst index 4be2e51..2e9d167 100644 --- a/docs/source/syntax.rst +++ b/docs/source/syntax.rst @@ -340,6 +340,8 @@ The following symbols are provided by default using the :py:meth:`~builtins.Buil symbols can be accessed through the ``$`` prefix, e.g. ``$pi``. The default values can be overridden by defining a custom subclass of :py:class:`~engine.Context` and setting the :py:attr:`~engine.Context.builtins` attribute. +.. _builtin-functions: + Functions ^^^^^^^^^ diff --git a/docs/source/types.rst b/docs/source/types.rst index 8c3c104..71ff237 100644 --- a/docs/source/types.rst +++ b/docs/source/types.rst @@ -19,12 +19,12 @@ compatible with. For a information regarding supported operations, see the | :py:attr:`~DataType.DATETIME` | :py:class:`datetime.date`, | | | :py:class:`datetime.datetime` | +-------------------------------+-------------------------------+ -| :py:attr:`~DataType.TIMEDELTA`| :py:class:`datetime.timedelta`| -+-------------------------------+-------------------------------+ | :py:attr:`~DataType.FLOAT` | :py:class:`int`, | | | :py:class:`float` | | | :py:class:`decimal.Decimal` | +-------------------------------+-------------------------------+ +| :py:attr:`~DataType.FUNCTION` | *anything callable* | ++-------------------------------+-------------------------------+ | :py:attr:`~DataType.MAPPING` | :py:class:`dict` | +-------------------------------+-------------------------------+ | :py:attr:`~DataType.NULL` | :py:class:`NoneType` | @@ -33,6 +33,8 @@ compatible with. For a information regarding supported operations, see the +-------------------------------+-------------------------------+ | :py:attr:`~DataType.STRING` | :py:class:`str` | +-------------------------------+-------------------------------+ +| :py:attr:`~DataType.TIMEDELTA`| :py:class:`datetime.timedelta`| ++-------------------------------+-------------------------------+ Compound Types -------------- @@ -50,6 +52,8 @@ operations apply to the members of :py:attr:`~DataType.ARRAY` and :py:attr:`~Dat FLOAT ----- +See :ref:`literal-float-values` for syntax. + Starting in :release:`3.0.0`, the ``FLOAT`` datatype is backed by Python's :py:class:`~decimal.Decimal` object. This makes the evaluation of arithmetic more intuitive for the audience of rule authors who are not assumed to be familiar with the nuances of binary floating point arithmetic. To take an example from the :py:mod:`decimal` documentation, rule @@ -66,10 +70,18 @@ Since Python's :py:class:`~decimal.Decimal` values are not always equivalent to ``0.1 != Decimal('0.1')``) it's important to know that Rule Engine will coerce and normalize these values. That means that while in Python ``0.1 in [ Decimal('0.1') ]`` will evaluate to ``False``, in a rule it will evaluate to ``True`` (e.g. ``Rule('0.1 in numbers').evaluate({'numbers': [Decimal('0.1')]})``). This also affects Python dictionaries that -are converted to Rule Engine ``MAPPING`` values. While in Python the value ``{0.1: 'a', Decimal('0.1'): 'a'}`` would -have a length of 2 with two unique keys, the same value once converted into a Rule Engine ``MAPPING`` would have a -length of 1 with a single unique key. For this reason, developers using Rule Engine should take care to not use compound -data types with a mix of Python :py:class:`float` and :py:class:`~decimal.Decimal` values. +are converted to Rule Engine :py:attr:`~DataType.MAPPING` values. While in Python the value +``{0.1: 'a', Decimal('0.1'): 'a'}`` would have a length of 2 with two unique keys, the same value once converted into a +Rule Engine :py:attr:`~DataType.MAPPING` would have a length of 1 with a single unique key. For this reason, developers +using Rule Engine should take care to not use compound data types with a mix of Python :py:class:`float` and +:py:class:`~decimal.Decimal` values. + +FUNCTION +-------- +Version :release:`4.0.0` added the :py:attr:`~DataType.FUNCTION` datatype. This can be used to make functions available +to rule authors. Rule Engine contains a few :ref:`builtin functions` that can be used by default. +Additional functions can be either added them to the evaluated object or by extending the builtin symbols. It is only +possible to call a function from within the rule text. Functions can not be defined as other data types can be. TIMEDELTA --------- @@ -82,8 +94,8 @@ such as "has it been 30 days since this thing happened?" or "how much time passe The following mathematical operations are supported: -* adding a timedelta to a datetime (result is a datetime) -* adding a timedelta to another timedelta (result is a timedelta) -* subtracting a timedelta from a datetime (result is a datetime) -* subtracting a datetime from another datetime (result is a timedelta) -* subtracting a timedelta from another timedelta (result is a timedelta) +* Adding a timedelta to a datetime (result is a datetime) +* Adding a timedelta to another timedelta (result is a timedelta) +* Subtracting a timedelta from a datetime (result is a datetime) +* Subtracting a datetime from another datetime (result is a timedelta) +* Subtracting a timedelta from another timedelta (result is a timedelta) diff --git a/lib/rule_engine/types.py b/lib/rule_engine/types.py index 491725b..ce24e5b 100644 --- a/lib/rule_engine/types.py +++ b/lib/rule_engine/types.py @@ -182,6 +182,9 @@ def __init__(self, name, python_type): self.name = name self.python_type = python_type self.is_scalar = True + if '__call__' in dir(self) and self.__call__.__doc__: + # patch the call docs into the top-level class for Sphinx + self.__class__.__doc__ = self.__call__.__doc__ @property def is_iterable(self): @@ -202,7 +205,12 @@ def __repr__(self): def is_compound(self): return not self.is_scalar -_DATA_TYPE_UNDEFINED = _DataTypeDef('UNDEFINED', errors.UNDEFINED) +class _UndefinedDataTypeDef(_DataTypeDef): + def __repr__(self): + return 'UNDEFINED' + +_DATA_TYPE_UNDEFINED = _UndefinedDataTypeDef('UNDEFINED', errors.UNDEFINED) + class _CollectionDataTypeDef(_DataTypeDef): __slots__ = ('value_type', 'value_type_nullable') def __init__(self, name, python_type, value_type=_DATA_TYPE_UNDEFINED, value_type_nullable=True): @@ -223,6 +231,10 @@ def iterable_type(self): return self.value_type def __call__(self, value_type, value_type_nullable=True): + """ + :param value_type: The type of the members. + :param bool value_type_nullable: Whether or not members are allowed to be :py:attr:`.NULL`. + """ return self.__class__( self.name, self.python_type, @@ -275,6 +287,11 @@ def iterable_type(self): return self.key_type def __call__(self, key_type, value_type=_DATA_TYPE_UNDEFINED, value_type_nullable=True): + """ + :param key_type: The type of the mapping keys. + :param value_type: The type of the mapping values. + :param bool value_type_nullable: Whether or not mapping values are allowed to be :py:attr:`.NULL`. + """ return self.__class__( self.name, self.python_type, @@ -325,6 +342,17 @@ def __init__(self, name, python_type, value_name=None, return_type=_DATA_TYPE_UN self.minimum_arguments = minimum_arguments def __call__(self, name, return_type=_DATA_TYPE_UNDEFINED, argument_types=_DATA_TYPE_UNDEFINED, minimum_arguments=None): + """ + .. versionadded:: 4.0.0 + + :param str name: The name of the function, e.g. "split". + :param return_type: The data type of the functions return value. + :param tuple argument_types: The data types of the functions arguments. + :param int minimum_arguments: The minimum number of arguments the function requires. + + If *argument_types* is specified and *minimum_arguments* is not, then *minimum_arguments* will default to the length + of *argument_types* effectively meaning that every defined argument is required. If + """ return self.__class__( self.name, self.python_type, @@ -404,43 +432,14 @@ class DataType(metaclass=DataTypeMeta): This checks that the types are compatible without any kind of conversion. When dealing with compound data types, this ensures that the member types are either the same or :py:attr:`~.UNDEFINED`. """ - ARRAY = _ArrayDataTypeDef('ARRAY', tuple) - """ - .. py:function:: __call__(value_type, value_type_nullable=True) - - :param value_type: The type of the array members. - :param bool value_type_nullable: Whether or not array members are allowed to be :py:attr:`.NULL`. - """ + ARRAY = staticmethod(_ArrayDataTypeDef('ARRAY', tuple)) BOOLEAN = _DataTypeDef('BOOLEAN', bool) DATETIME = _DataTypeDef('DATETIME', datetime.datetime) FLOAT = _DataTypeDef('FLOAT', decimal.Decimal) - FUNCTION = _FunctionDataTypeDef('FUNCTION', _PYTHON_FUNCTION_TYPE) - """ - .. py:function:: __call__(name, return_type=_DATA_TYPE_UNDEFINED, argument_types=_DATA_TYPE_UNDEFINED, minimum_arguments=None) - - .. versionadded:: 4.0.0 - - :param str name: The name of the function, e.g. "split". - :param return_type: The type of the functions return value. - :param tuple argument_types: The types of the functions arguments. - :param int minimum_arguments: The minimum number of arguments the function requires. - """ - MAPPING = _MappingDataTypeDef('MAPPING', dict) - """ - .. py:function:: __call__(key_type, value_type, value_type_nullable=True) - - :param key_type: The type of the mapping keys. - :param value_type: The type of the mapping values. - :param bool value_type_nullable: Whether or not mapping values are allowed to be :py:attr:`.NULL`. - """ + FUNCTION = staticmethod(_FunctionDataTypeDef('FUNCTION', _PYTHON_FUNCTION_TYPE)) + MAPPING = staticmethod(_MappingDataTypeDef('MAPPING', dict)) NULL = _DataTypeDef('NULL', NoneType) - SET = _SetDataTypeDef('SET', set) - """ - .. py:function:: __call__(value_type, value_type_nullable=True) - - :param value_type: The type of the set members. - :param bool value_type_nullable: Whether or not set members are allowed to be :py:attr:`.NULL`. - """ + SET = staticmethod(_SetDataTypeDef('SET', set)) STRING = _DataTypeDef('STRING', str) TIMEDELTA = _DataTypeDef('TIMEDELTA', datetime.timedelta) UNDEFINED = _DATA_TYPE_UNDEFINED diff --git a/tests/types.py b/tests/types.py index c4f5ecf..cd599a9 100644 --- a/tests/types.py +++ b/tests/types.py @@ -179,6 +179,8 @@ def test_data_type_function(self): def test_data_type_definitions_describe_themselves(self): for name in DataType: + if name == 'UNDEFINED': + continue data_type = getattr(DataType, name) self.assertRegex(repr(data_type), 'name=' + name)