From 6ad6548831bda2c8c8a9e30ad693e0c7649025f2 Mon Sep 17 00:00:00 2001 From: Jendrik Seipp Date: Sun, 22 Feb 2026 21:44:26 +0100 Subject: [PATCH 1/8] * Print peak memory usage. * Make package uv-ready. * Rename gbf to gbfs. --- .gitignore | 2 ++ README.md | 6 +++++- pyperplan/__main__.py | 12 +++++++++++- pyperplan/planner.py | 8 ++++++-- pyperplan/tools.py | 14 +++++++++++++- pyproject.toml | 16 ++++++++++++++++ 6 files changed, 53 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index a58ac436..bb81d372 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ *~ *.py[cod] +*.soln +uv.lock .tox/ build/ dist/ diff --git a/README.md b/README.md index 975838bf..74b3adf3 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,10 @@ From inside a repository clone: This makes the `pyperplan` command available globally or in your [virtual environment](https://docs.python.org/3/tutorial/venv.html) (recommended). +Alternatively, you can use [uv](https://docs.astral.sh/uv/): + + uv tool install pyperplan + # Usage The `pyperplan` executable accepts two arguments: a PDDL domain file and a @@ -45,7 +49,7 @@ By default, the planner performs a blind breadth-first search, which does not scale very well. Heuristic search algorithms are available. For example, to use greedy-best-first search with the FF heuristic, run - pyperplan -H hff -s gbf DOMAIN PROBLEM + pyperplan -H hff -s gbfs DOMAIN PROBLEM For a list of available search algorithms and heuristics, run diff --git a/pyperplan/__main__.py b/pyperplan/__main__.py index e9faf808..ad4c4392 100755 --- a/pyperplan/__main__.py +++ b/pyperplan/__main__.py @@ -33,6 +33,8 @@ write_solution, ) +from pyperplan import tools + def main(): # Commandline parsing @@ -74,8 +76,9 @@ def get_callable_names(callables, omit_string): format="%(asctime)s %(levelname)-8s %(message)s", stream=sys.stdout, ) + logging.info(f"Python version: {sys.version}") - hffpo_searches = ["gbf", "wastar", "ehs"] + hffpo_searches = ["gbfs", "wastar", "ehs"] if args.heuristic == "hffpo" and args.search not in hffpo_searches: print( "ERROR: hffpo can currently only be used with %s\n" % hffpo_searches, @@ -115,6 +118,13 @@ def get_callable_names(callables, omit_string): write_solution(solution, solution_file) validate_solution(args.domain, args.problem, solution_file) + try: + peak_memory = tools.get_peak_memory_in_kb() + except Warning as warning: + logging.warning(warning) + else: + logging.info("Peak memory: %d KB" % peak_memory) + if __name__ == "__main__": main() diff --git a/pyperplan/planner.py b/pyperplan/planner.py index bb716f5a..941f842c 100644 --- a/pyperplan/planner.py +++ b/pyperplan/planner.py @@ -30,7 +30,7 @@ SEARCHES = { "astar": search.astar_search, "wastar": search.weighted_astar_search, - "gbf": search.greedy_best_first_search, + "gbfs": search.greedy_best_first_search, "bfs": search.breadth_first_search, "ehs": search.enforced_hillclimbing_search, "ids": search.iterative_deepening_search, @@ -171,6 +171,7 @@ def search_plan( interface @return A list of actions that solve the problem """ + overall_start_time = time.process_time() problem = _parse(domain_file, problem_file) task = _ground(problem) heuristic = None @@ -181,7 +182,10 @@ def search_plan( solution = _search(task, search, heuristic, use_preferred_ops=True) else: solution = _search(task, search, heuristic) - logging.info("Search time: {:.2}".format(time.process_time() - search_start_time)) + logging.info("Search time: {:.2f}".format(time.process_time() - search_start_time)) + logging.info( + "Overall time: {:.2f}".format(time.process_time() - overall_start_time) + ) return solution diff --git a/pyperplan/tools.py b/pyperplan/tools.py index aa0b6f14..e3cb3664 100644 --- a/pyperplan/tools.py +++ b/pyperplan/tools.py @@ -15,7 +15,6 @@ # along with this program. If not, see # -import importlib import logging import os import subprocess @@ -47,3 +46,16 @@ def remove(filename): os.remove(filename) except OSError: pass + + +def get_peak_memory_in_kb(): + try: + # This will only work on Linux systems. + with open("/proc/self/status") as status_file: + for line in status_file: + parts = line.split() + if parts[0] == "VmPeak:": + return int(parts[1]) + except OSError: + pass + raise Warning("warning: could not determine peak memory") diff --git a/pyproject.toml b/pyproject.toml index a398c443..16e7d7c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,19 @@ +[project] +name = "pyperplan" +version = "2.1" +description = "A lightweight STRIPS planner written in Python." +readme = "README.md" +requires-python = ">=3.7" +license = { text = "GPL3+" } +dependencies = [] + +[project.scripts] +pyperplan = "pyperplan.__main__:main" + +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + # NOTE: you have to use single-quoted strings in TOML for regular expressions. # It's the equivalent of r-strings in Python. Multiline strings are treated as # verbose regular expressions by Black. Use [ ] to denote a significant space From 865e19afd3dc3215f4b3e02aa1f0360746095ce5 Mon Sep 17 00:00:00 2001 From: Jendrik Seipp Date: Sun, 22 Feb 2026 22:12:05 +0100 Subject: [PATCH 2/8] Polish code with ruff. --- pyperplan/__main__.py | 7 ++-- pyperplan/grounding.py | 5 +-- pyperplan/heuristics/landmarks.py | 2 +- pyperplan/heuristics/lm_cut.py | 16 ++++---- pyperplan/heuristics/relaxation.py | 5 +-- pyperplan/pddl/lisp_iterators.py | 2 +- pyperplan/pddl/parser.py | 19 ++++----- pyperplan/pddl/tree_visitor.py | 40 +++++++++---------- pyperplan/planner.py | 3 +- pyperplan/search/a_star.py | 2 +- pyperplan/search/breadth_first_search.py | 2 +- .../search/enforced_hillclimbing_search.py | 4 +- .../search/iterative_deepening_search.py | 1 - pyperplan/search/minisat.py | 1 - pyperplan/search/sat.py | 3 +- pyperplan/tests/heuristic_test_instances.py | 2 - pyperplan/tests/test_a_star.py | 1 - pyperplan/tests/test_all_problems.py | 3 +- pyperplan/tests/test_grounding.py | 6 +-- pyperplan/tests/test_landmarks.py | 20 +++++----- pyperplan/tests/test_lm_cut.py | 10 ++--- pyperplan/tests/test_parcprinter.py | 1 - pyperplan/tests/test_parser_pddl_complex.py | 13 +++--- pyperplan/tests/test_parser_pddl_simple.py | 13 +++--- pyperplan/tests/test_parser_regression.py | 3 +- pyperplan/tests/test_relaxation.py | 2 +- pyperplan/tests/test_sat.py | 3 +- pyperplan/tests/test_searchalgorithms.py | 8 ++-- pyperplan/tests/test_searchspace.py | 1 - pyperplan/tests/test_task.py | 1 - pyperplan/tests/test_tree_visitor.py | 5 +-- pyperplan/tests/test_validator.py | 1 - pyperplan/tools.py | 5 +-- 33 files changed, 92 insertions(+), 118 deletions(-) diff --git a/pyperplan/__main__.py b/pyperplan/__main__.py index ad4c4392..f2ce9f2d 100755 --- a/pyperplan/__main__.py +++ b/pyperplan/__main__.py @@ -24,17 +24,16 @@ import os import sys +from pyperplan import tools from pyperplan.planner import ( - find_domain, HEURISTICS, - search_plan, SEARCHES, + find_domain, + search_plan, validate_solution, write_solution, ) -from pyperplan import tools - def main(): # Commandline parsing diff --git a/pyperplan/grounding.py b/pyperplan/grounding.py index b8f94569..1e5de877 100644 --- a/pyperplan/grounding.py +++ b/pyperplan/grounding.py @@ -20,14 +20,13 @@ task. """ -from collections import defaultdict import itertools import logging import re +from collections import defaultdict from .task import Operator, Task - # controls mass log output verbose_logging = False @@ -159,7 +158,7 @@ def _relevance_analysis(operators, goals): if debug: logging.info("Relevance analysis removed %d facts" % len(debug_pruned_op)) # remove completely irrelevant operators - return [op for op in operators if not op in del_operators] + return [op for op in operators if op not in del_operators] def _get_statics(predicates, actions): diff --git a/pyperplan/heuristics/landmarks.py b/pyperplan/heuristics/landmarks.py index 207ef96b..1f1bf21b 100644 --- a/pyperplan/heuristics/landmarks.py +++ b/pyperplan/heuristics/landmarks.py @@ -19,8 +19,8 @@ Landmarks Heuristic """ -from collections import defaultdict import copy +from collections import defaultdict from .heuristic_base import Heuristic diff --git a/pyperplan/heuristics/lm_cut.py b/pyperplan/heuristics/lm_cut.py index 4bc58a28..7e589e46 100644 --- a/pyperplan/heuristics/lm_cut.py +++ b/pyperplan/heuristics/lm_cut.py @@ -19,8 +19,8 @@ Implementation of LM-cut heuristic. """ -from heapq import * import logging +from heapq import * from .heuristic_base import Heuristic @@ -165,7 +165,7 @@ def link_op_to_effect(relaxed_op, factname): self.relaxed_facts[fact] = RelaxedFact(fact) for op in task.operators: - assert not op.name in self.relaxed_ops + assert op.name not in self.relaxed_ops # build new relaxed operator from the task operator relaxed_op = RelaxedOp(op.name) # insert all preconditions into relaxed_op and @@ -175,7 +175,7 @@ def link_op_to_effect(relaxed_op, factname): # insert one fact that is always true if not already defined # --> this fact will be used for all operators with empty # preconditions - if not self.always_true in self.relaxed_facts: + if self.always_true not in self.relaxed_facts: self.relaxed_facts[self.always_true] = RelaxedFact(self.always_true) link_op_to_precondition(relaxed_op, self.always_true) else: @@ -235,7 +235,7 @@ def compute_hmax(self, state, clear_op_cost=True): # --> if this is not the case then precond_fulfilled might # still contain facts from a previous heuristic computation # hence we need to clear it first! - if not op in op_cleared: + if op not in op_cleared: op.clear(clear_op_cost) op_cleared.add(op) op.preconditions_unsat -= 1 @@ -251,13 +251,13 @@ def compute_hmax(self, state, clear_op_cost=True): op.hmax_value = hmax_value + op.cost hmax_next = op.hmax_supporter.hmax_value + op.cost for eff in op.effects: - if not eff in fact_cleared: + if eff not in fact_cleared: # clear fact if necessary eff.clear() fact_cleared.add(eff) if hmax_next < eff.hmax_value: eff.hmax_value = hmax_next - if not eff in facts_seen: + if eff not in facts_seen: # enqueue effect if not already explored facts_seen.add(eff) heappush(unexpanded, eff) @@ -305,7 +305,7 @@ def compute_goal_plateau(self, fact_name): fact_in_plateau = self.relaxed_facts[fact_name] if ( fact_in_plateau in self.reachable - and not fact_in_plateau in self.goal_plateau + and fact_in_plateau not in self.goal_plateau ): # add this fact to the goal plateau self.goal_plateau.add(fact_in_plateau) @@ -332,7 +332,7 @@ def find_cut(self, state): while unexpanded: fact_obj = heappop(unexpanded) for relaxed_op in fact_obj.precondition_of: - if not relaxed_op in op_cleared: + if relaxed_op not in op_cleared: relaxed_op.precond_unsat = len(relaxed_op.precondition) op_cleared.add(relaxed_op) relaxed_op.precond_unsat -= 1 diff --git a/pyperplan/heuristics/relaxation.py b/pyperplan/heuristics/relaxation.py index 4f85e2bd..fbed1a80 100644 --- a/pyperplan/heuristics/relaxation.py +++ b/pyperplan/heuristics/relaxation.py @@ -16,12 +16,9 @@ # import heapq -import logging -from ..task import Operator, Task from .heuristic_base import Heuristic - """ This module contains the relaxation heuristics hAdd, hMax, hSA and hFF. """ @@ -438,7 +435,7 @@ def calc_goal_h(self, return_relaxed_plan=False): # is not already expanded if ( fact.cheapest_achiever is not None - and not fact.cheapest_achiever in relaxed_plan + and fact.cheapest_achiever not in relaxed_plan ): # Add all preconditions of the cheapest achiever to the # queue. diff --git a/pyperplan/pddl/lisp_iterators.py b/pyperplan/pddl/lisp_iterators.py index e685670e..f516c2ce 100644 --- a/pyperplan/pddl/lisp_iterators.py +++ b/pyperplan/pddl/lisp_iterators.py @@ -60,7 +60,7 @@ def is_structure(self): def empty(self): self._raise_if(self.is_word(), "cannot call empty on word") - return self.peek() == None + return self.peek() is None def get_word(self): """If called on a word, return the word as a string. diff --git a/pyperplan/pddl/parser.py b/pyperplan/pddl/parser.py index 712289a7..4a60545d 100644 --- a/pyperplan/pddl/parser.py +++ b/pyperplan/pddl/parser.py @@ -21,7 +21,6 @@ from .parser_common import * from .tree_visitor import TraversePDDLDomain, TraversePDDLProblem, Visitable - """ This module contains the main parser logic. Partial parser for each AST node are implemented @@ -66,7 +65,7 @@ def __init__(self, name, types=None): """ self._visitorName = "visit_variable" self.name = name - self.typed = types != None # either True or False + self.typed = types is not None # either True or False self.types = types # either None or a List of Types @@ -254,7 +253,7 @@ def __init__( self.requirements = requirements # a RequirementsStmt self.types = types # a list of Types self.predicates = predicates # a PredicatesStmt - if actions == None: + if actions is None: self.actions = [] else: self.actions = actions # a list of ActionStmt @@ -347,7 +346,7 @@ def parse_list_template(f, iter): # parse all possible occurences up to the end of the substring for elem in iter: var = f(elem) - if var != None: + if var is not None: result.append(var) return result @@ -400,7 +399,7 @@ def _parse_type_helper(iter, type_class): result.append(type_class(tmpList.pop(), [ctype])) else: result.append(type_class(tmpList.pop(), ctype)) - elif var != None and var != "": + elif var is not None and var != "": # found new object definition --> enqueue if type_class == Variable: if var[0] != "?": @@ -523,8 +522,10 @@ def _parse_domain_helper(iter, keyword): return DomainStmt(name) -parse_domain_stmt = lambda it: _parse_domain_helper(it, "domain") -parse_problem_domain_stmt = lambda it: _parse_domain_helper(it, ":domain") +def parse_domain_stmt(it): + return _parse_domain_helper(it, "domain") +def parse_problem_domain_stmt(it): + return _parse_domain_helper(it, ":domain") def parse_predicate(iter): @@ -850,7 +851,7 @@ def set_prob_file(self, fname): argparser.add_argument(dest="domain", help="specify domain file") argparser.add_argument(dest="problem", help="specify problem file", nargs="?") options = argparser.parse_args() - if options.domain == None: + if options.domain is None: parser.print_usage() parser.error("Error domain file must be specified") pddlParser = Parser(options.domain) @@ -858,7 +859,7 @@ def set_prob_file(self, fname): domain = pddlParser.parse_domain() print("++++++++ parsed domain file successfully") print(domain) - if options.problem != None: + if options.problem is not None: print("-------- Starting to parse supplied problem file!") pddlParser.set_prob_file(options.problem) problem = pddlParser.parse_problem(domain) diff --git a/pyperplan/pddl/tree_visitor.py b/pyperplan/pddl/tree_visitor.py index 6b7387a8..d087f946 100644 --- a/pyperplan/pddl/tree_visitor.py +++ b/pyperplan/pddl/tree_visitor.py @@ -56,7 +56,7 @@ def __init__(self, vname=None): self._visitorName = vname def accept(self, visitor): - if self._visitorName == None: + if self._visitorName is None: raise ValueError("Error: visit method of uninitialized visitor " "called!") # get the appropriate method of the visitor instance m = getattr(visitor, self._visitorName) @@ -79,14 +79,14 @@ class PDDLVisitor: def visit_domain_def(self, node): node.requirements.accept(self) - if node.types != None: + if node.types is not None: for t in node.types: t.accept(self) - if node.constants != None: + if node.constants is not None: for c in node.constants: c.accept(self) node.predicates.accept(self) - if node.actions != None: + if node.actions is not None: for a in node.actions: a.accept(self) @@ -188,7 +188,7 @@ def visit_domain_def(self, node): node.requirements.accept(self) # Visit all type definitions. - if node.types != None: + if node.types is not None: for t in node.types: if t.name == "object": explicitObjectDef = True @@ -205,7 +205,7 @@ def visit_domain_def(self, node): # Object type has no parent. if t.name == "object": continue - if not t.parent in self._types: + if t.parent not in self._types: raise SemanticError("Error unknown parent type: " + t.parent) t.parent = self._types[t.parent] @@ -213,7 +213,7 @@ def visit_domain_def(self, node): node.predicates.accept(self) # Visit all actions. - if node.actions != None: + if node.actions is not None: for a in node.actions: a.accept(self) action = self.get_in(a) @@ -226,7 +226,7 @@ def visit_domain_def(self, node): self._actions[action.name] = action # Visit all constants. - if node.constants != None: + if node.constants is not None: for c in node.constants: c.accept(self) @@ -238,9 +238,9 @@ def visit_domain_def(self, node): def visit_object(self, node): """Visits a PDDL object definition.""" type_name = node.typeName - if type_name == None: + if type_name is None: type_name = "object" - if not type_name in self._types: + if type_name not in self._types: raise SemanticError( "Error: unknown type " + type_name + " used in object definition!" ) @@ -255,7 +255,7 @@ def visit_type(self, node): """Visits a PDDL type definition.""" # Store matching parent type in node # (if none is given, it's always object) - if node.parent == None: + if node.parent is None: self.set_in(node, pddl.Type(node.name, "object")) else: self.set_in(node, pddl.Type(node.name, node.parent)) @@ -312,7 +312,7 @@ def visit_variable(self, node): typelist = list() for t in node.types: # Check whether they have been defined. - if not t in self._types: + if t not in self._types: raise SemanticError( "Error unknown type " + t + " used in predicate definition" ) @@ -384,7 +384,7 @@ def visit_precondition_stmt(self, node): + "".join([c2.key.name + " " for c2 in formula.children]) ) # Check whether predicate was defined. - if not c.key in self._predicates: + if c.key not in self._predicates: raise SemanticError( "Error unknown predicate " + c.key @@ -394,7 +394,7 @@ def visit_precondition_stmt(self, node): self.add_precond(precond, c) else: # If not 'and' we only allow a single predicate in precondition. - if not formula.key in self._predicates: + if formula.key not in self._predicates: raise SemanticError("Error: predicate in precondition is not " "in CNF") # Call helper. self.add_precond(precond, formula) @@ -424,12 +424,12 @@ def add_effect(self, effect, c): else: nextPredicate = c # Check whether predicate was defined previously. - if not nextPredicate.key in self._predicates: + if nextPredicate.key not in self._predicates: raise SemanticError( "Error: unknown predicate %s used in effect " "of action" % nextPredicate.key ) - if nextPredicate == None: + if nextPredicate is None: raise SemanticError("Error: NoneType predicate used in effect of " "action") predDef = self._predicates[nextPredicate.key] signature = list() @@ -543,11 +543,11 @@ def visit_object(self, node): "Error multiple defines of object with name " + node.name ) # Untyped objects get the standard type 'object'. - if node.typeName == None: + if node.typeName is None: type_def = self._domain.types["object"] else: # Check whether used type was introduced in domain file. - if not node.typeName in self._domain.types: + if node.typeName not in self._domain.types: raise SemanticError( "Error: unknown type " + node.typeName @@ -574,7 +574,7 @@ def add_goal(self, goal, c): c -- a formula representing a goal we want to add to the goal list """ # Check whether predicate was introduced in domain file. - if not c.key in self._domain.predicates: + if c.key not in self._domain.predicates: raise SemanticError( "Error: unknown predicate " + c.key + " in goal definition" ) @@ -610,7 +610,7 @@ def visit_goal_stmt(self, node): self.add_goal(goal, c) else: # Only a single predicate is allowed then (s.a.) - if not formula.key in self._domain.predicates: + if formula.key not in self._domain.predicates: raise SemanticError( "Error: predicate in goal definition is " "not in CNF" ) diff --git a/pyperplan/planner.py b/pyperplan/planner.py index 941f842c..5d5c4d91 100644 --- a/pyperplan/planner.py +++ b/pyperplan/planner.py @@ -26,7 +26,6 @@ from . import grounding, heuristics, search, tools from .pddl.parser import Parser - SEARCHES = { "astar": search.astar_search, "wastar": search.weighted_astar_search, @@ -175,7 +174,7 @@ def search_plan( problem = _parse(domain_file, problem_file) task = _ground(problem) heuristic = None - if not heuristic_class is None: + if heuristic_class is not None: heuristic = heuristic_class(task) search_start_time = time.process_time() if use_preferred_ops and isinstance(heuristic, heuristics.hFFHeuristic): diff --git a/pyperplan/search/a_star.py b/pyperplan/search/a_star.py index 26e56210..79370b14 100644 --- a/pyperplan/search/a_star.py +++ b/pyperplan/search/a_star.py @@ -165,7 +165,7 @@ def astar_search( for op, succ_state in task.get_successor_states(pop_state): if use_relaxed_plan: - if rplan and not op.name in rplan: + if rplan and op.name not in rplan: # ignore this operator if we use the relaxed plan # criterion logging.debug( diff --git a/pyperplan/search/breadth_first_search.py b/pyperplan/search/breadth_first_search.py index 71f25b11..13fd1d1e 100644 --- a/pyperplan/search/breadth_first_search.py +++ b/pyperplan/search/breadth_first_search.py @@ -19,8 +19,8 @@ Implements the breadth first search algorithm. """ -from collections import deque import logging +from collections import deque from . import searchspace diff --git a/pyperplan/search/enforced_hillclimbing_search.py b/pyperplan/search/enforced_hillclimbing_search.py index af6f9f71..5f1a963b 100644 --- a/pyperplan/search/enforced_hillclimbing_search.py +++ b/pyperplan/search/enforced_hillclimbing_search.py @@ -19,8 +19,8 @@ Implements the enforced hill climbing search algorithm. """ -from collections import deque import logging +from collections import deque from . import searchspace @@ -70,7 +70,7 @@ def enforced_hillclimbing_search(planning_task, heuristic, use_preferred_ops=Fal # for the preferred operator version ignore all non preferred # operators if use_preferred_ops: - if rplan and not operator.name in rplan: + if rplan and operator.name not in rplan: # ignore this operator if we use the relaxed plan criterion logging.debug( "removing operator %s << not a preferred " diff --git a/pyperplan/search/iterative_deepening_search.py b/pyperplan/search/iterative_deepening_search.py index 027dba4d..a9dcff80 100644 --- a/pyperplan/search/iterative_deepening_search.py +++ b/pyperplan/search/iterative_deepening_search.py @@ -19,7 +19,6 @@ Implements the iterative deepening search algorithm. """ -from collections import deque import logging diff --git a/pyperplan/search/minisat.py b/pyperplan/search/minisat.py index 4c0b97fc..654f4e7e 100644 --- a/pyperplan/search/minisat.py +++ b/pyperplan/search/minisat.py @@ -5,7 +5,6 @@ from pyperplan import tools - INPUT = "input.cnf" OUTPUT = "output.txt" MINISAT = "minisat" diff --git a/pyperplan/search/sat.py b/pyperplan/search/sat.py index 20ed7363..ff64e04e 100644 --- a/pyperplan/search/sat.py +++ b/pyperplan/search/sat.py @@ -1,9 +1,8 @@ -from collections import defaultdict import logging +from collections import defaultdict from . import minisat - # Max number of steps in a plan HORIZON = 1000 diff --git a/pyperplan/tests/heuristic_test_instances.py b/pyperplan/tests/heuristic_test_instances.py index 804cba07..44b0dcab 100644 --- a/pyperplan/tests/heuristic_test_instances.py +++ b/pyperplan/tests/heuristic_test_instances.py @@ -1,8 +1,6 @@ from pyperplan import grounding from pyperplan.pddl.parser import Parser from pyperplan.search import astar_search, enforced_hillclimbing_search, searchspace -from pyperplan.task import Operator, Task - blocks_dom = """\ (define (domain BLOCKS) diff --git a/pyperplan/tests/test_a_star.py b/pyperplan/tests/test_a_star.py index 177c8039..13cad7db 100644 --- a/pyperplan/tests/test_a_star.py +++ b/pyperplan/tests/test_a_star.py @@ -2,7 +2,6 @@ from . import dummy_task - # create 4 dummy tasks task1 = dummy_task.get_search_space_at_goal() task2 = dummy_task.get_simple_search_space() diff --git a/pyperplan/tests/test_all_problems.py b/pyperplan/tests/test_all_problems.py index eb72dd61..a726930e 100644 --- a/pyperplan/tests/test_all_problems.py +++ b/pyperplan/tests/test_all_problems.py @@ -2,15 +2,14 @@ Tests for parsing and grounding all problems """ -from glob import glob import os +from glob import glob import pytest from pyperplan import planner from pyperplan.search import breadth_first_search - benchmarks = os.path.abspath( os.path.join(os.path.abspath(__file__), "../../../benchmarks") ) diff --git a/pyperplan/tests/test_grounding.py b/pyperplan/tests/test_grounding.py index 62fd64e8..b3c92806 100644 --- a/pyperplan/tests/test_grounding.py +++ b/pyperplan/tests/test_grounding.py @@ -142,7 +142,7 @@ def test_statics1(): def test_statics2(): - type_object = Type("object", None) + Type("object", None) predicate_a = Predicate("a", []) predicate_b = Predicate("b", []) @@ -300,7 +300,7 @@ def test_operators(): ) problem = Problem("test_problem", domain, objects, initial_state, goal_state) - task = grounding.ground(problem) + grounding.ground(problem) expected = [ ("(DRIVE-CAR red_car freiburg basel)", grounded_drive_car), @@ -315,7 +315,7 @@ def test_operators(): def test_create_operator(): - statics = grounding._get_statics( + grounding._get_statics( standard_domain.predicates.values(), [action_drive_car] ) initial_state = [ diff --git a/pyperplan/tests/test_landmarks.py b/pyperplan/tests/test_landmarks.py index 20b5e82a..76a3aaf6 100644 --- a/pyperplan/tests/test_landmarks.py +++ b/pyperplan/tests/test_landmarks.py @@ -75,7 +75,7 @@ def test_heuristics(): ) # task with one operator with equal precondition and effect, - task4b = Task( + Task( "task4b", {"A", "B", "C"}, frozenset(["A"]), @@ -85,7 +85,7 @@ def test_heuristics(): # task with one operator with several effects, # 2 operators have to be applied - task5 = Task( + Task( "task5", {"A", "B", "C", "D", "E", "F"}, ["A"], @@ -97,7 +97,7 @@ def test_heuristics(): ) # task with one operator with several preconditions - task6 = Task( + Task( "task6", {"A", "B", "C", "D", "E"}, ["A"], @@ -111,7 +111,7 @@ def test_heuristics(): ) # task with empty initial state: no operator can be applied - task7 = Task( + Task( "task7", {"A", "B", "C"}, [], @@ -120,7 +120,7 @@ def test_heuristics(): ) # task with initial state = goal state: no operator has to be applied - task8 = Task( + Task( "task8", {"A", "B", "C"}, ["C"], @@ -129,7 +129,7 @@ def test_heuristics(): ) # task with operator with empty precondition - task9 = Task( + Task( "task9", {"A", "B", "C"}, [], @@ -138,7 +138,7 @@ def test_heuristics(): ) # a more complex task - task10 = Task( + Task( "task10", {"v1", "v2", "v3", "v4", "v5", "v6", "g"}, ["v1"], @@ -154,7 +154,7 @@ def test_heuristics(): ) # another complex task - task12 = Task( + Task( "task12", {"A", "B", "C", "D", "E", "F", "G", "H", "I"}, ["A", "B"], @@ -169,7 +169,7 @@ def test_heuristics(): ) # task with no goal: - task13 = Task( + Task( "task13", {"A", "B", "C"}, ["A", "B"], @@ -177,7 +177,7 @@ def test_heuristics(): [Operator("op1", {"A", "B"}, {"C"}, set())], ) # task with no reachable goal: - task14 = Task( + Task( "task14", {"A", "B", "C"}, ["A"], diff --git a/pyperplan/tests/test_lm_cut.py b/pyperplan/tests/test_lm_cut.py index d06d682f..eb0fe380 100644 --- a/pyperplan/tests/test_lm_cut.py +++ b/pyperplan/tests/test_lm_cut.py @@ -3,7 +3,7 @@ from pyperplan import grounding from pyperplan.heuristics.lm_cut import LmCutHeuristic from pyperplan.pddl.parser import Parser -from pyperplan.search import astar_search, enforced_hillclimbing_search, make_root_node +from pyperplan.search import make_root_node from pyperplan.task import Operator, Task from .heuristic_test_instances import ( @@ -12,7 +12,6 @@ gen_blocks_test_astar, ) - """ Test problems """ @@ -207,7 +206,7 @@ def test_lm_cut_relaxed_operators(): assert heuristic.relaxed_ops["op3"].cost == 1 assert [f.name for f in heuristic.relaxed_ops["op3"].precondition] == ["var2"] assert [f.name for f in heuristic.relaxed_ops["op3"].effects] == ["var1"] - assert not "ALWAYSTRUE" in heuristic.relaxed_facts + assert "ALWAYSTRUE" not in heuristic.relaxed_facts def test_lm_cut_relaxed_operators2(): @@ -276,7 +275,7 @@ def test_lm_cut_hmax_intermediate_two_paths(): assert heuristic.relaxed_facts["v2"].hmax_value == 1.0 assert heuristic.relaxed_facts["v3"].hmax_value == 2.0 assert heuristic.relaxed_facts["v6"].hmax_value == float("inf") - assert not heuristic.relaxed_facts["v6"] in heuristic.reachable + assert heuristic.relaxed_facts["v6"] not in heuristic.reachable assert heuristic.relaxed_facts["v4"].hmax_value == 2.0 assert heuristic.relaxed_facts["v5"].hmax_value == 2.0 assert heuristic.relaxed_facts["v7"].hmax_value == 3.0 @@ -402,7 +401,6 @@ def test_lm_cut_blocksworld_complete_astar(): @pytest.mark.slow def test_lm_cut_blocksworld_complete_enforced_hillclimbing(): - true_h_values = [6.0, 5.0, 5.0, 4.0, 5.0, 5.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0, 0.0] - plan_length = 16 + pass # TODO: Result is currently nondeterministic. # gen_blocks_test_ehc(LmCutHeuristic, true_h_values, plan_length) diff --git a/pyperplan/tests/test_parcprinter.py b/pyperplan/tests/test_parcprinter.py index d7bc4c49..7c85b12f 100644 --- a/pyperplan/tests/test_parcprinter.py +++ b/pyperplan/tests/test_parcprinter.py @@ -8,7 +8,6 @@ from pyperplan import planner from pyperplan.search import searchspace - optimal_plan = """\ initialize blackfeeder-feed-letter sheet1 diff --git a/pyperplan/tests/test_parser_pddl_complex.py b/pyperplan/tests/test_parser_pddl_complex.py index 4b7d30f2..e68073d7 100644 --- a/pyperplan/tests/test_parser_pddl_complex.py +++ b/pyperplan/tests/test_parser_pddl_complex.py @@ -3,7 +3,6 @@ from pyperplan.pddl.lisp_parser import parse_lisp_iterator from pyperplan.pddl.parser import * - ### helper functions @@ -124,7 +123,7 @@ def test_parseTypes(): "place", "physobj", ] - assert [t.parent for t in types if t.parent != None] == [ + assert [t.parent for t in types if t.parent is not None] == [ "vehicle", "vehicle", "physobj", @@ -270,7 +269,7 @@ def test_parseDomainDef2(): ] iter = parse_lisp_iterator(test) with raises(ValueError): - dom = parse_domain_def(iter) + parse_domain_def(iter) def test_parseDomainDef(): @@ -327,7 +326,7 @@ def test_parseDomainDef(): ] iter = parse_lisp_iterator(test) with raises(ValueError): - dom = parse_domain_def(iter) + parse_domain_def(iter) def test_predList2(): @@ -351,7 +350,7 @@ def test_predList2(): assert [ p.parameters[0].types[0] for p in pred.predicates - if p.parameters[0].types != None + if p.parameters[0].types is not None ] == ["person", "person", "aircraft", "flevel"] @@ -366,7 +365,7 @@ def test_predList3(): ] iter = parse_lisp_iterator(test) with raises(ValueError): - pred = parse_predicates_stmt(iter) + parse_predicates_stmt(iter) def test_predList4(): @@ -380,7 +379,7 @@ def test_predList4(): ] iter = parse_lisp_iterator(test) with raises(ValueError): - pred = parse_predicates_stmt(iter) + parse_predicates_stmt(iter) def test_parseObjectsStmt(): diff --git a/pyperplan/tests/test_parser_pddl_simple.py b/pyperplan/tests/test_parser_pddl_simple.py index 90c0a2a2..5c5079a0 100644 --- a/pyperplan/tests/test_parser_pddl_simple.py +++ b/pyperplan/tests/test_parser_pddl_simple.py @@ -4,7 +4,6 @@ from pyperplan.pddl.lisp_parser import parse_lisp_iterator from pyperplan.pddl.parser import * - ## helper functions @@ -63,8 +62,8 @@ def test_parseVariableNoTyping(): iter = parse_lisp_iterator(test) key = parse_variable(next(iter)) assert key.name == "?x" - assert key.typed == False - assert key.types == None + assert key.typed is False + assert key.types is None def test_parseVariableTyping(): @@ -73,7 +72,7 @@ def test_parseVariableTyping(): vlist = parse_typed_var_list(iter) assert len(vlist) == 1 assert vlist[0].name == "?x" - assert vlist[0].typed == True + assert vlist[0].typed is True assert vlist[0].types[0] == "block" @@ -147,7 +146,7 @@ def test_parsePredicateMixed(): pred = parse_predicate(iter) assert pred.name == "on" assert [x.name for x in pred.parameters] == ["?x", "?y"] - assert [x.types[0] for x in pred.parameters if x.types != None] == ["block"] + assert [x.types[0] for x in pred.parameters if x.types is not None] == ["block"] def test_parsePredicateList(): @@ -186,7 +185,7 @@ def test_parseFormulaFail(): def test_parseFormulaLispFail2(): test = ["(and (on ?x table) (true) (free( ?x))"] with raises(ParseError): - iter = parse_lisp_iterator(test) + parse_lisp_iterator(test) def test_parse_variable(): @@ -199,7 +198,7 @@ def test_parse_variable(): def test_lisp_parser_start_brace(): test = ["test string)"] with raises(ParseError): - iter = parse_lisp_iterator(test) + parse_lisp_iterator(test) def test_parse_keyword_raise(): diff --git a/pyperplan/tests/test_parser_regression.py b/pyperplan/tests/test_parser_regression.py index 6ba0a0ce..70932bde 100644 --- a/pyperplan/tests/test_parser_regression.py +++ b/pyperplan/tests/test_parser_regression.py @@ -1,6 +1,5 @@ from pyperplan.pddl.parser import * - _parser = Parser("") @@ -28,7 +27,7 @@ def test_untyped_constants(): _parser.probInput = problem_input domain = _parser.parse_domain(False) - problem = _parser.parse_problem(domain, False) + _parser.parse_problem(domain, False) _parser.domInput = domain_input domain = _parser.parse_domain(False) diff --git a/pyperplan/tests/test_relaxation.py b/pyperplan/tests/test_relaxation.py index 5ebc4b73..9a3cc392 100644 --- a/pyperplan/tests/test_relaxation.py +++ b/pyperplan/tests/test_relaxation.py @@ -3,7 +3,7 @@ from pyperplan import grounding from pyperplan.heuristics.relaxation import * from pyperplan.pddl.parser import Parser -from pyperplan.search import a_star, enforced_hillclimbing_search, make_root_node +from pyperplan.search import make_root_node from pyperplan.task import Operator, Task from .heuristic_test_instances import * diff --git a/pyperplan/tests/test_sat.py b/pyperplan/tests/test_sat.py index eb3f1feb..16ccf4fd 100644 --- a/pyperplan/tests/test_sat.py +++ b/pyperplan/tests/test_sat.py @@ -7,7 +7,6 @@ from pyperplan.search import minisat, sat from pyperplan.task import Operator, Task - logging.basicConfig( level=logging.DEBUG, format="%(asctime)s %(levelname)-8s %(message)s", @@ -181,7 +180,7 @@ def test_sat_solve(): task5 = Task("task5", {"a", "b", "c"}, set(), {"c"}, [op1, op2, op4]) task6 = Task("task6", {"a", "b", "c", "d"}, {"a"}, {"d"}, [op2, op4, op5]) task7 = Task("task7c", {"a", "b", "c", "d"}, {"a"}, {"d"}, [op3, op5]) - task8 = Task( + Task( "task8", {"a", "b", "c", "d", "e", "f", "g"}, {"a"}, diff --git a/pyperplan/tests/test_searchalgorithms.py b/pyperplan/tests/test_searchalgorithms.py index 30686207..2f6b9c37 100644 --- a/pyperplan/tests/test_searchalgorithms.py +++ b/pyperplan/tests/test_searchalgorithms.py @@ -12,7 +12,7 @@ def test_breadth_first_search_at_goal(): task = dummy_task.get_search_space_at_goal() solution = breadth_first_search(task) print(solution) - assert solution != None + assert solution is not None assert len(solution) == 0 @@ -21,7 +21,7 @@ def test_breadth_first_search_no_solution(): task = dummy_task.get_search_space_no_solution() solution = breadth_first_search(task) print(solution) - assert solution == None + assert solution is None def test_breadth_first_search_three_step(): @@ -29,7 +29,7 @@ def test_breadth_first_search_three_step(): task = dummy_task.get_simple_search_space() solution = breadth_first_search(task) print(solution) - assert solution != None + assert solution is not None assert len(solution) == 3 @@ -38,5 +38,5 @@ def test_breadth_first_search_four_step(): task = dummy_task.get_simple_search_space_2() solution = breadth_first_search(task) print(solution) - assert solution != None + assert solution is not None assert len(solution) == 4 diff --git a/pyperplan/tests/test_searchspace.py b/pyperplan/tests/test_searchspace.py index 8e73f621..cc7eaaa5 100644 --- a/pyperplan/tests/test_searchspace.py +++ b/pyperplan/tests/test_searchspace.py @@ -4,7 +4,6 @@ from pyperplan.search.searchspace import make_child_node, make_root_node - # Construct a small tree in order to perform some needed test methods root = make_root_node("state1") diff --git a/pyperplan/tests/test_task.py b/pyperplan/tests/test_task.py index bdba35a1..98e069db 100644 --- a/pyperplan/tests/test_task.py +++ b/pyperplan/tests/test_task.py @@ -5,7 +5,6 @@ from pyperplan.task import Operator, Task - s1 = frozenset(["var1"]) s2 = frozenset(["var2"]) s3 = frozenset(["var1", "var2"]) diff --git a/pyperplan/tests/test_tree_visitor.py b/pyperplan/tests/test_tree_visitor.py index 34045b3e..b19d6c77 100644 --- a/pyperplan/tests/test_tree_visitor.py +++ b/pyperplan/tests/test_tree_visitor.py @@ -2,12 +2,11 @@ from pytest import raises -from pyperplan.pddl.lisp_parser import parse_lisp_iterator -from pyperplan.pddl.parser import parse_domain_def, parse_problem_def, Parser import pyperplan.pddl.tree_visitor as pddl_tree_visitor +from pyperplan.pddl.lisp_parser import parse_lisp_iterator +from pyperplan.pddl.parser import Parser, parse_domain_def, parse_problem_def from pyperplan.pddl.tree_visitor import SemanticError - _domain_input = """ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; 4 Op-blocks world diff --git a/pyperplan/tests/test_validator.py b/pyperplan/tests/test_validator.py index c28715cd..47a06258 100644 --- a/pyperplan/tests/test_validator.py +++ b/pyperplan/tests/test_validator.py @@ -7,7 +7,6 @@ from pyperplan import tools from pyperplan.planner import validate_solution, validator_available - DOMAIN_FILE = "DOMAIN.TEST" PROBLEM_FILE = "PROBLEM.TEST" CORRECT_SOLN_FILE = "CORRECT.SOLN.TEST" diff --git a/pyperplan/tools.py b/pyperplan/tools.py index e3cb3664..e762e2d9 100644 --- a/pyperplan/tools.py +++ b/pyperplan/tools.py @@ -15,11 +15,8 @@ # along with this program. If not, see # -import logging import os import subprocess -import sys -import traceback def command_available(command): @@ -33,7 +30,7 @@ def command_available(command): try: subprocess.check_call(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) return True - except (subprocess.CalledProcessError, OSError) as err: + except (subprocess.CalledProcessError, OSError): return False From 4801aa6e60e3410f828d5e603221199ae7c5932a Mon Sep 17 00:00:00 2001 From: Jendrik Seipp Date: Sun, 22 Feb 2026 22:15:40 +0100 Subject: [PATCH 3/8] Fold setup.py into pyproject.toml. --- dev/release.sh | 8 ++++---- setup.py | 45 --------------------------------------------- 2 files changed, 4 insertions(+), 49 deletions(-) delete mode 100644 setup.py diff --git a/dev/release.sh b/dev/release.sh index 53b779c2..74e50eec 100755 --- a/dev/release.sh +++ b/dev/release.sh @@ -7,7 +7,7 @@ CHANGES="/tmp/pyperplan-$VERSION-changes" function set_version { local version="$1" - sed -i -e "s/VERSION = \".*\"/VERSION = \"$version\"/" setup.py + sed -i -e "s/^version = \".*\"/version = \"$version\"/" pyproject.toml } cd $(dirname "$0")/../ @@ -36,9 +36,9 @@ git tag -a "v$VERSION" -m "v$VERSION" HEAD # Requirements: # pipx install twine -# pip install --user wheel -python3 setup.py sdist bdist_wheel --universal -twine upload dist/pyperplan-${VERSION}.tar.gz dist/pyperplan-${VERSION}-py2.py3-none-any.whl +# pipx install uv +uv build +twine upload dist/pyperplan-${VERSION}.tar.gz dist/pyperplan-${VERSION}-py3-none-any.whl git push git push --tags diff --git a/setup.py b/setup.py deleted file mode 100644 index c5d0f9e4..00000000 --- a/setup.py +++ /dev/null @@ -1,45 +0,0 @@ -#! /usr/bin/env python - -from setuptools import find_packages, setup - - -VERSION = "2.1" - - -with open("README.md") as f: - long_description = f.read() - - -setup( - name="pyperplan", - version=VERSION, - description="A lightweight STRIPS planner written in Python.", - long_description=long_description, - long_description_content_type="text/markdown", - keywords="classical planning STRIPS", - author="Jendrik Seipp", - author_email="jendrik.seipp@liu.se", - url="https://github.com/aibasel/pyperplan", - license="GPL3+", - packages=find_packages(exclude=["pyperplan.tests"]), - entry_points={"console_scripts": ["pyperplan = pyperplan.__main__:main"]}, - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Environment :: Console", - "Intended Audience :: Developers", - "Intended Audience :: Education", - "Intended Audience :: Science/Research", - "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", - "Programming Language :: Python", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Topic :: Scientific/Engineering", - ], - install_requires=["wheel"], - python_requires=">=3.7", -) From 437a873dc21c4f99ddf435e5fc3d6285671e8d52 Mon Sep 17 00:00:00 2001 From: Jendrik Seipp Date: Sun, 22 Feb 2026 22:28:53 +0100 Subject: [PATCH 4/8] Polish code further with ruff. --- pyperplan/heuristics/lm_cut.py | 2 +- pyperplan/heuristics/relaxation.py | 12 ++-- pyperplan/pddl/lisp_parser.py | 1 - pyperplan/pddl/parser.py | 25 +++---- pyperplan/pddl/pddl.py | 14 ++-- pyperplan/pddl/tree_visitor.py | 20 +++--- pyperplan/planner.py | 3 +- pyperplan/search/__init__.py | 19 ++++-- pyperplan/search/a_star.py | 3 +- .../search/enforced_hillclimbing_search.py | 1 - .../search/iterative_deepening_search.py | 2 +- pyperplan/search/sat.py | 2 +- pyperplan/tests/test_grounding.py | 5 +- pyperplan/tests/test_parser_pddl_complex.py | 2 +- pyperplan/tests/test_sat.py | 2 +- pyperplan/tests/test_task.py | 1 + pyproject.toml | 66 ++++++++++--------- tox.ini | 14 ++-- 18 files changed, 92 insertions(+), 102 deletions(-) diff --git a/pyperplan/heuristics/lm_cut.py b/pyperplan/heuristics/lm_cut.py index 7e589e46..b71f25f6 100644 --- a/pyperplan/heuristics/lm_cut.py +++ b/pyperplan/heuristics/lm_cut.py @@ -20,7 +20,7 @@ """ import logging -from heapq import * +from heapq import heappop, heappush from .heuristic_base import Heuristic diff --git a/pyperplan/heuristics/relaxation.py b/pyperplan/heuristics/relaxation.py index fbed1a80..c322cd72 100644 --- a/pyperplan/heuristics/relaxation.py +++ b/pyperplan/heuristics/relaxation.py @@ -323,14 +323,14 @@ def get_cost(self, operator, pre): if operator.preconditions: # Collect the sa-sets from all preconditions in a list. - l = [ + sa_sets = [ self.facts[pre].sa_set for pre in operator.preconditions if self.facts[pre].sa_set is not None ] - if l: + if sa_sets: # Union all these sets. - unioned_sets = set.union(*l) + unioned_sets = set.union(*sa_sets) # The heuristic value equals the cardinality of the unioned # sets. cost = len(unioned_sets) @@ -350,16 +350,16 @@ def calc_goal_h(self): """ if self.goals: # Collect the sa-sets of all facts that are part of the goal. - l = [ + sa_sets = [ self.facts[fact].sa_set for fact in self.goals if self.facts[fact].sa_set is not None ] # Check whether all subgoals are fulfilled. - if len(l) == len(self.goals): + if len(sa_sets) == len(self.goals): # Union all these sets and take the length of the union as # heuristic value. - h_value = len(set.union(*l)) + h_value = len(set.union(*sa_sets)) else: # Ff not, return infinty. h_value = float("inf") diff --git a/pyperplan/pddl/lisp_parser.py b/pyperplan/pddl/lisp_parser.py index aa1d03aa..42e67f26 100644 --- a/pyperplan/pddl/lisp_parser.py +++ b/pyperplan/pddl/lisp_parser.py @@ -17,7 +17,6 @@ """Basic functions for parsing simple Lisp files.""" - from .errors import ParseError from .lisp_iterators import LispIterator diff --git a/pyperplan/pddl/parser.py b/pyperplan/pddl/parser.py index 4a60545d..0cbf5bfc 100644 --- a/pyperplan/pddl/parser.py +++ b/pyperplan/pddl/parser.py @@ -386,7 +386,7 @@ def _parse_type_helper(iter, type_class): types_iter = next(iter) if not types_iter.try_match("either"): raise ValueError( - "Error multiple parent definition must " 'start with "either"' + 'Error multiple parent definition must start with "either"' ) tlist = parse_list_template(_parse_string_helper, types_iter) while len(tmpList) != 0: @@ -472,9 +472,7 @@ def parse_parameters(iter): """ # check that the parameters definition starts with the correct keyword if not iter.try_match(":parameters"): - raise ValueError( - 'Error keyword ":parameters" required before ' "parameter list!" - ) + raise ValueError('Error keyword ":parameters" required before parameter list!') varList = parse_typed_var_list(next(iter)) return varList @@ -485,9 +483,7 @@ def parse_requirements_stmt(iter): """ # check for requirements keyword if not iter.try_match(":requirements"): - raise ValueError( - "Error requirements list must contain keyword " '":requirements"' - ) + raise ValueError('Error requirements list must contain keyword ":requirements"') keywords = parse_keyword_list(iter) return RequirementsStmt(keywords) @@ -515,15 +511,15 @@ def _parse_domain_helper(iter, keyword): Returns a DomainStmt instance. """ if not iter.try_match(keyword): - raise ValueError( - "Error domain statement must be present before " "domain name!" - ) + raise ValueError("Error domain statement must be present before domain name!") name = parse_name(iter, "domain") return DomainStmt(name) def parse_domain_stmt(it): return _parse_domain_helper(it, "domain") + + def parse_problem_domain_stmt(it): return _parse_domain_helper(it, ":domain") @@ -579,7 +575,7 @@ def parse_formula(iter): key = iter.peek().get_word() next(iter) if key[0] in reserved: - raise ValueError("Error: Formula must not start with reserved " "char!") + raise ValueError("Error: Formula must not start with reserved char!") children = parse_list_template(parse_formula, iter) else: # non nested formula @@ -639,7 +635,7 @@ def parse_predicates_stmt(iter): """ if not iter.try_match(":predicates"): raise ValueError( - "Error predicate definition must start with " '":predicates" keyword!' + 'Error predicate definition must start with ":predicates" keyword!' ) preds = parse_predicate_list(iter) return PredicatesStmt(preds) @@ -654,8 +650,7 @@ def parse_domain_def(iter): defString = parse_name(iter, "domain def") if defString != "define": raise ValueError( - "Invalid domain definition! --> domain definition " - 'must start with "define"' + 'Invalid domain definition! --> domain definition must start with "define"' ) dom = parse_domain_stmt(next(iter)) # create new DomainDef @@ -688,7 +683,7 @@ def parse_domain_def(iter): next_iter = next(iter) key = parse_keyword(next_iter.peek()) if key.name != "action": - raise ValueError("Error: Found invalid keyword while parsing " "actions") + raise ValueError("Error: Found invalid keyword while parsing actions") action = parse_action_stmt(next_iter) domain.actions.append(action) # assert end is reached diff --git a/pyperplan/pddl/pddl.py b/pyperplan/pddl/pddl.py index 8653829f..f5d474cc 100644 --- a/pyperplan/pddl/pddl.py +++ b/pyperplan/pddl/pddl.py @@ -112,15 +112,11 @@ def __init__(self, name, types, predicates, actions, constants={}): self.constants = constants def __repr__(self): - return ( - "< Domain definition: %s Predicates: %s Actions: %s " - "Constants: %s >" - % ( - self.name, - [str(p) for p in self.predicates], - [str(a) for a in self.actions], - [str(c) for c in self.constants], - ) + return "< Domain definition: %s Predicates: %s Actions: %s Constants: %s >" % ( + self.name, + [str(p) for p in self.predicates], + [str(a) for a in self.actions], + [str(c) for c in self.constants], ) __str__ = __repr__ diff --git a/pyperplan/pddl/tree_visitor.py b/pyperplan/pddl/tree_visitor.py index d087f946..f02cf39a 100644 --- a/pyperplan/pddl/tree_visitor.py +++ b/pyperplan/pddl/tree_visitor.py @@ -57,14 +57,13 @@ def __init__(self, vname=None): def accept(self, visitor): if self._visitorName is None: - raise ValueError("Error: visit method of uninitialized visitor " "called!") + raise ValueError("Error: visit method of uninitialized visitor called!") # get the appropriate method of the visitor instance m = getattr(visitor, self._visitorName) # ensure that the method is callable if not hasattr(m, "__call__"): raise ValueError( - "Error: cannot call undefined method: %s on " - "visitor" % self._visitorName + "Error: cannot call undefined method: %s on visitor" % self._visitorName ) # and finally call the callback m(self) @@ -246,7 +245,7 @@ def visit_object(self, node): ) if node.name in self._constants: raise SemanticError( - "Error: multiple defines of object with " "name " + node.name + "Error: multiple defines of object with name " + node.name ) # Add constant with its corresponding type to the constants dict. self._constants[node.name] = self._types[type_name] @@ -395,7 +394,7 @@ def visit_precondition_stmt(self, node): else: # If not 'and' we only allow a single predicate in precondition. if formula.key not in self._predicates: - raise SemanticError("Error: predicate in precondition is not " "in CNF") + raise SemanticError("Error: predicate in precondition is not in CNF") # Call helper. self.add_precond(precond, formula) self.set_in(node, precond) @@ -417,7 +416,7 @@ def add_effect(self, effect, c): # This is a negative effect, only one child allowed. if len(c.children) != 1: raise SemanticError( - "Error not statement with multiple " "children in effect of action" + "Error not statement with multiple children in effect of action" ) nextPredicate = c.children[0] isNegative = True @@ -430,7 +429,7 @@ def add_effect(self, effect, c): "of action" % nextPredicate.key ) if nextPredicate is None: - raise SemanticError("Error: NoneType predicate used in effect of " "action") + raise SemanticError("Error: NoneType predicate used in effect of action") predDef = self._predicates[nextPredicate.key] signature = list() count = 0 @@ -585,8 +584,7 @@ def add_goal(self, goal, c): # Check whether the predicate uses the correct signature. if len(c.children) != len(predDef.signature): raise SemanticError( - "Error: wrong number of arguments for " - "predicate " + c.key + " in goal" + "Error: wrong number of arguments for predicate " + c.key + " in goal" ) for v in c.children: signature.append((v.key, predDef.signature[count][1])) @@ -611,9 +609,7 @@ def visit_goal_stmt(self, node): else: # Only a single predicate is allowed then (s.a.) if formula.key not in self._domain.predicates: - raise SemanticError( - "Error: predicate in goal definition is " "not in CNF" - ) + raise SemanticError("Error: predicate in goal definition is not in CNF") # Call helper. self.add_goal(goal, formula) self.set_in(node, goal) diff --git a/pyperplan/planner.py b/pyperplan/planner.py index 5d5c4d91..125ca6f1 100644 --- a/pyperplan/planner.py +++ b/pyperplan/planner.py @@ -191,8 +191,7 @@ def search_plan( def validate_solution(domain_file, problem_file, solution_file): if not validator_available(): logging.info( - "validate could not be found on the PATH so the plan can " - "not be validated." + "validate could not be found on the PATH so the plan can not be validated." ) return diff --git a/pyperplan/search/__init__.py b/pyperplan/search/__init__.py index 35697a5f..1681e9b4 100644 --- a/pyperplan/search/__init__.py +++ b/pyperplan/search/__init__.py @@ -15,9 +15,16 @@ # along with this program. If not, see # -from .a_star import astar_search, greedy_best_first_search, weighted_astar_search -from .breadth_first_search import breadth_first_search -from .enforced_hillclimbing_search import enforced_hillclimbing_search -from .iterative_deepening_search import iterative_deepening_search -from .sat import sat_solve -from .searchspace import make_child_node, make_root_node +from .a_star import astar_search as astar_search +from .a_star import greedy_best_first_search as greedy_best_first_search +from .a_star import weighted_astar_search as weighted_astar_search +from .breadth_first_search import breadth_first_search as breadth_first_search +from .enforced_hillclimbing_search import ( + enforced_hillclimbing_search as enforced_hillclimbing_search, +) +from .iterative_deepening_search import ( + iterative_deepening_search as iterative_deepening_search, +) +from .sat import sat_solve as sat_solve +from .searchspace import make_child_node as make_child_node +from .searchspace import make_root_node as make_root_node diff --git a/pyperplan/search/a_star.py b/pyperplan/search/a_star.py index 79370b14..d839b6c8 100644 --- a/pyperplan/search/a_star.py +++ b/pyperplan/search/a_star.py @@ -169,8 +169,7 @@ def astar_search( # ignore this operator if we use the relaxed plan # criterion logging.debug( - "removing operator %s << not a " - "preferred operator" % op.name + "removing operator %s << not a preferred operator" % op.name ) continue else: diff --git a/pyperplan/search/enforced_hillclimbing_search.py b/pyperplan/search/enforced_hillclimbing_search.py index 5f1a963b..7a1d568e 100644 --- a/pyperplan/search/enforced_hillclimbing_search.py +++ b/pyperplan/search/enforced_hillclimbing_search.py @@ -66,7 +66,6 @@ def enforced_hillclimbing_search(planning_task, heuristic, use_preferred_ops=Fal logging.debug("relaxed plan %s " % rplan) for operator, successor_state in planning_task.get_successor_states(node.state): - # for the preferred operator version ignore all non preferred # operators if use_preferred_ops: diff --git a/pyperplan/search/iterative_deepening_search.py b/pyperplan/search/iterative_deepening_search.py index a9dcff80..1bef80bc 100644 --- a/pyperplan/search/iterative_deepening_search.py +++ b/pyperplan/search/iterative_deepening_search.py @@ -121,7 +121,7 @@ def deepening_search_step(self, task, state, depth, step, path): # successor and return to the caller without a plan if successor_state not in path: if task.goal_reached(successor_state): - logging.info("Goal reached. Start extraction of " "solution.") + logging.info("Goal reached. Start extraction of solution.") self.maxreacheddepth = nextstep return [operator] else: diff --git a/pyperplan/search/sat.py b/pyperplan/search/sat.py index ff64e04e..ad326060 100644 --- a/pyperplan/search/sat.py +++ b/pyperplan/search/sat.py @@ -11,7 +11,7 @@ def _formula_str(formula, sep="&"): """Returns a representation of 'formula' for prettyprinting""" next_sep = "|" if sep == "&" else "&" items = [ - item if (type(item) == str) else _formula_str(item, next_sep) + item if isinstance(item, str) else _formula_str(item, next_sep) for item in formula ] return "({})".format(f" {sep} ".join(items)) diff --git a/pyperplan/tests/test_grounding.py b/pyperplan/tests/test_grounding.py index b3c92806..19ab4122 100644 --- a/pyperplan/tests/test_grounding.py +++ b/pyperplan/tests/test_grounding.py @@ -207,7 +207,6 @@ def test_collect_facts(): def test_operators(): - # action with signature with 2 types action_drive_vehicle = get_action( "DRIVE-VEHICLE", @@ -315,9 +314,7 @@ def test_operators(): def test_create_operator(): - grounding._get_statics( - standard_domain.predicates.values(), [action_drive_car] - ) + grounding._get_statics(standard_domain.predicates.values(), [action_drive_car]) initial_state = [ Predicate("at", [("ford", types["car"]), ("freiburg", types["city"])]) ] diff --git a/pyperplan/tests/test_parser_pddl_complex.py b/pyperplan/tests/test_parser_pddl_complex.py index e68073d7..92c9dfe3 100644 --- a/pyperplan/tests/test_parser_pddl_complex.py +++ b/pyperplan/tests/test_parser_pddl_complex.py @@ -157,7 +157,7 @@ def test_parsePredicatesLogistics(): ] == ["place", "physobj", "package"] -def test_parseDomainDef(): +def test_parseDomainDef_blocks(): test = [ """ (define (domain BLOCKS) diff --git a/pyperplan/tests/test_sat.py b/pyperplan/tests/test_sat.py index 16ccf4fd..0547379e 100644 --- a/pyperplan/tests/test_sat.py +++ b/pyperplan/tests/test_sat.py @@ -33,7 +33,7 @@ def sort_formula(formula): strings = [part for part in formula if isinstance(part, str)] lists = [part for part in formula if isinstance(part, list)] assert len(strings) + len(lists) == len(formula) - return sorted(strings) + sorted(sort_formula(l) for l in lists) + return sorted(strings) + sorted(sort_formula(subformula) for subformula in lists) def test_formula_string1(): diff --git a/pyperplan/tests/test_task.py b/pyperplan/tests/test_task.py index 98e069db..9b14e6d5 100644 --- a/pyperplan/tests/test_task.py +++ b/pyperplan/tests/test_task.py @@ -1,6 +1,7 @@ """ Tests for the task.py module """ + import pytest from pyperplan.task import Operator, Task diff --git a/pyproject.toml b/pyproject.toml index 16e7d7c2..50d8ed95 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,9 +4,30 @@ version = "2.1" description = "A lightweight STRIPS planner written in Python." readme = "README.md" requires-python = ">=3.7" -license = { text = "GPL3+" } +license = "GPL-3.0-or-later" +keywords = ["classical planning", "STRIPS"] +authors = [{ name = "Jendrik Seipp", email = "jendrik.seipp@liu.se" }] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: Developers", + "Intended Audience :: Education", + "Intended Audience :: Science/Research", + "Programming Language :: Python", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Scientific/Engineering", +] dependencies = [] +[project.urls] +Homepage = "https://github.com/aibasel/pyperplan" + [project.scripts] pyperplan = "pyperplan.__main__:main" @@ -14,35 +35,18 @@ pyperplan = "pyperplan.__main__:main" requires = ["setuptools"] build-backend = "setuptools.build_meta" -# NOTE: you have to use single-quoted strings in TOML for regular expressions. -# It's the equivalent of r-strings in Python. Multiline strings are treated as -# verbose regular expressions by Black. Use [ ] to denote a significant space -# character. +[tool.setuptools.packages.find] +include = ["pyperplan*"] -[tool.black] +[tool.ruff] line-length = 88 -target-version = ['py36', 'py37', 'py38', 'py39'] -include = '\.pyi?$' -exclude = ''' -/( - \.eggs - | \.git - | \.tox - | \.venv - | _build - | build - | dist -)/ -''' - -[tool.isort] -case_sensitive = false -force_single_line = false -force_sort_within_sections = true -include_trailing_comma = true -known_first_party = "pyperplan" -known_third_party = "pytest" -line_length = 88 -lines_after_imports = 2 # -1 puts 2 lines before classes and functions, otherwise 1 line -multi_line_output = 3 -order_by_type = false + +[tool.ruff.lint] +select = ["E", "F", "I"] + +[tool.ruff.lint.per-file-ignores] +"*/tests/*" = ["F403", "F405", "F811"] +"pyperplan/pddl/parser.py" = ["F403", "F405"] + +[tool.ruff.lint.isort] +known-first-party = ["pyperplan"] diff --git a/tox.ini b/tox.ini index 16c5e124..11c42071 100644 --- a/tox.ini +++ b/tox.ini @@ -22,18 +22,16 @@ commands = [testenv:style] skipsdist = true deps = - black==22.3.0 - isort[pyproject]==5.10.1 + ruff==0.15.2 commands = - black --check --diff . - isort --check-only pyperplan/ setup.py + ruff check . + ruff format --check . # Fix style with "tox -e fix-style". [testenv:fix-style] skipsdist = true deps = - black==22.3.0 - isort[pyproject]==5.10.1 + ruff==0.15.2 commands = - black . - isort pyperplan/ setup.py + ruff check --fix . + ruff format . From 5b5d6bc0b86b5b58466a0015a072a3d0e4743c41 Mon Sep 17 00:00:00 2001 From: Jendrik Seipp Date: Sun, 22 Feb 2026 22:31:20 +0100 Subject: [PATCH 5/8] Test Python 3.14 and Ubuntu 24.04. --- .github/workflows/ubuntu.yml | 6 +++--- pyproject.toml | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index e3959c4a..432c03ad 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -17,8 +17,8 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-20.04, ubuntu-22.04] - python-version: [3.7, 3.8, 3.9, '3.10', '3.11', '3.12', '3.13'] + os: [ubuntu-20.04, ubuntu-22.04, ubuntu-24.04] + python-version: [3.7, 3.8, 3.9, '3.10', '3.11', '3.12', '3.13', '3.14'] steps: - uses: actions/checkout@v3 @@ -39,7 +39,7 @@ jobs: sudo apt-get -y install minisat python3-pip tox - name: Check style - if: matrix.os == 'ubuntu-22.04' && matrix.python-version == '3.12' + if: matrix.os == 'ubuntu-24.04' && matrix.python-version == '3.14' run: | tox -e style diff --git a/pyproject.toml b/pyproject.toml index 50d8ed95..ea66133d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Scientific/Engineering", ] dependencies = [] From 4f2a8da46da4ce25054686ff2deb3ccfc4c8c2fd Mon Sep 17 00:00:00 2001 From: Jendrik Seipp Date: Sun, 22 Feb 2026 22:35:02 +0100 Subject: [PATCH 6/8] Use isolated builds for tox. --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 11c42071..7c28d097 100644 --- a/tox.ini +++ b/tox.ini @@ -3,6 +3,7 @@ envlist = slow, style basepython = python3 skip_missing_interpreters = true +isolated_build = true # Run "fast" tests with "tox -e py". [testenv] From 7e35e0c270849dc3ee26ad5ddf966649a4c98ae9 Mon Sep 17 00:00:00 2001 From: Jendrik Seipp Date: Mon, 23 Feb 2026 21:04:21 +0100 Subject: [PATCH 7/8] Revise README. --- README.md | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 74b3adf3..c07cb22f 100644 --- a/README.md +++ b/README.md @@ -13,26 +13,15 @@ is published under the terms of the GNU General Public License 3 Pyperplan supports the following PDDL fragment: STRIPS without action costs. -# Requirements - -Pyperplan requires [Python](https://python.org) >= 3.7. - # Installation -From the Python package index (PyPI): - - pip install pyperplan - -From inside a repository clone: +From the Python package index (PyPI) via [uv](https://docs.astral.sh/uv/): - pip install --editable . - -This makes the `pyperplan` command available globally or in your [virtual -environment](https://docs.python.org/3/tutorial/venv.html) (recommended). + uv tool install pyperplan -Alternatively, you can use [uv](https://docs.astral.sh/uv/): +From inside a repository clone via [uv](https://docs.astral.sh/uv/): - uv tool install pyperplan + uv pip install --editable . # Usage From c8e2251360437751155d6f1618cc7cedf9366440 Mon Sep 17 00:00:00 2001 From: Jendrik Seipp Date: Mon, 23 Feb 2026 21:27:29 +0100 Subject: [PATCH 8/8] Bump minimum tested Ubuntu version to 22.04 and minimum Python version to 3.8 --- .github/workflows/ubuntu.yml | 4 ++-- pyproject.toml | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index 432c03ad..2c987207 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -17,8 +17,8 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-20.04, ubuntu-22.04, ubuntu-24.04] - python-version: [3.7, 3.8, 3.9, '3.10', '3.11', '3.12', '3.13', '3.14'] + os: [ubuntu-22.04, ubuntu-24.04] + python-version: [3.8, 3.9, '3.10', '3.11', '3.12', '3.13', '3.14'] steps: - uses: actions/checkout@v3 diff --git a/pyproject.toml b/pyproject.toml index ea66133d..96243523 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "pyperplan" version = "2.1" description = "A lightweight STRIPS planner written in Python." readme = "README.md" -requires-python = ">=3.7" +requires-python = ">=3.8" license = "GPL-3.0-or-later" keywords = ["classical planning", "STRIPS"] authors = [{ name = "Jendrik Seipp", email = "jendrik.seipp@liu.se" }] @@ -14,7 +14,6 @@ classifiers = [ "Intended Audience :: Education", "Intended Audience :: Science/Research", "Programming Language :: Python", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10",