diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a50ea20..0c8445e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,6 +19,8 @@ jobs: run: pytest --cov --cov-report lcov - name: Check typing run: mypy + - name: Check style + run: ruff check --output-format=github . - name: Upload coverage report uses: coverallsapp/github-action@master diff --git a/pyproject.toml b/pyproject.toml index 47303ea..aab32bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ dev = [ "mypy ~= 1.4", "pytest ~= 7.2", "pytest-cov ~= 4.0", + "ruff", ] [project.urls] @@ -56,3 +57,13 @@ source = ["saulve"] [tool.coverage.report] skip_empty = true + +[tool.ruff] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "B", # flake8-bugbear + "C", # flake8-comprehension + "I", # isort +] diff --git a/saulve/__init__.py b/saulve/__init__.py index e8965dd..5ba1acc 100644 --- a/saulve/__init__.py +++ b/saulve/__init__.py @@ -1,5 +1,4 @@ from .app import App from .puzzle import Puzzle - __all__ = ['App', 'Puzzle'] diff --git a/saulve/__main__.py b/saulve/__main__.py index 10a12ad..4cafccb 100644 --- a/saulve/__main__.py +++ b/saulve/__main__.py @@ -1,4 +1,3 @@ from .cli import cli - cli() diff --git a/saulve/app.py b/saulve/app.py index a3bcb72..a0316a1 100644 --- a/saulve/app.py +++ b/saulve/app.py @@ -1,16 +1,15 @@ import sys from .challenges.base import Challenge, ChallengeLoader +from .errors import ChallengeNotFound, SaulveError from .import_module import append_module_path, import_instance -from .errors import SaulveError, ChallengeNotFound - __all__ = ['App', 'import_app'] class App: def __init__(self) -> None: - self.loaders: dict[str, ChallengeLoader] = dict() + self.loaders: dict[str, ChallengeLoader] = {} def register_challenge( self, @@ -36,10 +35,10 @@ def get_challenge(self, challenge_id: str) -> Challenge: """ try: challenge = self.loaders[challenge_id] - except KeyError: + except KeyError as e: raise ChallengeNotFound( f"No challenge exists with id {challenge_id}" - ) + ) from e return challenge.load() diff --git a/saulve/challenges/advent_of_code.py b/saulve/challenges/advent_of_code.py index a7a4bdd..33f36bd 100644 --- a/saulve/challenges/advent_of_code.py +++ b/saulve/challenges/advent_of_code.py @@ -1,16 +1,16 @@ """Advent of code challenge implementation """ -from pathlib import Path import re import types +from pathlib import Path from typing import NamedTuple -from .base import Challenge, ChallengeLoader, PuzzleView from saulve import Puzzle from saulve.errors import PuzzleNotFound, ValidationError from saulve.import_module import import_instance +from .base import Challenge, ChallengeLoader, PuzzleView YEAR_REGEX = re.compile(r'^year_(?P\d{4})$') PUZZLE_REGEX = re.compile(r'^day_(?P\d{1,2})\.py$') @@ -31,7 +31,10 @@ def __init__(self, puzzles: list[AdventOfCodePuzzle]) -> None: def find(self) -> list[PuzzleView]: return [ - PuzzleView(id=f'{puzzle.year} {puzzle.day:02}', name=puzzle.puzzle.name) + PuzzleView( + id=f'{puzzle.year} {puzzle.day:02}', + name=puzzle.puzzle.name, + ) for puzzle in self.puzzles ] @@ -41,13 +44,13 @@ def get(self, *args: str) -> Puzzle: try: year = int(args[0]) - except ValueError: - raise ValidationError(f"'{args[0]} is not a valid year.") + except ValueError as e: + raise ValidationError(f"'{args[0]} is not a valid year.") from e try: day = int(args[1]) - except ValueError: - raise ValidationError(f"'{args[1]} is not a valid day.") + except ValueError as e: + raise ValidationError(f"'{args[1]} is not a valid day.") from e for puzzle in self.puzzles: if puzzle.year == year and puzzle.day == day: diff --git a/saulve/challenges/base.py b/saulve/challenges/base.py index ce4a0ec..e2eaeed 100644 --- a/saulve/challenges/base.py +++ b/saulve/challenges/base.py @@ -1,6 +1,5 @@ from typing import NamedTuple, Protocol -from ..errors import PuzzleNotFound, ValidationError from ..puzzle import Puzzle @@ -13,6 +12,7 @@ class PuzzleView(NamedTuple): class Challenge(Protocol): """A collection of puzzle from a common source. """ + def find(self) -> list[PuzzleView]: """Get all known puzzles""" diff --git a/saulve/challenges/generic.py b/saulve/challenges/generic.py index 1eb8112..d861aed 100644 --- a/saulve/challenges/generic.py +++ b/saulve/challenges/generic.py @@ -5,16 +5,17 @@ """ import logging -from pathlib import Path import re import types +from pathlib import Path from typing import Optional from saulve.errors import MissingAttribute, PuzzleNotFound, ValidationError -from saulve.puzzle import Puzzle from saulve.import_module import import_instance -from .base import Challenge as BaseChallenge, ChallengeLoader, PuzzleView +from saulve.puzzle import Puzzle +from .base import Challenge as BaseChallenge +from .base import ChallengeLoader, PuzzleView logger = logging.getLogger(__name__) @@ -37,8 +38,8 @@ def get(self, *args: str) -> Puzzle: try: return self.puzzles[puzzle_id] - except KeyError: - raise PuzzleNotFound(f"Puzzle '{puzzle_id}' not found.") + except KeyError as e: + raise PuzzleNotFound(f"Puzzle '{puzzle_id}' not found.") from e class GenericLoader(ChallengeLoader): @@ -50,11 +51,14 @@ def __init__( """ Arguments: challenge_module: The module to load puzzle from - id_regexp: An optional regexp to use to extract an identifier from loaded puzzle name. - If not set or if the regexp does not match, the puzzle module name will set as id. + id_regexp: An optional regexp to use to extract an identifier from + loaded puzzle name. If not set or if the regexp does not match, + the puzzle module name will set as id. """ self.challenge_module = challenge_module - self.id_regexp = re.compile(id_regexp) if id_regexp is not None else None + self.id_regexp = ( + re.compile(id_regexp) if id_regexp is not None else None + ) def _generate_puzzle_id(self, puzzle_module_name: str) -> str: puzzle_module = puzzle_module_name.split('.')[-1] @@ -95,7 +99,10 @@ def load(self) -> Challenge: puzzle_id = self._generate_puzzle_id(puzzle_module_name) if puzzle_id in puzzles: - logger.warning(f'Duplicated puzzle id \'{puzzle_id}\' in {puzzle_module_name}') + logger.warning( + f'Duplicated puzzle id \'{puzzle_id}\' in ' + f'{puzzle_module_name}' + ) puzzles[puzzle_id] = puzzle return Challenge(puzzles) diff --git a/saulve/challenges/in_memory.py b/saulve/challenges/in_memory.py index 7a4cc9a..54b734b 100644 --- a/saulve/challenges/in_memory.py +++ b/saulve/challenges/in_memory.py @@ -4,6 +4,7 @@ """ from saulve.puzzle import Puzzle + from .base import Challenge, ChallengeLoader from .generic import Challenge as GenericChallenge diff --git a/saulve/cli.py b/saulve/cli.py index 9577df9..2baf9df 100644 --- a/saulve/cli.py +++ b/saulve/cli.py @@ -55,9 +55,9 @@ def solve(ctx: click.Context, puzzle_id: list[str]) -> None: try: puzzle = challenge.get(*puzzle_id) except ValidationError as e: - raise click.ClickException(f'Invalid puzzle id. {e}') - except PuzzleNotFound: - raise click.ClickException('Puzzle not found.') + raise click.ClickException(f'Invalid puzzle id. {e}') from e + except PuzzleNotFound as e: + raise click.ClickException('Puzzle not found.') from e solutions = puzzle.solve() diff --git a/saulve/import_module.py b/saulve/import_module.py index 47db1cc..0f83675 100644 --- a/saulve/import_module.py +++ b/saulve/import_module.py @@ -1,14 +1,13 @@ """Utility functions for dynamic imports """ -from importlib import import_module import importlib.util +from importlib import import_module from pathlib import Path from typing import Type, TypeVar from saulve.errors import MissingAttribute, WrongAttributeType - T = TypeVar('T') @@ -29,8 +28,8 @@ def import_instance(module_name: str, attr: str, cls: Type[T]) -> T: try: instance = getattr(module, attr) - except AttributeError: - raise MissingAttribute(module_name, attr) + except AttributeError as e: + raise MissingAttribute(module_name, attr) from e if not isinstance(instance, cls): raise WrongAttributeType(f'{module_name}.{attr}', cls) @@ -38,7 +37,6 @@ def import_instance(module_name: str, attr: str, cls: Type[T]) -> T: return instance - def append_module_path(module_name: str, sys_path: list[str]) -> None: """Given a module name, if the module is not directly importable tries to find if this module exists in the current working directory. diff --git a/saulve/puzzle.py b/saulve/puzzle.py index 0e50343..698e718 100644 --- a/saulve/puzzle.py +++ b/saulve/puzzle.py @@ -5,7 +5,6 @@ from .errors import PuzzleHasNoSolution - __all__ = ['Puzzle'] @@ -43,7 +42,12 @@ class Puzzle(Generic[PuzzleInput]): 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, + puzzle_input: PuzzleInput | None = None, + ) -> None: self.name = name self.puzzle_input = puzzle_input self.steps: list[SolutionFunction] = [] diff --git a/tests/challenges/fixtures/generic/contains_puzzle.py b/tests/challenges/fixtures/generic/contains_puzzle.py index d7fc6e4..9a22b73 100644 --- a/tests/challenges/fixtures/generic/contains_puzzle.py +++ b/tests/challenges/fixtures/generic/contains_puzzle.py @@ -1,4 +1,3 @@ from saulve import Puzzle - puzzle = Puzzle(name='A puzzle', puzzle_input=12) diff --git a/tests/challenges/test_advent_of_code.py b/tests/challenges/test_advent_of_code.py index 9c2e6e6..eda7c67 100644 --- a/tests/challenges/test_advent_of_code.py +++ b/tests/challenges/test_advent_of_code.py @@ -1,7 +1,7 @@ from unittest.mock import Mock -from saulve.puzzle import Puzzle from saulve.challenges.advent_of_code import AdventOfCodePuzzle, Calendar +from saulve.puzzle import Puzzle class TestCalendar: diff --git a/tests/challenges/test_generic.py b/tests/challenges/test_generic.py index 373f8fa..2e873ba 100644 --- a/tests/challenges/test_generic.py +++ b/tests/challenges/test_generic.py @@ -68,12 +68,16 @@ def test_extract_puzzle_id_from_regexp(self) -> None: def test_warn_about_duplicated_puzzle_id(self, caplog) -> None: loader = GenericLoader(generic_fixtures, id_regexp=r'(puzzle)') - with caplog.at_level(logging.WARNING, logger='saulve.challenge.generic'): + with caplog.at_level( + logging.WARNING, + logger='saulve.challenge.generic' + ): loader.load() assert ( re.search( - r"Duplicated puzzle id 'puzzle' in tests.challenges.fixtures.generic.\w*puzzle\w*", + r"Duplicated puzzle id 'puzzle' in " + r"tests.challenges.fixtures.generic.\w*puzzle\w*", caplog.text, ) is not None ), f'"{caplog.text}" does not match expected message.' diff --git a/tests/test_cli.py b/tests/test_cli.py index 4d803e6..e58af94 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -4,7 +4,6 @@ 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')