pytest-orm-boundaries 0.6.0__tar.gz → 0.7.1__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.6.0 → pytest_orm_boundaries-0.7.1}/PKG-INFO +33 -18
- {pytest_orm_boundaries-0.6.0 → pytest_orm_boundaries-0.7.1}/README.md +32 -17
- {pytest_orm_boundaries-0.6.0 → pytest_orm_boundaries-0.7.1}/pyproject.toml +1 -1
- {pytest_orm_boundaries-0.6.0 → pytest_orm_boundaries-0.7.1}/src/pytest_orm_boundaries/config.py +35 -5
- {pytest_orm_boundaries-0.6.0 → pytest_orm_boundaries-0.7.1}/src/pytest_orm_boundaries/report.py +2 -2
- {pytest_orm_boundaries-0.6.0 → pytest_orm_boundaries-0.7.1}/LICENSE +0 -0
- {pytest_orm_boundaries-0.6.0 → pytest_orm_boundaries-0.7.1}/src/pytest_orm_boundaries/__init__.py +0 -0
- {pytest_orm_boundaries-0.6.0 → pytest_orm_boundaries-0.7.1}/src/pytest_orm_boundaries/allows.py +0 -0
- {pytest_orm_boundaries-0.6.0 → pytest_orm_boundaries-0.7.1}/src/pytest_orm_boundaries/callstack.py +0 -0
- {pytest_orm_boundaries-0.6.0 → pytest_orm_boundaries-0.7.1}/src/pytest_orm_boundaries/crossings.py +0 -0
- {pytest_orm_boundaries-0.6.0 → pytest_orm_boundaries-0.7.1}/src/pytest_orm_boundaries/guard.py +0 -0
- {pytest_orm_boundaries-0.6.0 → pytest_orm_boundaries-0.7.1}/src/pytest_orm_boundaries/ignores.py +0 -0
- {pytest_orm_boundaries-0.6.0 → pytest_orm_boundaries-0.7.1}/src/pytest_orm_boundaries/model_resolution.py +0 -0
- {pytest_orm_boundaries-0.6.0 → pytest_orm_boundaries-0.7.1}/src/pytest_orm_boundaries/plugin.py +0 -0
- {pytest_orm_boundaries-0.6.0 → pytest_orm_boundaries-0.7.1}/src/pytest_orm_boundaries/prefetch_resolution.py +0 -0
- {pytest_orm_boundaries-0.6.0 → pytest_orm_boundaries-0.7.1}/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.
|
|
3
|
+
Version: 0.7.1
|
|
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
|
|
@@ -50,24 +50,37 @@ and reports access that crosses a configured boundary - through `__` lookups,
|
|
|
50
50
|
|
|
51
51
|
## Install
|
|
52
52
|
|
|
53
|
-
Install the plugin
|
|
53
|
+
Install the plugin in your Django project:
|
|
54
54
|
|
|
55
55
|
```bash
|
|
56
|
-
pip install
|
|
56
|
+
pip install pytest-orm-boundaries
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|
pytest discovers the plugin automatically.
|
|
60
60
|
|
|
61
|
+
The plugin uses the Django version already installed by your project. If you are
|
|
62
|
+
installing into an environment without Django and want pip to install it too,
|
|
63
|
+
use the optional extra:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install "pytest-orm-boundaries[django]"
|
|
67
|
+
```
|
|
68
|
+
|
|
61
69
|
## Configure
|
|
62
70
|
|
|
63
|
-
Declare your aggregates in `boundaries.toml` at the project
|
|
64
|
-
the file with `--boundaries-config` / the
|
|
71
|
+
Declare your aggregates and their Django models in `boundaries.toml` at the project
|
|
72
|
+
root (or point at the file with `--boundaries-config` / the
|
|
73
|
+
`boundaries_config` ini option):
|
|
65
74
|
|
|
66
75
|
```toml
|
|
67
|
-
[aggregates]
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
76
|
+
[aggregates.client]
|
|
77
|
+
models = ["bookshop.Client"]
|
|
78
|
+
|
|
79
|
+
[aggregates.book]
|
|
80
|
+
models = ["bookshop.Book"]
|
|
81
|
+
|
|
82
|
+
[aggregates.purchase]
|
|
83
|
+
models = ["bookshop.Purchase", "bookshop.PurchaseLine"]
|
|
71
84
|
```
|
|
72
85
|
|
|
73
86
|
Models are written as `app_label.Model`. Models not listed in any aggregate are
|
|
@@ -146,18 +159,21 @@ Places are ordered by how many tests they affect. Pass `-v` to see every affecte
|
|
|
146
159
|
|
|
147
160
|
## Allow and ignore
|
|
148
161
|
|
|
149
|
-
CQRS read models may cross boundaries
|
|
162
|
+
CQRS read models may cross aggregate boundaries by design, while existing
|
|
163
|
+
application code may contain crossings you want to fix over time. `[allow]` and
|
|
164
|
+
`[ignore]` let you tell the plugin which is which:
|
|
150
165
|
|
|
151
166
|
- `[allow]` - the crossing is **intentional**. Use it for code that is meant to
|
|
152
|
-
span aggregates, such as CQRS read models
|
|
153
|
-
|
|
167
|
+
span aggregates, such as CQRS read models. An allowed crossing is suppressed
|
|
168
|
+
and never reported.
|
|
154
169
|
- `[ignore]` - the crossing is **known debt** you plan to fix. It is suppressed
|
|
155
|
-
|
|
170
|
+
and the plugin reports the entry when its matching code runs without a
|
|
171
|
+
crossing.
|
|
156
172
|
|
|
157
173
|
```toml
|
|
158
174
|
[allow]
|
|
159
175
|
files = [
|
|
160
|
-
"app/
|
|
176
|
+
"app/read_models/sales.py",
|
|
161
177
|
]
|
|
162
178
|
|
|
163
179
|
[ignore]
|
|
@@ -173,13 +189,12 @@ resolved relative to pytest's root directory and matched against either:
|
|
|
173
189
|
- the file that issues the query, or
|
|
174
190
|
- the test file.
|
|
175
191
|
|
|
176
|
-
An `[ignore]` whose
|
|
177
|
-
boundary is stale
|
|
192
|
+
An `[ignore]` whose matching code runs through the whole suite without crossing
|
|
193
|
+
a boundary is stale, so the plugin lists it for removal:
|
|
178
194
|
|
|
179
195
|
```
|
|
180
196
|
======================= orm-boundaries: stale ignores ========================
|
|
181
|
-
These [ignore] entries
|
|
182
|
-
Remove them from boundaries.toml:
|
|
197
|
+
These [ignore] entries matched files that ran without crossing a boundary. Remove them from boundaries.toml:
|
|
183
198
|
- app/billing.py
|
|
184
199
|
```
|
|
185
200
|
|
|
@@ -21,24 +21,37 @@ and reports access that crosses a configured boundary - through `__` lookups,
|
|
|
21
21
|
|
|
22
22
|
## Install
|
|
23
23
|
|
|
24
|
-
Install the plugin
|
|
24
|
+
Install the plugin in your Django project:
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
|
-
pip install
|
|
27
|
+
pip install pytest-orm-boundaries
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
pytest discovers the plugin automatically.
|
|
31
31
|
|
|
32
|
+
The plugin uses the Django version already installed by your project. If you are
|
|
33
|
+
installing into an environment without Django and want pip to install it too,
|
|
34
|
+
use the optional extra:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install "pytest-orm-boundaries[django]"
|
|
38
|
+
```
|
|
39
|
+
|
|
32
40
|
## Configure
|
|
33
41
|
|
|
34
|
-
Declare your aggregates in `boundaries.toml` at the project
|
|
35
|
-
the file with `--boundaries-config` / the
|
|
42
|
+
Declare your aggregates and their Django models in `boundaries.toml` at the project
|
|
43
|
+
root (or point at the file with `--boundaries-config` / the
|
|
44
|
+
`boundaries_config` ini option):
|
|
36
45
|
|
|
37
46
|
```toml
|
|
38
|
-
[aggregates]
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
[aggregates.client]
|
|
48
|
+
models = ["bookshop.Client"]
|
|
49
|
+
|
|
50
|
+
[aggregates.book]
|
|
51
|
+
models = ["bookshop.Book"]
|
|
52
|
+
|
|
53
|
+
[aggregates.purchase]
|
|
54
|
+
models = ["bookshop.Purchase", "bookshop.PurchaseLine"]
|
|
42
55
|
```
|
|
43
56
|
|
|
44
57
|
Models are written as `app_label.Model`. Models not listed in any aggregate are
|
|
@@ -117,18 +130,21 @@ Places are ordered by how many tests they affect. Pass `-v` to see every affecte
|
|
|
117
130
|
|
|
118
131
|
## Allow and ignore
|
|
119
132
|
|
|
120
|
-
CQRS read models may cross boundaries
|
|
133
|
+
CQRS read models may cross aggregate boundaries by design, while existing
|
|
134
|
+
application code may contain crossings you want to fix over time. `[allow]` and
|
|
135
|
+
`[ignore]` let you tell the plugin which is which:
|
|
121
136
|
|
|
122
137
|
- `[allow]` - the crossing is **intentional**. Use it for code that is meant to
|
|
123
|
-
span aggregates, such as CQRS read models
|
|
124
|
-
|
|
138
|
+
span aggregates, such as CQRS read models. An allowed crossing is suppressed
|
|
139
|
+
and never reported.
|
|
125
140
|
- `[ignore]` - the crossing is **known debt** you plan to fix. It is suppressed
|
|
126
|
-
|
|
141
|
+
and the plugin reports the entry when its matching code runs without a
|
|
142
|
+
crossing.
|
|
127
143
|
|
|
128
144
|
```toml
|
|
129
145
|
[allow]
|
|
130
146
|
files = [
|
|
131
|
-
"app/
|
|
147
|
+
"app/read_models/sales.py",
|
|
132
148
|
]
|
|
133
149
|
|
|
134
150
|
[ignore]
|
|
@@ -144,13 +160,12 @@ resolved relative to pytest's root directory and matched against either:
|
|
|
144
160
|
- the file that issues the query, or
|
|
145
161
|
- the test file.
|
|
146
162
|
|
|
147
|
-
An `[ignore]` whose
|
|
148
|
-
boundary is stale
|
|
163
|
+
An `[ignore]` whose matching code runs through the whole suite without crossing
|
|
164
|
+
a boundary is stale, so the plugin lists it for removal:
|
|
149
165
|
|
|
150
166
|
```
|
|
151
167
|
======================= orm-boundaries: stale ignores ========================
|
|
152
|
-
These [ignore] entries
|
|
153
|
-
Remove them from boundaries.toml:
|
|
168
|
+
These [ignore] entries matched files that ran without crossing a boundary. Remove them from boundaries.toml:
|
|
154
169
|
- app/billing.py
|
|
155
170
|
```
|
|
156
171
|
|
|
@@ -4,7 +4,7 @@ build-backend = "uv_build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pytest-orm-boundaries"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.7.1"
|
|
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"
|
{pytest_orm_boundaries-0.6.0 → pytest_orm_boundaries-0.7.1}/src/pytest_orm_boundaries/config.py
RENAMED
|
@@ -62,13 +62,41 @@ def _parse_aggregates(*, data: dict[str, Any], path: Path) -> dict[str, str]:
|
|
|
62
62
|
Model labels are lower-cased ("app_label.modelname") for case-insensitive
|
|
63
63
|
matching.
|
|
64
64
|
"""
|
|
65
|
+
aggregate_definitions = data.get("aggregates", {})
|
|
66
|
+
if not isinstance(aggregate_definitions, dict):
|
|
67
|
+
raise BoundariesConfigError(
|
|
68
|
+
f"{path}: define aggregates as named sections, for example "
|
|
69
|
+
"[aggregates.order] with models = [...]"
|
|
70
|
+
)
|
|
71
|
+
|
|
65
72
|
aggregates_by_model: dict[str, str] = {}
|
|
66
|
-
for aggregate,
|
|
67
|
-
if isinstance(
|
|
68
|
-
|
|
73
|
+
for aggregate, definition in aggregate_definitions.items():
|
|
74
|
+
if not isinstance(definition, dict):
|
|
75
|
+
raise BoundariesConfigError(
|
|
76
|
+
f"{path}: aggregate '{aggregate}' must define models in its own "
|
|
77
|
+
"section: "
|
|
78
|
+
f"[aggregates.{aggregate}] with models = [...]"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
unknown_fields = set(definition) - {"models"}
|
|
82
|
+
if unknown_fields:
|
|
83
|
+
fields = ", ".join(sorted(unknown_fields))
|
|
84
|
+
raise BoundariesConfigError(
|
|
85
|
+
f"{path}: aggregate '{aggregate}' has unknown field(s): {fields}"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
if "models" not in definition:
|
|
89
|
+
raise BoundariesConfigError(
|
|
90
|
+
f"{path}: aggregate '{aggregate}' is missing required 'models'"
|
|
91
|
+
)
|
|
92
|
+
members = definition["models"]
|
|
69
93
|
if not isinstance(members, list):
|
|
70
94
|
raise BoundariesConfigError(
|
|
71
|
-
f"{path}: aggregate '{aggregate}' must be a list
|
|
95
|
+
f"{path}: aggregate '{aggregate}' models must be a list"
|
|
96
|
+
)
|
|
97
|
+
if not members:
|
|
98
|
+
raise BoundariesConfigError(
|
|
99
|
+
f"{path}: aggregate '{aggregate}' models must not be empty"
|
|
72
100
|
)
|
|
73
101
|
for label in members:
|
|
74
102
|
if not isinstance(label, str):
|
|
@@ -90,7 +118,9 @@ def _parse_file_globs(
|
|
|
90
118
|
"""Read ``[allow]`` and ``[ignore] files`` into a list of glob patterns."""
|
|
91
119
|
section_data = data.get(section_name, {})
|
|
92
120
|
if not isinstance(section_data, dict):
|
|
93
|
-
raise BoundariesConfigError(
|
|
121
|
+
raise BoundariesConfigError(
|
|
122
|
+
f"{path}: [{section_name}] must be a section with a 'files' list"
|
|
123
|
+
)
|
|
94
124
|
|
|
95
125
|
files = section_data.get("files", [])
|
|
96
126
|
if not isinstance(files, list):
|
{pytest_orm_boundaries-0.6.0 → pytest_orm_boundaries-0.7.1}/src/pytest_orm_boundaries/report.py
RENAMED
|
@@ -88,8 +88,8 @@ def report_stale_ignores(
|
|
|
88
88
|
return
|
|
89
89
|
terminalreporter.section("orm-boundaries: stale ignores", yellow=True, bold=True)
|
|
90
90
|
terminalreporter.write_line(
|
|
91
|
-
"These [ignore] entries
|
|
92
|
-
f"
|
|
91
|
+
"These [ignore] entries matched files that ran without crossing a "
|
|
92
|
+
f"boundary. Remove them from {CONFIG_FILE_NAME}:",
|
|
93
93
|
yellow=True,
|
|
94
94
|
)
|
|
95
95
|
for pattern in stale:
|
|
File without changes
|
{pytest_orm_boundaries-0.6.0 → pytest_orm_boundaries-0.7.1}/src/pytest_orm_boundaries/__init__.py
RENAMED
|
File without changes
|
{pytest_orm_boundaries-0.6.0 → pytest_orm_boundaries-0.7.1}/src/pytest_orm_boundaries/allows.py
RENAMED
|
File without changes
|
{pytest_orm_boundaries-0.6.0 → pytest_orm_boundaries-0.7.1}/src/pytest_orm_boundaries/callstack.py
RENAMED
|
File without changes
|
{pytest_orm_boundaries-0.6.0 → pytest_orm_boundaries-0.7.1}/src/pytest_orm_boundaries/crossings.py
RENAMED
|
File without changes
|
{pytest_orm_boundaries-0.6.0 → pytest_orm_boundaries-0.7.1}/src/pytest_orm_boundaries/guard.py
RENAMED
|
File without changes
|
{pytest_orm_boundaries-0.6.0 → pytest_orm_boundaries-0.7.1}/src/pytest_orm_boundaries/ignores.py
RENAMED
|
File without changes
|
|
File without changes
|
{pytest_orm_boundaries-0.6.0 → pytest_orm_boundaries-0.7.1}/src/pytest_orm_boundaries/plugin.py
RENAMED
|
File without changes
|
|
File without changes
|
{pytest_orm_boundaries-0.6.0 → pytest_orm_boundaries-0.7.1}/src/pytest_orm_boundaries/sql_parsing.py
RENAMED
|
File without changes
|