diff --git a/README.md b/README.md index a1d99f0..1e7ad37 100644 --- a/README.md +++ b/README.md @@ -37,11 +37,10 @@ Thanks to all [contributors](CONTRIBUTORS.txt). ```python from pyswip import Prolog -prolog = Prolog() -prolog.assertz("father(michael,john)") -prolog.assertz("father(michael,gina)") -list(prolog.query("father(michael,X)")) == [{'X': 'john'}, {'X': 'gina'}] -for soln in prolog.query("father(X,Y)"): +Prolog.assertz("father(michael,john)") +Prolog.assertz("father(michael,gina)") +list(Prolog.query("father(michael,X)")) == [{'X': 'john'}, {'X': 'gina'}] +for soln in Prolog.query("father(X,Y)"): print(soln["X"], "is the father of", soln["Y"]) # michael is the father of john # michael is the father of gina @@ -52,8 +51,7 @@ Assuming the filename "knowledge_base.pl" and the Python is being run in the sam ```python from pyswip import Prolog -prolog = Prolog() -prolog.consult("knowledge_base.pl") +Prolog.consult("knowledge_base.pl") ``` ### Foreign Functions @@ -67,10 +65,9 @@ hello.arity = 1 registerForeign(hello) -prolog = Prolog() -prolog.assertz("father(michael,john)") -prolog.assertz("father(michael,gina)") -print(list(prolog.query("father(michael,X), hello(X)"))) +Prolog.assertz("father(michael,john)") +Prolog.assertz("father(michael,gina)") +print(list(Prolog.query("father(michael,X), hello(X)"))) ``` ### Pythonic interface (Experimental) diff --git a/examples/README.md b/examples/README.md index 39e3381..2dbd50d 100644 --- a/examples/README.md +++ b/examples/README.md @@ -7,7 +7,7 @@ The ones marked with (clp) requires `clp` library of SWI-Prolog. * (clp) `coins/` * (clp) `draughts/` * `hanoi/` : Towers of Hanoi -* (clp) `sendmoremoney/` : if, SEND * MORE = MONEY, what is S, E, N, D, M, O, R, Y? -* (clp) `sudoku/` : Sudoku solver (Prolog code is contributed by Markus Triska) -* `create_term.py` : shows creating a Prolog term -* `register_foreign.py` : shows registering a foreign function +* (clp) `sendmoremoney/` : If, SEND * MORE = MONEY, what is S, E, N, D, M, O, R, Y? +* (clp) `sudoku/` : Moved to `pyswip.examples.sudoku` package +* `create_term.py` : Shows creating a Prolog term +* `register_foreign.py` : Shows registering a foreign function diff --git a/examples/coins/coins.py b/examples/coins/coins.py index a1ae124..3061ff2 100644 --- a/examples/coins/coins.py +++ b/examples/coins/coins.py @@ -25,17 +25,16 @@ def main(): - prolog = Prolog() - prolog.consult("coins.pl", relative_to=__file__) + Prolog.consult("coins.pl", relative_to=__file__) count = int(input("How many coins (default: 100)? ") or 100) total = int(input("What should be the total (default: 500)? ") or 500) - for i, soln in enumerate(prolog.query("coins(S, %d, %d)." % (count, total))): + for i, soln in enumerate(Prolog.query("coins(S, %d, %d)." % (count, total))): S = zip(soln["S"], [1, 5, 10, 50, 100]) print(i, end=" ") for c, v in S: print(f"{c}x{v}", end=" ") print() - list(prolog.query(f"coins(S, {count}, {total}).")) + list(Prolog.query(f"coins(S, {count}, {total}).")) if __name__ == "__main__": diff --git a/examples/coins/coins_new.py b/examples/coins/coins_new.py index e12f08b..a435c52 100644 --- a/examples/coins/coins_new.py +++ b/examples/coins/coins_new.py @@ -25,8 +25,7 @@ def main(): - prolog = Prolog() - prolog.consult("coins.pl", relative_to=__file__) + Prolog.consult("coins.pl", relative_to=__file__) count = int(input("How many coins (default: 100)? ") or 100) total = int(input("What should be the total (default: 500)? ") or 500) coins = Functor("coins", 3) diff --git a/examples/create_term.py b/examples/create_term.py index 8bb33f0..0ff0135 100644 --- a/examples/create_term.py +++ b/examples/create_term.py @@ -26,8 +26,6 @@ def main(): - prolog = Prolog() - a1 = PL_new_term_refs(2) a2 = a1 + 1 t = PL_new_term_ref() @@ -42,7 +40,7 @@ def main(): PL_cons_functor_v(ta, assertz, t) PL_call(ta, None) - print(list(prolog.query("animal(X,Y)", catcherrors=True))) + print(list(Prolog.query("animal(X,Y)", catcherrors=True))) if __name__ == "__main__": diff --git a/examples/draughts/puzzle1.py b/examples/draughts/puzzle1.py index e44bfcb..e5345b1 100644 --- a/examples/draughts/puzzle1.py +++ b/examples/draughts/puzzle1.py @@ -37,10 +37,9 @@ def main(): - prolog = Prolog() - prolog.consult("puzzle1.pl", relative_to=__file__) + Prolog.consult("puzzle1.pl", relative_to=__file__) - for soln in prolog.query("solve(B)."): + for soln in Prolog.query("solve(B)."): B = soln["B"] # [NW,N,NE,W,E,SW,S,SE] diff --git a/examples/father.py b/examples/father.py index 655b5c0..6d8ecdb 100644 --- a/examples/father.py +++ b/examples/father.py @@ -21,19 +21,16 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import print_function from pyswip import * def main(): - p = Prolog() - father = Functor("father", 2) mother = Functor("mother", 2) - p.assertz("father(john,mich)") - p.assertz("father(john,gina)") - p.assertz("mother(jane,mich)") + Prolog.assertz("father(john,mich)") + Prolog.assertz("father(john,gina)") + Prolog.assertz("mother(jane,mich)") Y = Variable() Z = Variable() @@ -47,7 +44,7 @@ def main(): q.closeQuery() # Newer versions of SWI-Prolog do not allow nested queries print("\nQuery with strings\n") - for s in p.query("father(john,Y),mother(Z,Y)"): + for s in Prolog.query("father(john,Y),mother(Z,Y)"): print(s["Y"], s["Z"]) diff --git a/examples/hanoi/hanoi.py b/examples/hanoi/hanoi.py index 528a260..8d93563 100644 --- a/examples/hanoi/hanoi.py +++ b/examples/hanoi/hanoi.py @@ -76,12 +76,11 @@ def draw(self): def main(): n = 3 - prolog = Prolog() tower = Tower(n, True) notifier = Notifier(tower.move) registerForeign(notifier.notify) - prolog.consult("hanoi.pl", relative_to=__file__) - list(prolog.query("hanoi(%d)" % n)) + Prolog.consult("hanoi.pl", relative_to=__file__) + list(Prolog.query("hanoi(%d)" % n)) if __name__ == "__main__": diff --git a/examples/hanoi/hanoi_simple.py b/examples/hanoi/hanoi_simple.py index 0fb72ca..b7d9014 100644 --- a/examples/hanoi/hanoi_simple.py +++ b/examples/hanoi/hanoi_simple.py @@ -31,10 +31,9 @@ def notify(t): notify.arity = 1 - prolog = Prolog() registerForeign(notify) - prolog.consult("hanoi.pl", relative_to=__file__) - list(prolog.query(f"hanoi({N})")) + Prolog.consult("hanoi.pl", relative_to=__file__) + list(Prolog.query(f"hanoi({N})")) if __name__ == "__main__": diff --git a/examples/register_foreign.py b/examples/register_foreign.py index cd7ad20..ca69214 100644 --- a/examples/register_foreign.py +++ b/examples/register_foreign.py @@ -33,6 +33,5 @@ def atom_checksum(*a): return False -p = Prolog() registerForeign(atom_checksum, arity=2) -print(list(p.query("X='Python', atom_checksum(X, Y)", catcherrors=False))) +print(list(Prolog.query("X='Python', atom_checksum(X, Y)", catcherrors=False))) diff --git a/examples/register_foreign_simple.py b/examples/register_foreign_simple.py index edb818e..1c5c8ad 100644 --- a/examples/register_foreign_simple.py +++ b/examples/register_foreign_simple.py @@ -23,7 +23,6 @@ # Demonstrates registering a Python function as a Prolog predicate through SWI-Prolog's FFI. -from __future__ import print_function from pyswip.prolog import Prolog from pyswip.easy import registerForeign @@ -37,10 +36,9 @@ def hello(t): def main(): registerForeign(hello) - prolog = Prolog() - prolog.assertz("father(michael,john)") - prolog.assertz("father(michael,gina)") - list(prolog.query("father(michael,X), hello(X)")) + Prolog.assertz("father(michael,john)") + Prolog.assertz("father(michael,gina)") + list(Prolog.query("father(michael,X), hello(X)")) if __name__ == "__main__": diff --git a/examples/sendmoremoney/money.py b/examples/sendmoremoney/money.py index 21a4d2d..b2edfca 100644 --- a/examples/sendmoremoney/money.py +++ b/examples/sendmoremoney/money.py @@ -30,9 +30,8 @@ from pyswip import Prolog letters = list("SENDMORY") -prolog = Prolog() -prolog.consult("money.pl", relative_to=__file__) -for result in prolog.query("sendmore(X)"): +Prolog.consult("money.pl", relative_to=__file__) +for result in Prolog.query("sendmore(X)"): r = result["X"] for i, letter in enumerate(letters): print(letter, "=", r[i]) diff --git a/examples/sendmoremoney/money_new.py b/examples/sendmoremoney/money_new.py index fcbc7a4..956c3ea 100644 --- a/examples/sendmoremoney/money_new.py +++ b/examples/sendmoremoney/money_new.py @@ -32,9 +32,8 @@ def main(): letters = list("SENDMORY") - prolog = Prolog() sendmore = Functor("sendmore") - prolog.consult("money.pl", relative_to=__file__) + Prolog.consult("money.pl", relative_to=__file__) X = Variable() call(sendmore(X)) diff --git a/examples/sudoku/sudoku.py b/examples/sudoku/sudoku.py deleted file mode 100644 index 7108bfa..0000000 --- a/examples/sudoku/sudoku.py +++ /dev/null @@ -1,89 +0,0 @@ -# -*- coding: utf-8 -*- - -# pyswip -- Python SWI-Prolog bridge -# Copyright (c) 2007-2018 Yüce Tekol -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -from __future__ import print_function -from pyswip.prolog import Prolog -from pyswip.easy import * - - -_ = 0 -puzzle1 = [ - [_, 6, _, 1, _, 4, _, 5, _], - [_, _, 8, 3, _, 5, 6, _, _], - [2, _, _, _, _, _, _, _, 1], - [8, _, _, 4, _, 7, _, _, 6], - [_, _, 6, _, _, _, 3, _, _], - [7, _, _, 9, _, 1, _, _, 4], - [5, _, _, _, _, _, _, _, 2], - [_, _, 7, 2, _, 6, 9, _, _], - [_, 4, _, 5, _, 8, _, 7, _], -] - - -puzzle2 = [ - [_, _, 1, _, 8, _, 6, _, 4], - [_, 3, 7, 6, _, _, _, _, _], - [5, _, _, _, _, _, _, _, _], - [_, _, _, _, _, 5, _, _, _], - [_, _, 6, _, 1, _, 8, _, _], - [_, _, _, 4, _, _, _, _, _], - [_, _, _, _, _, _, _, _, 3], - [_, _, _, _, _, 7, 5, 2, _], - [8, _, 2, _, 9, _, 7, _, _], -] - - -def pretty_print(table): - print("".join(["/---", "----" * 8, "\\"])) - for row in table: - print("".join(["|", "|".join(" %s " % (i or " ") for i in row), "|"])) - print("".join(["\\---", "----" * 8, "/"])) - - -def solve(problem): - prolog.consult("sudoku.pl") - p = str(problem).replace("0", "_") - result = list(prolog.query("L=%s,sudoku(L)" % p, maxresult=1)) - if result: - result = result[0] - return result["L"] - else: - return False - - -def main(): - puzzle = puzzle1 - print("-- PUZZLE --") - pretty_print(puzzle) - print() - print(" -- SOLUTION --") - solution = solve(puzzle) - if solution: - pretty_print(solution) - else: - print("This puzzle has no solutions [is it valid?]") - - -if __name__ == "__main__": - prolog = Prolog() - main() diff --git a/examples/sudoku/sudoku_daily.py b/examples/sudoku/sudoku_daily.py deleted file mode 100644 index 3ebf852..0000000 --- a/examples/sudoku/sudoku_daily.py +++ /dev/null @@ -1,102 +0,0 @@ -# -*- coding: utf-8 -*- - -# pyswip -- Python SWI-Prolog bridge -# Copyright (c) 2007-2018 Yüce Tekol -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - - -# Sudoku auto-solver. Get today's sudoku at http://www.sudoku.org.uk/daily.asp -# and solve it - -from __future__ import print_function -from pyswip.prolog import Prolog -from pyswip.easy import * - -from html.parser import HTMLParser - -import urllib.request as urllib_request - - -class DailySudokuPuzzle(HTMLParser): - def __init__(self): - self.puzzle = [] - self.__in_td = False - HTMLParser.__init__(self) - - def handle_starttag(self, tag, attrs): - if tag == "td": - for attr in attrs: - if attr[0] == "class" and attr[1] == "InnerTDone": - self.__in_td = True - break - elif tag == "input": - if self.__in_td: - self.puzzle.append(0) - - def handle_endtag(self, tag): - if tag == "td": - self.__in_td = False - - def handle_data(self, data): - if self.__in_td: - self.puzzle.append(int(data)) - - -def pretty_print(table): - print("".join(["/---", "----" * 8, "\\"])) - for row in table: - print("".join(["|", "|".join(" %s " % (i or " ") for i in row), "|"])) - print("".join(["\\---", "----" * 8, "/"])) - - -def get_daily_sudoku(url): - puzzle = DailySudokuPuzzle() - f = urllib_request.urlopen(url) - puzzle.feed(f.read().decode("latin-1")) - puzzle = puzzle.puzzle - return [puzzle[i * 9 : i * 9 + 9] for i in range(9)] - - -def solve(problem): - prolog.consult("sudoku.pl") - p = str(problem).replace("0", "_") - result = list(prolog.query("Puzzle=%s,sudoku(Puzzle)" % p, maxresult=1)) - if result: - result = result[0] - return result["Puzzle"] - else: - return False - - -if __name__ == "__main__": - URL = "http://www.sudoku.org.uk/daily.asp" - - prolog = Prolog() # having this in `solve` bites! because of __del__ - print("Getting puzzle from:", URL) - puzzle = get_daily_sudoku(URL) - print("-- PUZZLE --") - pretty_print(puzzle) - print() - print(" -- SOLUTION --") - solution = solve(puzzle) - if solution: - pretty_print(solution) - else: - print("This puzzle has no solutions [is it valid?]") diff --git a/src/pyswip/examples/__init__.py b/src/pyswip/examples/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/sudoku/sudoku.pl b/src/pyswip/examples/sudoku.pl similarity index 100% rename from examples/sudoku/sudoku.pl rename to src/pyswip/examples/sudoku.pl diff --git a/src/pyswip/examples/sudoku.py b/src/pyswip/examples/sudoku.py new file mode 100644 index 0000000..955b8dc --- /dev/null +++ b/src/pyswip/examples/sudoku.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- + +# pyswip -- Python SWI-Prolog bridge +# Copyright (c) 2007-2024 Yüce Tekol +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import sys +from typing import List, Union, Literal +from io import StringIO + +from pyswip.prolog import Prolog + + +__all__ = "Matrix", "solve", "prolog_source" + +_DIMENSION = 9 +_SOURCE_PATH = "sudoku.pl" + + +Prolog.consult(_SOURCE_PATH, relative_to=__file__) + + +class Matrix: + def __init__(self, matrix: List[List[int]]) -> None: + if not matrix: + raise ValueError("matrix must be given") + if len(matrix) != _DIMENSION: + raise ValueError("Matrix dimension must be 9") + self._dimension = len(matrix) + self._validate(self._dimension, matrix) + self.matrix = matrix + + @classmethod + def from_text(cls, text: str) -> "Matrix": + lines = text.strip().split("\n") + dimension = len(lines) + rows = [] + for i, line in enumerate(lines): + cols = line.split() + if len(cols) != dimension: + raise ValueError( + f"All rows must have {dimension} columns, line {i+1} has {len(cols)}" + ) + rows.append([0 if x == "." else int(x) for x in cols]) + return cls(rows) + + @classmethod + def _validate(cls, dimension: int, matrix: List[List[int]]): + if len(matrix) != dimension: + raise ValueError(f"Matrix must have {dimension} rows, it has {len(matrix)}") + for i, row in enumerate(matrix): + if len(row) != dimension: + raise ValueError( + f"All rows must have {dimension} columns, row {i+1} has {len(row)}" + ) + + def __len__(self) -> int: + return self._dimension + + def __str__(self) -> str: + sio = StringIO() + self.pretty_print(file=sio) + return sio.getvalue() + + def __repr__(self) -> str: + return str(self.matrix) + + def pretty_print(self, *, file=sys.stdout) -> None: + for row in self.matrix: + row = " ".join(str(x or ".") for x in row) + print(row, file=file) + + +def solve(matrix: Matrix) -> Union[Matrix, Literal[False]]: + """ + Solves the given Sudoku puzzle + + Parameters: + matrix (Matrix): The matrix that contains the Sudoku puzzle + + Returns: + Matrix: Solution matrix + False: If no solutions was found + """ + p = repr(matrix).replace("0", "_") + result = list(Prolog.query(f"L={p},sudoku(L)", maxresult=1)) + if not result: + return False + result = result[0].get("L") + if not result: + return False + return Matrix(result) + + +def prolog_source() -> str: + from pathlib import Path + + path = Path(__file__).parent / _SOURCE_PATH + with open(path) as f: + return f.read() + + +def main(): + puzzle = Matrix.from_text(""" +. . 5 . 7 . 2 6 8 +. . 4 . . 2 . . . +. . 1 . 9 . . . . +. 8 . . . . 1 . . +. 2 . 9 . . . 7 . +. . 6 . . . . 3 . +. . 2 . 4 . 7 . . +. . . 5 . . 9 . . +9 5 7 . 3 . . . . + """) + print("\n-- PUZZLE --") + puzzle.pretty_print() + print("\n-- SOLUTION --") + solution = solve(puzzle) + if solution: + solution.pretty_print() + else: + print("This puzzle has no solutions. Is it valid?") + + +if __name__ == "__main__": + main() diff --git a/src/pyswip/prolog.py b/src/pyswip/prolog.py index 569edff..192854a 100644 --- a/src/pyswip/prolog.py +++ b/src/pyswip/prolog.py @@ -218,14 +218,13 @@ def query(cls, query, maxresult=-1, catcherrors=True, normalize=True): If the query is a yes/no question, returns {} for yes, and nothing for no. Otherwise returns a generator of dicts with variables as keys. - >>> prolog = Prolog() - >>> prolog.assertz("father(michael,john)") - >>> prolog.assertz("father(michael,gina)") - >>> bool(list(prolog.query("father(michael,john)"))) + >>> Prolog.assertz("father(michael,john)") + >>> Prolog.assertz("father(michael,gina)") + >>> bool(list(Prolog.query("father(michael,john)"))) True - >>> bool(list(prolog.query("father(michael,olivia)"))) + >>> bool(list(Prolog.query("father(michael,olivia)"))) False - >>> print sorted(prolog.query("father(michael,X)")) + >>> print sorted(Prolog.query("father(michael,X)")) [{'X': 'gina'}, {'X': 'john'}] """ return cls._QueryWrapper()(query, maxresult, catcherrors, normalize) diff --git a/tests/examples/__init__.py b/tests/examples/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/examples/test_sudoku.py b/tests/examples/test_sudoku.py new file mode 100644 index 0000000..2cc35be --- /dev/null +++ b/tests/examples/test_sudoku.py @@ -0,0 +1,58 @@ +import unittest + +from pyswip.examples.sudoku import Matrix, solve, prolog_source + + +class MatrixTestCase(unittest.TestCase): + FIXTURE = """ +. 6 . 1 . 4 . 5 . +. . 8 3 . 5 6 . . +2 . . . . . . . 1 +8 . . 4 . 7 . . 6 +. . 6 . . . 3 . . +7 . . 9 . 1 . . 4 +5 . . . . . . . 2 +. . 7 2 . 6 9 . . +. 4 . 5 . 8 . 7 . + """ + + def test_matrix_from_text(self): + got = Matrix.from_text(self.FIXTURE) + target = [ + [0, 6, 0, 1, 0, 4, 0, 5, 0], + [0, 0, 8, 3, 0, 5, 6, 0, 0], + [2, 0, 0, 0, 0, 0, 0, 0, 1], + [8, 0, 0, 4, 0, 7, 0, 0, 6], + [0, 0, 6, 0, 0, 0, 3, 0, 0], + [7, 0, 0, 9, 0, 1, 0, 0, 4], + [5, 0, 0, 0, 0, 0, 0, 0, 2], + [0, 0, 7, 2, 0, 6, 9, 0, 0], + [0, 4, 0, 5, 0, 8, 0, 7, 0], + ] + self.assertListEqual(target, got.matrix) + + def test_solve_success(self): + puzzle = Matrix.from_text(self.FIXTURE) + solution = solve(puzzle) + target = [ + [9, 6, 3, 1, 7, 4, 2, 5, 8], + [1, 7, 8, 3, 2, 5, 6, 4, 9], + [2, 5, 4, 6, 8, 9, 7, 3, 1], + [8, 2, 1, 4, 3, 7, 5, 9, 6], + [4, 9, 6, 8, 5, 2, 3, 1, 7], + [7, 3, 5, 9, 6, 1, 8, 2, 4], + [5, 8, 9, 7, 1, 3, 4, 6, 2], + [3, 1, 7, 2, 4, 6, 9, 8, 5], + [6, 4, 2, 5, 9, 8, 1, 7, 3], + ] + self.assertEqual(target, solution.matrix) + + def test_solve_failure(self): + fixture = " 8" + self.FIXTURE[2:] + puzzle = Matrix.from_text(fixture) + solution = solve(puzzle) + self.assertFalse(solution) + + def test_prolog_source(self): + text = prolog_source() + self.assertIn("Prolog Sudoku Solver", text) diff --git a/tests/test_examples.py b/tests/test_examples.py index d48acf3..0dd8059 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -26,280 +26,26 @@ ensure stability in several platforms. """ -import unittest - import pytest from pyswip import * +examples = [ + "create_term.py", + "father.py", + "register_foreign.py", + "register_foreign_simple.py", + "knowledgebase.py", + "hanoi/hanoi_simple.py", + "sendmoremoney/money.py", + "sendmoremoney/money_new.py", +] -class TestExamples(unittest.TestCase): - """ - Each test method is named after one example in $PYSWIP/examples. - - WARNING: Since it is not possible to unload things from the Prolog base, the - examples have to be 'orthogonal'. - - """ - - def test_create_term(self): - """ - Simple example of term creation. - """ - - prolog = Prolog() - - a1 = PL_new_term_refs(2) - a2 = a1 + 1 - t = PL_new_term_ref() - ta = PL_new_term_ref() - - animal2 = PL_new_functor(PL_new_atom("animal"), 2) - assertz = PL_new_functor(PL_new_atom("assertz"), 1) - - PL_put_atom_chars(a1, "gnu") - PL_put_integer(a2, 50) - PL_cons_functor_v(t, animal2, a1) - PL_cons_functor_v(ta, assertz, t) - PL_call(ta, None) - - result = list(prolog.query("animal(X,Y)", catcherrors=True)) - self.assertEqual(len(result), 1) - self.assertEqual(result[0], {"X": "gnu", "Y": 50}) - - def test_knowledgebase(self): - """ - Tests usage of modules. - """ - - _ = Prolog() - - assertz = Functor("assertz") - parent = Functor("parent", 2) - test1 = newModule("test1") - test2 = newModule("test2") - - call(assertz(parent("john", "bob")), module=test1) - call(assertz(parent("jane", "bob")), module=test1) - - call(assertz(parent("mike", "bob")), module=test2) - call(assertz(parent("gina", "bob")), module=test2) - - # Test knowledgebase module test1 - - result = set() - X = Variable() - q = Query(parent(X, "bob"), module=test1) - while q.nextSolution(): - result.add(X.value.value) # X.value is an Atom - q.closeQuery() - self.assertEqual(result, set(["john", "jane"])) - - # Test knowledgebase module test2 - - result = set() - q = Query(parent(X, "bob"), module=test2) - while q.nextSolution(): - result.add(X.value.value) - q.closeQuery() - self.assertEqual(result, set(["mike", "gina"])) - - def test_father(self): - """ - Tests basic inferences. - """ - - p = Prolog() - - father = Functor("father", 2) - mother = Functor("mother", 2) - - p.assertz("father(john,mich)") - p.assertz("father(john,gina)") - p.assertz("mother(jane,mich)") - - Y = Variable() - Z = Variable() - - result = [] - q = Query(father("john", Y), mother(Z, Y)) - while q.nextSolution(): - y = Y.value.value - z = Z.value.value - result.append({"Y": y, "Z": z}) - q.closeQuery() - - self.assertEqual(len(result), 1) - self.assertEqual(result[0], {"Y": "mich", "Z": "jane"}) - - # Repeat the same query but using strings - result = [] - for s in p.query("father(john,Y),mother(Z,Y)"): - result.append(s) - self.assertEqual(len(result), 1) - self.assertEqual(result[0], {"Y": "mich", "Z": "jane"}) - - def test_coins(self): - """ - Runs the coins example (uses clp library of SWI-Prolog). - """ - - prolog = Prolog() - prolog.consult(example_path("coins/coins.pl")) - count = 100 - total = 500 - coins = Functor("coins", 3) - S = Variable() - q = Query(coins(S, count, total)) - - solutions = [] - while q.nextSolution(): - solutions.append(S.value) - q.closeQuery() - self.assertEqual(len(solutions), 105) - - # Now do the same test, but using the prolog.query interface - solutions = list(prolog.query("coins(S, %d, %d)." % (count, total))) - self.assertEqual(len(solutions), 105) - - def test_draughts(self): - """ - Runs the draughts example (uses clp library of SWI-Prolog). - """ - - prolog = Prolog() - prolog.consult(example_path("draughts/puzzle1.pl")) - solutions = [] - for soln in prolog.query("solve(B)."): - solutions.append(soln["B"]) - self.assertEqual(len(solutions), 37) - - # Now do the same test, but using the prolog.query interface - solutions = list(prolog.query("solve(B).")) - self.assertEqual(len(solutions), 37) - - def test_hanoi(self): - """ - Runs the hanoi example. - """ - - N = 3 # Number of disks - - result = [] - - def notify(t): - result.append((t[0].value, t[1].value)) - - notify.arity = 1 - - prolog = Prolog() - registerForeign(notify) - prolog.consult(example_path("hanoi/hanoi.pl")) - list(prolog.query("hanoi(%d)" % N)) # Forces the query to run completely - - self.assertEqual(len(result), 7) - self.assertEqual(result[0], ("left", "right")) - self.assertEqual(result[1], ("left", "center")) - self.assertEqual(result[2], ("right", "center")) - - def test_sendmoremoney(self): - """ - Runs the sendmoremoney example:: - - S E N D - M O R E - + ------- - M O N E Y - - So, what should be the values of S, E, N, D, M, O, R, Y - if they are all distinct digits. - """ - - letters = "S E N D M O R Y".split() - prolog = Prolog() - sendmore = Functor("sendmore") - prolog.consult(example_path("sendmoremoney/money.pl")) - - X = Variable() - call(sendmore(X)) - r = X.value - val = {} - for i, letter in enumerate(letters): - val[letter] = r[i] - - self.assertEqual(len(val), 8) - - send = val["S"] * 1e3 + val["E"] * 1e2 + val["N"] * 1e1 + val["D"] * 1e0 - more = val["M"] * 1e3 + val["O"] * 1e2 + val["R"] * 1e1 + val["E"] * 1e0 - money = ( - val["M"] * 1e4 - + val["O"] * 1e3 - + val["N"] * 1e2 - + val["E"] * 1e1 - + val["Y"] * 1e0 - ) - self.assertEqual(money, send + more) - - def test_sudoku(self): - """ - Runs the sudoku example (uses clp library of SWI-Prolog). - """ - - _ = 0 - puzzle1 = [ - [_, 6, _, 1, _, 4, _, 5, _], - [_, _, 8, 3, _, 5, 6, _, _], - [2, _, _, _, _, _, _, _, 1], - [8, _, _, 4, _, 7, _, _, 6], - [_, _, 6, _, _, _, 3, _, _], - [7, _, _, 9, _, 1, _, _, 4], - [5, _, _, _, _, _, _, _, 2], - [_, _, 7, 2, _, 6, 9, _, _], - [_, 4, _, 5, _, 8, _, 7, _], - ] - - puzzle2 = [ - [_, _, 1, _, 8, _, 6, _, 4], - [_, 3, 7, 6, _, _, _, _, _], - [5, _, _, _, _, _, _, _, _], - [_, _, _, _, _, 5, _, _, _], - [_, _, 6, _, 1, _, 8, _, _], - [_, _, _, 4, _, _, _, _, _], - [_, _, _, _, _, _, _, _, 3], - [_, _, _, _, _, 7, 5, 2, _], - [8, _, 2, _, 9, _, 7, _, _], - ] - - prolog = Prolog() - prolog.consult(example_path("sudoku/sudoku.pl")) - - for i, problem in enumerate((puzzle1, puzzle2)): - p = str(problem).replace("0", "_") - result = list(prolog.query("L=%s,sudoku(L)" % p, maxresult=1)) - if result: - # Does a simple check on the result - result = result[0] - for j, line in enumerate(result["L"]): - self.assertEqual( - len(set(line)), 9, "Failure in line %d: %s" % (j, line) - ) - else: - self.fail("Failed while running example number %d" % i) - - @pytest.mark.slow - def test_large_db(self): - """ - Generates a large database, then runs query - """ - - num_facts = 125000 - prolog = Prolog() - for i in range(num_facts): - prolog.assertz("p(%s)" % i) - - results = [r for r in prolog.query("p(I)")] - self.assertEqual(len(results), num_facts) +@pytest.mark.parametrize("example", examples) +def test_example(example): + path = example_path(example) + execfile(path) def example_path(path): @@ -310,3 +56,16 @@ def example_path(path): os.path.split(os.path.abspath(__file__))[0], "..", "examples", path ) ).replace("\\", "\\\\") + + +def execfile(filepath, globals=None, locals=None): + if globals is None: + globals = {} + globals.update( + { + "__file__": filepath, + "__name__": "__main__", + } + ) + with open(filepath, "rb") as file: + exec(compile(file.read(), filepath, "exec"), globals, locals) diff --git a/tests/test_foreign.py b/tests/test_foreign.py index 12a654c..d9a7249 100644 --- a/tests/test_foreign.py +++ b/tests/test_foreign.py @@ -23,10 +23,9 @@ def hello(t): registerForeign(hello) - prolog = Prolog() - prolog.assertz("father(michael,john)") - prolog.assertz("father(michael,gina)") - result = list(prolog.query("father(michael,X), hello(X)")) + Prolog.assertz("mother(emily,john)") + Prolog.assertz("mother(emily,gina)") + result = list(Prolog.query("mother(emily,X), hello(X)")) self.assertEqual(len(result), 2, "Query should return two results") for name in ("john", "gina"): self.assertTrue( @@ -34,8 +33,6 @@ def hello(t): ) def test_nondeterministic_foreign(self): - prolog = Prolog() - def nondet(a, context): control = PL_foreign_control(context) context = PL_foreign_context(context) @@ -55,7 +52,7 @@ def nondet(a, context): nondet.arity = 1 registerForeign(nondet, flags=PL_FA_NONDETERMINISTIC) - result = list(prolog.query("nondet(X)")) + result = list(Prolog.query("nondet(X)")) self.assertEqual(len(result), 10, "Query should return 10 results") for i in range(10): @@ -78,9 +75,7 @@ def test_for_string(string, test_result): registerForeign(get_str) registerForeign(test_for_string) - prolog = Prolog() - - result = list(prolog.query("get_str(String), test_for_string(String, Result)")) + result = list(Prolog.query("get_str(String), test_for_string(String, Result)")) self.assertEqual( result[0]["Result"], "true", @@ -100,17 +95,14 @@ def get_list_of_lists(result): registerForeign(get_list_of_lists) - prolog = Prolog() - - result = list(prolog.query("get_list_of_lists(Result)")) + result = list(Prolog.query("get_list_of_lists(Result)")) self.assertTrue( {"Result": [[1], [2]]} in result, "Nested lists should be unified correctly as return value.", ) def test_dictionary(self): - prolog = Prolog() - result = list(prolog.query("X = dict{key1:value1 , key2: value2}")) + result = list(Prolog.query("X = dict{key1:value1 , key2: value2}")) dict = result[0] self.assertTrue( {"key1": "value1", "key2": "value2"} == dict["X"], @@ -118,8 +110,7 @@ def test_dictionary(self): ) def test_empty_dictionary(self): - prolog = Prolog() - result = list(prolog.query("X = dict{}")) + result = list(Prolog.query("X = dict{}")) dict = result[0] self.assertTrue( dict["X"] == {}, @@ -127,8 +118,7 @@ def test_empty_dictionary(self): ) def test_nested_dictionary(self): - prolog = Prolog() - result = list(prolog.query("X = dict{key1:nested{key:value} , key2: value2}")) + result = list(Prolog.query("X = dict{key1:nested{key:value} , key2: value2}")) dict = result[0] self.assertTrue( {"key1": {"key": "value"}, "key2": "value2"} == dict["X"], diff --git a/tests/test_issues.py b/tests/test_issues.py index ba4c437..01a7c6a 100644 --- a/tests/test_issues.py +++ b/tests/test_issues.py @@ -68,8 +68,7 @@ def test_issue_1(self): # issue 13, if it does not work, it will segfault Python. from pyswip import Prolog - prolog = Prolog() - prolog.assertz("randomTerm(michael,john)") + Prolog.assertz("randomTerm(michael,john)") def test_issue_8(self): """ @@ -89,10 +88,9 @@ def hello(t): registerForeign(hello) - prolog = Prolog() - prolog.assertz("parent(michael,john)") - prolog.assertz("parent(michael,gina)") - p = prolog.query("parent(michael,X), hello(X)") + Prolog.assertz("parent(michael,john)") + Prolog.assertz("parent(michael,gina)") + p = Prolog.query("parent(michael,X), hello(X)") result = list(p) # Will run over the iterator self.assertEqual(len(callsToHello), 2) # ['john', 'gina'] @@ -246,13 +244,12 @@ def test_issue_62(self): """ from pyswip import Prolog - prolog = Prolog() - prolog.consult("test_unicode.pl", catcherrors=True, relative_to=__file__) - atoms = list(prolog.query("unicode_atom(B).")) + Prolog.consult("test_unicode.pl", catcherrors=True, relative_to=__file__) + atoms = list(Prolog.query("unicode_atom(B).")) self.assertEqual(len(atoms), 3, "Query should return exactly three atoms") - strings = list(prolog.query("unicode_string(B).")) + strings = list(Prolog.query("unicode_string(B).")) self.assertEqual(len(strings), 1, "Query should return exactly one string") self.assertEqual(strings[0]["B"], b"\xd1\x82\xd0\xb5\xd1\x81\xd1\x82") diff --git a/tests/test_prolog.py b/tests/test_prolog.py index a870789..a84b870 100644 --- a/tests/test_prolog.py +++ b/tests/test_prolog.py @@ -112,7 +112,6 @@ def test_prolog_read_file(self): See: https://github.com/yuce/pyswip/issues/10 """ current_dir = os.path.dirname(os.path.abspath(__file__)) - prolog = pl.Prolog() path = os.path.join(current_dir, "test_read.pl") - prolog.consult(path) - list(prolog.query(f'read_file("{path}", S)')) + pl.Prolog.consult(path) + list(pl.Prolog.query(f'read_file("{path}", S)'))