From 1142925140eba4182b68366247979a0fd84b58bd Mon Sep 17 00:00:00 2001 From: Nicolas Appriou Date: Sun, 1 Oct 2023 19:36:06 +0200 Subject: [PATCH] closes #2 Make puzzle input optional --- saulve/puzzle.py | 46 +++++++++++-------- .../fixtures/generic/contains_puzzle.py | 2 +- .../fixtures/generic/other_puzzle.py | 2 +- tests/test_cli.py | 4 +- tests/test_puzzle.py | 20 ++++++-- 5 files changed, 47 insertions(+), 27 deletions(-) diff --git a/saulve/puzzle.py b/saulve/puzzle.py index 698e718..d4cd9ad 100644 --- a/saulve/puzzle.py +++ b/saulve/puzzle.py @@ -1,7 +1,7 @@ """Declare puzzle and their associated solution. """ -from typing import Callable, Generic, NamedTuple, TypeVar +from typing import Callable, Concatenate, NamedTuple, ParamSpec, TypeVar from .errors import PuzzleHasNoSolution @@ -19,14 +19,15 @@ def is_solved(self) -> bool: return self.solution is not None -PuzzleInput = TypeVar('PuzzleInput') - PuzzleStepResult = int | str | None -SolutionFunction = Callable[[PuzzleInput], PuzzleStepResult] +T = TypeVar('T') +U = TypeVar('U') + +P = ParamSpec('P') -class Puzzle(Generic[PuzzleInput]): +class Puzzle: """Holds solutions of a puzzle and its input data. A puzzle is first defined by its name and input. @@ -35,22 +36,15 @@ class Puzzle(Generic[PuzzleInput]): Arguments: name: A verbose name for this puzzle - puzzle_input: The input data of the puzzle. Attributes: name: A verbose title for the current puzzle - puzzle_input: Input problem to solve steps: A list of callable implementing puzzle solutions """ - def __init__( - self, - name: str, - puzzle_input: PuzzleInput | None = None, - ) -> None: + def __init__(self, name: str) -> None: self.name = name - self.puzzle_input = puzzle_input - self.steps: list[SolutionFunction] = [] + self.steps: list[Callable[[], PuzzleStepResult]] = [] def __repr__(self) -> str: return f'<{self.__class__.__name__}: {self.name}>' @@ -58,20 +52,32 @@ def __repr__(self) -> str: def __str__(self) -> str: return self.name + def with_input(self, puzzle_input: U) -> Callable[ + [Callable[Concatenate[U, P], T]], + Callable[P, T], + ]: + def decorator(fn: Callable[Concatenate[U, P], T]) -> Callable[P, T]: + def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: + return fn(puzzle_input, *args, **kwargs) + + return wrapper + + return decorator # type: ignore[return-value] # pending issue 9 + def solution( self, - fn: SolutionFunction, - ) -> SolutionFunction: + fn: Callable[[], PuzzleStepResult], + ) -> Callable[[], PuzzleStepResult]: """Register a solution function for the puzzle. The function takes the puzzle input as argument and should return a solution. Example: - >>> puzzle = Puzzle(title="double", puzzle_input=12) + >>> puzzle = Puzzle(title="should be two") >>> @puzzle.solution - ... def double_input(number): - ... return number * 2 + ... def get_two(): + ... return 2 """ self.steps.append(fn) return fn @@ -92,7 +98,7 @@ def solve(self) -> list[PuzzleSolution]: solutions: list[PuzzleSolution] = [] for solve_step in self.steps: - solution = solve_step(self.puzzle_input) + solution = solve_step() solutions.append(PuzzleSolution( str(solution) if solution is not None diff --git a/tests/challenges/fixtures/generic/contains_puzzle.py b/tests/challenges/fixtures/generic/contains_puzzle.py index 9a22b73..738d9a6 100644 --- a/tests/challenges/fixtures/generic/contains_puzzle.py +++ b/tests/challenges/fixtures/generic/contains_puzzle.py @@ -1,3 +1,3 @@ from saulve import Puzzle -puzzle = Puzzle(name='A puzzle', puzzle_input=12) +puzzle = Puzzle(name='A puzzle') diff --git a/tests/challenges/fixtures/generic/other_puzzle.py b/tests/challenges/fixtures/generic/other_puzzle.py index 15cefcf..5126d55 100644 --- a/tests/challenges/fixtures/generic/other_puzzle.py +++ b/tests/challenges/fixtures/generic/other_puzzle.py @@ -1,3 +1,3 @@ from saulve import Puzzle -puzzle = Puzzle(name='Other puzzle', puzzle_input=12) +puzzle = Puzzle(name='Other puzzle') diff --git a/tests/test_cli.py b/tests/test_cli.py index e58af94..46c986a 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -4,8 +4,8 @@ from saulve.challenges.in_memory import InMemoryLoader from saulve.cli import cli -puzzle = Puzzle(name='Test puzzle', puzzle_input='foo') -puzzle.solution(lambda a: 'bar') +puzzle = Puzzle(name='Test puzzle') +puzzle.solution(lambda _: 'bar') app = App() diff --git a/tests/test_puzzle.py b/tests/test_puzzle.py index c019f91..8c2491b 100644 --- a/tests/test_puzzle.py +++ b/tests/test_puzzle.py @@ -5,17 +5,31 @@ def test_cannot_solve_puzzle_without_registered_solution() -> None: - puzzle = Puzzle[None](name='Test Puzzle') + puzzle = Puzzle(name='Test Puzzle') with pytest.raises(PuzzleHasNoSolution): puzzle.solve() def test_solve_puzzle_steps() -> None: - puzzle = Puzzle[int](name='Test Puzzle') - puzzle.solution(lambda _: 12) + puzzle = Puzzle(name='Test Puzzle') + puzzle.solution(lambda: 12) solutions = puzzle.solve() assert len(solutions) == 1 assert solutions[0].solution == '12' + + +def test_injects_puzzle_input() -> None: + puzzle = Puzzle(name='Test puzzle') + + @puzzle.solution + @puzzle.with_input('foobar') + def solution(puzzle_input) -> str: + return puzzle_input + + solutions = puzzle.solve() + + assert len(solutions) == 1 + assert solutions[0].solution == 'foobar'