Skip to content

Commit

Permalink
Merge pull request #133 from potassco/document_type_annotations
Browse files Browse the repository at this point in the history
More type annotation fixes and documentation
  • Loading branch information
daveraja committed Feb 19, 2024
2 parents 24d053f + f40b095 commit 4cd480e
Show file tree
Hide file tree
Showing 19 changed files with 965 additions and 1,042 deletions.
292 changes: 120 additions & 172 deletions README.rst

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion clorm/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from .orm import *

__version__ = "1.4.3"
__version__ = "1.5.0"
__author__ = "David Rajaratnam"
__email__ = "daver@gemarex.com.au"
__copyright__ = "Copyright (c) 2018 David Rajaratnam"
Expand Down
40 changes: 33 additions & 7 deletions clorm/orm/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@
overload,
)

if sys.version_info >= (3, 10):
from types import UnionType

from clorm.orm.types import (
ConstantStr,
HeadList,
Expand Down Expand Up @@ -1467,6 +1470,15 @@ def field(
default_factory: Any = MISSING,
kw_only: bool = False,
) -> Any:
"""Return a field definition.
This function is used to override the type annotation field specification. A different
field can be specificied as well as default values.
This function operates in a similar way to ``dataclass.field()`` in that it allows for more
field specification options.
"""
if default is not MISSING and default_factory is not MISSING:
raise ValueError("can not specify both default and default_factory")
if isinstance(basefield, tuple):
Expand Down Expand Up @@ -2208,13 +2220,20 @@ def define_enum_field(
Example:
.. code-block:: python
class IO(str,Enum):
class IO(ConstantStr,Enum):
IN="in"
OUT="out"
# A field that unifies against ASP constants "in" and "out"
IOField = define_enum_field(ConstantField,IO)
# Note, when specified within a Predicate it is possible to avoid calling this
# function directly and the predicate class should simply reference the ``IO`` type
# annotation.
class X(Predicate):
x: IO
Positional argument:
field_class: the field that is being sub-classed
Expand Down Expand Up @@ -2829,8 +2848,15 @@ def _is_bad_predicate_inner_class_declaration(name, obj):
return obj.__name__ == name


def _is_union_type(type_: Type[Any]) -> bool:
if sys.version_info >= (3, 10):
return type_ is Union or type_ is UnionType
return type_ is Union


# infer fielddefinition based on a given type
def infer_field_definition(type_: Type[Any], module: str) -> Optional[Type[BaseField]]:
"""Given an type annotation specification return the matching clorm field."""
origin = get_origin(type_)
args = get_args(type_)

Expand All @@ -2846,7 +2872,7 @@ def infer_field_definition(type_: Type[Any], module: str) -> Optional[Type[BaseF
if origin is TailListReversed:
field = infer_field_definition(args[0], module)
return define_nested_list_field(field, headlist=False, reverse=True) if field else None
if origin is Union:
if _is_union_type(origin):
fields: List[Type[BaseField]] = []
for arg in args:
field = infer_field_definition(arg, module)
Expand Down Expand Up @@ -3206,9 +3232,9 @@ class Predicate(object, metaclass=_PredicateMeta):
.. code-block:: python
class Booking(Predicate):
date = StringField(index = True)
time = StringField(index = True)
name = StringField(default = "relax")
date: str
time: str
name: str = field(StringField, default="relax")
b1 = Booking("20190101", "10:00")
b2 = Booking("20190101", "11:00", "Dinner")
Expand Down Expand Up @@ -3290,7 +3316,7 @@ def raw(self) -> Symbol:
@_classproperty
@classmethod
def Field(cls) -> Type[BaseField]:
"""A BaseField sub-class corresponding to a Field for this class."""
"""A BaseField sub-class corresponding to a field definition for this class."""
return cls._field

# Clone the object with some differences
Expand Down Expand Up @@ -3332,7 +3358,7 @@ def clone(self: _P, **kwargs: Any) -> _P:
@_classproperty
@classmethod
def meta(cls) -> PredicateDefn:
"""The meta data (definitional information) for the Predicate/Complex-term"""
"""The meta data (definitional information) for the Predicate."""
return cls._meta

# --------------------------------------------------------------------------
Expand Down
2 changes: 2 additions & 0 deletions clorm/orm/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
else:

class ConstantStr(str):
"""A special ``str`` sub-class for specifying constant logical terms."""

pass


Expand Down
107 changes: 47 additions & 60 deletions docs/clorm/advanced.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,20 @@ understanding of the internal operations of Clorm.
Introspection of Predicate Definitions
--------------------------------------

A number of properties of a ``Predicate`` or ``ComplexTerm`` definition can be
accessed through the ``meta`` property of the class. To highlight these features
we assume the following definitions:
A number of properties of a ``Predicate`` definition can be accessed through the ``meta``
property of the class. To highlight these features we assume the following definitions:

.. code-block:: python
from clorm import Predicate, ComplexTerm, ConstantField, StringField
from clorm import Predicate
class Address(ComplexTerm):
street = StringField
city = StringField(index=true)
class Address(Predicate):
street: str
city: str
class Person(Predicate):
name = StringField(index=true)
address = Address.Field
name: str
address: Address
Firstly the name and arities of the complex term and predicate can be examined:

Expand Down Expand Up @@ -68,27 +67,25 @@ use the ``clorm.clingo`` integration module but instead to use the main
Unification
^^^^^^^^^^^

In logical terms, unification involves transforming one expression into another
through term substitution. We co-op this terminology for the process of
transforming ``Clingo.Symbol`` objects into Clorm facts. This unification
process is integral to using Clorm since it is the main process by which the
symbols within a Clingo model are transformed into Clorm facts.
In logical terms, unification involves transforming one expression into another through term
substitution. We co-op this terminology for the process of transforming ``Clingo.Symbol``
objects into Clorm facts. This unification process is integral to using Clorm since it is the
main process by which the symbols within a Clingo model are transformed into Clorm facts.

A ``unify`` function is provided that takes at least two parameters; a *unifier*
and a list of raw clingo symbols. It then tries to unify the list of raw symbols
with the predicates in the unifier. It returns a ``FactBase`` containing the
facts, or a list of facts if the parameter ``ordered=True``, that resulted from
the unification of the symbols with the first matching predicate. If a symbol
was not able to unify with any predicate it is ignored.
A ``unify`` function is provided that takes at least two parameters; a *unifier* and a list of
raw clingo symbols. It then tries to unify the list of raw symbols with the predicates in the
unifier. It returns a ``FactBase`` containing the facts, or a list of facts if the parameter
``ordered=True``, that resulted from the unification of the symbols with the first matching
predicate. If a symbol was not able to unify with any predicate it is ignored.

.. code-block:: python
from clingo import Function, String
from clorm import Predicate, StringField, unify
from clorm import Predicate, unify
class Person(Predicate):
name = StringField(index=True)
address = StringField
name: str
address: str
good_raw = Function("person", [String("Dave"),String("UNSW")])
bad_raw = Function("nonperson", [])
Expand All @@ -97,59 +94,49 @@ was not able to unify with any predicate it is ignored.
assert len(fb.indexes) == 1
.. note:: In general it is a good idea to avoid defining multiple predicate
definitions that can unify to the same symbol. However, if a symbol can unify
with multiple predicate definitions then the ``unify`` function will match
only the first predicate definition in the list of predicates.
.. note:: In general it is a good idea to avoid defining multiple predicate definitions that
can unify to the same symbol. However, if a symbol can unify with multiple predicate
definitions then the ``unify`` function will match only the first predicate definition in
the list of predicates.

By default, the fact base object returned by the ``unify`` function will be
initialised with any indexed fields as specified by the matching predicate
declaration.

To get more fined grained behaviour, such as controlling which fields are
indexed, the user can also use a ``SymbolPredicateUnfier`` helper function.
This class also provides a decorator function that can be used to register the
class and any indexes at the point where the predicate is defined. The symbol
predicate unifer can then be passed to the unify function instead of a list of
predicates.
To get more fined grained behaviour, such as controlling which fields are indexed, the user can
also use a ``SymbolPredicateUnfier`` helper function. The symbol predicate unifer can then be
passed to the unify function instead of a list of predicates.

.. code-block:: python
from clingo import Function, String
from clorm import Predicate, StringField, unify
from clorm import Predicate, ConstantStr, unify
spu = SymbolPredicateUnifier(supress_auto_index=True)
spu = SymbolPredicateUnifier()
@spu.register
class Person(Predicate):
name = StringField(index=True)
address = StringField
name: str
address: str
class Person(Predicate):
id = ConstantField()
address = StringField()
id: ConstantStr
address: str
good_raw = Function("person", [String("Dave"),String("UNSW")])
bad_raw = Function("nonperson", [])
fb = spu.unify([bad_raw, good_raw])
fb = unify(spu, [bad_raw, good_raw])
assert list(fb) == [Person(name="Dave", address="UNSW")]
assert len(fb.indexes) == 0
This function has two other useful features. Firtly, the option
``raise_on_empty=True`` will throw an error if no clingo symbols unify with the
registered predicates, which can be useful for debugging purposes.

The final option is the ``delayed_init=True`` option that allow for a delayed
initialisation of the ``FactBase``. What this means is that the symbols are only
processed (i.e., they are not unified agaist the predicates to generate facts)
when the ``FactBase`` object is actually used.

This is also useful because there are cases where a fact base object is never
actually used and is simply discarded. In particular this can happen when the
ASP solver generates models as part of the ``on_model()`` callback function. If
applications only cares about an optimal model or there is a timeout being
applied then only the last model generated will actually be processed and all
the earlier models may be discarded (see :ref:`api_clingo_integration`).
This function has two other useful features. Firtly, the option ``raise_on_empty=True`` will
throw an error if no clingo symbols unify with the registered predicates, which can be useful
for debugging purposes.

The final option is the ``delayed_init=True`` option that allow for a delayed initialisation of
the ``FactBase``. What this means is that the symbols are only processed (i.e., they are not
unified agaist the predicates to generate facts) when the ``FactBase`` object is actually used.

This is also useful because there are cases where a fact base object is never actually used and
is simply discarded. In particular this can happen when the ASP solver generates models as part
of the ``on_model()`` callback function. If applications only cares about an optimal model or
there is a timeout being applied then only the last model generated will actually be processed
and all the earlier models may be discarded (see :ref:`api_clingo_integration`).



Expand Down

0 comments on commit 4cd480e

Please sign in to comment.