Skip to content

Commit

Permalink
Merge pull request #1 from RonnyPfannschmidt/ronny/cleanup
Browse files Browse the repository at this point in the history
get linting tasks happy and mypy strict
  • Loading branch information
ctheune authored Dec 1, 2023
2 parents e6b37fa + a258609 commit b2863d2
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 87 deletions.
22 changes: 11 additions & 11 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -29,13 +29,13 @@ repos:
- hooks:
- id: black
repo: https://github.com/psf/black
rev: 23.3.0
- hooks:
- args:
- --ignore
- E501
- --ignore
- F401
id: ruff
repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.0.289
rev: 23.11.0
- repo: https://github.com/astral-sh/ruff-pre-commit
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
39 changes: 25 additions & 14 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -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 = "[email protected]" },
]
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 = []

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"

Expand All @@ -41,7 +48,6 @@ path = "src/pytest_patterns/__about__.py"
[tool.hatch.envs.default]
dependencies = [
"coverage[toml]>=6.5",
"pytest",
"pytest-cov",
]

Expand All @@ -59,12 +65,13 @@ python = ["3.7", "3.8", "3.9", "3.10", "3.11"]
[tool.hatch.envs.lint]
detached = true
dependencies = [
"pytest>=7",
"black>=23.1.0",
"mypy>=1.0.0",
"ruff>=0.0.243",
]
[tool.hatch.envs.lint.scripts]
typing = "mypy --install-types --non-interactive {args:src/pytest_patterns tests}"
typing = "mypy {args:src/pytest_patterns tests}"
style = [
"ruff {args:.}",
"black --check --diff {args:.}",
Expand Down Expand Up @@ -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
Expand All @@ -160,6 +171,6 @@ exclude_lines = [
"if TYPE_CHECKING:",
]

[tool.isort]
profile = "black"
line_length = 80
[tool.mypy]
strict=true
python_version = "3.8"
97 changes: 54 additions & 43 deletions src/pytest_patterns/plugin.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
from __future__ import annotations

import enum
import re
from typing import Iterable, List, Set, Tuple
from typing import Any, Iterator

import pytest


@pytest.fixture
def patterns():
yield PatternsLib()
def patterns() -> PatternsLib:
return PatternsLib()


def pytest_assertrepr_compare(op, left, right):
def pytest_assertrepr_compare(
op: str, left: Any, right: Any
) -> list[str] | None:
if op != "==":
return
if left.__class__.__name__ == "Pattern":
return None
if isinstance(left, Pattern):
return list(left._audit(right).report())
elif right.__class__.__name__ == "Pattern":
elif isinstance(right, Pattern):
return list(right._audit(left).report())
else:
return None


class Status(enum.Enum):
Expand All @@ -26,7 +32,7 @@ class Status(enum.Enum):
REFUSED = 4

@property
def symbol(self):
def symbol(self) -> str:
return STATUS_SYMBOLS[self]


Expand All @@ -40,16 +46,16 @@ def symbol(self):
EMPTY_LINE_PATTERN = "<empty-line>"


def match(pattern, line):
def match(pattern: str, line: str) -> bool | re.Match[str] | None:
if pattern == EMPTY_LINE_PATTERN:
if not line:
return True
pattern = pattern.replace("\t", " " * 8)
line = line.replace("\t", " " * 8)
pattern = re.escape(pattern)
pattern = pattern.replace(r"\.\.\.", ".*?")
pattern = re.compile("^" + pattern + "$")
return pattern.match(line)
re_pattern = re.compile("^" + pattern + "$")
return re_pattern.match(line)


class Line:
Expand All @@ -59,10 +65,10 @@ class Line:
def __init__(self, data: str):
self.data = data

def matches(self, expectation: str):
def matches(self, expectation: str) -> bool:
return bool(match(expectation, self.data))

def mark(self, status: Status, cause: str):
def mark(self, status: Status, cause: str) -> None:
if status.value <= self.status.value:
# Stay in the current status
return
Expand All @@ -71,9 +77,9 @@ def mark(self, status: Status, cause: str):


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 = []
Expand All @@ -83,10 +89,10 @@ def __init__(self, content: str):
for line in content.splitlines():
self.content.append(Line(line))

def cursor(self):
def cursor(self) -> Iterator[Line]:
return iter(self.content)

def in_order(self, name: str, expected_lines: List[str]):
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()
Expand All @@ -100,7 +106,7 @@ def in_order(self, name: str, expected_lines: List[str]):
# Reset the scan, maybe the other lines will match
cursor = self.cursor()

def optional(self, name: str, tolerated_lines: List[str]):
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.
"""
Expand All @@ -109,14 +115,14 @@ def optional(self, name: str, tolerated_lines: List[str]):
if line.matches(tolerated_line):
line.mark(Status.OPTIONAL, name)

def refused(self, name: str, refused_lines: List[str]):
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]):
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():
Expand Down Expand Up @@ -148,7 +154,7 @@ def continuous(self, name: str, continuous_lines: List[str]):
[(name, line) for i, line in continuous_cursor]
)

def report(self):
def report(self) -> Iterator[str]:
yield "String did not meet the expectations."
yield ""
yield " | ".join(
Expand All @@ -170,16 +176,16 @@ def report(self):
yield ""
yield "These are the unmatched expected lines: "
yield ""
for name, line in self.unmatched_expectations:
yield format_line_report(Status.REFUSED.symbol, name, line)
for name, line_str in self.unmatched_expectations:
yield format_line_report(Status.REFUSED.symbol, name, line_str)
if self.matched_refused:
yield ""
yield "These are the matched refused lines: "
yield ""
for name, line in self.matched_refused:
yield format_line_report(Status.REFUSED.symbol, name, line)
for name, line_str in self.matched_refused:
yield format_line_report(Status.REFUSED.symbol, name, line_str)

def is_ok(self):
def is_ok(self) -> bool:
if self.unmatched_expectations:
return False
for line in self.content:
Expand All @@ -188,69 +194,74 @@ def is_ok(self):
return True


def format_line_report(symbol, cause, line):
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:
def __init__(self, library, name):
name: str
library: PatternsLib
ops: list[tuple[str, str, Any]]
inherited: set[str]

def __init__(self, library: PatternsLib, name: str):
self.name = name
self.library = library
self.ops = []
self.inherited = set()

# Modifiers (Verbs)

def merge(self, *base_patterns):
"""Merge the rules from those patterns (recursively) into this pattern."""
def merge(self, *base_patterns: str) -> None:
"""Merge rules from base_patterns (recursively) into this pattern."""
self.inherited.update(base_patterns)

def normalize(self, mode: str):
def normalize(self, mode: str) -> None:
pass

# Matches (Adjectives)

def continuous(self, lines: str):
def continuous(self, lines: str) -> None:
"""These lines must appear once and they must be continuous."""
self.ops.append(("continuous", self.name, pattern_lines(lines)))

def in_order(self, lines: str):
def in_order(self, lines: str) -> None:
"""These lines must appear once and they must be in order."""
self.ops.append(("in_order", self.name, pattern_lines(lines)))

def optional(self, lines: str):
def optional(self, lines: str) -> None:
"""These lines are optional."""
self.ops.append(("optional", self.name, pattern_lines(lines)))

def refused(self, lines: str):
def refused(self, lines: str) -> None:
"""If those lines appear they are refused."""
self.ops.append(("refused", self.name, pattern_lines(lines)))

# Internal API

def flat_ops(self):
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

def _audit(self, content):
def _audit(self, content: str) -> Audit:
audit = Audit(content)
for op, *args in self.flat_ops():
getattr(audit, op)(*args)
return audit

def __eq__(self, other):
def __eq__(self, other: object) -> bool:
assert isinstance(other, str)
audit = self._audit(other)
return audit.is_ok()


class PatternsLib:
def __getattr__(self, name):
self.__dict__[name] = Pattern(self, name)
return self.__dict__[name]
def __getattr__(self, name: str) -> Pattern:
res = self.__dict__[name] = Pattern(self, name)
return res
Loading

0 comments on commit b2863d2

Please sign in to comment.