diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ce58e1d..1380ed2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,7 +16,7 @@ repos: - exclude: "(?x)^(\n environments/.*/secret.*|\n .*\\.patch\n)$\n" id: check-toml repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 - hooks: - args: - --profile @@ -29,8 +29,13 @@ repos: - hooks: - id: black repo: https://github.com/psf/black - rev: 23.3.0 + rev: 23.11.0 - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.0.289 + rev: v0.1.6 hooks: - id: ruff + args: [--fix, --exit-non-zero-on-fix] +- repo: https://github.com/tox-dev/pyproject-fmt + rev: "1.5.2" + hooks: + - id: pyproject-fmt diff --git a/pyproject.toml b/pyproject.toml index 52a9e76..2a5291b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,37 +1,44 @@ [build-system] -requires = ["hatchling"] build-backend = "hatchling.build" +requires = [ + "hatchling", +] [project] name = "pytest-patterns" -dynamic = ["version"] -description = 'pytest plugin to make testing complicated long string output easy to write and easy to debug' +description = "pytest plugin to make testing complicated long string output easy to write and easy to debug" readme = "README.md" -requires-python = ">=3.7" +keywords = [ +] license = "MIT" -keywords = [] authors = [ { name = "Christian Theune", email = "ct@flyingcircus.io" }, ] +requires-python = ">=3.7" classifiers = [ - "Framework :: Pytest", "Development Status :: 4 - Beta", + "Framework :: Pytest", "Programming Language :: Python", + "Programming Language :: Python :: 3 :: Only", "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 :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ] -dependencies = ["pytest>=7",] - +dynamic = [ + "version", +] +dependencies = [ + "pytest>=7", +] [project.urls] Documentation = "https://github.com/unknown/pytest-patterns#readme" Issues = "https://github.com/unknown/pytest-patterns/issues" Source = "https://github.com/unknown/pytest-patterns" - [project.entry-points.pytest11] myproject = "pytest_patterns.plugin" @@ -141,6 +148,10 @@ ban-relative-imports = "all" # Tests can use magic values, assertions, and relative imports "tests/**/*" = ["PLR2004", "S101", "TID252"] +[tool.isort] +profile = "black" +line_length = 80 + [tool.coverage.run] source_pkgs = ["pytest_patterns", "tests"] branch = true @@ -160,10 +171,6 @@ exclude_lines = [ "if TYPE_CHECKING:", ] -[tool.isort] -profile = "black" -line_length = 80 - [tool.mypy] strict=true -python_version = "3.8" \ No newline at end of file +python_version = "3.8" diff --git a/src/pytest_patterns/plugin.py b/src/pytest_patterns/plugin.py index 465f21a..3ca016b 100644 --- a/src/pytest_patterns/plugin.py +++ b/src/pytest_patterns/plugin.py @@ -1,16 +1,20 @@ +from __future__ import annotations + import enum import re -from typing import List, Set, Tuple, Any, Iterator, Optional, Union +from typing import Any, Iterator import pytest @pytest.fixture -def patterns() -> "PatternsLib": +def patterns() -> PatternsLib: return PatternsLib() -def pytest_assertrepr_compare(op: str, left: Any, right: Any) -> Optional[List[str]]: +def pytest_assertrepr_compare( + op: str, left: Any, right: Any +) -> list[str] | None: if op != "==": return None if isinstance(left, Pattern): @@ -21,7 +25,6 @@ def pytest_assertrepr_compare(op: str, left: Any, right: Any) -> Optional[List[s return None - class Status(enum.Enum): UNEXPECTED = 1 OPTIONAL = 2 @@ -43,7 +46,7 @@ def symbol(self) -> str: EMPTY_LINE_PATTERN = "" -def match(pattern: str, line: str) -> Optional[ Union[bool, "re.Match[str]"]]: +def match(pattern: str, line: str) -> bool | re.Match[str] | None: if pattern == EMPTY_LINE_PATTERN: if not line: return True @@ -74,9 +77,9 @@ def mark(self, status: Status, cause: str) -> None: class Audit: - content: List[Line] - unmatched_expectations: List[Tuple[str, str]] - matched_refused: Set[Tuple[str, str]] + content: list[Line] + unmatched_expectations: list[tuple[str, str]] + matched_refused: set[tuple[str, str]] def __init__(self, content: str): self.unmatched_expectations = [] @@ -89,7 +92,7 @@ def __init__(self, content: str): def cursor(self) -> Iterator[Line]: return iter(self.content) - def in_order(self, name: str, expected_lines: List[str]) -> None: + def in_order(self, name: str, expected_lines: list[str]) -> None: """Expect all lines exist and come in order, but they may be interleaved with other lines.""" cursor = self.cursor() @@ -103,7 +106,7 @@ def in_order(self, name: str, expected_lines: List[str]) -> None: # Reset the scan, maybe the other lines will match cursor = self.cursor() - def optional(self, name: str, tolerated_lines: List[str]) -> None: + def optional(self, name: str, tolerated_lines: list[str]) -> None: """Those lines may exist and then they may appear anywhere a number of times, or they may not exist. """ @@ -112,14 +115,14 @@ def optional(self, name: str, tolerated_lines: List[str]) -> None: if line.matches(tolerated_line): line.mark(Status.OPTIONAL, name) - def refused(self, name: str, refused_lines: List[str]) -> None: + def refused(self, name: str, refused_lines: list[str]) -> None: for refused_line in refused_lines: for line in self.cursor(): if line.matches(refused_line): line.mark(Status.REFUSED, name) self.matched_refused.add((name, refused_line)) - def continuous(self, name: str, continuous_lines: List[str]) -> None: + def continuous(self, name: str, continuous_lines: list[str]) -> None: continuous_cursor = enumerate(continuous_lines) continuous_index, continuous_line = next(continuous_cursor) for line in self.cursor(): @@ -195,18 +198,18 @@ def format_line_report(symbol: str, cause: str, line: str) -> str: return symbol + " " + cause.ljust(15)[:15] + " | " + line -def pattern_lines(lines: str) -> List[str]: +def pattern_lines(lines: str) -> list[str]: # Remove leading whitespace, ignore empty lines. return list(filter(None, lines.splitlines())) class Pattern: name: str - library: "PatternsLib" - ops: List[Tuple[str, str, Any]] - inherited: Set[str] + library: PatternsLib + ops: list[tuple[str, str, Any]] + inherited: set[str] - def __init__(self, library: "PatternsLib", name: str): + def __init__(self, library: PatternsLib, name: str): self.name = name self.library = library self.ops = [] @@ -241,7 +244,7 @@ def refused(self, lines: str) -> None: # Internal API - def flat_ops(self) -> Iterator[Tuple[str, str, Any]]: + def flat_ops(self) -> Iterator[tuple[str, str, Any]]: for inherited_pattern in self.inherited: yield from getattr(self.library, inherited_pattern).flat_ops() yield from self.ops diff --git a/tests/test_basics.py b/tests/test_basics.py index 788bce3..5b96d87 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -1,4 +1,5 @@ import pytest + from pytest_patterns.plugin import PatternsLib GENERIC_HEADER = [ @@ -15,7 +16,7 @@ def test_patternslib_multiple_accesses(patterns: PatternsLib) -> None: assert patterns.foo is patterns.foo -def test_empty_pattern_empty_string_is_ok(patterns:PatternsLib) -> None: +def test_empty_pattern_empty_string_is_ok(patterns: PatternsLib) -> None: # This is fine IMHO. The whole general assumption is that we only reject # unexpected content and fail if required content is missing. If there is # no content, then there is no unexpected content and if you didn't expect @@ -25,7 +26,7 @@ def test_empty_pattern_empty_string_is_ok(patterns:PatternsLib) -> None: assert audit.is_ok() -def test_unexpected_lines_fail(patterns:PatternsLib) -> None: +def test_unexpected_lines_fail(patterns: PatternsLib) -> None: audit = patterns.nothing._audit("This is an unexpected line") assert list(audit.report()) == [ *GENERIC_HEADER, @@ -34,7 +35,7 @@ def test_unexpected_lines_fail(patterns:PatternsLib) -> None: assert not audit.is_ok() -def test_empty_lines_do_not_match(patterns :PatternsLib) -> None: +def test_empty_lines_do_not_match(patterns: PatternsLib) -> None: patterns.nothing.optional("") audit = patterns.nothing._audit( """ @@ -107,7 +108,9 @@ def test_comprehensive(patterns: PatternsLib) -> None: ) -def test_in_order_lines_clear_with_intermittent_input(patterns: PatternsLib) -> None: +def test_in_order_lines_clear_with_intermittent_input( + patterns: PatternsLib, +) -> None: pattern = patterns.in_order pattern.in_order( """ @@ -173,7 +176,9 @@ def test_refused_lines_fail(patterns: PatternsLib) -> None: assert not audit.is_ok() -def test_continuous_lines_only_clear_if_not_interrupted(patterns: PatternsLib) -> None: +def test_continuous_lines_only_clear_if_not_interrupted( + patterns: PatternsLib, +) -> None: pattern = patterns.focus pattern.optional("asdf") pattern.continuous( @@ -240,7 +245,9 @@ def test_continuous_lines_only_clear_if_not_interrupted(patterns: PatternsLib) - assert not audit.is_ok() -def test_continuous_lines_fail_and_report_if_first_line_isnt_matching(patterns: PatternsLib) -> None: +def test_continuous_lines_fail_and_report_if_first_line_isnt_matching( + patterns: PatternsLib, +) -> None: pattern = patterns.focus pattern.continuous( """