Skip to content

Commit

Permalink
closes #2 Make puzzle input optional
Browse files Browse the repository at this point in the history
  • Loading branch information
Nicals committed Oct 3, 2023
1 parent af69a3c commit 1142925
Show file tree
Hide file tree
Showing 5 changed files with 47 additions and 27 deletions.
46 changes: 26 additions & 20 deletions saulve/puzzle.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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.
Expand All @@ -35,43 +36,48 @@ 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}>'

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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion tests/challenges/fixtures/generic/contains_puzzle.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from saulve import Puzzle

puzzle = Puzzle(name='A puzzle', puzzle_input=12)
puzzle = Puzzle(name='A puzzle')
2 changes: 1 addition & 1 deletion tests/challenges/fixtures/generic/other_puzzle.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from saulve import Puzzle

puzzle = Puzzle(name='Other puzzle', puzzle_input=12)
puzzle = Puzzle(name='Other puzzle')
4 changes: 2 additions & 2 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
20 changes: 17 additions & 3 deletions tests/test_puzzle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'

0 comments on commit 1142925

Please sign in to comment.