pytest-orm-boundaries 0.7.1__tar.gz → 0.7.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pytest_orm_boundaries-0.7.1 → pytest_orm_boundaries-0.7.3}/PKG-INFO +1 -1
- {pytest_orm_boundaries-0.7.1 → pytest_orm_boundaries-0.7.3}/pyproject.toml +2 -2
- {pytest_orm_boundaries-0.7.1 → pytest_orm_boundaries-0.7.3}/src/pytest_orm_boundaries/crossings.py +62 -1
- {pytest_orm_boundaries-0.7.1 → pytest_orm_boundaries-0.7.3}/src/pytest_orm_boundaries/guard.py +15 -2
- {pytest_orm_boundaries-0.7.1 → pytest_orm_boundaries-0.7.3}/src/pytest_orm_boundaries/ignores.py +11 -0
- {pytest_orm_boundaries-0.7.1 → pytest_orm_boundaries-0.7.3}/src/pytest_orm_boundaries/plugin.py +17 -1
- {pytest_orm_boundaries-0.7.1 → pytest_orm_boundaries-0.7.3}/LICENSE +0 -0
- {pytest_orm_boundaries-0.7.1 → pytest_orm_boundaries-0.7.3}/README.md +0 -0
- {pytest_orm_boundaries-0.7.1 → pytest_orm_boundaries-0.7.3}/src/pytest_orm_boundaries/__init__.py +0 -0
- {pytest_orm_boundaries-0.7.1 → pytest_orm_boundaries-0.7.3}/src/pytest_orm_boundaries/allows.py +0 -0
- {pytest_orm_boundaries-0.7.1 → pytest_orm_boundaries-0.7.3}/src/pytest_orm_boundaries/callstack.py +0 -0
- {pytest_orm_boundaries-0.7.1 → pytest_orm_boundaries-0.7.3}/src/pytest_orm_boundaries/config.py +0 -0
- {pytest_orm_boundaries-0.7.1 → pytest_orm_boundaries-0.7.3}/src/pytest_orm_boundaries/model_resolution.py +0 -0
- {pytest_orm_boundaries-0.7.1 → pytest_orm_boundaries-0.7.3}/src/pytest_orm_boundaries/prefetch_resolution.py +0 -0
- {pytest_orm_boundaries-0.7.1 → pytest_orm_boundaries-0.7.3}/src/pytest_orm_boundaries/report.py +0 -0
- {pytest_orm_boundaries-0.7.1 → pytest_orm_boundaries-0.7.3}/src/pytest_orm_boundaries/sql_parsing.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytest-orm-boundaries
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.3
|
|
4
4
|
Summary: Pytest plugin that fails tests when ORM queries cross DDD aggregate boundaries (Django supported today).
|
|
5
5
|
Keywords: django,pytest,plugin,orm,ddd,aggregate,boundaries,architecture
|
|
6
6
|
Author: Evgeniia Chibisova
|
|
@@ -4,7 +4,7 @@ build-backend = "uv_build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pytest-orm-boundaries"
|
|
7
|
-
version = "0.7.
|
|
7
|
+
version = "0.7.3"
|
|
8
8
|
description = "Pytest plugin that fails tests when ORM queries cross DDD aggregate boundaries (Django supported today)."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -54,4 +54,4 @@ exclude-newer = "2 weeks"
|
|
|
54
54
|
testpaths = ["tests"]
|
|
55
55
|
|
|
56
56
|
[dependency-groups]
|
|
57
|
-
dev = ["django>=4.2"]
|
|
57
|
+
dev = ["django>=4.2", "pytest-xdist>=3.8"]
|
{pytest_orm_boundaries-0.7.1 → pytest_orm_boundaries-0.7.3}/src/pytest_orm_boundaries/crossings.py
RENAMED
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from dataclasses import dataclass, field
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from typing import TYPE_CHECKING
|
|
7
|
+
from typing import TYPE_CHECKING, TypedDict
|
|
8
8
|
|
|
9
9
|
from pytest_orm_boundaries.callstack import find_frames_inside_project
|
|
10
10
|
|
|
@@ -26,6 +26,24 @@ class CrossingRecord:
|
|
|
26
26
|
tests: set[str] = field(default_factory=set)
|
|
27
27
|
|
|
28
28
|
|
|
29
|
+
class SerializedCrossing(TypedDict):
|
|
30
|
+
"""One crossing encoded using values supported by xdist's worker channel."""
|
|
31
|
+
|
|
32
|
+
file: str
|
|
33
|
+
line_number: int
|
|
34
|
+
crossed_aggregates: list[str]
|
|
35
|
+
involved_models: list[str]
|
|
36
|
+
tests: list[str]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class SerializedTrackerState(TypedDict):
|
|
40
|
+
"""All process-local result state that the xdist controller must merge."""
|
|
41
|
+
|
|
42
|
+
crossings: list[SerializedCrossing]
|
|
43
|
+
seen_ignore_patterns: list[str]
|
|
44
|
+
used_ignore_patterns: list[str]
|
|
45
|
+
|
|
46
|
+
|
|
29
47
|
class CrossingTracker:
|
|
30
48
|
"""Checks label sets against the aggregate rule and records the crossings,
|
|
31
49
|
skipping allowed and ignored files and naming each crossing's call place and
|
|
@@ -137,3 +155,46 @@ class CrossingTracker:
|
|
|
137
155
|
def find_stale_patterns(self) -> list[str]:
|
|
138
156
|
"""Ignore globs whose file ran but never crossed -- safe to delete."""
|
|
139
157
|
return self._ignore_tracker.find_stale_patterns()
|
|
158
|
+
|
|
159
|
+
def serialize_state(self) -> SerializedTrackerState:
|
|
160
|
+
"""Encode collected results using only xdist-serializable values."""
|
|
161
|
+
seen, used = self._ignore_tracker.export_matched_patterns()
|
|
162
|
+
return {
|
|
163
|
+
"crossings": [
|
|
164
|
+
{
|
|
165
|
+
"file": crossing.file,
|
|
166
|
+
"line_number": crossing.line_number,
|
|
167
|
+
"crossed_aggregates": list(crossing.crossed_aggregates),
|
|
168
|
+
"involved_models": list(crossing.involved_models),
|
|
169
|
+
"tests": sorted(crossing.tests),
|
|
170
|
+
}
|
|
171
|
+
for crossing in self.crossings
|
|
172
|
+
],
|
|
173
|
+
"seen_ignore_patterns": sorted(seen),
|
|
174
|
+
"used_ignore_patterns": sorted(used),
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
def merge_state(self, state: SerializedTrackerState) -> None:
|
|
178
|
+
"""Merge results collected by another process into this tracker."""
|
|
179
|
+
for crossing in state["crossings"]:
|
|
180
|
+
crossed = (
|
|
181
|
+
tuple(crossing["crossed_aggregates"]),
|
|
182
|
+
tuple(crossing["involved_models"]),
|
|
183
|
+
)
|
|
184
|
+
tests = crossing["tests"]
|
|
185
|
+
if not tests:
|
|
186
|
+
self.add_record(
|
|
187
|
+
call_place=(crossing["file"], crossing["line_number"]),
|
|
188
|
+
crossing=crossed,
|
|
189
|
+
test=None,
|
|
190
|
+
)
|
|
191
|
+
for test in tests:
|
|
192
|
+
self.add_record(
|
|
193
|
+
call_place=(crossing["file"], crossing["line_number"]),
|
|
194
|
+
crossing=crossed,
|
|
195
|
+
test=test,
|
|
196
|
+
)
|
|
197
|
+
self._ignore_tracker.merge_matched_patterns(
|
|
198
|
+
seen=state["seen_ignore_patterns"],
|
|
199
|
+
used=state["used_ignore_patterns"],
|
|
200
|
+
)
|
{pytest_orm_boundaries-0.7.1 → pytest_orm_boundaries-0.7.3}/src/pytest_orm_boundaries/guard.py
RENAMED
|
@@ -12,7 +12,7 @@ from functools import wraps
|
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
from typing import TYPE_CHECKING
|
|
14
14
|
|
|
15
|
-
from pytest_orm_boundaries.crossings import CrossingTracker
|
|
15
|
+
from pytest_orm_boundaries.crossings import CrossingTracker, SerializedTrackerState
|
|
16
16
|
from pytest_orm_boundaries.model_resolution import resolve_labels
|
|
17
17
|
from pytest_orm_boundaries.prefetch_resolution import resolve_prefetch_step_models
|
|
18
18
|
from pytest_orm_boundaries.sql_parsing import extract_table_names, looks_like_data_query
|
|
@@ -158,6 +158,14 @@ class BoundaryGuard:
|
|
|
158
158
|
def find_stale_patterns(self) -> list[str]:
|
|
159
159
|
return self._tracker.find_stale_patterns()
|
|
160
160
|
|
|
161
|
+
def serialize_state(self) -> SerializedTrackerState:
|
|
162
|
+
"""Return process-local results for transport to an xdist controller."""
|
|
163
|
+
return self._tracker.serialize_state()
|
|
164
|
+
|
|
165
|
+
def merge_state(self, state: SerializedTrackerState) -> None:
|
|
166
|
+
"""Merge results received by the xdist controller from one worker."""
|
|
167
|
+
self._tracker.merge_state(state)
|
|
168
|
+
|
|
161
169
|
def _handle_query(self, sql: str, vendor: str) -> None:
|
|
162
170
|
"""Map one executed data query to its table labels and check them."""
|
|
163
171
|
table_names = extract_table_names(sql, vendor)
|
|
@@ -170,7 +178,12 @@ class BoundaryGuard:
|
|
|
170
178
|
"""Map one prefetch to the model pair per relation step and check them."""
|
|
171
179
|
if not model_instances:
|
|
172
180
|
return
|
|
173
|
-
|
|
181
|
+
from django.db.models import Model
|
|
182
|
+
|
|
183
|
+
first = model_instances[0]
|
|
184
|
+
if not isinstance(first, Model):
|
|
185
|
+
return
|
|
186
|
+
source_model = type(first)
|
|
174
187
|
step_model_pairs = resolve_prefetch_step_models(
|
|
175
188
|
source_model=source_model, lookups=lookups
|
|
176
189
|
)
|
{pytest_orm_boundaries-0.7.1 → pytest_orm_boundaries-0.7.3}/src/pytest_orm_boundaries/ignores.py
RENAMED
|
@@ -31,5 +31,16 @@ class IgnoreTracker:
|
|
|
31
31
|
def find_stale_patterns(self) -> list[str]:
|
|
32
32
|
return sorted(self._seen - self._used)
|
|
33
33
|
|
|
34
|
+
def export_matched_patterns(self) -> tuple[set[str], set[str]]:
|
|
35
|
+
"""Return copies of the patterns seen and used during this run."""
|
|
36
|
+
return set(self._seen), set(self._used)
|
|
37
|
+
|
|
38
|
+
def merge_matched_patterns(
|
|
39
|
+
self, *, seen: Iterable[str], used: Iterable[str]
|
|
40
|
+
) -> None:
|
|
41
|
+
"""Merge ignore activity collected by another process."""
|
|
42
|
+
self._seen.update(seen)
|
|
43
|
+
self._used.update(used)
|
|
44
|
+
|
|
34
45
|
def _find_matching_patterns(self, file_paths: Iterable[str]) -> set[str]:
|
|
35
46
|
return {p for f in file_paths for p in self._patterns if fnmatch(f, p)}
|
{pytest_orm_boundaries-0.7.1 → pytest_orm_boundaries-0.7.3}/src/pytest_orm_boundaries/plugin.py
RENAMED
|
@@ -20,6 +20,7 @@ from pytest_orm_boundaries.ignores import IgnoreTracker
|
|
|
20
20
|
|
|
21
21
|
config_path_key = pytest.StashKey[Path | None]()
|
|
22
22
|
guard_key = pytest.StashKey["BoundaryGuard"]()
|
|
23
|
+
worker_output_key = "pytest_orm_boundaries"
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
class BoundariesConfigWarning(UserWarning):
|
|
@@ -120,10 +121,25 @@ def pytest_runtest_protocol(item: pytest.Item, nextitem: pytest.Item | None):
|
|
|
120
121
|
def pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None:
|
|
121
122
|
"""A clean-but-violating run must still fail the run (via the exit code)."""
|
|
122
123
|
guard = _installed_guard(session.config)
|
|
123
|
-
if guard is
|
|
124
|
+
if guard is None:
|
|
125
|
+
return
|
|
126
|
+
if hasattr(session.config, "workerinput"):
|
|
127
|
+
session.config.workeroutput[worker_output_key] = guard.serialize_state()
|
|
128
|
+
return
|
|
129
|
+
if guard.crossings and exitstatus == pytest.ExitCode.OK:
|
|
124
130
|
session.exitstatus = pytest.ExitCode.TESTS_FAILED
|
|
125
131
|
|
|
126
132
|
|
|
133
|
+
@pytest.hookimpl(optionalhook=True)
|
|
134
|
+
def pytest_testnodedown(node, error) -> None:
|
|
135
|
+
"""Merge one xdist worker's results into the controller's guard."""
|
|
136
|
+
guard = _installed_guard(node.config)
|
|
137
|
+
worker_output = getattr(node, "workeroutput", {})
|
|
138
|
+
state = worker_output.get(worker_output_key)
|
|
139
|
+
if guard is not None and state is not None:
|
|
140
|
+
guard.merge_state(state)
|
|
141
|
+
|
|
142
|
+
|
|
127
143
|
def pytest_terminal_summary(
|
|
128
144
|
terminalreporter: pytest.TerminalReporter,
|
|
129
145
|
exitstatus: int,
|
|
File without changes
|
|
File without changes
|
{pytest_orm_boundaries-0.7.1 → pytest_orm_boundaries-0.7.3}/src/pytest_orm_boundaries/__init__.py
RENAMED
|
File without changes
|
{pytest_orm_boundaries-0.7.1 → pytest_orm_boundaries-0.7.3}/src/pytest_orm_boundaries/allows.py
RENAMED
|
File without changes
|
{pytest_orm_boundaries-0.7.1 → pytest_orm_boundaries-0.7.3}/src/pytest_orm_boundaries/callstack.py
RENAMED
|
File without changes
|
{pytest_orm_boundaries-0.7.1 → pytest_orm_boundaries-0.7.3}/src/pytest_orm_boundaries/config.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pytest_orm_boundaries-0.7.1 → pytest_orm_boundaries-0.7.3}/src/pytest_orm_boundaries/report.py
RENAMED
|
File without changes
|
{pytest_orm_boundaries-0.7.1 → pytest_orm_boundaries-0.7.3}/src/pytest_orm_boundaries/sql_parsing.py
RENAMED
|
File without changes
|