nplus1 1.0.0__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.
- nplus1-1.0.0/.gitignore +109 -0
- nplus1-1.0.0/AUTHORS.rst +13 -0
- nplus1-1.0.0/LICENSE +22 -0
- nplus1-1.0.0/PKG-INFO +346 -0
- nplus1-1.0.0/README.md +284 -0
- nplus1-1.0.0/nplusone/__init__.py +3 -0
- nplus1-1.0.0/nplusone/core/__init__.py +1 -0
- nplus1-1.0.0/nplusone/core/exceptions.py +5 -0
- nplus1-1.0.0/nplusone/core/listeners.py +440 -0
- nplus1-1.0.0/nplusone/core/notifiers.py +75 -0
- nplus1-1.0.0/nplusone/core/profiler.py +152 -0
- nplus1-1.0.0/nplusone/core/signals.py +81 -0
- nplus1-1.0.0/nplusone/core/stack.py +21 -0
- nplus1-1.0.0/nplusone/ext/__init__.py +1 -0
- nplus1-1.0.0/nplusone/ext/celery.py +71 -0
- nplus1-1.0.0/nplusone/ext/django/__init__.py +17 -0
- nplus1-1.0.0/nplusone/ext/django/middleware.py +205 -0
- nplus1-1.0.0/nplusone/ext/django/patch.py +531 -0
- nplus1-1.0.0/nplusone/ext/flask_sqlalchemy.py +139 -0
- nplus1-1.0.0/nplusone/ext/peewee.py +165 -0
- nplus1-1.0.0/nplusone/ext/sqlalchemy.py +224 -0
- nplus1-1.0.0/nplusone/ext/wsgi.py +25 -0
- nplus1-1.0.0/nplusone/py.typed +0 -0
- nplus1-1.0.0/pyproject.toml +151 -0
nplus1-1.0.0/.gitignore
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
env/
|
|
12
|
+
build/
|
|
13
|
+
develop-eggs/
|
|
14
|
+
dist/
|
|
15
|
+
downloads/
|
|
16
|
+
eggs/
|
|
17
|
+
.eggs/
|
|
18
|
+
lib/
|
|
19
|
+
lib64/
|
|
20
|
+
parts/
|
|
21
|
+
sdist/
|
|
22
|
+
var/
|
|
23
|
+
wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
|
|
28
|
+
# PyInstaller
|
|
29
|
+
# Usually these files are written by a python script from a template
|
|
30
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
31
|
+
*.manifest
|
|
32
|
+
*.spec
|
|
33
|
+
|
|
34
|
+
# Installer logs
|
|
35
|
+
pip-log.txt
|
|
36
|
+
pip-delete-this-directory.txt
|
|
37
|
+
|
|
38
|
+
# Unit test / coverage reports
|
|
39
|
+
htmlcov/
|
|
40
|
+
.tox/
|
|
41
|
+
.coverage
|
|
42
|
+
.coverage.*
|
|
43
|
+
.cache
|
|
44
|
+
nosetests.xml
|
|
45
|
+
coverage.xml
|
|
46
|
+
*.cover
|
|
47
|
+
.hypothesis/
|
|
48
|
+
.pytest_cache/
|
|
49
|
+
|
|
50
|
+
# Translations
|
|
51
|
+
*.mo
|
|
52
|
+
*.pot
|
|
53
|
+
|
|
54
|
+
# Django stuff:
|
|
55
|
+
*.log
|
|
56
|
+
local_settings.py
|
|
57
|
+
|
|
58
|
+
# Flask stuff:
|
|
59
|
+
instance/
|
|
60
|
+
.webassets-cache
|
|
61
|
+
|
|
62
|
+
# Scrapy stuff:
|
|
63
|
+
.scrapy
|
|
64
|
+
|
|
65
|
+
# Sphinx documentation
|
|
66
|
+
docs/_build/
|
|
67
|
+
html/
|
|
68
|
+
|
|
69
|
+
# PyBuilder
|
|
70
|
+
target/
|
|
71
|
+
|
|
72
|
+
# Jupyter Notebook
|
|
73
|
+
.ipynb_checkpoints
|
|
74
|
+
|
|
75
|
+
# Dask worker cache
|
|
76
|
+
dask-worker-space/
|
|
77
|
+
|
|
78
|
+
# celery beat schedule file
|
|
79
|
+
celerybeat-schedule
|
|
80
|
+
|
|
81
|
+
# SageMath parsed files
|
|
82
|
+
*.sage.py
|
|
83
|
+
|
|
84
|
+
# dotenv
|
|
85
|
+
.env
|
|
86
|
+
|
|
87
|
+
# virtualenv
|
|
88
|
+
.venv
|
|
89
|
+
.venv-*/
|
|
90
|
+
venv/
|
|
91
|
+
ENV/
|
|
92
|
+
|
|
93
|
+
# Spyder project settings
|
|
94
|
+
.spyderproject
|
|
95
|
+
.spyproject
|
|
96
|
+
|
|
97
|
+
# Rope project settings
|
|
98
|
+
.ropeproject
|
|
99
|
+
|
|
100
|
+
# mkdocs documentation
|
|
101
|
+
/site
|
|
102
|
+
|
|
103
|
+
# mypy
|
|
104
|
+
.mypy_cache/
|
|
105
|
+
|
|
106
|
+
# IDE settings
|
|
107
|
+
.vscode/
|
|
108
|
+
.idea/
|
|
109
|
+
|
nplus1-1.0.0/AUTHORS.rst
ADDED
nplus1-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Huy Nguyen
|
|
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.
|
|
22
|
+
|
nplus1-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nplus1
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: nplusone is a library for detecting the n+1 queries problem in Python ORMs, including SQLAlchemy, Peewee, and the Django ORM
|
|
5
|
+
Project-URL: Homepage, https://github.com/huynguyengl99/nplus1
|
|
6
|
+
Project-URL: Repository, https://github.com/huynguyengl99/nplus1
|
|
7
|
+
Author-email: Huy Nguyen <ndhgl99@gmail.com>
|
|
8
|
+
License: MIT License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2026, Huy Nguyen
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
SOFTWARE.
|
|
29
|
+
|
|
30
|
+
License-File: AUTHORS.rst
|
|
31
|
+
License-File: LICENSE
|
|
32
|
+
Classifier: Environment :: Web Environment
|
|
33
|
+
Classifier: Intended Audience :: Developers
|
|
34
|
+
Classifier: Intended Audience :: Information Technology
|
|
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 :: Only
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
42
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
43
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
44
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
45
|
+
Classifier: Topic :: Software Development
|
|
46
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
47
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
48
|
+
Requires-Python: <4.0,>=3.11
|
|
49
|
+
Requires-Dist: blinker>=1.9
|
|
50
|
+
Provides-Extra: celery
|
|
51
|
+
Requires-Dist: celery>=5.3; extra == 'celery'
|
|
52
|
+
Provides-Extra: django
|
|
53
|
+
Requires-Dist: django>=4.2; extra == 'django'
|
|
54
|
+
Provides-Extra: flask
|
|
55
|
+
Requires-Dist: flask-sqlalchemy>=3.0; extra == 'flask'
|
|
56
|
+
Requires-Dist: flask>=2.0; extra == 'flask'
|
|
57
|
+
Provides-Extra: peewee
|
|
58
|
+
Requires-Dist: peewee>=3.15; extra == 'peewee'
|
|
59
|
+
Provides-Extra: sqlalchemy
|
|
60
|
+
Requires-Dist: sqlalchemy>=2.0; extra == 'sqlalchemy'
|
|
61
|
+
Description-Content-Type: text/markdown
|
|
62
|
+
|
|
63
|
+
# nplusone
|
|
64
|
+
|
|
65
|
+
Detect the N+1 queries problem in Python ORMs — SQLAlchemy, Peewee, and Django ORM.
|
|
66
|
+
|
|
67
|
+
A modern rewrite of the [original nplusone](https://github.com/jmcarp/nplusone) library, targeting Python 3.11+ with full type annotations, SQLAlchemy 2.0 support, and fixes for false positives found in production Django+DRF codebases.
|
|
68
|
+
|
|
69
|
+
[](https://pypi.org/project/nplus1/)
|
|
70
|
+
[](https://pypi.org/project/nplus1/)
|
|
71
|
+
[](https://github.com/huynguyengl99/nplus1/actions)
|
|
72
|
+
[](https://codecov.io/gh/huynguyengl99/nplus1)
|
|
73
|
+
|
|
74
|
+
## Installation
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pip install nplus1
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
With optional ORM/framework dependencies:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pip install nplus1[django] # Django 4.2+
|
|
84
|
+
pip install nplus1[sqlalchemy] # SQLAlchemy 2.0+
|
|
85
|
+
pip install nplus1[flask] # Flask + Flask-SQLAlchemy
|
|
86
|
+
pip install nplus1[peewee] # Peewee 3.15+
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Quick Start
|
|
90
|
+
|
|
91
|
+
### Django
|
|
92
|
+
|
|
93
|
+
Add the middleware to your **dev** settings:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
# settings/dev.py
|
|
97
|
+
MIDDLEWARE = [
|
|
98
|
+
...
|
|
99
|
+
"nplusone.ext.django.NPlusOneMiddleware",
|
|
100
|
+
...
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
NPLUSONE_ENABLED = True # set False in prod (zero overhead)
|
|
104
|
+
NPLUSONE_LOG = True # log detections (default)
|
|
105
|
+
NPLUSONE_RAISE = True # raise exceptions in dev/test
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Flask + SQLAlchemy
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from nplusone.ext.flask_sqlalchemy import NPlusOne
|
|
112
|
+
|
|
113
|
+
app = Flask(__name__)
|
|
114
|
+
NPlusOne(app)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Standalone (any code)
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from nplusone.core.profiler import Profiler
|
|
121
|
+
|
|
122
|
+
with Profiler():
|
|
123
|
+
users = session.query(User).all()
|
|
124
|
+
for user in users:
|
|
125
|
+
user.addresses # NPlusOneError raised
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Celery
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from nplusone.ext.celery import NPlusOneCelery
|
|
132
|
+
|
|
133
|
+
app = Celery("myapp")
|
|
134
|
+
NPlusOneCelery(app)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Or manually with signals:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from celery.signals import task_prerun, task_postrun
|
|
141
|
+
from nplusone.core.profiler import setup, teardown
|
|
142
|
+
|
|
143
|
+
@task_prerun.connect()
|
|
144
|
+
def on_prerun(**kwargs):
|
|
145
|
+
setup()
|
|
146
|
+
|
|
147
|
+
@task_postrun.connect()
|
|
148
|
+
def on_postrun(**kwargs):
|
|
149
|
+
teardown()
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## What It Detects
|
|
153
|
+
|
|
154
|
+
### N+1 lazy loads
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
users = User.objects.all() # 1 query
|
|
158
|
+
for user in users:
|
|
159
|
+
print(user.addresses) # N queries — flagged!
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**Fix:** use `select_related` or `prefetch_related`:
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
users = User.objects.select_related("addresses").all()
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Unnecessary eager loads
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
users = User.objects.select_related("occupation").all()
|
|
172
|
+
for user in users:
|
|
173
|
+
print(user.name) # occupation never accessed — flagged!
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Configuration
|
|
177
|
+
|
|
178
|
+
All settings work across Django, Flask, and Celery:
|
|
179
|
+
|
|
180
|
+
| Setting | Default | Description |
|
|
181
|
+
|---------|---------|-------------|
|
|
182
|
+
| `NPLUSONE_ENABLED` | `True` | Master switch. Set `False` in prod for zero overhead. |
|
|
183
|
+
| `NPLUSONE_LOG` | `True` | Log detections to the `nplusone` logger. |
|
|
184
|
+
| `NPLUSONE_RAISE` | `False` | Raise `NPlusOneError` on detection. |
|
|
185
|
+
| `NPLUSONE_WHITELIST` | `[]` | List of rule dicts to suppress specific warnings. |
|
|
186
|
+
| `NPLUSONE_LOGGER` | `logging.getLogger("nplusone")` | Custom logger instance. |
|
|
187
|
+
| `NPLUSONE_LOG_LEVEL` | `DEBUG` | Log level for detections. |
|
|
188
|
+
| `NPLUSONE_DEBUG` | `False` | Verbose signal logging to `nplusone.debug` logger. |
|
|
189
|
+
| `NPLUSONE_REPORT_MODE` | `"immediate"` | `"immediate"` or `"batch"`. Batch collects all detections and reports at end of request. |
|
|
190
|
+
| `NPLUSONE_SKIP_EAGER_ON_ERROR` | `True` | Skip eager load checks on error responses (>= 400). |
|
|
191
|
+
| `NPLUSONE_EAGER_LOAD_SKIP` | `None` | Callable `(request, response) -> bool` for custom skip logic. |
|
|
192
|
+
| `NPLUSONE_SKIP_EMPTY_PREFETCH` | `False` | Skip flagging `prefetch_related` that returns zero rows. |
|
|
193
|
+
|
|
194
|
+
### Whitelisting
|
|
195
|
+
|
|
196
|
+
Suppress specific warnings by model, field, or pattern:
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
NPLUSONE_WHITELIST = [
|
|
200
|
+
{"model": "User", "field": "profile"}, # exact match
|
|
201
|
+
{"model": "myapp.User"}, # Django app_label.Model format
|
|
202
|
+
{"model": "User*"}, # fnmatch wildcard
|
|
203
|
+
{"label": "unused_eager_load"}, # suppress all eager load warnings
|
|
204
|
+
]
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Prod/Dev Split
|
|
208
|
+
|
|
209
|
+
Only add the middleware in dev/test settings — no need for it in production:
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
# settings/dev.py (or settings/test.py)
|
|
213
|
+
MIDDLEWARE = [
|
|
214
|
+
...
|
|
215
|
+
"nplusone.ext.django.NPlusOneMiddleware",
|
|
216
|
+
...
|
|
217
|
+
]
|
|
218
|
+
NPLUSONE_RAISE = True
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
No `INSTALLED_APPS` entry is needed — the ORM patches are applied
|
|
222
|
+
automatically when the middleware is imported.
|
|
223
|
+
|
|
224
|
+
For Celery, use `NPLUSONE_ENABLED` to control whether detection runs:
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
# settings/base.py
|
|
228
|
+
NPLUSONE_ENABLED = False # Celery setup() is a no-op
|
|
229
|
+
|
|
230
|
+
# settings/dev.py
|
|
231
|
+
NPLUSONE_ENABLED = True # Celery detection active
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Debug Mode
|
|
235
|
+
|
|
236
|
+
Enable `NPLUSONE_DEBUG = True` to see every signal fire during a request:
|
|
237
|
+
|
|
238
|
+
```
|
|
239
|
+
[nplusone.debug] REQUEST START: GET /api/orders/
|
|
240
|
+
[nplusone.debug] EAGER_REGISTER: Order.customer (5 instances) at views.py:42 in get_queryset
|
|
241
|
+
[nplusone.debug] EAGER_ACCESS: Order.customer (1 instances) at serializers.py:18 in to_representation
|
|
242
|
+
[nplusone.debug] DETECTED: Potential unnecessary eager load on Order.shipping_address
|
|
243
|
+
[nplusone.debug] REQUEST END: GET /api/orders/ → 200
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Detection messages include the registration site (inspired by
|
|
247
|
+
[django-zeal](https://github.com/taobojlen/django-zeal)'s `ZEAL_SHOW_ALL_CALLERS`):
|
|
248
|
+
|
|
249
|
+
```
|
|
250
|
+
Potential unnecessary eager load detected on `Order.shipping_address`
|
|
251
|
+
Registered at: myapp/views.py:42 in get_queryset
|
|
252
|
+
qs.select_related("customer", "shipping_address")
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## Comparison
|
|
256
|
+
|
|
257
|
+
### vs. [jmcarp/nplusone](https://github.com/jmcarp/nplusone) (original)
|
|
258
|
+
|
|
259
|
+
This library is a ground-up rewrite of the original nplusone, which has been
|
|
260
|
+
unmaintained since 2020. We preserve the same detection architecture
|
|
261
|
+
(blinker signals + ORM monkey-patching) but modernize everything else:
|
|
262
|
+
|
|
263
|
+
| | Original nplusone | This library |
|
|
264
|
+
|---|---|---|
|
|
265
|
+
| Python | 2.7+ / 3.3+ | 3.11+ |
|
|
266
|
+
| Type hints | None | Full (mypy strict + pyright strict) |
|
|
267
|
+
| SQLAlchemy | 1.x only | 2.0+ |
|
|
268
|
+
| Django | 1.8+ (compat code) | 4.2 – 5.2 (clean) |
|
|
269
|
+
| Nullable FK | False positive | Skipped (valid optimization) |
|
|
270
|
+
| MTI / Polymorphic | False positives | PK-based cross-model matching |
|
|
271
|
+
| Error responses | False positive | Skipped on 4xx/5xx (configurable) |
|
|
272
|
+
| Celery | Not supported | `NPlusOneCelery(app)` + `setup()`/`teardown()` |
|
|
273
|
+
| Debug/trace mode | Not available | `NPLUSONE_DEBUG` with full signal logging |
|
|
274
|
+
| Stack traces | Not in messages | Registration site in every detection |
|
|
275
|
+
| Batch reporting | Not available | `NPLUSONE_REPORT_MODE = "batch"` |
|
|
276
|
+
| Prod switch | Not available | `NPLUSONE_ENABLED = False` (zero overhead) |
|
|
277
|
+
| Dependencies | `six`, `blinker` | `blinker` only |
|
|
278
|
+
|
|
279
|
+
### vs. [django-zeal](https://github.com/taobojlen/django-zeal)
|
|
280
|
+
|
|
281
|
+
django-zeal is a Django-only N+1 detector with a different approach.
|
|
282
|
+
|
|
283
|
+
| | django-zeal | This library |
|
|
284
|
+
|---|---|---|
|
|
285
|
+
| ORMs | Django only | Django, SQLAlchemy, Peewee |
|
|
286
|
+
| Detect N+1 lazy loads | Yes | Yes |
|
|
287
|
+
| Detect unused eager loads | No | Yes |
|
|
288
|
+
| Detect `.defer()`/`.only()` issues | Yes | No |
|
|
289
|
+
| Configurable threshold | Yes (`ZEAL_NPLUSONE_THRESHOLD`) | No (flags on first repeat) |
|
|
290
|
+
| Non-invasive in prod | Yes (no patching when inactive) | Yes (`NPLUSONE_ENABLED = False` skips all setup) |
|
|
291
|
+
| Stack traces | Yes (`ZEAL_SHOW_ALL_CALLERS`) | Yes (always included) |
|
|
292
|
+
| Celery | Manual `setup()`/`teardown()` | `NPlusOneCelery(app)` or manual |
|
|
293
|
+
| Batch reporting | No | Yes |
|
|
294
|
+
|
|
295
|
+
**Choose nplusone** if you need multi-ORM support, unused eager load detection,
|
|
296
|
+
or work with complex Django patterns (MTI, polymorphic models, DRF).
|
|
297
|
+
|
|
298
|
+
**Choose django-zeal** if you only use Django and want `.defer()`/`.only()`
|
|
299
|
+
detection or configurable thresholds.
|
|
300
|
+
|
|
301
|
+
## Development
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
# Setup
|
|
305
|
+
uv sync
|
|
306
|
+
|
|
307
|
+
# Run tests
|
|
308
|
+
python -m pytest tests/
|
|
309
|
+
|
|
310
|
+
# Run tests for specific ORM
|
|
311
|
+
tox -e py311-django52
|
|
312
|
+
tox -e py311-sqlalchemy
|
|
313
|
+
tox -e py311-peewee
|
|
314
|
+
tox -e py311-flask
|
|
315
|
+
|
|
316
|
+
# Lint and type check
|
|
317
|
+
ruff check nplusone/ tests/
|
|
318
|
+
python -m mypy nplusone/
|
|
319
|
+
npx pyright
|
|
320
|
+
|
|
321
|
+
# Coverage
|
|
322
|
+
python -m pytest tests/ --cov=nplusone --cov-report=term-missing
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Multi-version Testing
|
|
326
|
+
|
|
327
|
+
```bash
|
|
328
|
+
# Full matrix
|
|
329
|
+
tox
|
|
330
|
+
|
|
331
|
+
# Specific Python + Django version
|
|
332
|
+
tox -e py312-django51
|
|
333
|
+
tox -e py313-django42
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### Docker (PostgreSQL)
|
|
337
|
+
|
|
338
|
+
```bash
|
|
339
|
+
docker compose up -d
|
|
340
|
+
cp .env.EXAMPLE .env
|
|
341
|
+
python -m pytest tests/testapp/
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
## License
|
|
345
|
+
|
|
346
|
+
MIT. See [LICENSE](LICENSE).
|