pytest-querycount 0.3.0__py3-none-any.whl
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_querycount/__init__.py +31 -0
- pytest_querycount/backends/__init__.py +59 -0
- pytest_querycount/backends/sqlalchemy.py +215 -0
- pytest_querycount/checks.py +166 -0
- pytest_querycount/errors.py +39 -0
- pytest_querycount/explain.py +175 -0
- pytest_querycount/normalize.py +97 -0
- pytest_querycount/plugin.py +284 -0
- pytest_querycount/py.typed +0 -0
- pytest_querycount/recorder.py +214 -0
- pytest_querycount/records.py +66 -0
- pytest_querycount/report.py +64 -0
- pytest_querycount-0.3.0.dist-info/METADATA +336 -0
- pytest_querycount-0.3.0.dist-info/RECORD +17 -0
- pytest_querycount-0.3.0.dist-info/WHEEL +4 -0
- pytest_querycount-0.3.0.dist-info/entry_points.txt +2 -0
- pytest_querycount-0.3.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""The end-of-session table.
|
|
2
|
+
|
|
3
|
+
This is the part people leave switched on. A budget tells you when you crossed a
|
|
4
|
+
line you drew; the report tells you where the lines should go.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class TestStats:
|
|
14
|
+
nodeid: str
|
|
15
|
+
count: int
|
|
16
|
+
duration: float
|
|
17
|
+
worst_duplicate: int
|
|
18
|
+
worst_sql: str | None = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def build(stats: dict[str, TestStats], top: int = 10) -> list[str]:
|
|
22
|
+
"""Render the summary as terminal lines. Empty when nothing was recorded."""
|
|
23
|
+
recorded = [entry for entry in stats.values() if entry.count]
|
|
24
|
+
if not recorded:
|
|
25
|
+
return []
|
|
26
|
+
|
|
27
|
+
recorded.sort(key=lambda entry: (entry.count, entry.duration), reverse=True)
|
|
28
|
+
shown = recorded[:top]
|
|
29
|
+
|
|
30
|
+
id_width = max(len(entry.nodeid) for entry in shown)
|
|
31
|
+
id_width = min(max(id_width, 4), 70)
|
|
32
|
+
|
|
33
|
+
lines = [
|
|
34
|
+
f"{'queries':>7} {'time':>9} {'dupes':>5} test",
|
|
35
|
+
f"{'-' * 7} {'-' * 9} {'-' * 5} {'-' * id_width}",
|
|
36
|
+
]
|
|
37
|
+
for entry in shown:
|
|
38
|
+
dupes = str(entry.worst_duplicate) if entry.worst_duplicate > 1 else "-"
|
|
39
|
+
flag = "!" if entry.worst_duplicate > 1 else " "
|
|
40
|
+
lines.append(
|
|
41
|
+
f"{entry.count:>7} {entry.duration * 1000:>7.1f}ms "
|
|
42
|
+
f"{dupes:>4}{flag} {_trim(entry.nodeid, id_width)}"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
total_queries = sum(entry.count for entry in recorded)
|
|
46
|
+
total_time = sum(entry.duration for entry in recorded)
|
|
47
|
+
hidden = len(recorded) - len(shown)
|
|
48
|
+
footer = f"{total_queries} queries in {total_time * 1000:.1f}ms across {len(recorded)} tests"
|
|
49
|
+
if hidden:
|
|
50
|
+
footer += f" ({hidden} more not shown, raise --querycount-top)"
|
|
51
|
+
lines += ["", footer]
|
|
52
|
+
|
|
53
|
+
flagged = [entry for entry in shown if entry.worst_duplicate > 1]
|
|
54
|
+
if flagged:
|
|
55
|
+
lines.append(
|
|
56
|
+
"! marks a repeated query shape -- add @pytest.mark.no_n_plus_one to see the detail."
|
|
57
|
+
)
|
|
58
|
+
return lines
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _trim(text: str, width: int) -> str:
|
|
62
|
+
if len(text) <= width:
|
|
63
|
+
return text
|
|
64
|
+
return "…" + text[-(width - 1) :]
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pytest-querycount
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Fail your tests when they run too many SQL queries. Catch N+1 problems in CI, not in production.
|
|
5
|
+
Project-URL: Homepage, https://github.com/ldavidsm/pytest-querycount
|
|
6
|
+
Project-URL: Issues, https://github.com/ldavidsm/pytest-querycount/issues
|
|
7
|
+
Project-URL: Changelog, https://github.com/ldavidsm/pytest-querycount/blob/main/CHANGELOG.md
|
|
8
|
+
Author: Luis David Senra
|
|
9
|
+
License: MIT License
|
|
10
|
+
|
|
11
|
+
Copyright (c) 2026 Luis David Senra
|
|
12
|
+
|
|
13
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
14
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
15
|
+
in the Software without restriction, including without limitation the rights
|
|
16
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
17
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
18
|
+
furnished to do so, subject to the following conditions:
|
|
19
|
+
|
|
20
|
+
The above copyright notice and this permission notice shall be included in all
|
|
21
|
+
copies or substantial portions of the Software.
|
|
22
|
+
|
|
23
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
24
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
25
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
26
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
27
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
28
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
29
|
+
SOFTWARE.
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Keywords: database,n+1,orm,performance,pytest,query-count,sql,sqlalchemy,testing
|
|
32
|
+
Classifier: Development Status :: 4 - Beta
|
|
33
|
+
Classifier: Framework :: Pytest
|
|
34
|
+
Classifier: Intended Audience :: Developers
|
|
35
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
36
|
+
Classifier: Operating System :: OS Independent
|
|
37
|
+
Classifier: Programming Language :: Python :: 3
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
42
|
+
Classifier: Topic :: Database
|
|
43
|
+
Classifier: Topic :: Software Development :: Testing
|
|
44
|
+
Classifier: Typing :: Typed
|
|
45
|
+
Requires-Python: >=3.10
|
|
46
|
+
Requires-Dist: pytest>=8.0
|
|
47
|
+
Provides-Extra: dev
|
|
48
|
+
Requires-Dist: mypy>=1.8; extra == 'dev'
|
|
49
|
+
Requires-Dist: psycopg[binary]>=3.1; extra == 'dev'
|
|
50
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
51
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
52
|
+
Requires-Dist: sqlalchemy>=2.0; extra == 'dev'
|
|
53
|
+
Provides-Extra: postgresql
|
|
54
|
+
Requires-Dist: psycopg[binary]>=3.1; extra == 'postgresql'
|
|
55
|
+
Requires-Dist: sqlalchemy>=2.0; extra == 'postgresql'
|
|
56
|
+
Provides-Extra: sqlalchemy
|
|
57
|
+
Requires-Dist: sqlalchemy>=2.0; extra == 'sqlalchemy'
|
|
58
|
+
Description-Content-Type: text/markdown
|
|
59
|
+
|
|
60
|
+
# pytest-querycount
|
|
61
|
+
|
|
62
|
+
[](https://github.com/ldavidsm/pytest-querycount/actions/workflows/ci.yml)
|
|
63
|
+
[](https://pypi.org/project/pytest-querycount/)
|
|
64
|
+
[](https://pypi.org/project/pytest-querycount/)
|
|
65
|
+
[](LICENSE)
|
|
66
|
+
|
|
67
|
+
Fail your tests when they run too many SQL queries.
|
|
68
|
+
|
|
69
|
+
An endpoint that quietly grows from 2 queries to 200 does not break any test. It
|
|
70
|
+
just gets slower, until one day it is slow enough to notice. This plugin turns
|
|
71
|
+
that drift into a red test.
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
@pytest.mark.max_queries(2)
|
|
75
|
+
def test_billing_summary(db):
|
|
76
|
+
assert len(serialise(db)) == 12
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Add a lazy-loaded relationship to that serialiser and the test fails on the next
|
|
80
|
+
CI run, naming the repeated query and the line of your code that caused it.
|
|
81
|
+
|
|
82
|
+
## Install
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
pip install "pytest-querycount[sqlalchemy]"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Requires Python 3.10+, pytest 8+, and SQLAlchemy 2.x. There is nothing to
|
|
89
|
+
configure: the plugin instruments every engine your suite creates, sync or async.
|
|
90
|
+
|
|
91
|
+
## What it gives you
|
|
92
|
+
|
|
93
|
+
### A query budget
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
@pytest.mark.max_queries(2)
|
|
97
|
+
def test_billing_summary(db):
|
|
98
|
+
assert len(serialise(db)) == 12
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
When the serialiser loops, you get this -- real output, not an illustration:
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
_____________________________ test_billing_summary _____________________________
|
|
105
|
+
|
|
106
|
+
E TooManyQueriesError: test_billing_summary ran 13 queries, budget is 2 (11 over).
|
|
107
|
+
|
|
108
|
+
Repeated query shapes -- most likely where the extra queries come from:
|
|
109
|
+
12x SELECT invoices.id, invoices.customer_id, invoices.cents FROM invoices WHERE ? = invoices.custome…
|
|
110
|
+
from test_billing.py:10 (12x)
|
|
111
|
+
|
|
112
|
+
13 queries in 0.1ms
|
|
113
|
+
1. [ 0.02ms] SELECT customers.id, customers.email FROM customers
|
|
114
|
+
from test_billing.py:11
|
|
115
|
+
2-13. [ 0.06ms] 12x SELECT invoices.id, invoices.customer_id, invoices.cents FROM invoices WHERE …
|
|
116
|
+
from test_billing.py:10
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Three things to notice, because they are the reason this is a package and not a
|
|
120
|
+
snippet:
|
|
121
|
+
|
|
122
|
+
- **The diagnosis comes first.** `12x` on one query shape, before any listing.
|
|
123
|
+
In a CI log you get the culprit in the first three lines.
|
|
124
|
+
- **`test_billing.py:10` is your code**, not a line inside SQLAlchemy. The
|
|
125
|
+
plugin walks up the stack past the ORM to find the line that actually caused
|
|
126
|
+
the query.
|
|
127
|
+
- **The twelve identical queries print as one line.** Twelve copies of the same
|
|
128
|
+
SELECT tell you nothing that `12x` does not, and they would push the useful
|
|
129
|
+
lines off the screen.
|
|
130
|
+
|
|
131
|
+
### N+1 detection without picking a number
|
|
132
|
+
|
|
133
|
+
Sometimes you do not want to choose a budget, you just want to assert that
|
|
134
|
+
nothing is looping.
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
@pytest.mark.no_n_plus_one
|
|
138
|
+
def test_billing_summary(db):
|
|
139
|
+
assert len(serialise(db)) == 12
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
This groups queries by *shape* -- `WHERE id = 42` and `WHERE id = 43` are the
|
|
143
|
+
same shape -- and fails when one shape repeats. By default it looks only at
|
|
144
|
+
SELECTs, since repeated `SAVEPOINT`s are just how transactions work.
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
@pytest.mark.no_n_plus_one(threshold=5) # tolerate up to 4 repeats
|
|
148
|
+
@pytest.mark.no_n_plus_one(kinds=("select", "insert"))
|
|
149
|
+
@pytest.mark.no_n_plus_one(kinds=None) # consider every statement
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Missing indexes (PostgreSQL)
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
@pytest.mark.no_seq_scan
|
|
156
|
+
def test_customer_search(db):
|
|
157
|
+
assert len(find_by_city(db, "city3")) == 1
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
_____________________________ test_customer_search _____________________________
|
|
162
|
+
|
|
163
|
+
E SeqScanError: test_customer_search ran 1 query that no index could serve.
|
|
164
|
+
|
|
165
|
+
PostgreSQL still chose a sequential scan with enable_seqscan disabled, which
|
|
166
|
+
means no index covers the filtered columns.
|
|
167
|
+
|
|
168
|
+
Seq Scan on "demo_customers" Filter: ((city)::text = 'city3'::text)
|
|
169
|
+
try: CREATE INDEX ON demo_customers (city);
|
|
170
|
+
from test_search.py:7
|
|
171
|
+
SELECT demo_customers.id, ... FROM demo_customers WHERE demo_customers.…
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
That table holds **eight rows**, and that matters more than it looks.
|
|
175
|
+
|
|
176
|
+
The obvious way to write this check does not work. Failing on the presence of a
|
|
177
|
+
`Seq Scan` is useless, because on a test database of twenty rows PostgreSQL picks
|
|
178
|
+
a sequential scan *even when a perfect index exists* -- reading twenty rows is
|
|
179
|
+
cheaper than descending a B-tree. So the plan alone cannot distinguish a missing
|
|
180
|
+
index from a small table. And thresholding on table size, the usual next idea,
|
|
181
|
+
means the check never fires on test data at all. Either way you get a check that
|
|
182
|
+
lies to you.
|
|
183
|
+
|
|
184
|
+
So this asks a different question: not "did it scan?" but "**could** it have used
|
|
185
|
+
an index?". The plan is taken with `enable_seqscan` disabled, which makes the
|
|
186
|
+
planner treat sequential scans as enormously expensive. If it still picks one, no
|
|
187
|
+
index can serve that filter -- and that answer does not depend on data volume.
|
|
188
|
+
The test suite for this feature runs against eight rows on purpose, to keep that
|
|
189
|
+
claim honest.
|
|
190
|
+
|
|
191
|
+
Excluding a table you read whole deliberately:
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
@pytest.mark.no_seq_scan(ignore=("countries", "settings"))
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Three things this is careful about:
|
|
198
|
+
|
|
199
|
+
- The `EXPLAIN` runs on a raw DBAPI cursor, invisible to SQLAlchemy's events, so
|
|
200
|
+
it is never counted among your test's queries and cannot recurse.
|
|
201
|
+
- It is wrapped in a savepoint, which both scopes the `enable_seqscan` change and
|
|
202
|
+
absorbs a failed `EXPLAIN` that would otherwise abort your test's transaction.
|
|
203
|
+
- An unfiltered `Seq Scan` is never reported. Reading a whole table is sometimes
|
|
204
|
+
exactly what you meant.
|
|
205
|
+
|
|
206
|
+
Needs `pip install "pytest-querycount[postgresql]"`. On SQLite or MySQL the check
|
|
207
|
+
raises rather than passing, because a check that cannot fail is not a check.
|
|
208
|
+
|
|
209
|
+
### A fixture, for when a marker is too coarse
|
|
210
|
+
|
|
211
|
+
A marker covers the whole test. When you only care about one block:
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
def test_detail(db, querycount):
|
|
215
|
+
with querycount(max_queries=2, no_seq_scan=True) as queries:
|
|
216
|
+
serialise(db)
|
|
217
|
+
|
|
218
|
+
assert queries.count == 2
|
|
219
|
+
assert queries.duplicates() == []
|
|
220
|
+
print(queries.report())
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Call it with no arguments to observe without asserting -- that is how you find
|
|
224
|
+
out what the budget should be before committing to one. The object from the
|
|
225
|
+
`with` is the recorder, and it keeps its records after the block ends.
|
|
226
|
+
|
|
227
|
+
### The report you leave switched on
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
pytest --querycount-report
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
```
|
|
234
|
+
============================== querycount summary ==============================
|
|
235
|
+
queries time dupes test
|
|
236
|
+
------- --------- ----- -------------------------------------------
|
|
237
|
+
13 0.1ms 12! test_billing.py::test_billing_summary
|
|
238
|
+
2 0.0ms - test_billing.py::test_billing_summary_fixed
|
|
239
|
+
|
|
240
|
+
15 queries in 0.1ms across 2 tests
|
|
241
|
+
! marks a repeated query shape -- add @pytest.mark.no_n_plus_one to see the detail.
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
A budget tells you when you crossed a line you drew. This tells you where the
|
|
245
|
+
lines should go, and it is the honest way to adopt the plugin on an existing
|
|
246
|
+
suite: run it once, look at the rows with a `!`, write budgets for those.
|
|
247
|
+
|
|
248
|
+
## Options
|
|
249
|
+
|
|
250
|
+
| Flag | Effect |
|
|
251
|
+
|---|---|
|
|
252
|
+
| `--querycount-report` | Print the summary table |
|
|
253
|
+
| `--querycount-top=N` | How many tests the table lists (default 10) |
|
|
254
|
+
| `--querycount-max=N` | Apply a budget of N to every test without an explicit one |
|
|
255
|
+
| `--querycount-no-seq-scan` | Apply the missing-index check to every test |
|
|
256
|
+
|
|
257
|
+
`--querycount-max` is how you ratchet: set it just above your current worst
|
|
258
|
+
test, then lower it as you fix things.
|
|
259
|
+
|
|
260
|
+
In `pyproject.toml`:
|
|
261
|
+
|
|
262
|
+
```toml
|
|
263
|
+
[tool.pytest.ini_options]
|
|
264
|
+
querycount_max = "20"
|
|
265
|
+
querycount_duplicate_threshold = "3"
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## Design decisions worth knowing
|
|
269
|
+
|
|
270
|
+
- **Only the call phase is measured.** Fixture setup and teardown run outside it,
|
|
271
|
+
so creating a schema and seeding it never consume a budget.
|
|
272
|
+
- **A failing test is never second-guessed.** If your assertion fails, you see
|
|
273
|
+
that failure, not a complaint about query counts on top of it.
|
|
274
|
+
- **`executemany` counts as one query**, because it is one round trip. That is
|
|
275
|
+
the point of it, and the fix for a loop of inserts.
|
|
276
|
+
- **No backend is an error, not a pass.** If nothing is instrumented, every
|
|
277
|
+
count is zero and every budget is trivially satisfied. The plugin raises
|
|
278
|
+
instead, because a budget that cannot fail is worse than no budget.
|
|
279
|
+
|
|
280
|
+
## Why not just count them yourself
|
|
281
|
+
|
|
282
|
+
You can, in about twenty lines of `event.listen`. What you will not have is the
|
|
283
|
+
fingerprinting that tells `WHERE id = 42` and `WHERE id = 43` apart from each
|
|
284
|
+
other but together against `WHERE email = ?`; the walk up the stack to find the
|
|
285
|
+
line in *your* code rather than in SQLAlchemy's; the collapsing of `IN (?,?,?)`
|
|
286
|
+
so that batch size does not change the shape; or the failure message that names
|
|
287
|
+
the culprit before it shows the evidence. That is what this package is.
|
|
288
|
+
|
|
289
|
+
## Prior art
|
|
290
|
+
|
|
291
|
+
[`nplusone`](https://pypi.org/project/nplusone/) did the detection half of this
|
|
292
|
+
for Django and SQLAlchemy, but its last release was in **May 2018** and it does
|
|
293
|
+
not support SQLAlchemy 2.x. Django users have `assertNumQueries` built in, which
|
|
294
|
+
covers budgets but not shapes. This plugin is aimed at the SQLAlchemy side --
|
|
295
|
+
FastAPI, Flask, Litestar -- where nothing is currently maintained.
|
|
296
|
+
|
|
297
|
+
## Limitations
|
|
298
|
+
|
|
299
|
+
- SQLAlchemy 2.x only for now. Django and raw psycopg are on the roadmap.
|
|
300
|
+
- Under `pytest-xdist` the summary table is per worker, so it will be partial.
|
|
301
|
+
Budgets and N+1 detection are unaffected.
|
|
302
|
+
- `no_seq_scan` is PostgreSQL only, and costs one `EXPLAIN` per SELECT while
|
|
303
|
+
enabled. Budgets and N+1 detection work on any SQLAlchemy backend.
|
|
304
|
+
- The suggested `CREATE INDEX` is a starting point, not advice. Which columns to
|
|
305
|
+
index, in what order, and whether the index earns its write cost need the whole
|
|
306
|
+
query pattern, not one plan node. A filter over a function call
|
|
307
|
+
(`lower(email) = ...`) yields no suggestion at all rather than a wrong one.
|
|
308
|
+
|
|
309
|
+
## Contributing
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
python -m venv .venv && . .venv/bin/activate
|
|
313
|
+
pip install -e ".[dev]"
|
|
314
|
+
|
|
315
|
+
# The plan tests need PostgreSQL; without one they skip.
|
|
316
|
+
docker run -d --name qc-pg -e POSTGRES_PASSWORD=querycount \
|
|
317
|
+
-e POSTGRES_DB=querycount -p 55432:5432 postgres:17-alpine
|
|
318
|
+
|
|
319
|
+
pytest && ruff check src tests && mypy
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Set `QUERYCOUNT_REQUIRE_PG=1` to turn those skips into failures, as CI does, and
|
|
323
|
+
`QUERYCOUNT_TEST_PG_URL` to point elsewhere.
|
|
324
|
+
|
|
325
|
+
The suite runs pytest inside pytest via `pytester`: it writes a throwaway test,
|
|
326
|
+
runs it in a subprocess, and asserts on the outcome. That is the only honest way
|
|
327
|
+
to test a plugin whose job is to make tests fail.
|
|
328
|
+
|
|
329
|
+
## Releasing
|
|
330
|
+
|
|
331
|
+
See [RELEASING.md](RELEASING.md). Publication goes through PyPI Trusted
|
|
332
|
+
Publishing, so there is no API token in this repository or its secrets.
|
|
333
|
+
|
|
334
|
+
## Licence
|
|
335
|
+
|
|
336
|
+
MIT
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
pytest_querycount/__init__.py,sha256=mE-xK3UiI4aPN5oMNlAnMtfUmctyN3aXVSwzdLMifmk,730
|
|
2
|
+
pytest_querycount/checks.py,sha256=RAP3PypqTXxgzEx0re04MOa51W4Xi4FN4QsFuo-B2zQ,5903
|
|
3
|
+
pytest_querycount/errors.py,sha256=-xC9HpEEm_z1on1AqEIlZBTk69Qy3q2FhXtp-jbG_RM,1099
|
|
4
|
+
pytest_querycount/explain.py,sha256=nqsnlbqeRpUsEVGuhS3xZa-VzjtygZ1reNgis8k4apc,6017
|
|
5
|
+
pytest_querycount/normalize.py,sha256=xUenZDOMDdgCmuhishuAAIu5gp82PhOj-cu3kO5mZDM,3667
|
|
6
|
+
pytest_querycount/plugin.py,sha256=H85cxtuk7JUOYkpo8AcbWom4qWjR4TZ74kjYykcVrAI,9185
|
|
7
|
+
pytest_querycount/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
pytest_querycount/recorder.py,sha256=Hq18W2UMeY5rgx5kTTXC8a_G366p5DoouQ6LDswlN9A,7645
|
|
9
|
+
pytest_querycount/records.py,sha256=XVLYjXA5CbqA4cRa2gGGFbIHJfi6KsHHLlM3v5UOQ_Y,2126
|
|
10
|
+
pytest_querycount/report.py,sha256=J_dX6ArnVF_nysxK0MIHIYoD2zwWyvPEHyMqgZxu8ZY,2055
|
|
11
|
+
pytest_querycount/backends/__init__.py,sha256=bpzplqoZw_-5QZkwBdB24uYB1fbTPZhKhVFhIRv7ar0,1831
|
|
12
|
+
pytest_querycount/backends/sqlalchemy.py,sha256=mbXwb7DFBNUQv_fkr3oBYqkfyiPk8Po4YCnczh0pE4I,7167
|
|
13
|
+
pytest_querycount-0.3.0.dist-info/METADATA,sha256=73I2wxovgiPYbpsO_0upqfb7rEhGGDwoj3K4rxXAtbg,13781
|
|
14
|
+
pytest_querycount-0.3.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
15
|
+
pytest_querycount-0.3.0.dist-info/entry_points.txt,sha256=oex8Hidov-J-QUtO2H-9jWjk88Yf98ctT1KGJDvf9tM,49
|
|
16
|
+
pytest_querycount-0.3.0.dist-info/licenses/LICENSE,sha256=x67ZEM8AygvcybK9jS9iViUqb1igphougfREcPlJsYM,1073
|
|
17
|
+
pytest_querycount-0.3.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Luis David Senra
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|