From a9f5c715c77ab763494e96a69c56cffac32606ba Mon Sep 17 00:00:00 2001 From: "Brandon T. Willard" Date: Thu, 28 Jul 2022 14:18:54 -0500 Subject: [PATCH 1/2] Update pre-commit settings and add mypy typing to kanren.core --- .pre-commit-config.yaml | 54 +++++++++++------- examples/commutative.py | 1 + examples/corleone.py | 1 + examples/states.py | 1 + examples/user_classes.py | 1 + examples/zebra-puzzle.py | 54 +++++++++++++++--- kanren/__init__.py | 1 + kanren/assoccomm.py | 1 + kanren/constraints.py | 3 +- kanren/core.py | 120 +++++++++++++++++++++++++-------------- kanren/py.typed | 0 kanren/util.py | 13 +---- setup.cfg | 31 +++++++--- setup.py | 5 ++ tests/test_assoccomm.py | 68 +++++++++------------- tests/test_core.py | 15 ++--- tests/test_sudoku.py | 1 + 17 files changed, 227 insertions(+), 143 deletions(-) create mode 100644 kanren/py.typed diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 116b0d2..5bc03ce 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,35 +1,45 @@ +exclude: | + (?x)^( + versioneer\.py| + kanren/_version\.py| + doc/.*| + bin/.* + )$ repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.1.0 + hooks: + - id: debug-statements + exclude: | + (?x)^( + kanren/core\.py| + )$ + - id: check-merge-conflict - repo: https://github.com/psf/black - rev: 20.8b1 + rev: 22.3.0 hooks: - id: black language_version: python3 - exclude: | - (?x)^( - versioneer\.py| - kanren/_version\.py| - doc/.*| - bin/.* - )$ - repo: https://gitlab.com/pycqa/flake8 rev: 3.8.4 hooks: - id: flake8 - exclude: | - (?x)^( - versioneer\.py| - kanren/_version\.py| - doc/.*| - bin/.* - )$ - repo: https://github.com/pycqa/isort - rev: 5.5.2 + rev: 5.6.4 hooks: - id: isort + - repo: https://github.com/humitos/mirrors-autoflake.git + rev: v1.1 + hooks: + - id: autoflake exclude: | - (?x)^( - versioneer\.py| - kanren/_version\.py| - doc/.*| - bin/.* - )$ + (?x)^( + .*/?__init__\.py| + )$ + args: ['--in-place', '--remove-all-unused-imports', '--remove-unused-variable'] + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v0.931 + hooks: + - id: mypy + additional_dependencies: + - types-setuptools diff --git a/examples/commutative.py b/examples/commutative.py index 9cc6841..aac0b55 100644 --- a/examples/commutative.py +++ b/examples/commutative.py @@ -2,6 +2,7 @@ from kanren.assoccomm import associative, commutative from kanren.assoccomm import eq_assoccomm as eq + # Define some dummy Operationss add = "add" mul = "mul" diff --git a/examples/corleone.py b/examples/corleone.py index 7ccc371..8438427 100644 --- a/examples/corleone.py +++ b/examples/corleone.py @@ -7,6 +7,7 @@ from kanren import Relation, conde, facts, run, var + father = Relation() mother = Relation() diff --git a/examples/states.py b/examples/states.py index 589009f..383a7c0 100644 --- a/examples/states.py +++ b/examples/states.py @@ -8,6 +8,7 @@ """ from kanren import Relation, fact, run, var + adjacent = Relation() coastal = Relation() diff --git a/examples/user_classes.py b/examples/user_classes.py index a4091cc..17c3010 100644 --- a/examples/user_classes.py +++ b/examples/user_classes.py @@ -5,6 +5,7 @@ from kanren.core import lall from kanren.term import applyo, term # noqa: F401 + unifiable(Account) # Register Account class accounts = ( diff --git a/examples/zebra-puzzle.py b/examples/zebra-puzzle.py index 2f1bc9a..059a888 100644 --- a/examples/zebra-puzzle.py +++ b/examples/zebra-puzzle.py @@ -2,16 +2,17 @@ Zebra puzzle as published in Life International in 1962. https://en.wikipedia.org/wiki/Zebra_Puzzle """ -from typing import Union from dataclasses import dataclass, field +from typing import Union -from kanren import eq, conde, lall, membero, run -from unification import unifiable, var, vars, Var +from unification import Var, unifiable, var, vars + +from kanren import conde, eq, lall, membero, run @unifiable @dataclass -class House(): +class House: nationality: Union[str, Var] = field(default_factory=var) drink: Union[str, Var] = field(default_factory=var) animal: Union[str, Var] = field(default_factory=var) @@ -24,6 +25,7 @@ def righto(right, left, houses): neighbors = tuple(zip(houses[:-1], houses[1:])) return membero((left, right), neighbors) + def nexto(a, b, houses): """Express that `a` and `b` are next to each other.""" return conde([righto(a, b, houses)], [righto(b, a, houses)]) @@ -53,8 +55,42 @@ def nexto(a, b, houses): results = run(0, houses, goals) print(results) -# ([House(nationality='Norwegian', drink='water', animal='fox', cigarettes='Kools', color='yellow'), -# House(nationality='Ukrainian', drink='tea', animal='horse', cigarettes='Chesterfields', color='blue'), -# House(nationality='Englishman', drink='milk', animal='snails', cigarettes='Old Gold', color='red'), -# House(nationality='Spaniard', drink='orange juice', animal='dog', cigarettes='Lucky Strike', color='ivory'), -# House(nationality='Japanese', drink='coffee', animal='zebra', cigarettes='Parliaments', color='green')],) +# ( +# [ +# House( +# nationality="Norwegian", +# drink="water", +# animal="fox", +# cigarettes="Kools", +# color="yellow", +# ), +# House( +# nationality="Ukrainian", +# drink="tea", +# animal="horse", +# cigarettes="Chesterfields", +# color="blue", +# ), +# House( +# nationality="Englishman", +# drink="milk", +# animal="snails", +# cigarettes="Old Gold", +# color="red", +# ), +# House( +# nationality="Spaniard", +# drink="orange juice", +# animal="dog", +# cigarettes="Lucky Strike", +# color="ivory", +# ), +# House( +# nationality="Japanese", +# drink="coffee", +# animal="zebra", +# cigarettes="Parliaments", +# color="green", +# ), +# ], +# ) diff --git a/kanren/__init__.py b/kanren/__init__.py index a081bb5..0890892 100644 --- a/kanren/__init__.py +++ b/kanren/__init__.py @@ -19,5 +19,6 @@ ) from .term import arguments, operator, term, unifiable_with_term + __version__ = get_versions()["version"] del get_versions diff --git a/kanren/assoccomm.py b/kanren/assoccomm.py index 99edc58..bacd070 100644 --- a/kanren/assoccomm.py +++ b/kanren/assoccomm.py @@ -44,6 +44,7 @@ from .graph import term_walko from .term import term + associative = Relation("associative") commutative = Relation("commutative") diff --git a/kanren/constraints.py b/kanren/constraints.py index c488a38..317b676 100644 --- a/kanren/constraints.py +++ b/kanren/constraints.py @@ -2,6 +2,7 @@ from abc import ABC, abstractmethod from collections import UserDict from collections.abc import Mapping +from typing import Optional from cons.core import ConsPair from toolz import groupby @@ -26,7 +27,7 @@ class ConstraintStore(ABC): """ __slots__ = ("lvar_constraints",) - op_str = None + op_str: Optional[str] = None def __init__(self, lvar_constraints=None): # self.lvar_constraints = weakref.WeakKeyDictionary(lvar_constraints) diff --git a/kanren/core.py b/kanren/core.py index 3da6b60..6e44c17 100644 --- a/kanren/core.py +++ b/kanren/core.py @@ -1,23 +1,40 @@ -from collections.abc import Generator, Sequence +from collections.abc import Sequence from functools import partial, reduce from itertools import tee from operator import length_hint +from typing import ( + Any, + Callable, + Iterable, + Iterator, + MutableMapping, + Optional, + Tuple, + Union, + cast, +) from cons.core import ConsPair from toolz import interleave, take +from typing_extensions import Literal from unification import isvar, reify, unify from unification.core import isground -def fail(s): +StateType = Union[MutableMapping, Literal[False]] +StateStreamType = Iterator[StateType] +GoalType = Callable[[StateType], StateStreamType] + + +def fail(s: StateType) -> Iterator[StateType]: return iter(()) -def succeed(s): +def succeed(s: StateType) -> Iterator[StateType]: return iter((s,)) -def eq(u, v): +def eq(u: Any, v: Any) -> GoalType: """Construct a goal stating that its arguments must unify. See Also @@ -25,7 +42,7 @@ def eq(u, v): unify """ - def eq_goal(s): + def eq_goal(s: StateType) -> StateStreamType: s = unify(u, v, s) if s is not False: return iter((s,)) @@ -35,7 +52,7 @@ def eq_goal(s): return eq_goal -def ldisj_seq(goals): +def ldisj_seq(goals: Iterable[GoalType]) -> GoalType: """Produce a goal that returns the appended state stream from all successful goal arguments. In other words, it behaves like logical disjunction/OR for goals. @@ -44,7 +61,9 @@ def ldisj_seq(goals): if length_hint(goals, -1) == 0: return succeed - def ldisj_seq_goal(S): + assert isinstance(goals, Iterable) + + def ldisj_seq_goal(S: StateType) -> StateStreamType: nonlocal goals goals, _goals = tee(goals) @@ -54,14 +73,14 @@ def ldisj_seq_goal(S): return ldisj_seq_goal -def bind(z, g): +def bind(z: StateStreamType, g: GoalType) -> StateStreamType: """Apply a goal to a state stream and then combine the resulting state streams.""" # We could also use `chain`, but `interleave` preserves the old behavior. # return chain.from_iterable(map(g, z)) - return interleave(map(g, z)) + return cast(StateStreamType, interleave(map(g, z))) -def lconj_seq(goals): +def lconj_seq(goals: Iterable[GoalType]) -> GoalType: """Produce a goal that returns the appended state stream in which all goals are necessarily successful. In other words, it behaves like logical conjunction/AND for goals. @@ -70,7 +89,9 @@ def lconj_seq(goals): if length_hint(goals, -1) == 0: return succeed - def lconj_seq_goal(S): + assert isinstance(goals, Iterable) + + def lconj_seq_goal(S: StateType) -> StateStreamType: nonlocal goals goals, _goals = tee(goals) @@ -85,35 +106,39 @@ def lconj_seq_goal(S): return lconj_seq_goal -def ldisj(*goals): +def ldisj(*goals: Union[GoalType, Iterable[GoalType]]) -> GoalType: """Form a disjunction of goals.""" - if len(goals) == 1 and isinstance(goals[0], Generator): - goals = goals[0] + if len(goals) == 1 and isinstance(goals[0], Iterable): + return ldisj_seq(goals[0]) - return ldisj_seq(goals) + return ldisj_seq(cast(Tuple[GoalType, ...], goals)) -def lconj(*goals): +def lconj(*goals: Union[GoalType, Iterable[GoalType]]) -> GoalType: """Form a conjunction of goals.""" - if len(goals) == 1 and isinstance(goals[0], Generator): - goals = goals[0] + if len(goals) == 1 and isinstance(goals[0], Iterable): + return lconj_seq(goals[0]) - return lconj_seq(goals) + return lconj_seq(cast(Tuple[GoalType, ...], goals)) -def conde(*goals): +def conde( + *goals: Union[Iterable[GoalType], Iterator[Iterable[GoalType]]] +) -> Union[GoalType, StateStreamType]: """Form a disjunction of goal conjunctions.""" - if len(goals) == 1 and isinstance(goals[0], Generator): - goals = goals[0] + if len(goals) == 1 and isinstance(goals[0], Iterator): + return ldisj_seq( + lconj_seq(g) for g in cast(Iterator[Iterable[GoalType]], goals[0]) + ) - return ldisj_seq(lconj_seq(g) for g in goals) + return ldisj_seq(lconj_seq(g) for g in cast(Tuple[Iterable[GoalType], ...], goals)) lall = lconj lany = ldisj -def ground_order_key(S, x): +def ground_order_key(S: StateType, x: Any) -> Literal[-1, 0, 1, 2]: if isvar(x): return 2 elif isground(x, S): @@ -124,10 +149,10 @@ def ground_order_key(S, x): return 0 -def ground_order(in_args, out_args): +def ground_order(in_args: Any, out_args: Any) -> GoalType: """Construct a non-relational goal that orders a list of terms based on groundedness (grounded precede ungrounded).""" # noqa: E501 - def ground_order_goal(S): + def ground_order_goal(S: StateType) -> StateStreamType: nonlocal in_args, out_args in_args_rf, out_args_rf = reify((in_args, out_args), S) @@ -144,10 +169,10 @@ def ground_order_goal(S): return ground_order_goal -def ifa(g1, g2): +def ifa(g1: GoalType, g2: GoalType) -> GoalType: """Create a goal operator that returns the first stream unless it fails.""" - def ifa_goal(S): + def ifa_goal(S: StateType) -> StateStreamType: g1_stream = g1(S) S_new = next(g1_stream, None) @@ -160,17 +185,22 @@ def ifa_goal(S): return ifa_goal -def Zzz(gctor, *args, **kwargs): +def Zzz(gctor: Callable[[Any], GoalType], *args, **kwargs) -> GoalType: """Create an inverse-η-delay for a goal.""" - def Zzz_goal(S): + def Zzz_goal(S: StateType) -> StateStreamType: yield from gctor(*args, **kwargs)(S) return Zzz_goal -def run(n, x, *goals, results_filter=None): - """Run a logic program and obtain n solutions that satisfy the given goals. +def run( + n: Union[None, int], + x: Any, + *goals: GoalType, + results_filter: Optional[Callable[[Iterator[Any]], Any]] = None +) -> Union[Tuple[Any, ...], Iterator[Any]]: + """Run a logic program and obtain `n` solutions that satisfy the given goals. >>> from kanren import run, var, eq >>> x = var() @@ -179,19 +209,25 @@ def run(n, x, *goals, results_filter=None): Parameters ---------- - n: int - The number of desired solutions. `n=0` returns a tuple - with all results and `n=None` returns a lazy sequence of all results. - x: object - The form to reify and output. Usually contains logic variables used in + n + The number of desired solutions. ``n=0`` returns a tuple with all + results and ``n=None`` returns a lazy sequence of all results. + x + The form to reify and return. Usually contains logic variables used in the given goals. - goals: Callables + goals A sequence of goals that must be true in logical conjunction (i.e. `lall`). - results_filter: Callable + results_filter A function to apply to the results stream (e.g. a `unique` filter). + + Returns + ------- + Either an iterable or tuple of reified `x` values that satisfy the goals. + """ - results = map(partial(reify, x), lall(*goals)({})) + g = lall(*goals) + results = map(partial(reify, x), g({})) if results_filter is not None: results = results_filter(results) @@ -204,11 +240,11 @@ def run(n, x, *goals, results_filter=None): return tuple(take(n, results)) -def dbgo(*args, msg=None): # pragma: no cover +def dbgo(*args: Any, msg: Optional[Any] = None) -> GoalType: # pragma: no cover """Construct a goal that sets a debug trace and prints reified arguments.""" from pprint import pprint - def dbgo_goal(S): + def dbgo_goal(S: StateType) -> StateStreamType: nonlocal args args = reify(args, S) diff --git a/kanren/py.typed b/kanren/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/kanren/util.py b/kanren/util.py index caa0b81..df52a2a 100644 --- a/kanren/util.py +++ b/kanren/util.py @@ -2,6 +2,7 @@ from collections.abc import Hashable, Iterable, Mapping, MutableSet, Set from itertools import chain + HashableForm = namedtuple("HashableForm", ["type", "data"]) @@ -68,18 +69,6 @@ def __le__(self, other): def __ge__(self, other): raise NotImplementedError() - def __ior__(self, item): - raise NotImplementedError() - - def __iand__(self, item): - raise NotImplementedError() - - def __ixor__(self, item): - raise NotImplementedError() - - def __isub__(self, item): - raise NotImplementedError() - def __iter__(self): return chain(self.set, self.list) diff --git a/setup.cfg b/setup.cfg index c7b61c0..21c3cf7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -37,12 +37,11 @@ exclude_lines = show_missing = 1 [isort] -multi_line_output = 3 -include_trailing_comma = True -force_grid_wrap = 0 -use_parentheses = True -ensure_newline_before_comments = True -line_length = 88 +profile = black +lines_after_imports = 2 +lines_between_sections = 1 +honor_noqa = True +skip_gitignore = True [flake8] max-line-length = 88 @@ -54,4 +53,22 @@ per-file-ignores = max-line-length = 88 [pylint.messages_control] -disable = C0330, C0326 \ No newline at end of file +disable = C0330, C0326 + +[mypy] +ignore_missing_imports = True +no_implicit_optional = True +check_untyped_defs = False +strict_equality = True +warn_redundant_casts = True +warn_unused_configs = True +warn_unused_ignores = True +warn_return_any = True +warn_no_return = False +warn_unreachable = True +show_error_codes = True +allow_redefinition = False +files = kanren,tests + +[mypy-versioneer] +check_untyped_defs = False \ No newline at end of file diff --git a/setup.py b/setup.py index 7de84a8..8623686 100644 --- a/setup.py +++ b/setup.py @@ -5,6 +5,7 @@ import versioneer + setup( name="miniKanren", version=versioneer.get_version(), @@ -21,7 +22,11 @@ "multipledispatch", "etuples >= 0.3.1", "logical-unification >= 0.4.1", + "typing_extensions", ], + package_data={ + "kanren": ["py.typed"], + }, tests_require=["pytest", "sympy"], long_description=open("README.md").read() if exists("README.md") else "", long_description_content_type="text/markdown", diff --git a/tests/test_assoccomm.py b/tests/test_assoccomm.py index fd5b55a..1aa81f8 100644 --- a/tests/test_assoccomm.py +++ b/tests/test_assoccomm.py @@ -320,19 +320,16 @@ def test_eq_assoc_args(): == () ) - assert ( - run( - 0, - True, - eq_assoc_args( - assoc_op, - (1, (assoc_op, 2, 3)), - ((assoc_op, 1, 2), 3), - no_ident=True, - ), - ) - == (True,) - ) + assert run( + 0, + True, + eq_assoc_args( + assoc_op, + (1, (assoc_op, 2, 3)), + ((assoc_op, 1, 2), 3), + no_ident=True, + ), + ) == (True,) def test_eq_assoc(): @@ -417,37 +414,26 @@ def test_assoc_flatten(): fact(commutative, mul) fact(associative, mul) - assert ( - run( - 0, - True, - assoc_flatten( - (mul, 1, (add, 2, 3), (mul, 4, 5)), (mul, 1, (add, 2, 3), 4, 5) - ), - ) - == (True,) - ) + assert run( + 0, + True, + assoc_flatten((mul, 1, (add, 2, 3), (mul, 4, 5)), (mul, 1, (add, 2, 3), 4, 5)), + ) == (True,) x = var() - assert ( - run( - 0, - x, - assoc_flatten((mul, 1, (add, 2, 3), (mul, 4, 5)), x), - ) - == ((mul, 1, (add, 2, 3), 4, 5),) - ) + assert run( + 0, + x, + assoc_flatten((mul, 1, (add, 2, 3), (mul, 4, 5)), x), + ) == ((mul, 1, (add, 2, 3), 4, 5),) - assert ( - run( - 0, - True, - assoc_flatten( - ("op", 1, (add, 2, 3), (mul, 4, 5)), ("op", 1, (add, 2, 3), (mul, 4, 5)) - ), - ) - == (True,) - ) + assert run( + 0, + True, + assoc_flatten( + ("op", 1, (add, 2, 3), (mul, 4, 5)), ("op", 1, (add, 2, 3), (mul, 4, 5)) + ), + ) == (True,) assert run(0, x, assoc_flatten(("op", 1, (add, 2, 3), (mul, 4, 5)), x)) == ( ("op", 1, (add, 2, 3), (mul, 4, 5)), diff --git a/tests/test_core.py b/tests/test_core.py index b1a761e..c7e47b9 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -235,15 +235,12 @@ def test_ifa(): == () ) - assert ( - run( - 0, - y, - eq(x, True), - ifa(lall(eq(x, True), eq(y, 1)), lall(eq(x, True), eq(y, 2))), - ) - == (1,) - ) + assert run( + 0, + y, + eq(x, True), + ifa(lall(eq(x, True), eq(y, 1)), lall(eq(x, True), eq(y, 2))), + ) == (1,) def test_ground_order(): diff --git a/tests/test_sudoku.py b/tests/test_sudoku.py index 7c7fb90..9d1016e 100644 --- a/tests/test_sudoku.py +++ b/tests/test_sudoku.py @@ -9,6 +9,7 @@ from kanren.core import lall from kanren.goals import permuteq + DIGITS = tuple(range(1, 10)) From 9e0810deb9dca978dfcc4690c96cf222bcfa95de Mon Sep 17 00:00:00 2001 From: "Brandon T. Willard" Date: Thu, 28 Jul 2022 14:49:07 -0500 Subject: [PATCH 2/2] Update Relation's __str__ and __repr__ implementations --- kanren/facts.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/kanren/facts.py b/kanren/facts.py index c0b693e..0e80fa1 100644 --- a/kanren/facts.py +++ b/kanren/facts.py @@ -73,9 +73,10 @@ def goal(substitution): return goal def __str__(self): - return "Rel: " + self.name + return f"Rel: {self.name}" - __repr__ = __str__ + def __repr__(self): + return f"{type(self).__name__}({self.name}, {self.index}, {self.facts})" def fact(rel, *args):