From 7e6888b2badc8119f4b20c5f388b7c6dfa17c339 Mon Sep 17 00:00:00 2001 From: David Rajaratnam Date: Sun, 18 Feb 2024 11:40:45 +1100 Subject: [PATCH 1/6] Update README --- README.rst | 292 +++++++++++++++---------------------- tests/test_orm_atsyntax.py | 19 ++- 2 files changed, 138 insertions(+), 173 deletions(-) diff --git a/README.rst b/README.rst index 13953f9..9e2260b 100644 --- a/README.rst +++ b/README.rst @@ -99,7 +99,7 @@ First the relevant libraries need to be imported. .. code-block:: python - from clorm import Predicate, ConstantField, IntegerField + from clorm import Predicate, ConstantStr from clorm.clingo import Control Note: Importing from ``clorm.clingo`` instead of ``clingo``. @@ -112,74 +112,68 @@ Note: Importing from ``clorm.clingo`` instead of ``clingo``. is your preferred approach (see the `documentation `_). -The next step is to define a data model that maps the Clingo predicates to -Python classes. Clorm introduces a number basic concepts for defining the data -model: a ``Predicate`` class that must be sub-classed, and various *Field* -classes that correspond to definitions of allowable *logical terms* that form -the parameters of predicates. +The next step is to define a data model that maps the Clingo predicates to Python objects. A +Clingo predicate is mapped to Python by subclassing from a ``Predicate`` class. Similarly, to a +standard Python dataclass the predicate class contains *fields*. In this case, each field maps +to an ASP *term* and the type specification of the field determines the translation between +Clingo and Python. -Clorm provides three standard field classes, ``ConstantField``, ``StringField``, -and ``IntegerField``, that correspond to the standard *logic programming* data -types of integer, constant, and string. These fields are sub-classed from -``RawField``. +ASP's *logic programming* syntax allows for three primitive types: integer, string, and +constant. From the Python side this corresponds to the standard types ``int`` and ``str``, as +well as a special Clorm defined type ``ConstantStr``. .. code-block:: python class Driver(Predicate): - name=ConstantField + name: ConstantStr class Item(Predicate): - name=ConstantField + name: ConstantStr class Assignment(Predicate): - item=ConstantField - driver=ConstantField - time=IntegerField + item: ConstantStr + driver: ConstantStr + time: int The above code defines three classes to match the ASP program's input and output -predicates. +predicates. ``Driver`` maps to the ``driver/1`` predicate, ``Item`` maps to ``item/1``, and +``Assignment`` maps to ``assignment/3`` (note: the ``/n`` is a common logic programming +notation for specifying the arity of a predicate or function). A predicate can contain zero or +more fields. -``Driver`` maps to the ``driver/1`` predicate, ``Item`` maps to ``item/1``, and -``Assignment`` maps to ``assignment/3`` (note: the ``/n`` is a common logic -programming notation for specifying the arity of a predicate or function). A -predicate can contain zero or more fields. +The number of fields in the ``Predicate`` declaration must match the predicate arity and the +order in which they are declared must also match the position of each term in the ASP +predicate. -The number of fields in the ``Predicate`` declaration must match the predicate -arity and the order in which they are declared must also match the position of -each term in the ASP predicate. +Having defined the data model we now show how to dynamically add a problem instance, solve the +resulting ASP program, and print the solution. -Having defined the data model we now show how to dynamically add a problem -instance, solve the resulting ASP program, and print the solution. - -First the Clingo ``Control`` object needs to be created and initialised, and the -static problem domain encoding must be loaded. +First the Clingo ``Control`` object needs to be created and initialised, and the static problem +domain encoding must be loaded. .. code-block:: python ctrl = Control(unifier=[Driver, Item, Assignment]) ctrl.load("quickstart.lp") -The ``clorm.clingo.Control`` object controls how the ASP solver is run. When the -solver runs it generates *models*. These models constitute the solutions to the -problem. Facts within a model are encoded as ``clingo.Symbol`` objects. The -``unifier`` argument defines how these symbols are turned into Predicate -instances. +The ``clorm.clingo.Control`` object controls how the ASP solver is run. When the solver runs it +generates *models*. These models constitute the solutions to the problem. Facts within a model +are encoded as ``clingo.Symbol`` objects. The ``unifier`` argument defines how these symbols +are turned into Predicate instances. -For every symbol fact in the model, Clorm will successively attempt to *unify* -(or match) the symbol against the Predicates in the unifier list. When a match -is found the symbol is used to define an instance of the matching predicate. Any -symbol that does not unify against any of the predicates is ignored. +For every symbol fact in the model, Clorm will successively attempt to *unify* (or match) the +symbol against the Predicates in the unifier list. When a match is found the symbol is used to +define an instance of the matching predicate. Any symbol that does not unify against any of the +predicates is ignored. -Once the control object is created and the unifiers specified the static ASP -program is loaded. +Once the control object is created and the unifiers specified the static ASP program is loaded. -Next we generate a problem instance by generating a lists of ``Driver`` and -``Item`` objects. These items are added to a ``clorm.FactBase`` object. +Next we generate a problem instance by generating a lists of ``Driver`` and ``Item`` +objects. These items are added to a ``clorm.FactBase`` object. -The ``clorm.FactBase`` class provides a specialised set-like container for -storing facts (i.e., predicate instances). It provides the standard set -operations but also implements a querying mechanism for a more database-like -interface. +The ``clorm.FactBase`` class provides a specialised set-like container for storing facts (i.e., +predicate instances). It provides the standard set operations but also implements a querying +mechanism for a more database-like interface. .. code-block:: python @@ -189,26 +183,23 @@ interface. items = [ Item(name="item{}".format(i)) for i in range(1,6) ] instance = FactBase(drivers + items) -The ``Driver`` and ``Item`` constructors use named parameters that match the -declared field names. Note: while you can use positional arguments to initialise -instances, doing so will potentially make the code harder to refactor. So in -general you should avoid using positional arguments except for a few cases (eg., -simple tuples where the order is unlikely to change). +The ``Driver`` and ``Item`` constructors use named parameters that match the declared field +names. Note: while you can use positional arguments to initialise instances, doing so will +potentially make the code harder to refactor. So in general you should avoid using positional +arguments except for a few cases (eg., simple tuples where the order is unlikely to change). -These facts can now be added to the control object and the combined ASP program -grounded. +These facts can now be added to the control object and the combined ASP program grounded. .. code-block:: python ctrl.add_facts(instance) ctrl.ground([("base",[])]) -At this point the control object is ready to be run and generate -solutions. There are a number of ways in which the ASP solver can be run (see -the `Clingo API documentation -`_). For this -example, we use a mode where a callback function is specified. This -function will then be called each time a model is found. +At this point the control object is ready to be run and generate solutions. There are a number +of ways in which the ASP solver can be run (see the `Clingo API documentation +`_). +For this example, we use a mode where a callback function is specified. This function will then +be called each time a model is found. .. code-block:: python @@ -222,21 +213,19 @@ function will then be called each time a model is found. if not solution: raise ValueError("No solution found") -The ``on_model()`` callback is triggered for every new model. Because of the ASP -optimisation statements this callback can potentially be triggered multiple times -before an optimal model is found. Also, note that if the problem is -unsatisfiable then it will never be called and you should always check for this -case. +The ``on_model()`` callback is triggered for every new model. Because of the ASP optimisation +statements this callback can potentially be triggered multiple times before an optimal model is +found. Also, note that if the problem is unsatisfiable then it will never be called and you +should always check for this case. -The line ``solution = model.facts(atoms=True)`` extracts only instances of the -predicates that were registered with the ``unifier`` parameter. As mentioned -earlier, any facts that fail to unify are ignored. In this case it ignores the -``working_driver/1`` instances. The unified facts are stored and returned in -a ``clorm.FactBase`` object. +The line ``solution = model.facts(atoms=True)`` extracts only instances of the predicates that +were registered with the ``unifier`` parameter. As mentioned earlier, any facts that fail to +unify are ignored. In this case it ignores the ``working_driver/1`` instances. The unified +facts are stored and returned in a ``clorm.FactBase`` object. -The final step in this Python program involves querying the solution to print out -the relevant parts. To do this we call the ``FactBase.select()`` member function -that returns a suitable ``Select`` object. +The final step in this Python program involves querying the solution to print out the relevant +parts. To do this we call the ``FactBase.select()`` member function that returns a suitable +``Select`` object. .. code-block:: python @@ -246,18 +235,17 @@ that returns a suitable ``Select`` object. .where(Assignment.driver == ph1_)\ .order_by(Assignment.time) -A Clorm query can be viewed as a simplified version of a traditional database -query, and the function call syntax will be familiar to users of Python ORM's -such as SQLAlchemy or Peewee. +A Clorm query can be viewed as a simplified version of a traditional database query, and the +function call syntax will be familiar to users of Python ORM's such as SQLAlchemy or Peewee. -Here we want to find ``Assignment`` instances that match the ``driver`` field to -a special placeholder object ``ph1_`` and to return the results sorted by the -assignment time. The value of the ``ph1_`` placeholder will be provided when the -query is actually executed; separating specification from execution allows the -query to be re-run multiple times with different values. +Here we want to find ``Assignment`` instances that match the ``driver`` field to a special +placeholder object ``ph1_`` and to return the results sorted by the assignment time. The value +of the ``ph1_`` placeholder will be provided when the query is actually executed; separating +specification from execution allows the query to be re-run multiple times with different +values. -In particular, we now iterate over the list of drivers and execute the query for -each driver and print the result. +In particular, we now iterate over the list of drivers and execute the query for each driver +and print the result. .. code-block:: python @@ -270,10 +258,10 @@ each driver and print the result. for a in assignments: print("\t Item {} at time {}".format(a.item, a.time)) -Calling ``query.bind(d.name)`` first creates a new query with the placeholder -values assigned. Because ``d.name`` is the first parameter it matches against -the placeholder ``ph1_`` in the query definition. Clorm has four predefined -placeholders but more can be created using the ``ph_`` function. +Calling ``query.bind(d.name)`` first creates a new query with the placeholder values assigned. +Because ``d.name`` is the first parameter it matches against the placeholder ``ph1_`` in the +query definition. Clorm has four predefined placeholders but more can be created using the +``ph_`` function. Running this example produces the following results: @@ -290,9 +278,9 @@ Running this example produces the following results: Item item3 at time 3 Driver michael is not working today -The above example shows some of the main features of Clorm and how to match the -Python data model to the defined ASP predicates. For more details about how to -use Clorm see the `documentation `_. +The above example shows some of the main features of Clorm and how to match the Python data +model to the defined ASP predicates. For more details about how to use Clorm see the +`documentation `_. Other Clorm Features -------------------- @@ -300,127 +288,87 @@ Other Clorm Features Beyond the basic features outlined above there are many other features of the Clorm library. These include: -* You can define new sub-classes of ``RawField`` for custom data - conversions. For example, you can define a ``DateField`` that represents dates - in clingo in a string YYYY-MM-DD format and then use it in a predicate - definition. +* Predicate definitions with complex-terms; by specifying an existing ``Predicate`` class, or + Python tuples, as the field of a new ``Predicate`` sub-class. .. code-block:: python - from clorm import StringField # StringField is a sub-class of RawField - import datetime - - class DateField(StringField): # DateField is a sub-class of StringField - pytocl = lambda dt: dt.strftime("%Y-%m-%d") - cltopy = lambda s: datetime.datetime.strptime(s,"%Y-%m-%d").date() + class Event(Predicate): + date: str + name: str - class Delivery(Predicate): - item=ConstantField - date=DateField + class Log(Predicate): + event: Event + level: int - dd1=Delivery(item="item1", date=datetime.date(2019,14,5)) # Create delivery + l1=Log(event=Event(date="2019-4-5",name="goto shops"),level=0) .. code-block:: prolog % Corresponding ASP code - delivery(item1, "2019-04-05"). + log(event("2019-04-05", "goto shops"), 0). -* Clorm supports predicate definitions with complex-terms; using either a - ``ComplexTerm`` class (which is in fact an alias for Predicate) or Python - tuples. Every defined complex term has an associated ``RawField`` sub-class - that can be accessed as a ``Field`` property of the complex term class. +* Extending the mapping to specialised types. In the above example the event date is specified + as a string. This puts a burden on the Python developer to ensure that only strings of the + appropriate format are used when instantiating an ``Event`` object. Instead a specialised + translation can be specified by subclassing a ``BaseField`` or one of it's subclasses. A + ``BaseField`` class is a a special type of class that contains the functions to map between + Python objects and the underlying Clingo API ``Symbol`` objects. .. code-block:: python - from clorm import ComplexTerm + from clorm import StringField # StringField is a sub-class of BaseField + from clorm import field + import datetime - class Event(ComplexTerm): - date=DateField - name=StringField + class DateField(StringField): + pytocl = lambda dt: dt.strftime("%Y-%m-%d") + cltopy = lambda s: datetime.datetime.strptime(s,"%Y-%m-%d").date() - class Log(Predicate): - event=Event.Field - level=IntegerField + class Event(Predicate): + date: datetime.date = field + name: str - l1=Log(event=Event(date=datetime.date(2019,4,5),name="goto shops"),level=0) + l2=Log(event=Event(date=datetime.date(2019,3,15),name="travel"),level=0) .. code-block:: prolog % Corresponding ASP code - log(event("2019-04-05", "goto shops"), 0). + log(event("2019-03-15", "travel"), 0). + -* Function definitions can be decorated with a data conversion signature to - perform automatic type conversion for writing Python functions that can be - called from an ASP program using the @-syntax. +* Function definitions can be decorated with a data conversion signature to perform automatic + type conversion for writing Python functions that can be called from an ASP program using the + @-syntax. - For example a function ``add`` can be decorated with an data conversion - signature that accepts two input integers and expects an output integer. + For example a function ``add`` can be decorated with a data conversion signature that + accepts two input integers and expects an output integer. .. code-block:: python - @make_function_asp_callable(IntegerField, IntegerField, IntegerField) - def add(a,b): a+b + @make_function_asp_callable + def add(a: int, b: int)-> int: + a+b .. code-block:: prolog % Calling the add function from ASP f(@add(5,6)). % grounds to f(11). -* The data conversion signature can also be specified using Python 3.x function - annotations. So for an equivalent specification of ``add`` above: - -.. code-block:: python - - @make_function_asp_callable - def add(a : IntegerField, b : IntegerField) -> IntegerField: a+b - -* Note, the Clingo API does already perform some automatic data - conversions. However these conversions are ad-hoc, in the sense that it will - automatically convert numbers and strings, but cannot deal with other types - such as constants or more complex terms. +* Note, the Clingo API does already perform some automatic data conversions. However these + conversions are somewhat ad-hoc. Numbers and strings are automatically converted, but there + is no mechanism to deal with constants or more complex terms. - In contrast the Clorm mechanism of a data conversion signatures provide a more - complete and transparent approach; it can deal with arbitrary conversions and - all data conversions are clear since they are specified as part of the - signature. + The Clorm mechanism of a data conversion signatures provide a more complete and transparent + approach; it can deal with arbitrary conversions and all data conversions are clear since + they are specified as part of the signature. Development ----------- -* Python version: Clorm is actively developed using recent Python versions (3.8+) -* Clingo version: Clorm is typically tested with both Clingo version 5.4 and 5.5 - -Ideas for the Future --------------------- -Here are some thoughts on how to extend the library. - -* Add more examples to show how to use the Clorm. - -* Build a library of resuable ASP integration components. I've started on this - but am unsure how useful it would be. While there are some general concepts - that you might consider encoding (e.g., date and time), however, how you - actually want to encode them could be application specific. For example, - encoding time down to the second or minute level is probably not what you want - for a calendar scheduling application. In such a case a higher granularity, - say 15 min blocks, is better. - - It could be that rather than a library of components, a set of example - templates that could be copied and modified might be more useful. - -* Add a debug library. There are two aspects to debugging: debugging your - Python-ASP integration code, and debugging the ASP code itself. For the first - case, I should at least go through Clorm to make sure that any generated - exceptions have meaningful error messages. - - Debugging ASP code itself is trickier. It is often a painful process; when you - mess up you often end up with an unsatisfiable problem, which doesn't tell you - anything about what went wrong. You then end up commenting out constraints - until it becomes satisfiable and you can look at the models being - generated. My ideas are only vague at this stage. Maybe a tool that - automatically weakens constraints until the problem becomes satisfiable. There - are a few papers on debugging ASP so need to chase these up and see if there - is something I can use. +* Python version: Clorm works with Python versions (3.8+) +* Clingo version: Clorm is typically tested with Clingo versions 5.5 - 5.7 Alternatives ------------ diff --git a/tests/test_orm_atsyntax.py b/tests/test_orm_atsyntax.py index 81d7f84..d38a0ea 100644 --- a/tests/test_orm_atsyntax.py +++ b/tests/test_orm_atsyntax.py @@ -12,7 +12,7 @@ import unittest from typing import Tuple -from clingo import Function, Number, String +from clingo import Function, Number, String, Tuple_ from clingo import __version__ as clingo_version # Official Clorm API imports @@ -318,6 +318,23 @@ def test_sig2(dt: DateField) -> [(IntegerField, DateField)]: self.assertEqual(test_sig1(t1_raw), t1_raw) self.assertEqual(test_sig2(s_raw), [t1_raw, t2_raw]) + def test_make_function_asp_callable_with_type_annotations(self): + + # Some complicated signatures + + @make_function_asp_callable + def _sig1(x: int, y: int) -> int: + return x+y + + @make_function_asp_callable + def _sig2(pair: tuple[int, int]) -> tuple[int, int]: + return (pair[1], pair[0]) + + self.assertEqual(_sig1(Number(1), Number(2)), Number(3)) + self.assertEqual(_sig2(Tuple_([Number(1), Number(2)])), Tuple_([Number(2), Number(1)])) + + + # -------------------------------------------------------------------------- # Improving the error messages generated by the wrappers # -------------------------------------------------------------------------- From b85a6746b5be036a45132d2305608160b649ea90 Mon Sep 17 00:00:00 2001 From: David Rajaratnam Date: Mon, 19 Feb 2024 09:26:22 +1100 Subject: [PATCH 2/6] Add support for type annotated fields with | instead of typing.Union typing.Union is deprecated from 3.10+ using "|" is prefered. Also updated examples and tweaks to test scripts for PyPy --- clorm/orm/core.py | 12 +++++- examples/combine_fields/combine_fields.lp | 23 ++++++----- examples/quickstart/embedded_quickstart.lp | 12 +++--- examples/quickstart/quickstart.py | 12 +++--- tests/test_forward_ref.py | 46 ++++++++++++++++++++++ tests/test_orm_atsyntax.py | 18 ++++++--- tests/test_orm_core.py | 23 ++++++++--- tests/test_orm_noclingo.py | 9 ++++- 8 files changed, 119 insertions(+), 36 deletions(-) diff --git a/clorm/orm/core.py b/clorm/orm/core.py index 6a9da2f..2c6a658 100644 --- a/clorm/orm/core.py +++ b/clorm/orm/core.py @@ -38,6 +38,9 @@ overload, ) +if sys.version_info >= (3, 10): + from types import UnionType + from clorm.orm.types import ( ConstantStr, HeadList, @@ -2829,8 +2832,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_) @@ -2846,7 +2856,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) diff --git a/examples/combine_fields/combine_fields.lp b/examples/combine_fields/combine_fields.lp index e4fb7a6..f0d991e 100644 --- a/examples/combine_fields/combine_fields.lp +++ b/examples/combine_fields/combine_fields.lp @@ -17,9 +17,9 @@ holds(F,0) :- init(F). #script(python) from clorm.clingo import Control -from clorm import Predicate, ComplexTerm, ConstantField, IntegerField, combine_fields +from clorm import Predicate, ComplexTerm, ConstantStr, combine_fields from clorm import ph1_ - +from typing import Union #-------------------------------------------------------------------------- # Define a data model - we only care about defining the input and output @@ -27,24 +27,23 @@ from clorm import ph1_ #-------------------------------------------------------------------------- class Time(Predicate): - time=IntegerField + time: int class Light(ComplexTerm): - status=ConstantField + status: ConstantStr -class RobotLocation(ComplexTerm): - robot=ConstantField - location=ConstantField - class Meta: name = "robotlocation" +class RobotLocation(ComplexTerm, name="robotlocation"): + robot: ConstantStr + location: ConstantStr -FluentField=combine_fields([Light.Field,RobotLocation.Field]) +#FluentField=combine_fields([Light.Field,RobotLocation.Field]) class Init(Predicate): - fluent=FluentField + fluent: Union[Light, RobotLocation] class Holds(Predicate): - fluent=FluentField - time=IntegerField + fluent: Union[Light, RobotLocation] + time: int #-------------------------------------------------------------------------- # main diff --git a/examples/quickstart/embedded_quickstart.lp b/examples/quickstart/embedded_quickstart.lp index c91aa4a..621ad05 100644 --- a/examples/quickstart/embedded_quickstart.lp +++ b/examples/quickstart/embedded_quickstart.lp @@ -19,7 +19,7 @@ working_driver(D) :- assignment(_,D,_). #script(python) from clorm.clingo import Control -from clorm import Predicate, ConstantField, IntegerField, FactBase +from clorm import Predicate, ConstantStr, FactBase from clorm import ph1_ @@ -29,15 +29,15 @@ from clorm import ph1_ #-------------------------------------------------------------------------- class Driver(Predicate): - name=ConstantField + name: ConstantStr class Item(Predicate): - name=ConstantField + name: ConstantStr class Assignment(Predicate): - item=ConstantField - driver=ConstantField - time=IntegerField + item: ConstantStr + driver: ConstantStr + time: int #-------------------------------------------------------------------------- # main diff --git a/examples/quickstart/quickstart.py b/examples/quickstart/quickstart.py index 7b0dd99..dfa6347 100755 --- a/examples/quickstart/quickstart.py +++ b/examples/quickstart/quickstart.py @@ -6,7 +6,7 @@ from clingo import Control -from clorm import ConstantField, FactBase, IntegerField, Predicate, ph1_ +from clorm import ConstantStr, FactBase, Predicate, ph1_ ASP_PROGRAM = "quickstart.lp" @@ -17,17 +17,17 @@ class Driver(Predicate): - name = ConstantField + name: ConstantStr class Item(Predicate): - name = ConstantField + name: ConstantStr class Assignment(Predicate): - item = ConstantField - driver = ConstantField - time = IntegerField + item: ConstantStr + driver: ConstantStr + time: int # -------------------------------------------------------------------------- diff --git a/tests/test_forward_ref.py b/tests/test_forward_ref.py index 005607d..4ff1209 100644 --- a/tests/test_forward_ref.py +++ b/tests/test_forward_ref.py @@ -106,6 +106,52 @@ class P3(Predicate): p = module.P3(a=module.P2(a=42)) self.assertEqual(str(p), "p3(p2(42))") + def test_postponed_annotations_complex2(self): + code = """ +from __future__ import annotations +from clorm import Predicate +from typing import Union + +class P1(Predicate): + a: int + b: str + +class P2(Predicate): + a: int + +class P3(Predicate): + a: P1 | P2 +""" + if sys.version_info >= (3, 10): + with self._create_module(code) as module: + p = module.P3(a=module.P1(a=3, b="42")) + self.assertEqual(str(p), 'p3(p1(3,"42"))') + p = module.P3(a=module.P2(a=42)) + self.assertEqual(str(p), "p3(p2(42))") + + def test_postponed_annotations_complex3(self): + code = """ +from __future__ import annotations +from clorm import Predicate +from typing import Union + +class P1(Predicate): + a: int + b: str + +class P2(Predicate): + a: int + +class P3(Predicate): + a: 'P1 | P2' +""" + if sys.version_info >= (3, 10): + with self._create_module(code) as module: + p = module.P3(a=module.P1(a=3, b="42")) + self.assertEqual(str(p), 'p3(p1(3,"42"))') + p = module.P3(a=module.P2(a=42)) + self.assertEqual(str(p), "p3(p2(42))") + def test_postponed_annotations_nonglobal1(self): code = """ from __future__ import annotations diff --git a/tests/test_orm_atsyntax.py b/tests/test_orm_atsyntax.py index d38a0ea..7313b21 100644 --- a/tests/test_orm_atsyntax.py +++ b/tests/test_orm_atsyntax.py @@ -9,6 +9,7 @@ import calendar import datetime +import sys import unittest from typing import Tuple @@ -33,6 +34,9 @@ from .support import check_errmsg +# Error messages for CPython and PyPy vary +PYPY = sys.implementation.name == "pypy" + # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ @@ -324,7 +328,7 @@ def test_make_function_asp_callable_with_type_annotations(self): @make_function_asp_callable def _sig1(x: int, y: int) -> int: - return x+y + return x + y @make_function_asp_callable def _sig2(pair: tuple[int, int]) -> tuple[int, int]: @@ -333,8 +337,6 @@ def _sig2(pair: tuple[int, int]) -> tuple[int, int]: self.assertEqual(_sig1(Number(1), Number(2)), Number(3)) self.assertEqual(_sig2(Tuple_([Number(1), Number(2)])), Tuple_([Number(2), Number(1)])) - - # -------------------------------------------------------------------------- # Improving the error messages generated by the wrappers # -------------------------------------------------------------------------- @@ -354,7 +356,10 @@ def test_sig2(v: IntegerField) -> IntegerField: if clingo_version >= "5.5.0": with self.assertRaises(TypeError) as ctx: test_sig1(Number(1)) - check_errmsg("an integer is required for output of test_sig1()", ctx) + if PYPY: + check_errmsg("expected integer, got float object for output of test_sig1()", ctx) + else: + check_errmsg("an integer is required for output of test_sig1()", ctx) with self.assertRaises(ValueError) as ctx: test_sig2(Number(1)) @@ -382,7 +387,10 @@ def test_sig2(self, v: IntegerField) -> IntegerField: if clingo_version >= "5.5.0": with self.assertRaises(TypeError) as ctx: tmp.test_sig1(Number(1)) - check_errmsg("an integer is required for output of test_sig1()", ctx) + if PYPY: + check_errmsg("expected integer, got float object for output of test_sig1()", ctx) + else: + check_errmsg("an integer is required for output of test_sig1()", ctx) with self.assertRaises(ValueError) as ctx: tmp.test_sig2(Number(1)) diff --git a/tests/test_orm_core.py b/tests/test_orm_core.py index a157786..9ec7fc9 100644 --- a/tests/test_orm_core.py +++ b/tests/test_orm_core.py @@ -73,6 +73,10 @@ from .support import check_errmsg, check_errmsg_contains +# Error messages for CPython and PyPy vary +PYPY = sys.implementation.name == "pypy" + + # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ @@ -488,9 +492,12 @@ def test_api_catch_bad_field_instantiation(self): with self.assertRaises(TypeError) as ctx: fld5 = IntegerField(unknown1=5, unknown2="f") - check_errmsg_contains( - ("__init__() got an unexpected keyword argument " "'unknown1'"), ctx - ) + if PYPY: + check_errmsg_contains(("__init__() got 2 unexpected keyword arguments"), ctx) + else: + check_errmsg_contains( + ("__init__() got an unexpected keyword argument " "'unknown1'"), ctx + ) # -------------------------------------------------------------------------- # Test setting the index flag for a field @@ -751,7 +758,10 @@ def test_api_nested_list_field(self): with self.assertRaises(TypeError) as ctx: tmp = INLField.pytocl([1, "b", 3]) - check_errmsg("an integer is required", ctx) + if PYPY: + check_errmsg("expected integer", ctx) + else: + check_errmsg("an integer is required", ctx) with self.assertRaises(TypeError) as ctx: tmp = INLField.pytocl(1) @@ -787,7 +797,10 @@ def test_api_flat_list_field(self): # Test some failures with self.assertRaises(TypeError) as ctx: tmp = ILField.pytocl(("a", "b", "c")) - check_errmsg("an integer is required", ctx) + if PYPY: + check_errmsg("expected integer", ctx) + else: + check_errmsg("an integer is required", ctx) with self.assertRaises(TypeError) as ctx: tmp = CLField.pytocl((1, 2, 3)) diff --git a/tests/test_orm_noclingo.py b/tests/test_orm_noclingo.py index 19d53e7..eada2ef 100644 --- a/tests/test_orm_noclingo.py +++ b/tests/test_orm_noclingo.py @@ -4,6 +4,7 @@ import importlib import os +import sys import unittest import clingo @@ -26,6 +27,9 @@ clingo_version = clingo.__version__ +# Error messages for CPython and PyPy vary +PYPY = sys.implementation.name == "pypy" + # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ @@ -258,7 +262,10 @@ def _get_error(func, *args): def _assertEqualError(c_error, nc_error): self.assertEqual(type(c_error), type(nc_error)) - self.assertEqual(str(c_error), str(nc_error)) + # NOTE: Not testing the exact error message because PyPy and CPython produce + # different error messages and I don't know how to use python itself to generate + # the message. + # self.assertEqual(str(c_error), str(nc_error)) _assertEqualError(_get_error(clingo.Number, "a"), _get_error(noclingo.NoNumber, "a")) _assertEqualError(_get_error(clingo.Number, ["a"]), _get_error(noclingo.NoNumber, ["a"])) From 64f9f76af246b91299de76137183e6d76c6e03e8 Mon Sep 17 00:00:00 2001 From: David Rajaratnam Date: Mon, 19 Feb 2024 10:18:31 +1100 Subject: [PATCH 3/6] Python version testing - change tuple to typing.Tuple for atsyntax Remove 'tuple' in type annotation for @-syntax generation to remove need for python version specific tests. Instead added these test to test_forward_ref were there are already many tests that are python specific. --- tests/test_forward_ref.py | 26 ++++++++++++++++++++++++++ tests/test_orm_atsyntax.py | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/tests/test_forward_ref.py b/tests/test_forward_ref.py index 4ff1209..1693371 100644 --- a/tests/test_forward_ref.py +++ b/tests/test_forward_ref.py @@ -152,6 +152,32 @@ class P3(Predicate): p = module.P3(a=module.P2(a=42)) self.assertEqual(str(p), "p3(p2(42))") + def test_postponed_annotations_tuple1(self): + code = """ +from __future__ import annotations +from clorm import Predicate + +class P(Predicate): + a: tuple[int, str] +""" + if sys.version_info >= (3, 10): + with self._create_module(code) as module: + p = module.P(a=(123, "Hello")) + self.assertEqual(str(p), 'p((123,"Hello"))') + + def test_postponed_annotations_tuple1(self): + code = """ +from __future__ import annotations +from clorm import Predicate +from typing import Tuple + +class P(Predicate): + a: Tuple[int, str] +""" + with self._create_module(code) as module: + p = module.P(a=(123, "Hello")) + self.assertEqual(str(p), 'p((123,"Hello"))') + def test_postponed_annotations_nonglobal1(self): code = """ from __future__ import annotations diff --git a/tests/test_orm_atsyntax.py b/tests/test_orm_atsyntax.py index 7313b21..b23a968 100644 --- a/tests/test_orm_atsyntax.py +++ b/tests/test_orm_atsyntax.py @@ -331,7 +331,7 @@ def _sig1(x: int, y: int) -> int: return x + y @make_function_asp_callable - def _sig2(pair: tuple[int, int]) -> tuple[int, int]: + def _sig2(pair: Tuple[int, int]) -> Tuple[int, int]: return (pair[1], pair[0]) self.assertEqual(_sig1(Number(1), Number(2)), Number(3)) From d938bd6a61df5db6e57390421cd846b24a10a272 Mon Sep 17 00:00:00 2001 From: David Rajaratnam Date: Mon, 19 Feb 2024 15:51:48 +1100 Subject: [PATCH 4/6] Update documentation for new type annotations --- clorm/orm/core.py | 17 ++- clorm/orm/types.py | 2 + docs/clorm/api.rst | 76 ++++++----- docs/clorm/installation.rst | 2 +- docs/clorm/quickstart.rst | 154 +++------------------- examples/combine_fields/combine_fields.lp | 15 ++- 6 files changed, 89 insertions(+), 177 deletions(-) diff --git a/clorm/orm/core.py b/clorm/orm/core.py index 2c6a658..c925acb 100644 --- a/clorm/orm/core.py +++ b/clorm/orm/core.py @@ -2211,13 +2211,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 @@ -3216,9 +3223,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") @@ -3300,7 +3307,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 diff --git a/clorm/orm/types.py b/clorm/orm/types.py index 9763348..35a3d1d 100644 --- a/clorm/orm/types.py +++ b/clorm/orm/types.py @@ -16,6 +16,8 @@ else: class ConstantStr(str): + """A special ``str`` sub-class for specifying constant logical terms.""" + pass diff --git a/docs/clorm/api.rst b/docs/clorm/api.rst index 8840ad5..c9d6471 100644 --- a/docs/clorm/api.rst +++ b/docs/clorm/api.rst @@ -12,13 +12,30 @@ arities and the appropriate field for each of the parameters. Fields ------ -Fields provide a specification for how to convert ``clingo.Symbol`` objects into -more intuitive Python objects. +Fields provide a specification for how to convert ``clingo.Symbol`` objects into the +appropriate/intuitive Python object. As of Clorm version 1.5, the preferred mechanism to +specify fields is to use standard Python type annotations. The primitive logical terms +*integer* and *string* are specified using the standard Python type ``int`` and ``str`` +respectively, while logical *constant* terms are specified using a specially defined type: + + +.. autoclass:: clorm.ConstantStr + +Internally within Clorm the type specifiers are mapped to a set of special classes that contain +the functions for converting between Python and the ``clingo.Symbol`` objects. These special +classes are referred to as *field definitions* and are subclassed from a common base class +:class:`~clorm.BaseField`. .. autoclass:: clorm.BaseField :members: cltopy, pytocl, default, has_default, has_default_factory, index -.. autoclass:: clorm.RawField +For sub-classes of :class:`~clorm.BaseField` the abstract member functions +:py:meth:`~clorm.BaseField.cltopy` and :py:meth:`~clorm.BaseField.pytocl` must be implemented +to provide the conversion from Clingo to Python and Python to Clingo (respectively). + +Clorm provides three standard sub-classes corresponding to the string, constant, and integer +primitive logical terms: :class:`~clorm.StringField`, :class:`~clorm.ConstantField`, and +:class:`~clorm.IntegerField`. .. autoclass:: clorm.StringField @@ -26,11 +43,37 @@ more intuitive Python objects. .. autoclass:: clorm.IntegerField + +Special Fields +^^^^^^^^^^^^^^ + +Clorm provides a number of fields for some special cases. The :class:`~clorm.SimpleField` can +be used to define a field that matches to any primitive type. Note, however that special care +should be taken when using :class:`~clorm.SimpleField`. While it is easy to disambiguate the +Clingo to Python direction of the translation, it is harder to disambiguate between a string +and constant when converting from Python to Clingo, since strings and constants are both +represented using ``str``. For this direction a regular expression is used to perform the +disambiguation, and the user should therefore be careful not to pass strings that look like +constants if the intention is to treat it like a logical string. + .. autoclass:: clorm.SimpleField +A :class:`~clorm.RawField` is useful when it is necessary to match any term, whether it is a +primitive term or a complex term. This encoding provides no traslation of the underlying Clingo +Symbol object and simply wraps it in a special :class:`~clorm.Raw` class. + .. autoclass:: clorm.Raw :members: symbol +.. autoclass:: clorm.RawField + +Finally, there are a number of function generators that can be used to define some special +cases; refining or combining existing fields or allowing for a complicated pattern of +fields. While Clorm doesn't explicitly allow recursive terms to be defined it does provide a +number of encodings of list like terms. Note, it is sometimes possible to avoid the explicit +use of these functions and instead to rely on some predefined type annotation mappings. + + .. autofunction:: clorm.refine_field .. autofunction:: clorm.define_enum_field @@ -280,30 +323,3 @@ documentation for more details. .. autoclass:: clorm.clingo.SolveHandle :members: -Experimental Features ---------------------- - -The following are experimental features and may be modified or removed -completely from the library in future versions. - -JSON Encoding and Decoding -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. note:: - - Don't use these function.I think converting to/from JSON would be better - served by using a third-party library like `Pydantic - `_. One option for the future would be - to look at ways of automatically generating Pydantic data models from Clorm - data models and offering convenience functions for converting between the - two. - -Clorm allows clingo.Symbols, Predicates, and FactBases to be translated to/from -JSON. - -.. autoclass:: clorm.json.FactBaseCoder - :members: - -.. autofunction:: clorm.json.symbol_encoder - -.. autofunction:: clorm.json.symbol_decoder diff --git a/docs/clorm/installation.rst b/docs/clorm/installation.rst index e0359c3..d494e3b 100644 --- a/docs/clorm/installation.rst +++ b/docs/clorm/installation.rst @@ -1,7 +1,7 @@ Installation ============ -Clorm requires Python 3.7+ and Clingo 5.4+ (as of writing Clingo 5.5.0 is the +Clorm requires Python 3.7+ and Clingo 5.4+ (as of writing Clingo 5.7.1 is the latest version). There are many ways to install Clorm. In order of simplicity, it can be installed using `pip`, `conda`, from the `Potassco PPA `_ (for Ubuntu users), and finally from source. diff --git a/docs/clorm/quickstart.rst b/docs/clorm/quickstart.rst index 89860d0..7a94440 100644 --- a/docs/clorm/quickstart.rst +++ b/docs/clorm/quickstart.rst @@ -78,7 +78,7 @@ First the relevant libraries need to be imported. .. code-block:: python - from clorm import Predicate, ConstantField, IntegerField + from clorm import Predicate, ConstantStr from clorm.clingo import Control .. note:: Importing from ``clorm.clingo`` instead of ``clingo``. @@ -94,29 +94,31 @@ First the relevant libraries need to be imported. Defining the Data Model ----------------------- -The most important step is to define a *data model* that maps the Clingo -predicates to Python classes. Clorm provides the :class:`~clorm.Predicate` base -class for this purpose; a :class:`~clorm.Predicate` sub-class defines a direct -mapping to an underlying ASP logical predicate. The parameters of the predicate -are specified using a number of *field* classes. Fields can be thought of as -*term definitions*, as they define how a logical *term* is converted to, and -from, a Python object. Clorm provides three standard field classes, -:class:`~clorm.ConstantField`, :class:`~clorm.StringField`, and -:class:`~clorm.IntegerField`, that correspond to the standard *logic -programming* data types of constant, string, and integer. +The most important step is to define a *data model* that maps the Clingo predicates to Python +classes. Clorm provides the :class:`~clorm.Predicate` base class for this purpose; a +:class:`~clorm.Predicate` sub-class defines a direct mapping to an underlying ASP logical +predicate. The parameters of the predicate are specified using a number of *fields*, similar to +a standard Python ``dataclass``. In the Clorm context the fields can be thought of as *term +definitions*, as they define how a logical *term* is converted to, and from, a Python object. + +ASP's *logic programming* syntax allows for three primitive types: integer, string, and +constant. From the Python side this corresponds to the standard types ``int`` and ``str``, as +well as a special Clorm defined type ``ConstantStr``. Note: ``ConstantStr`` is sub-classed from +``str`` in order to disambiguate between ASP constants and strings, while still offering the +same Python type checking behaviour of the ``str`` parent class. .. code-block:: python class Driver(Predicate): - name=ConstantField + name: ConstantStr class Item(Predicate): - name=ConstantField + name: ConstantStr class Assignment(Predicate): - item=ConstantField - driver=ConstantField - time=IntegerField + item: ConstantStr + driver: ConstantStr + time: int The above code defines three classes to match the ASP program's input and output predicates, where the name of the predicate to map to is derived from the @@ -335,123 +337,3 @@ It is also worth noting that the :py:meth:`Query.select()` projection operator performs a similar function to an SQL ``SELECT`` clause to modify the output. Here, instead of returning the assignment item itself, it returns the two relevant parameter values. - -Old Query API -^^^^^^^^^^^^^ - -For comparison the following shows the same example but using the old query API. - -.. code-block:: python - - from clorm import ph1_ - - query=solution.select(Assignment)\ - .where(Assignment.driver == ph1_).order_by(Assignment.time) - - for d in drivers: - assignments = query.get(d.name) - if not assignments: - print("Driver {} is not working today".format(d.name)) - else: - print("Driver {} must deliver: ".format(d.name)) - for a in assignments: - print("\t Item {} at time {}".format(a.item, a.time)) - - -The old interface starts with a -:py:meth:`FactBase.select()` call to return a -:class:`~clorm.Select` object. This specifies both the predicate to be queried -and the output format of the query. Comparing this to the new query API the old -interface only allows a single predicate to be queried and the output format is -fixed and cannot be modified. - -Calling ``query.get(d.name)`` executes the query for the given driver as well as -binding the placeholders. An important difference between the old and new -interfaces is that the call to :py:meth:`Select.get()` -executes the query and returns a list of the results. In contrast the call to -:py:meth:`Query.all()` returns a generator and the query is -executed by the generator during its iteration. - -Other Clorm Features --------------------- - -The above example shows some of the basic features of Clorm and how to match the -Python data model to the defined ASP predicates. However, beyond the basics -outlined above there are other important features that will be useful for more -complex interactions. These include: - -* Defining complex-terms. Many ASP programs include complex terms (i.e., either - tuples or functional objects). Clorm supports predicate definitions that - include complex-terms using a ``ComplexTerm`` class. Every defined complex - term has an associated ``Field`` property that can be used within a Predicate - definition. - -.. code-block:: python - - from clorm import ComplexTerm - - class Event(ComplexTerm): - date=StringField - name=StringField - - class Log(Predicate): - event=Event.Field - level=IntegerField - -The above definition can be used to match against an ASP fact containing a -complex term. - -.. code-block:: prolog - - log(event("2019-04-05", "goto shops"), 0). - -* Custom fields. The ``IntegerField``, ``StringField``, and ``ConstantField`` - classes are in fact sub-classes of a ``RawField`` class. Custom data - conversions can be performed by sub-classing ``RawField`` directly, or by - sub-classing one of its existing sub-classes. For example, a ``DateField`` can - be defined that converts Python date objects into Clingo strings with - YYYY-MM-DD formatting. - -.. code-block:: python - - from clorm import StringField # StringField is a sub-class of RawField - from datetime import datetime - - class DateField(StringField): - pytocl = lambda dt: dt.strftime("%Y-%m-%d") - cltopy = lambda s: datetime.datetime.strptime(s,"%Y-%m-%d").date() - - class DeliveryDate(Predicate): - item=ConstantField() - date=DateField() - -* Clingo allows Python functions to be called from within an ASP program using - the @-syntax. Values are passed to these Python functions as ``clingo.Symbol`` - objects and it is up to the Python code to perform all appropriate data - conversions. - - Clorm provides a number of mechanisms to automate this data conversion process - through the specification of a *data conversion signature*. - - In the following example the function ``add`` is decorated with an automatic - data conversion signature that accepts two input integers and returns an - output integer. - -.. code-block:: python - - @make_function_asp_callable(IntegerField, IntegerField, IntegerField) - def add(a,b): a+b - -.. code-block:: prolog - - f(@add(5,6)). % grounds to f(11). - -The default behaviour of the Clingo API does infact provide some minimal -automatic data conversion for the @-functions. In particular, it will -automatically convert numbers and strings, however it cannot deal with other -types such as constants or more complex terms. - -The Clorm mechanism of a data conversion signatures provide a more principled -and transparent approach; it can deal with arbitrary conversions and all data -conversions are clear since they are specified as part of the signature. - diff --git a/examples/combine_fields/combine_fields.lp b/examples/combine_fields/combine_fields.lp index f0d991e..225b2ce 100644 --- a/examples/combine_fields/combine_fields.lp +++ b/examples/combine_fields/combine_fields.lp @@ -17,7 +17,8 @@ holds(F,0) :- init(F). #script(python) from clorm.clingo import Control -from clorm import Predicate, ComplexTerm, ConstantStr, combine_fields +from enum import Enum +from clorm import Predicate, ConstantStr, combine_fields from clorm import ph1_ from typing import Union @@ -26,13 +27,17 @@ from typing import Union # predicates. #-------------------------------------------------------------------------- +class Status(ConstantStr, Enum): + ON="on" + OFF="off" + class Time(Predicate): time: int -class Light(ComplexTerm): - status: ConstantStr +class Light(Predicate): + status: Status -class RobotLocation(ComplexTerm, name="robotlocation"): +class RobotLocation(Predicate, name="robotlocation"): robot: ConstantStr location: ConstantStr @@ -53,7 +58,7 @@ def main(ctrl_): ctrl = Control(control_=ctrl_, unifier=[Time,Holds]) # Some initial state - initstate=[Init(Light("on")),Init(RobotLocation("roby","kitchen"))] + initstate=[Init(Light(Status.ON)),Init(RobotLocation("roby","kitchen"))] # Add the initial state and ground the ASP program and solve solution=None From 068b102b5c11e9e053c5ded006c874aa4b45e26d Mon Sep 17 00:00:00 2001 From: David Rajaratnam Date: Mon, 19 Feb 2024 19:02:07 +1100 Subject: [PATCH 5/6] Update documentation for type annotation syntax --- clorm/orm/core.py | 11 +- docs/clorm/advanced.rst | 107 ++-- docs/clorm/api.rst | 21 +- docs/clorm/embedding.rst | 66 +-- docs/clorm/experimental.rst | 34 +- docs/clorm/factbase.rst | 26 +- docs/clorm/predicate.rst | 994 +++++++++++++++++------------------- 7 files changed, 598 insertions(+), 661 deletions(-) diff --git a/clorm/orm/core.py b/clorm/orm/core.py index c925acb..3cf997a 100644 --- a/clorm/orm/core.py +++ b/clorm/orm/core.py @@ -1470,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): @@ -3349,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 # -------------------------------------------------------------------------- diff --git a/docs/clorm/advanced.rst b/docs/clorm/advanced.rst index df9c353..cc54521 100644 --- a/docs/clorm/advanced.rst +++ b/docs/clorm/advanced.rst @@ -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: @@ -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", []) @@ -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`). diff --git a/docs/clorm/api.rst b/docs/clorm/api.rst index c9d6471..18ac0a2 100644 --- a/docs/clorm/api.rst +++ b/docs/clorm/api.rst @@ -43,6 +43,7 @@ primitive logical terms: :class:`~clorm.StringField`, :class:`~clorm.ConstantFie .. autoclass:: clorm.IntegerField +.. autofunction:: clorm.field Special Fields ^^^^^^^^^^^^^^ @@ -86,28 +87,26 @@ use of these functions and instead to rely on some predefined type annotation ma .. _api_predicates: -Predicates and Complex Terms +Predicates and complex terms ---------------------------- -In logical terminology predicates and terms are both considered *non logical -symbols*; where the logical symbols are the operator symbols for *conjunction*, -*negation*, *implication*, etc. While simple terms (constants, strings, and -integers) are handled by Clorm as special cases, complex terms and predicates -are both encapsulated in the ``Predicate`` class, with ``ComplexTerm`` simply -being an alias to this class. +In logical terminology predicates and terms are both considered *non logical symbols*; where +the logical symbols are the operator symbols for *conjunction*, *negation*, *implication*, +etc. While simple terms (constants, strings, and integers) are handled by Clorm as special +cases, complex terms and predicates are both encapsulated in the ``Predicate`` class, with +``ComplexTerm`` simply being an alias to this class. .. autoclass:: clorm.Predicate - :members: + :no-members: .. attribute:: Field - A RawField sub-class corresponding to a Field for this class + A BaseField sub-class corresponding to a field definition for this class. .. attribute:: meta - Meta data (definitional information) for the predicate/complex-term. This - includes: + The meta data (definitional information) for the Predicate. It contains: .. attribute:: name diff --git a/docs/clorm/embedding.rst b/docs/clorm/embedding.rst index 0cd3dff..cf3838b 100644 --- a/docs/clorm/embedding.rst +++ b/docs/clorm/embedding.rst @@ -105,8 +105,8 @@ between string objects and dates. .. code-block:: python - @make_function_asp_callable - def date_range(start : DateField, end : DateField) -> [(IntegerField, DateField)]: + @make_function_asp_callable(DateField, DateField, [(IntegerField, DateField)]) + def date_range(start, end): inc = timedelta(days=1) tmp = [] while start < end: @@ -114,36 +114,24 @@ between string objects and dates. start += inc return list(enumerate(tmp)) -This decorator supports the specification of the type cast signature as part of -the function's **annotations** (a Python 3 feature) to provide a neater -specification. Note, the signature could equally be passed as decorator function -arguments: - -.. code-block:: python +The important point is that the type cast signature provides a mechanism to specify arbitrary +data conversions both for the input and output data; including conversions generated from very +complex terms specified as Clorm ``Predicate`` sub-classes. Consequently, the programmer does +not have to explicitly write the type conversion code and even existing functions can be +decorated to be used as callable ASP functions. - @make_function_asp_callable(DateField, DateField, [(IntegerField, DateField)]) - def date_range: - ... - -The important point is that the type cast signature provides a mechanism to -specify arbitrary data conversions both for the input and output data; including -conversions generated from very complex terms specified as Clorm ``ComplexTerm`` -sub-classes. Consequently, the programmer does not have to explicitly write the -type conversion code and even existing functions can be decorated to be used as -callable ASP functions. - -Another point to note is that the Clorm specification is also able to use the -simplified tuple syntax from the Clingo API to specify the enumerated pairs. In -fact this code can be viewed as a short-hand for an explicit declaration of a -``ComplexTerm`` tuple and internally Clorm does generate a signature equivalent -to the following: +Another point to note is that the Clorm specification is also able to use the simplified tuple +syntax from the Clingo API to specify the enumerated pairs. In fact this code can be viewed as +a short-hand for an explicit declaration of a ``ComplexTerm`` tuple and internally Clorm +generates a signature equivalent to the following: .. code-block:: python - class EnumDate(ComplexTerm): - idx = IntegerField() - dt = DateField() - class Meta: is_tuple=True + from clorm import Predicate, field + + class EnumDate(Predicate, name=""): + idx: int + dt: datetime.date = field(DateField) @make_function_asp_callable def date_range(start : DateField, end : DateField) -> [EnumDate.Field]: @@ -151,18 +139,16 @@ to the following: There are two decorator functions that Clorm provides: -* ``make_function_asp_callable``: Wraps a normal function. Every function - parameter is converted to and from the clingo equivalent. -* ``make_method_asp_callable``: Wraps a member function. The first paramater is - the object's ``self`` parameter so is passed through and only the remaining - parameters are converted to their clingo equivalents. - -In summary, the Clorm type cast signature has two distinct advantages over the -built in Clingo API for handling external functions. Firstly, it provides a -principled approach for specifying arbitrarily complex type conversions, unlike -the limited ad-hoc approach of the built-in Clingo API. Secondly, by making this -type conversion specification explicit it is clear what conversions will be -performed and therefore makes for clearer and more re-usable code. +* ``make_function_asp_callable``: Wraps a normal function. Every function parameter is + converted to and from the clingo equivalent. +* ``make_method_asp_callable``: Wraps a member function. The first paramater is the object's + ``self`` parameter so is passed through and only the remaining parameters are converted to + their clingo equivalents. + +In summary, the Clorm type cast signature provides a principled approach for specifying +arbitrarily complex type conversions. Furthermore, by making this type conversion specification +explicit it is clear what conversions will be performed and therefore makes for clearer and +more re-usable code. Specifying a Grounding Context ------------------------------ diff --git a/docs/clorm/experimental.rst b/docs/clorm/experimental.rst index 25c4338..18b7d19 100644 --- a/docs/clorm/experimental.rst +++ b/docs/clorm/experimental.rst @@ -5,9 +5,8 @@ Experimental Features .. warning:: - The following are experimental features that may change between minor library - version increments. They shouldn't be considered part of the official Clorm - API. + The following are experimental features that shouldn't be considered part of the official + Clorm API. It is not meant to be used and mostly for exploring and developing ideas. Reusable Components ------------------- @@ -49,12 +48,12 @@ construction. .. code-block:: python - from clorm import Predicate, IntegerField, StringField + from clorm import Predicate from clorm.json import FactBaseCoder class Afact(Predicate): - aint = IntegerField() - astr = StringField() + aint: int + astr: str fb_coder = FactBaseCoder([Afact]) @@ -63,29 +62,28 @@ predicates. .. code-block:: python - from clorm import Predicate, IntegerField, StringField + from clorm import Predicate from clorm.json import FactBaseCoder fb_coder = FactBaseCoder() - class Fun(ComplexTerm): - aint = IntegerField() - astr = StringField() + class Fun(Predicate): + aint: int + astr: str - class Tup(ComplexTerm): - aint = IntegerField() - astr = StringField() - class Meta: is_tuple = True + class Tup(Predicate, name=""): + aint: int + astr: str @fb_coder.register class Afact(Predicate): - aint = IntegerField() - afun = Fun.Field() + aint: int + afun: Fun @fb_coder.register class Bfact(Predicate): - astr = StringField() - atup = Tup.Field() + astr: str + atup: Tup Once the fact coder has been created and the appropriate predicates registered, it then provides ``encoder`` and ``decoder`` member functions that can be used diff --git a/docs/clorm/factbase.rst b/docs/clorm/factbase.rst index d961f14..090177b 100644 --- a/docs/clorm/factbase.rst +++ b/docs/clorm/factbase.rst @@ -19,15 +19,15 @@ a database-like query mechanism. .. code-block:: python - from clorm import Predicate, ConstantField, StringField, FactBase + from clorm import Predicate, ConstantStr, FactBase class Person(Predicate): - id = ConstantField - address = StringField + id: ConstantStr + address: str class Pet(Predicate): - owner = ConstantField - petname = StringField + owner: ConstantStr + petname: str dave = Person(id="dave", address="UNSW") morri = Person(id="morri", address="UNSW") @@ -332,11 +332,11 @@ definition earlier to something containing a tuple: .. code-block:: python - from clorm import Predicate, ConstantField, StringField, FactBase + from clorm import Predicate, ConstantStr, FactBase class PersonAlt(Predicate): - id = ConstantField - address = (StringField,StringField) + id: ConstantStr + address: tuple[str, str] dave = PersonAlt(id="dave", address=("Newcastle","UNSW")) morri = PersonAlt(id="morri", address=("Sydney","UNSW")) @@ -462,10 +462,10 @@ support querying based on the sign of a fact or complex term. .. code-block:: python - from clorm import IntegerField + from clorm import Predicate class P(Predicate): - a = IntegerField + a: int p1 = P(1) neg_p2 = P(2,sign=False) @@ -579,9 +579,11 @@ indexed and the other is not. .. code-block:: python + from clorm import Predicate + class Num(Predicate): - to_idx=IntegerField - not_to_idx=IntegerField + to_idx: int + not_to_idx: int fb4 = FactBase([Num(to_idx=n,not_to_idx=n) for n in range(0,100000)], indexes=[Num.to_idx]) diff --git a/docs/clorm/predicate.rst b/docs/clorm/predicate.rst index 63b9b9b..f195efd 100644 --- a/docs/clorm/predicate.rst +++ b/docs/clorm/predicate.rst @@ -1,17 +1,16 @@ Predicates and Fields ===================== -The heart of an ORM is defining the mapping between the predicates and Python -objects. In Clorm this is acheived by sub-classing the ``Predicate`` class and -specifying fields that map to the ASP predicate parameters. +The heart of an ORM is defining the mapping between the predicates and Python objects. In Clorm +this is acheived by sub-classing the ``Predicate`` class and specifying fields that map to the +ASP predicate parameters. The Basics ---------- -It is easiest to explain this mapping by way of a simple example. Consider the -following ground atoms for the predicates ``address/2`` and ``pets/2``. This -specifies that the address of the entity ``dave`` is ``"UNSW Sydney"`` and -``dave`` has 1 pet. +It is easiest to explain this mapping by way of a simple example. Consider the following ground +atoms for the predicates ``address/2`` and ``pets/2``. This specifies that the address of the +entity ``dave`` is ``"UNSW Sydney"`` and ``dave`` has 1 pet. .. code-block:: prolog @@ -21,139 +20,120 @@ specifies that the address of the entity ``dave`` is ``"UNSW Sydney"`` and .. note:: - A note on ASP syntax. All predicates must start with a lower-case letter and - consist of only alphanumeric characters (and underscore). ASP supports three - basic types of *terms* (i.e., the parameters of a predicate); a *constant*, a - *string*, and an *integer*. Like the predicate names, constants consist of - only alphanumeric characters (and underscore) with a starting lower-case - character. This is different to a string, which is quoted and can contain - arbitrary characters including spaces. + A note on ASP syntax. All predicates must start with a lower-case letter and consist of only + alphanumeric characters (and underscore). ASP supports three basic types of *terms* (i.e., + the parameters of a predicate); a *constant*, a *string*, and an *integer*. Like the + predicate names, constants consist of only alphanumeric characters (and underscore) with a + starting lower-case character. This is different to a string, which is quoted and can + contain arbitrary characters including spaces. - ASP syntax also supports *complex terms* (also called *functions* but we will - avoid this usage to prevent confusion with Python functions) which we will - discuss later. Note, however that ASP does not support real number values. + ASP syntax also supports *complex terms* (also called *functions* but we will avoid this + usage to prevent confusion with Python functions) which we will discuss later. Note, however + that ASP does not support real number values. To provide a mapping that satisfies the above predicate we need to sub-class the -:class:`~clorm.Predicate` class and use the :class:`~clorm.ConstantField` and -:class:`~clorm.StringField` classes. These field classes, including the -:class:`~clorm.IntegerField`, are all sub-classes of the base -:class:`~clorm.BaseField` class. +:class:`~clorm.Predicate` class and use the :class:`~clorm.ConstantStr` type specifier as well +as the standard ``int`` and ``str`` to define the individual terms. .. code-block:: python - from clorm import Predicate, ConstantField, StringField, IntegerField + from clorm import Predicate, ConstantStr, IntegerField, field class Address(Predicate): - entity = ConstantField - details = StringField + entity: ConstantStr + details: str - class Pets(Predicate): - entity = ConstantField - num = IntegerField(default=0) + class Pet(Predicate): + entity: ConstantStr + num: int = field(IntegerField, default=0) -Typically, when instantiating a predicate all field values must be provided. The -exception is when the field has been defined with a default value, such as with -the above definition for the ``num`` field of the ``Pets``. So, with the above -class definitions we can instantiate some objects: +The type annotations specify how the fields are to be translated to Clingo. The ``entity`` +fields map Python strings to ASP constants, while the ``Pet``'s ``details`` field maps Python +strings to ASP strings. In constrast the ``Pet``'s ``num`` field overrides its default ``int`` +mapping by using the :py:func:`~clorm.field` function to explicitly provide the field mapping +as well as a default value. So, with the above class definitions we can instantiate some +objects: .. code-block:: python fact1 = Address(entity="bob", details="Sydney uni") - fact2 = Pets(entity="bob") - fact3 = Pets(entity="bill", num=2) + fact2 = Pet(entity="bob") + fact3 = Pet(entity="bill", num=2) These object correspond to the following ASP *ground atoms* (i.e., facts): .. code-block:: prolog - address(bob, "Sydney uni"). - pets(bob, 0). - pets(bill, 2). + address(bob,"Sydney uni"). + pet(bob,0). + pet(bill,2). There are some things to note here: -* *Predicate names*: ASP uses standard logic-programming syntax, which requires - that the names of all predicate/complex-terms must begin with a lower-case - letter and can contain only alphanumeric characters or underscore. Unless - overriden, Clorm will automatically generate a predicate name for a - :class:`~clorm.Predicate` sub-class by transforming the class name based on - some simple rules: +* *Predicate names*: ASP uses standard logic-programming syntax, which requires that the names + of all predicate/complex-terms must begin with a lower-case letter and can contain only + alphanumeric characters or underscore. Unless overriden, Clorm will automatically generate a + predicate name for a :class:`~clorm.Predicate` sub-class by transforming the class name based + on some simple rules: - * If the first letter is a lower-case character then this is a valid predicate - name so the name is left unchanged (e.g., ``myPredicate`` => - ``myPredicate``). + * If the first letter is a lower-case character then this is a valid predicate name so the + name is left unchanged (e.g., ``myPredicate`` => ``myPredicate``). - * Otherwise, replace any sequence of upper-case only characters that occur at - the beginning of the string or immediately after an underscore with - lower-case equivalents. The sequence of upper-case characters can include - non-alphabetic characters (eg., numbers) and this will still be treated as a - single sequence of upper-case characters. + * Otherwise, replace any sequence of upper-case only characters that occur at the beginning + of the string or immediately after an underscore with lower-case equivalents. The sequence + of upper-case characters can include non-alphabetic characters (eg., numbers) and this will + still be treated as a single sequence of upper-case characters. * The above criteria covers a number of common naming conventions: - * Snake-case: ``My_Predicate`` => ``my_predicate``, ``MY_Predicate`` => - ``my_predicate``, ``My_Predicate_1A`` => ``my_predicate_1a``, + * Snake-case: ``My_Predicate`` => ``my_predicate``, ``MY_Predicate`` => ``my_predicate``, + ``My_Predicate_1A`` => ``my_predicate_1a``, - * Camel-case: ``MyPredicate`` => ``myPredicate``, ``MyPredicate1A`` => - ``myPredicate1A``. + * Camel-case: ``MyPredicate`` => ``myPredicate``, ``MyPredicate1A`` => ``myPredicate1A``. * Acronym: ``TCP1`` => ``tcp1``. -* *Field order*: the order of declared term defintions in the predicate - class is important. +* *Field order*: the order of declared term defintions in the predicate class is important. -* *Field names*: besides the Python keywords, Clorm also disallows the following - reserved words: ``raw``, ``meta``, ``clone``, ``Field`` as these are used as - properties or functions of a :class:`~clorm.Predicate` object. +* *Field names*: besides the Python keywords, Clorm also disallows the following reserved + words: ``raw``, ``meta``, ``clone``, ``Field`` as these are used as properties or functions + of a :class:`~clorm.Predicate` object. -* *Constant vs string*: In the above example ``"bob"`` and ``"Sydney uni"`` are - both Python strings but because of the ``entity`` field is declared as a - :class:`~clorm.ConstantField` this ensures that the Python string ``"bob"`` is - treated as an ASP constant. Note, currently it is the users' responsibility to - ensure that the Python string passed to a constant term satisfies the - syntactic restriction. +* *Constant vs string*: In the above example ``"bob"`` and ``"Sydney uni"`` are both Python + strings but because of the ``entity`` field is declared as a :class:`~clorm.ConstantStr` (or + the explicit :class:`~clorm.ConstantField` specifier) this ensures that the Python string + ``"bob"`` is treated as an ASP constant. Note, currently it is the users' responsibility to + ensure that the Python string passed to a constant term satisfies the syntactic restriction. -* The use of a *default value*: all term types support the specification of a - default value. +* The use of a *default value*: all term types support the specification of a default value. -* If the specified default is a function then this function will be called (with - no arguments) when the predicate/complex-term object is instantiated. This can - be used to generated unique ids or a date/time stamp. +* If the specified default is a function then this function will be called (with no arguments) + when the predicate/complex-term object is instantiated. This can be used to generated unique + ids or a date/time stamp. Overriding the Predicate Name ----------------------------- -As mentioned above, by default the predicate name is calculated from the -corresponding class name by transforming the class name to match a number of -common naming conventions. However, it is also possible to over-ride the default -predicate name with an explicit name. +As mentioned above, by default the predicate name is calculated from the corresponding class +name by transforming the class name to match a number of common naming conventions. However, it +is also possible to override the default predicate name with an explicit name. -There are many reasons why you might not want to use the default predicate name -mapping. For example, the Python class name that would produce the desired -predicate name may already be taken. Alternatively, you might want to -distinguish between predicates with the same name but different arities. Note: -having predicates with the same name and different arities is a legitimate and -common practice with ASP programming. - -Overriding the default predicate name requires declaring a ``Meta`` sub-class -for the predicate definition. +There are many reasons why you might not want to use the default predicate name mapping. For +example, the Python class name that would produce the desired predicate name may already be +taken. Alternatively, you might want to distinguish between predicates with the same name but +different arities. Note: having predicates with the same name and different arities is a +legitimate and common practice with ASP programming. .. code-block:: python - class Address2(Predicate): - entity = ConstantField - details = StringField - - class Meta: - name = "address" - - class Address3(Predicate): - entity = ConstantField - details = StringField - country = StringField + class Address2(Predicate, name="address"): + entity: ConstantStr + details: str - class Meta: - name = "address" + class Address3(Predicate, name="address"): + entity: ConstantStr + details: str + country: str Instantiating these classes: @@ -172,9 +152,8 @@ will produce the following matching ASP facts: Nullary Predicates ------------------ -A nullary predicate is a predicate with no parameters and is also a legitimate and -reasonable thing to see in an ASP program. Defining a corresponding Python class -is straightforward: +A nullary predicate is a predicate with no parameters and is also a legitimate and reasonable +thing to see in an ASP program. Defining a corresponding Python class is straightforward: .. code-block:: python @@ -183,8 +162,8 @@ is straightforward: fact = ANullary() -The important thing to note here is that every instantiation of ``ANullary`` -will correspond to the same ASP fact: +The important thing to note here is that every instantiation of ``ANullary`` will correspond to +the same ASP fact: .. code-block:: prolog @@ -193,97 +172,96 @@ will correspond to the same ASP fact: Complex Terms ------------- -So far we have shown how to create Python definitions that match predicates with -simple terms. However, in ASP it is common to also use complex terms within a -predicate, such as: +So far we have shown how to create Python definitions that match predicates with simple +terms. However, in ASP it is common to also use complex terms within a predicate, such as: .. code-block:: prolog booking("2018-12-31", location("Sydney", "Australia")). -The Clorm :class:`~clorm.Predicate` class definition is able to support the -flexiblity required to deal with complex terms. A :class:`~clorm.ComplexTerm` -class is introduced simply as an alias for the :class:`~clorm.Predicate` class. +The Clorm :class:`~clorm.Predicate` class definition is able to support the flexiblity required +to deal with complex terms. .. code-block:: python - from clorm import ComplexTerm + from clorm import Predicate - class Location(ComplexTerm): - city = StringField - country = StringField + class Location(Predicate): + city: str + country: str -The definition for a complex term can be included within a new -:class:`~clorm.Predicate` definition by using the -:py:meth:`Field` property of the -:class:`~clorm.ComplexTerm` sub-class. + class Booking(Predicate): + date: str + location: Location -.. code-block:: python - class Booking(Predicate): - date=StringField - location=Location.Field +.. note:: + + There is also a :class:`~clorm.ComplexTerm` class which is an alias for the + :class:`~clorm.Predicate` class. For personal stylistic reasons you may prefer to use this + alias to define classes that will only be used as complex terms. However there are cases + where this separation breaks down. For example when dealing with the *reification* of facts + there is nothing to be gained by providing two definitions for the predicate and complex + term versions of the same non logical term: + + .. code-block:: prolog + + p(q(1)). + q(1) :- p(q(1)). + + In this example ``q/1`` is both a complex term and predicate and when providing the Python + Clorm mapping it is simpler not to separate the two versions: + + .. code-block:: python + + class Q(Predicate): + a: int + + class P(Predicate): + a: Q -The :py:meth:`Location.Field` property returns a -:class:`~clorm.BaseField` sub-class that is generated automatically when -:class:`~clorm.Predicate` (or :class:`~clorm.ComplexTerm`) is sub-classed. It -provides the functions to automatically convert to, and from, the Predicate -sub-class instances and the Clingo symbol objects. -The predicate class containing complex terms can be instantiated in the obvious -way: +The predicate class containing complex terms can be instantiated in the obvious way: .. code-block:: python bk=Booking(date="2018-12-31", location=Location(city="Sydney",country="Australia")) -Note: as with the field definition for simple terms it is possible to specify a -complex field definition with ``default`` or ``index`` parameters. For example, -the above ``Booking`` class could be replaced with: + +As with the primitive terms it is possible to override the translation of complex terms, for +example to provide defaults, by using the :py:func:`~clorm.field` function. While the first +parameter of the function must be a sub-class of :class:`~clorm.BaseField`, fortunately, every +predicate sub-class has a corresponding, internally generated, :class:`~clorm.BaseField` +sub-class which can be accessed though the :py:attr:`Field` property of +that predicate class. So for example we can modify the ``Booking`` class definition to provide +a default location. .. code-block:: python class Booking(Predicate): - date=StringField - location=Location.Field(index=True, - default=LocationTuple(city="Sydney", country="Australia")) + date: str + location: Location = field(Location.Field, default=Location("Potsdam", "Germany") + bk2=Booking(date="2019-12-14") -Note: From a code readability and conceptual stand point it may be convenient to -treat predicates and complex terms as separate classes, however there are cases -where this separation breaks down. For example when dealing with the -*reification* of facts there is nothing to be gained by providing two -definitions for the predicate and complex term versions of the same non logical -term: +This second booking instance will correspond to the fact: .. code-block:: prolog - p(q(1)). - q(1) :- p(q(1)). - -In this example ``q/1`` is both a complex term and predicate and when providing -the Python Clorm mapping it is simpler not to separate the two versions: - -.. code-block:: python - - class Q(Predicate): - a = IntegerField + booking("2019-12-14", location("Potsdam", "Germany")). - class P(Predicate): - a = Q.Field Negative Facts -------------- -ASP follows standard logic programming syntax and treats the ``not`` keyword as -**default negation** (also **negation as failure**). Using default negation is -important to ASP programming as it can lead to more readable and compact -modelling of a problem. +ASP follows standard logic programming syntax and treats the ``not`` keyword as **default +negation** (also **negation as failure**). Using default negation is important to ASP +programming as it can lead to more readable and compact modelling of a problem. -However, there may be times when having an explicit notion of negation is also -useful, and ASP/Clingo does have support for **classical negation**; indicated -syntactically using the ``-`` symbol: +However, there may be times when having an explicit notion of negation is also useful, and +ASP/Clingo does have support for **classical negation**; indicated syntactically using the +``-`` symbol: .. code-block:: prolog @@ -291,11 +269,10 @@ syntactically using the ``-`` symbol: -b(N) :- a(N). -a(N) :- b(N). -The above program chooses amongst the ``a/1`` and ``b/1`` predicates, then for -every positive ``a/1`` fact, the corresponding ``b/1`` fact is negated and -vice-versa. This will generate nine stable models. For example, if ``a(2)`` and -``b(1)`` are chosen, then the corresponding negative literals will be ``-b(2)`` -and ``-a(1)`` respectively. +The above program chooses amongst the ``a/1`` and ``b/1`` predicates, then for every positive +``a/1`` fact, the corresponding ``b/1`` fact is negated and vice-versa. This will generate nine +stable models. For example, if ``a(2)`` and ``b(1)`` are chosen, then the corresponding +negative literals will be ``-b(2)`` and ``-a(1)`` respectively. Note: Clingo supports negated literals as well as terms. However, tuples cannot be negated. @@ -304,45 +281,38 @@ Note: Clingo supports negated literals as well as terms. However, tuples cannot f(-g(a)). % This is valid f(-(a,b)). % Error!!! -Clorm supports negation for any fact or term that can be negated by -Clingo. Specifying a negative literal simply involves setting ``sign=False`` -when instantiating the Predicate (or ComplexTerm). Note: unlike the field -parameters, the ``sign`` parameter must be specified as a named parameter and -cannot be specified using positional arguments. +Clorm supports negation for any fact or term that can be negated by Clingo. Specifying a +negative literal simply involves setting ``sign=False`` when instantiating the Predicate (or +ComplexTerm). Note: unlike the field parameters, the ``sign`` parameter must be specified as a +named parameter and cannot be specified using positional arguments. .. code-block:: python class P(Predicate): - a = IntegerField + a: int neg_p1 = P(a=1,sign=False) neg_p1_alt = P(1,sign=False) assert neg_p1 == neg_p1_alt -Once instantiated, checking whether a fact (or a complex term) is negated can be -determined using the ``sign`` attribute of Predicate instance. +Once instantiated, checking whether a fact (or a complex term) is negated can be determined +using the ``sign`` attribute of Predicate instance. .. code-block:: python assert neg_p1.sign == False -Finally, for finer control of the unification process, a Predicate/ComplexTerm -can be specified to only unify with either positive or negative facts/terms by -setting a ``sign`` meta attribute declaration. +Finally, for finer control of the unification process, a Predicate/ComplexTerm can be specified +to only unify with either positive or negative facts/terms by setting a ``sign`` meta attribute +declaration. .. code-block:: python - class P_pos(Predicate): - a = IntegerField - class Meta: - sign = True - name = "p" + class P_pos(Predicate, name="p", sign=True): + a: int - class P_neg(Predicate): - a = IntegerField - class Meta: - sign = False - name = "p" + class P_neg(Predicate, name="p", sign=False): + a: int % Instatiating facts pos_p = P_pos(1) % Ok @@ -360,101 +330,73 @@ setting a ``sign`` meta attribute declaration. Field Definitions ----------------- -Clorm provides a number of standard definitions that specify the mapping between -Clingo's internal representation (some form of ``Clingo.Symbol``) to more -natural Python representations. ASP has three *simple terms*: *integer*, -*string*, and *constant*, and Clorm provides three standard definition classes -to provide a mapping to these fields: :class:`~clorm.IntegerField`, -:class:`~clorm.StringField`, and :class:`~clorm.ConstantField`. +Clorm provides a number of standard definitions that specify the mapping between Clingo's +internal representation (some form of ``Clingo.Symbol``) to more natural Python +representations. ASP has three *simple terms*: *integer*, *string*, and *constant*, and Clorm +provides three standard definition classes to provide a mapping to these fields: +:class:`~clorm.IntegerField`, :class:`~clorm.StringField`, and :class:`~clorm.ConstantField`. -Clorm also provides a :class:`~clorm.SimpleField` class that can match to any -simple term. This is useful when the parameter of a defined predicate can -contain arbitrary simple term types. Clorm takes care of converting the ASP -string, constant or integer to a Python string or integer object. Note that both -ASP strings and constants are both converted to Python string objects. +Clorm also provides a :class:`~clorm.SimpleField` class that can match to any simple term. This +is useful when the parameter of a defined predicate can contain arbitrary simple term +types. Clorm takes care of converting the ASP string, constant or integer to a Python string or +integer object. Note that both ASP strings and constants are both converted to Python string +objects. In order to convert from a Python string object to an ASP string or constant, -:class:`~clorm.SimpleField` uses a regular expression to determine if the string -matches the pattern of a constant and treats it accordingly. For this reason -:class:`~clorm.SimpleField` should be used with care in order to ensure expected -behaviour, and using the distinct field types is often preferable. +:class:`~clorm.SimpleField` uses a regular expression to determine if the string matches the +pattern of a constant and treats it accordingly. For this reason :class:`~clorm.SimpleField` +should be used with care in order to ensure expected behaviour, and using the distinct field +types is often preferable. -.. note:: - - It is worth highlighting that in the above predicate declarations, the field - classes do not represent instances of the actual fields. For example, the - date string "2018-12-31" is not stored in a :class:`~clorm.StringField` - object. Rather the field classes provide the implementation of the functions - that perform the necessary data conversions. Instantiating a field class in a - predicate definition is only necessary to allow options to be specified, such - as default values or indexing. - -Simple Term Definition Options -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -There are currently two options when specifying the Python fields for a -predicate. We have already seen the ``default`` option, but there is also the -``index`` option. - -Specifying ``index = True`` can affect the behaviour when a -:class:`~clorm.FactBase` container objects are created. While the -:class:`~clorm.FactBase` class will be discussed in greater detail in the next -chapter, here we simply note that it is a convenience container for storing sets -of facts. They can be thought of as mini-databases and have some indexing -support for improved query performance. Sub-classing Field Definitions ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -All field classes inherit from a base class :class:`~clorm.BaseField` and it's -possible to define arbitrary data conversions by sub-classing -:class:`~clorm.BaseField`. Clorm provides the standard sub-classes -:class:`~clorm.StringField`, :class:`~clorm.ConstantField`, and -:class:`~clorm.IntegerField`. Clorm also automatically generates an appropriate -sub-class for every :class:`~clorm.ComplexTerm` definition. - -However, it is sometimes also useful to explicitly sub-class the -:class:`~clorm.BaseField` class, or sub-class one of its sub-classes. By -sub-classing a sub-class it is possible to form a *data conversion chain*. To -understand why this is useful we consider an example of specifying a date field. - -Consider the example of an application that needs a date term for an event -tracking application. From the Python code perspective it would be natural to -use Python ``datetime.date`` objects. However, it then becomes a question of how -to encode these Python date objects in ASP (noting that ASP only has three -simple term types). - -A useful encoding would be to encode a date as a string in **YYYYMMDD** format -(or **YYYY-MM-DD** for greater readability). Dates encoded in this format -satisfy some useful properties such as the comparison operators will produce the -expected results (e.g., ``"20180101" < "20180204"``). A string is also -preferable to using a similiarly encoded integer value. For example, encoding -the date in the same way as an integer would allow incrementing or subtracting a -date encoded number, which could lead to unwanted values (e.g., ``20180131 + 1 = -20180132`` does not correspond to a valid date). - -So, adopting a date encoded string we can consider a date based fact for the -booking application that simply encodes that there is a New Year's eve party on -the 31st December 2018. +All field classes inherit from a base class :class:`~clorm.BaseField` and it's possible to +define arbitrary data conversions by sub-classing :class:`~clorm.BaseField`. Clorm provides the +standard sub-classes :class:`~clorm.StringField`, :class:`~clorm.ConstantField`, and +:class:`~clorm.IntegerField`. Clorm also automatically generates an appropriate sub-class for +every :class:`~clorm.Predicate` definition for use in a complex term. + +However, it is sometimes also useful to explicitly sub-class the :class:`~clorm.BaseField` +class, or sub-class one of its sub-classes. By sub-classing a sub-class it is possible to form +a *data conversion chain*. To understand why this is useful we consider an example of +specifying a date field. + +Consider the example of an application that needs a date term for an event tracking +application. From the Python code perspective it would be natural to use Python +``datetime.date`` objects. However, it then becomes a question of how to encode these Python +date objects in ASP (noting that ASP only has three simple term types). + +A useful encoding would be to encode a date as a string in **YYYYMMDD** format (or +**YYYY-MM-DD** for greater readability). Dates encoded in this format satisfy some useful +properties such as the comparison operators will produce the expected results (e.g., +``"20180101" < "20180204"``). A string is also preferable to using a similiarly encoded integer +value. For example, encoding the date in the same way as an integer would allow incrementing +or subtracting a date encoded number, which could lead to unwanted values (e.g., ``20180131 + 1 += 20180132`` does not correspond to a valid date). + +So, adopting a date encoded string we can consider a date based fact for the booking +application that simply encodes that there is a New Year's eve party on the 31st December 2018. .. code-block:: prolog booking("2018-12-31", "NYE party"). -Using Clorm this fact can be captured by the following Python -:class:`~clorm.Predicate` sub-class definition: +Using Clorm this fact can be captured by the following Python :class:`~clorm.Predicate` +sub-class definition: .. code-block:: python from clorm import * class Booking(Predicate): - date = StringField - description = StringField + date: str + description: str -However, since we encoded the date as simply a :class:`~clorm.StringField` it is -now up to the user of the ``Booking`` class to perform the necessary -translations to and from a Python ``datetime.date`` objects when necessary. For +However, since we encoded the date as simply a ``str`` (which internally maps to +:class:`~clorm.StringField`) it is now up to the user of the ``Booking`` class to perform the +necessary translations to and from a Python ``datetime.date`` objects when necessary. For example: .. code-block:: python @@ -463,27 +405,24 @@ example: nye = datetime.date(2018, 12, 31) nyeparty = Booking(date=int(nye.strftime("%Y-%m-%d")), description="NYE Party") -Here the Python ``nyeparty`` variable corresponds to the encoded ASP event, with -the ``date`` term capturing the string encoding of the date. - -In the opposite direction to extract the date it is necessary to turn the date -encoded string into an actual ``datetime.date`` object: +Here the Python ``nyeparty`` variable corresponds to the encoded ASP event, with the ``date`` +term capturing the string encoding of the date. In the opposite direction to extract the date +it is necessary to turn the date encoded string into an actual ``datetime.date`` object: .. code-block:: python nyedate = datetime.datetime.strptime(str(nyepart.date), "%Y-%m-%d") -The problem with the above code is that the process of creating and using the -date in the ``Booking`` object is cumbersome and error-prone. You have to -remember to make the correct translation both in creating and reading the -date. Furthermore the places in the code where these translations are made may -be far apart, leading to potential problems when code needs to be refactored. - -The solution to this problem is to create a sub-class of -:class:`~clorm.BaseField` that performs the appropriate data conversion. However, -sub-classing :class:`~clorm.Basefield` directly requires dealing with raw Clingo -``Symbol`` objects. A better alternative is to sub-class the -:class:`~clorm.StringField` class so you need to only deal with the string to +The problem with the above code is that the process of creating and using the date in the +``Booking`` object is cumbersome and error-prone. You have to remember to make the correct +translation both in creating and reading the date. Furthermore the places in the code where +these translations are made may be far apart, leading to potential problems when code needs to +be refactored. + +The solution to this problem is to create a sub-class of :class:`~clorm.BaseField` that +performs the appropriate data conversion. However, sub-classing :class:`~clorm.Basefield` +directly requires dealing with raw Clingo ``Symbol`` objects. A better alternative is to +sub-class the :class:`~clorm.StringField` class so you only need to deal with the string to date conversion. .. code-block:: python @@ -496,20 +435,18 @@ date conversion. cltopy = lambda s: datetime.datetime.strptime(s,"%Y-%m-%d").date() class Booking(Predicate): - date=DateField - description = StringField + date: datetime.date = field(DateField) + description: StringField -The ``pytocl`` definition specifies the conversion that takes place in the -direction of converting Python data to Clingo data, and ``cltopy`` handles the -opposite direction. Because the :class:`~clorm.DateField` inherits from -:class:`~clorm.StringField` therefore the ``pytocl`` function must output a -Python string object. In the opposite direction, ``cltopy`` must be passed a -Python string object and performs the desired conversion, in this case producing -a ``datetime.date`` object. +The ``pytocl`` definition specifies the conversion that takes place in the direction of +converting Python data to Clingo data, and ``cltopy`` handles the opposite direction. Because +the :class:`~clorm.DateField` inherits from :class:`~clorm.StringField` therefore the +``pytocl`` function must output a Python string object. In the opposite direction, ``cltopy`` +must be passed a Python string object and performs the desired conversion, in this case +producing a ``datetime.date`` object. -With the newly defined ``DateField`` the conversion functions are all captured -within the one class definition and interacting with the objects can be done in -a more natural manner. +With the newly defined ``DateField`` the conversion functions are all captured within the one +class definition and interacting with the objects can be done in a more natural manner. .. code-block:: python @@ -527,22 +464,20 @@ will print the expected output: .. note:: - The ``pytocl`` and ``cltopy`` functions can potentially be passed bad - input. For example when converting a clingo String symbol to a date object - the passed string may not correspond to an actual date. In such cases these - functions can legitamately throw either a ``TypeError`` or a ``ValueError`` - exception. These exceptions will be treated as a failure to unify when trying - to unify clingo symbols to facts. However, any other exception is passed - through as a genuine error. This should be kept in mind if you are writing - your own field class. + The ``pytocl`` and ``cltopy`` functions can potentially be passed bad input. For example, + when converting a clingo String symbol to a date object the passed string may not correspond + to an actual date. In such cases these functions can legitimately throw either a + ``TypeError`` or a ``ValueError`` exception. Internally, Clorm's framework will catch these + two types of exceptions and will treat them as failures to unify when trying to unify clingo + symbols to facts. Any other exception is passed through as a genuine error. This should be + kept in mind if you are writing your own field class. Restricted Sub-class of a Field Definition ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Another reason to sub-class a field definition is to restrict the set of values -that the field can hold. For example you could have an application where an -argument of a predicate is restricted to a specific set of constants, such as -the days of the week. +Another reason to sub-class a field definition is to restrict the set of values that the field +can hold. For example you could have an application where an argument of a predicate is +restricted to a specific set of constants, such as the days of the week. .. code-block:: prolog @@ -560,29 +495,26 @@ When defining a predicate corresponding to cooking/2 it is possible to simply us person = StringField class Meta: name = "cooking" -However, this would potentiallly allow for creating erroneous instances that -don't correspond to actual days of the week (for example, with a spelling -mistake): +However, this would potentiallly allow for creating erroneous instances that don't correspond +to actual days of the week (for example, with a spelling mistake): .. code-block:: python ck = Cooking1(dow="mnday",person="Bob") -In order to avoid these errors it is necessary to subclass the -:class:`~clorm.ConstantField` in order to restrict the set of values to the -desired set. Clorm provides a helper function :py:func:`~clorm.refine_field` for -this use-case. It dynamically defines a new class that restricts the values of -an existing field class. +In order to avoid these errors it is necessary to subclass the :class:`~clorm.ConstantField` in +order to restrict the set of values to the desired set. Clorm provides a helper function +:py:func:`~clorm.refine_field` for this use-case. It dynamically defines a new class that +restricts the values of an existing field class. .. code-block:: python DowField = refine_field(ConstantField, ["sunday","monday","tuesday","wednesday","thursday","friday","saturday"]) - class Cooking2(Predicate): - dow = DowField - person = StringField - class Meta: name = "cooking" + class Cooking2(Predicate, name="cooking"): + dow: ConstantStr = field(DowField) + person:str ok=Cooking2(dow="monday",person="Bob") @@ -593,30 +525,29 @@ an existing field class. .. note:: - The :py:func:`~clorm.refine_field` function can also be called with only two - arguments, rather than three, by ignoring the name for the generated - class. In this case an anonymously generated name will be used. + The :py:func:`~clorm.refine_field` function can also be called with only two arguments, + rather than three, by ignoring the name for the generated class. In this case an anonymously + generated name will be used. -As well as explictly specifying the set of refinement values, -:py:func:`~clorm.refine_field` also provides a more general approach where a -function/functor/lambda can be provided. This function must take a single input -and return ``True`` if that value is valid for the field. For example, to define -a field that accepts only positive integers: +As well as explictly specifying the set of refinement values, :py:func:`~clorm.refine_field` +also provides a more general approach where a function/functor/lambda can be provided. This +function must take a single input and return ``True`` if that value is valid for the field. For +example, to define a field that accepts only positive integers: .. code-block:: python PosIntField = refine_field(NumberField, lambda x : x >= 0) -An alternative to using :py:func:`~clorm.refine_field` to restrict the allowable -values is to an explicitly specified set is to use -:py:func:`~clorm.define_enum_field`. This function allows Clorm to be used with -standard Python Enum classes. So, the day-of-week example could be rewritten to -use an Enum class: +An alternative to using :py:func:`~clorm.refine_field` to restrict the allowable values is to +an explicitly specified set is to use :py:func:`~clorm.define_enum_field`. This function allows +Clorm to be used with standard Python Enum classes. So, the day-of-week example could be +rewritten to use an Enum class: .. code-block:: python import enum - class DOW(str,enum.Enum): + + class DOW(ConstantStr, enum.Enum): SUNDAY="sunday" MONDAY="monday" TUESDAY="tuesday" @@ -625,53 +556,51 @@ use an Enum class: FRIDAY="friday" SATURDAY="saturday" - class Cooking3(Predicate): - dow = define_enum_field("DowField",ConstantField,DOW) - person = StringField - class Meta: name = "cooking" + class Cooking3(Predicate, name="cooking"): + dow: DOW + person: str ok = Cooking3(dow=DOW.MONDAY,person="Bob") -Finally, it should be highlighted that this mechanism for defining a field -restriction works not just for validating the inputs into an ASP program. It can -also be used to filter the outputs of the ASP solver as the invalid field values -will not *unify* with the predicate. +One useful advantage of using an enumeration is Clorm has built in handling to allow it to be +specified as a type annotation. This means that you do not have to explicitly call the +:py:func:`~clorm.define_enum_field` function to generate the appropriate field definition. + +Finally, it should be highlighted that this mechanism for defining a field restriction works +not just for validating the inputs into an ASP program. It can also be used to filter the +outputs of the ASP solver as the invalid field values will not *unify* with the predicate. -For example, in the above program you can separate the cooks on the weekend -from the weekday cooks. +For example, in the above program you can separate the cooks on the weekend from the weekday +cooks. .. code-block:: python - WeekendField = refine_field(ConstantField, - ["sunday","saturday"]) - WeekdayField = refine_field(ConstantField, - ["monday","tuesday","wednesday","thursday","friday"]) + WeekendField = refine_field(ConstantField, ["sunday","saturday"]) + WeekdayField = refine_field(ConstantField, ["monday","tuesday","wednesday","thursday","friday"]) - class WeekendCooking(Predicate): - dow = WeekendField - person = StringField - class Meta: name = "cooking" + class WeekendCooking(Predicate, name="cooking"): + dow: str = field(WeekendField) + person: str - class WeekdayCooking(Predicate): - dow = WeekdayField - person = StringField - class Meta: name = "cooking" + class WeekdayCooking(Predicate, name="cooking"): + dow: str = field(WeekdayField) + person: str Using Positional Arguments -------------------------- -So far we have shown how to create Clorm predicate and complex term instances -using keyword arguments that match their defined field names, as well as -accessing the arguments via the fields as named properties. For example: +So far we have shown how to create Clorm predicate and complex term instances using keyword +arguments that match their defined field names, as well as accessing the arguments via the +fields as named properties. For example: .. code-block:: python from clorm import * class Contact(Predicate): - cid=IntegerField - name=StringField + cid: int + name: str c1 = Contact(cid=1, name="Bob") @@ -689,77 +618,81 @@ positional arguments: assert c2[0] == 2 assert c2[1] == "Bill" -While Clorm does support the use of positional arguments for predicates, -nevertheless it should be used sparingly because it can lead to brittle code -that can be hard to debug, and can also be more difficult to refactor as the ASP -program changes. However, there are genuine use-cases where it can be convenient -to use positional arguments. In particular when defining very simple tuples, -where the position of arguments is unlikely to change as the ASP program -changes. We discuss Clorm's support for these cases in the following section. +While Clorm does support the use of positional arguments for predicates, nevertheless it should +be used sparingly because it can lead to brittle code that can be hard to debug, and can also +be more difficult to refactor as the ASP program changes. However, there are genuine use-cases +where it can be convenient to use positional arguments. In particular when defining very simple +tuples, where the position of arguments is unlikely to change as the ASP program changes. We +discuss Clorm's support for these cases in the following section. Working with Tuples ------------------- -Tuples are a special case of complex terms that often appear in ASP -programs. For example: +Tuples are a special case of complex terms that often appear in ASP programs. For example: .. code-block:: none booking("2018-12-31", ("Sydney", "Australia)). -For Clorm tuples are simply a :class:`~clorm.ComplexTerm` sub-class where the -name of the corresponding predicate is empty. While this can be set using an -``is_tuple`` property of the complex term's meta class, Clorm also provides -specialised support using the more intuitive syntax of a Python tuple. For -example, a predicate definition that unifies with the above fact can be defined -simply (using the ``DateField`` defined earlier): +For Clorm tuples are simply a :class:`~clorm.Predicate` sub-class where the name of the +corresponding predicate is empty. While this can be set using an ``is_tuple`` property of the +complex term's class, Clorm also provides specialised support using the more intuitive syntax +of a Python tuple type annotations. For example, a predicate definition that unifies with the +above fact can be defined simply (using the ``DateField`` defined earlier): .. code-block:: python class Booking(Predicate): - date=DateField - location=(StringField,StringField) + date: datetime.date = field(DateField) + location: tuple[str, str] + +.. note:: + + For Python versions earlier than 3.9 you need to specify the tuple type using the ``Tuple`` + identifier from the ``typing`` module: + + .. code-block:: python + + from typing import Tuple + + class Booking(Predicate): + date: datetime.date = field(DateField) + location: Tuple[str, str] -Here the ``location`` field is defined as a pair of string fields, without -having to explictly define a separate :class:`~clorm.ComplexTerm` sub-class that -corresponds to this pair. To instantiate the ``Booking`` class a Python tuple -can also be used for the values of ``location`` field. For example, the -following creates a ``Boooking`` instance corresponding to the ``booking/2`` -fact above: + +Here the ``location`` field is defined as a pair of strings, without having to explictly define +a separate :class:`~clorm.ComplexTerm` sub-class that corresponds to this pair. To instantiate +the ``Booking`` class a Python tuple can also be used for the values of ``location`` field. For +example, the following creates a ``Boooking`` instance corresponding to the ``booking/2`` fact +above: .. code-block:: python bk = Booking(date=datetime.date(2018,12,31), location=("Sydney","Australia")) -While it is unnecessary to define a seperate :class:`~clorm.ComplexTerm` -sub-class corresponding to the tuple, internally this is in fact exactly what -Clorm does. Clorm will transform the above definition into something similar to -the following: +While it is unnecessary to define a seperate :class:`~clorm.Predicate` sub-class corresponding +to the tuple, internally this is in fact exactly what Clorm does. Clorm will transform the +above definition into something similar to the following: .. code-block:: python - class SomeAnonymousName(ComplexTerm): - city = StringField - country = StringField - class Meta: - is_tuple = True + class SomeAnonymousName(Predicate, name=""): + city: str + country: str class Booking(Predicate): - date=DateField - location=SomeAnonymousName.Field + date: datetime.date = field(DateField) + location: tuple[str, str] = field(SomeAnonymousName.Field) -Here the :class:`~clorm.ComplexTerm` has an internal ``Meta`` class with the -property ``is_tuple`` set to ``True``. This means that the -:class:`~clorm.ComplexTerm` will be treated as a tuple rather than a complex -term with a function name. +Here the :class:`~clorm.Predicate` has an empty name, so it will be treated as a tuple rather +than a complex term with a function name. -One important difference between the implicitly defined and explicitly defined -versions of a tuple is that the explicit version allows for field names to be -given, while the implicit version will have automatically generated -names. However, for simple implicitly defined tuples it would be more common to -use positional arguments anyway, so in many cases it can be the preferred -alternative. For example: +One important difference between the implicitly defined and explicitly defined versions of a +tuple is that the explicit version allows for field names to be given, while the implicit +version will have automatically generated names. However, for simple implicitly defined tuples +it would be more common to use positional arguments anyway, so in many cases it can be the +preferred alternative. For example: .. code-block:: python @@ -769,35 +702,32 @@ alternative. For example: .. note:: - As mentioned previously, using positional arguments is something that should - be used sparingly as it can lead to brittle code that is more difficult to - refactor. It should mainly be used for cases where the ordering of the fields - in the tuple is unlikely to change when the ASP program is refactored. + As mentioned previously, using positional arguments is something that should be used + sparingly as it can lead to brittle code that is more difficult to refactor. It should + mainly be used for cases where the ordering of the fields in the tuple is unlikely to change + when the ASP program is refactored. Debugging Auxiliary Predicates ------------------------------ -When integrating an ASP program into a Python based application there will be a -set of predicates that are important for inputting a problem instance and -outputting a solution. Clorm is intended to provide a clean way of interacting -with these predicates. +When integrating an ASP program into a Python based application there will be a set of +predicates that are important for inputting a problem instance and outputting a solution. Clorm +is intended to provide a clean way of interacting with these predicates. -However, there will typically be other auxiliary predicates that are used as -part of the problem formalisation. While they may not be important from the -Python application point of view they do become important during the process of -developing and debugging the ASP program. During this process it can be -cumbersome to build a detailed Clorm predicate definition for each one of these, -especially when all you need to do is print the predicate instances to the -screen, possibly sorted in some order. +However, there will typically be other auxiliary predicates that are used as part of the +problem formalisation. While they may not be important from the Python application point of +view they do become important during the process of developing and debugging the ASP +program. During this process it can be cumbersome to build a detailed Clorm predicate +definition for each one of these, especially when all you need to do is print the predicate +instances to the screen, possibly sorted in some order. Clorm solves this issue by providing a factory helper function -:py:func:`~clorm.simple_predicate` that returns a :class:`~clorm.Predicate` -sub-class that will map to any predicate instance with that name and arity. +:py:func:`~clorm.simple_predicate` that returns a :class:`~clorm.Predicate` sub-class that will +map to any predicate instance with that name and arity. -For example this function could be used for the above booking example if we -wanted to extract the ``booking/2`` facts from the model but didn't care about -mapping the data types for the individual parameters. For example to match the -ASP fact: +For example this function could be used for the above booking example if we wanted to extract +the ``booking/2`` facts from the model but didn't care about mapping the data types for the +individual parameters. For example to match the ASP fact: .. code-block:: none @@ -814,32 +744,29 @@ instead of the explicit ``Booking`` definition above we could use the Booking_alt = simple_predicate("booking",2) bk_alt = Booking_alt(String("2018-12-31"), Function("",[String("Sydney"),String("Australia")])) -Note, in this case in order to create these objects within Python it is -necessary to use the Clingo functions to explictly create ``clingo.Symbol`` -objects. +Note, in this case in order to create these objects within Python it is necessary to use the +Clingo functions to explictly create ``clingo.Symbol`` objects. Dealing with Raw Clingo Symbols ------------------------------- -As well as supporting simple and complex terms it is sometimes useful to deal -with the raw ``clingo.Symbol`` objects created through the underlying Clingo -Python API. +As well as supporting simple and complex terms it is sometimes useful to deal with the raw +``clingo.Symbol`` objects created through the underlying Clingo Python API. .. _raw-symbol-label: Raw Clingo Symbols ^^^^^^^^^^^^^^^^^^ -The Clingo API uses ``clingo.Symbol`` objects for dealing with facts; and there -are a number of functions for creating the appropriate type of symbol objects -(i.e., ``clingo.Function()``, ``clingo.Number()``, ``clingo.String()``). +The Clingo API uses ``clingo.Symbol`` objects for dealing with facts; and there are a number of +functions for creating the appropriate type of symbol objects (i.e., ``clingo.Function()``, +``clingo.Number()``, ``clingo.String()``). -In essence the Clorm :class:`~clorm.Predicate` and :class:`~clorm.ComplexTerm` -classes simply provide a more convenient and intuitive way of constructing and -dealing with these ``clingo.Symbol`` objects. In fact the underlying symbols can -be accessed using the ``raw`` property of a :class:`~clorm.Predicate` or -:class:`~clorm.ComplexTerm` object. +In essence the Clorm :class:`~clorm.Predicate` class simply provides a more convenient and +intuitive way of constructing and dealing with these ``clingo.Symbol`` objects. In fact the +underlying symbols can be accessed using the ``raw`` property of a :class:`~clorm.Predicate` +instance. .. code-block:: python @@ -847,8 +774,8 @@ be accessed using the ``raw`` property of a :class:`~clorm.Predicate` or from clingo import * # Function, String class Address(Predicate): - entity = ConstantField - details = StringField + entity: ConstantStr + details: str address = Address(entity="dave", details="UNSW Sydney") @@ -858,26 +785,23 @@ be accessed using the ``raw`` property of a :class:`~clorm.Predicate` or .. note:: - To construct clorm objects from raw clingo symbols involves *unifying* the - clingo symbol with the :class:`~clorm.Predicate` or - :class:`~clorm.ComplexTerm` sub-class. This typically happens when you have - a list of symbols corresponding to a clingo model and you want to turn them - into a set of clorm facts. See :ref:`advanced_unification`, - :ref:`api_clingo_integration`, and :py:func:`~clorm.unify` for details about - unification. + To construct clorm objects from raw clingo symbols involves *unifying* the clingo symbol + with the :class:`~clorm.Predicate` or :class:`~clorm.ComplexTerm` sub-class. This typically + happens when you have a list of symbols corresponding to a clingo model and you want to turn + them into a set of clorm facts. See :ref:`advanced_unification`, + :ref:`api_clingo_integration`, and :py:func:`~clorm.unify` for details about unification. Integrating Clingo Symbols into a Predicate Definition ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -There are some cases when it might be convenient to combine the simplicity and -the structure of the Clorm predicate interface with the flexibility of the -underlying Clingo symbol API. For this case it is possible to use the -:class:`~clorm.RawField` class. +There are some cases when it might be convenient to combine the simplicity and the structure of +the Clorm predicate interface with the flexibility of the underlying Clingo symbol API. For +this case it is possible to use the :class:`~clorm.RawField` class. -For example when modeling dynamic domains it is often useful to provide a -predicate that defines what *fluents* hold (i.e., are true) at a given time -point, but to allow the fluents themselves to have an arbitrary form. +For example when modeling dynamic domains it is often useful to provide a predicate that +defines what *fluents* hold (i.e., are true) at a given time point, but to allow the fluents +themselves to have an arbitrary form. .. code-block:: prolog @@ -891,34 +815,30 @@ point, but to allow the fluents themselves to have an arbitrary form. holds(light(on), 0). holds(robotlocation(roby,kitchen), 0). -In this example instances of the ``holds/2`` predicate can have two distinctly -different signatures for the first term (i.e., ``light/1`` and -``robotlocation/2``). While the definition of the fluent is important at the -ASP level, however, at the Python level we may not be interested in the -structure of the fluent, only whether it holds or not. In such a case we can -use a :class:`~clorm.RawField` to define the raw mapping from the fluent term -to a Python object. +In this example instances of the ``holds/2`` predicate can have two distinctly different +signatures for the first term (i.e., ``light/1`` and ``robotlocation/2``). While the definition +of the fluent is important at the ASP level, however, at the Python level we may not be +interested in the structure of the fluent, only whether it holds or not. In such a case we can +use a :class:`~clorm.RawField` to define the raw mapping from the fluent term to a Python +object. .. code-block:: python - from clorm import * + from clorm import Raw, Predicate class Holds(Predicate): - fluent = RawField - time = IntegerField + fluent: Raw + time: int -:class:`~clorm.RawField` provides no data translation between ASP and Python -and therefore has the has the useful property that it will unify with any -``clingo.Symbol`` object; in particular in this case it can be used to capture -both the ``light/1`` and ``robotlocation/2`` complex terms. +:class:`~clorm.RawField` provides no data translation between ASP and Python and therefore has +the useful property that it will unify with any ``clingo.Symbol`` object; in particular in this +case it can be used to capture both the ``light/1`` and ``robotlocation/2`` complex terms. -When translating from Python to clingo, :class:`~clorm.RawField` expects objects -of the type :class:`~clorm.Raw`, and returns objects of this type when -translating from clingo to Python. :class:`~clorm.Raw` is simply a thin wrapper -around the underlying ``clingo.Symbol``. +When translating from Python to clingo, :class:`~clorm.RawField` expects objects of the type +:class:`~clorm.Raw`, and returns objects of this type when translating from clingo to +Python. :class:`~clorm.Raw` is simply a thin wrapper around the underlying ``clingo.Symbol``. -For example, to create a create a Python fact that specifies that the light is -on at time 0: +For example, to create a create a Python fact that specifies that the light is on at time 0: .. code-block:: python @@ -932,89 +852,125 @@ on at time 0: Combining Field Definitions --------------------------- -The above example is useful for cases where you don't care about accessing the -details of individual fluents and therefore it makes sense to simply treat them -as a :class:`~clorm.RawField` complex term. However, the question naturally -arises what to do if you do want more fine-grained access to these fluents. +The above example is useful for cases where you don't care about accessing the details of +individual fluents and therefore it makes sense to simply treat them as a +:class:`~clorm.RawField` complex term. However, the question naturally arises what to do if you +do want more fine-grained access to these fluents. -There are a few possible solutions to this problem, but one obvious answer is to -use a field that combines together multiple fields. Such a combined field could -be specified manually by explicitly defining a :class:`~clorm.BaseField` -sub-class. However, to simplify this process the -:py:func:`~clorm.combine_fields` factory function has been provided that will -return such a combined sub-class. +There are a few possible solutions to this problem, but one obvious answer is to use a field +that combines together multiple fields. Such a combined field could be specified manually by +explicitly defining a :class:`~clorm.BaseField` sub-class. However, to simplify this process +the :py:func:`~clorm.combine_fields` factory function has been provided that will return such a +combined sub-class. In fact Clorm uses standard Python union type annotation to implicitly +generate such a mapping. -With reference to the ASP code of the previous example we could add the -following Python integration: +With reference to the ASP code of the previous example we could add the following Python +integration: .. code-block:: python from clorm import Predicate, ComplexTerm, IntegerField, ConstantField, combine_fields - class Light(ComplexTerm): - status=ConstantField + class Light(Predicate): + status: ConstantStr - class RobotLocation(ComplexTerm): - robot=ConstantField - location=ConstantField - class Meta: name = "robotlocation" + class RobotLocation(Predicate, name="robotlocation"): + robot: ConstantStr + location: ConstantStr class Holds(Predicate): - fluent = combine_fields([Light.Field, RobotLocation.Field]) - time = IntegerField + fluent: Light | RobotLocation + time: int + +.. note:: + + For Python versions earlier than 3.11 you need to specify the union type using the ``Union`` + identifier from the ``typing`` module: + .. code-block:: python -The :py:func:`~clorm.combine_fields` function takes two arguments; the first is -an optional field name argument and the second is a list of the sub-fields to -combine. Note: when trying to unify a value with a combined field the raw symbol -values will be unified with the underlying field definitions in the order that -they are listed in the call to :py:func:`~clorm.combine_fields`. This means that -care needs to be taken if the raw symbol values could unify with multiple -sub-fields; it will only unify with the first successful sub-field. In the above -example this is not a problem as the two fluent field definitions do not -overlap. + from typing import Tuple + + class Holds(Predicate): + fluent: Union[Light, RobotLocation] + time: int + + +When used explicitly, the :py:func:`~clorm.combine_fields` function takes two arguments; the +first is an optional field name argument and the second is a list of the sub-fields to +combine. Note: when trying to unify a value with a combined field the raw symbol values will be +unified with the underlying field definitions in the order that they are listed in the call to +:py:func:`~clorm.combine_fields`. This means that care needs to be taken if the raw symbol +values could unify with multiple sub-fields; it will only unify with the first successful +sub-field. In the above example this is not a problem as the two fluent field definitions do +not overlap. Dealing with Nested Lists ------------------------- -ASP does not have an explicit representation for lists. However a common -convention for encoding lists is using a nesting of head-tail pairs; where the -head of the pair is the element of the list and the tail is the remainder of the -list, being another pair or an empty tuple to indicate the end of the list. +ASP does not have an explicit representation for lists. However a common convention for +encoding lists is using a nesting of head-tail pairs; where the head of the pair is the element +of the list and the tail is the remainder of the list, being another pair or an empty tuple to +indicate the end of the list. -For example encoding a list of "nodes" [1,2,c] for some predicate ``p``, might -take the form: +For example encoding a list of "nodes" [1,2,c] for some predicate ``p``, might take the form: .. code-block:: prolog p(nodes,(1,(2,(c,())))). -While, such an encoding can be problematic and can lead to a grounding blowout, -nevertheless when used with care can be very useful. +While, such an encoding can be problematic and can lead to a grounding blowout, nevertheless +when used with care can be very useful. -Unfortunately, getting facts containing these sorts of nested lists into and out -of Clingo can be very cumbersome. To help support this type of encoding Clorm -provides the :py:func:`~clorm.define_nested_list_field()` function. This factory -function takes an element field class, as well as an optional new class name, -and returns a newly created :class:`~clorm.BaseField` sub-class that can be used -to convert to and from a list of elements of that field class. +Unfortunately, getting facts containing these sorts of nested lists into and out of Clingo can +be very cumbersome. To help support this type of encoding Clorm provides the +:py:func:`~clorm.define_nested_list_field()` function. This factory function takes an element +field class, as well as an optional new class name, and returns a newly created +:class:`~clorm.BaseField` sub-class that can be used to convert to and from a list of elements +of that field class. Clorm provides implicit support for this helper function with some extra +type identifiers. .. code-block:: python - from clorm import Predicate, ConstantField, SimpleField, \ - define_nested_list_field - - SNLField=define_nested_list(SimpleField) + from clorm import Predicate, ConstantStr, HeadList class P(Predicate): - param=ConstantField - alist=SNLField + param: ConstantStr + alist: HeadList[int] + + p = P("nodes",[1,2,3]) + assert str(p) == "p(nodes,(1,(2,(3,()))))" + + +Old Syntax +---------- + +The preferred syntax for specifying predicates has changed with Clorm 1.5. The new syntax looks +very similar to standard Python dataclasses or a modern Python library such as +`Pydantic `_. This new syntax integrates better with modern +Python programming practices, for example using linters and type checkers. - p = P("nodes",[1,2,"c"]) - assert str(p) == "p(nodes,(1,(2,(c,()))))" +The old syntax does not use Python type annotations and instead required the user to explicitly +a :class:`~clorm.BaseField` sub-class for each term. It also required the use of a ``Meta`` +sub-class to provide predicate meta-data, for example, to override the name of the predicate. - p = P("nodes",[1,2,"c","A string"]) - assert str(p) == '''p(nodes,(1,(2,(c,("A string",())))))''' + .. code-block:: python + + from clorm import Predicate, StringField, IntegerField + + class Location(Predicate): + city = StringField + country = StringField + + class Meta: + name = "mylocation" + + class Booking(Predicate): + date = StringField + location = Location.Field +While the old syntax still works it should only be used as a fallback if it is not possible to +specify some requirement using the new syntax. The old syntax will likely be deprecated at some +point and eventually removed completely. From f40b0955f2ac8c497a8f661b55a012d4fa2402c0 Mon Sep 17 00:00:00 2001 From: David Rajaratnam Date: Mon, 19 Feb 2024 19:02:51 +1100 Subject: [PATCH 6/6] Update clorm version to 1.5.0 --- clorm/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clorm/__init__.py b/clorm/__init__.py index 8f1ac73..c5b6931 100644 --- a/clorm/__init__.py +++ b/clorm/__init__.py @@ -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"