Metadata-Version: 2.4
Name: nplus1
Version: 1.1.5
Summary: nplusone is a library for detecting the n+1 queries problem in Python ORMs, including SQLAlchemy, Peewee, and the Django ORM
Project-URL: Homepage, https://github.com/huynguyengl99/nplus1
Project-URL: Repository, https://github.com/huynguyengl99/nplus1
Author-email: Huy Nguyen <ndhgl99@gmail.com>
License: MIT License
        
        Copyright (c) 2026, Huy Nguyen
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
License-File: AUTHORS.rst
License-File: LICENSE
Classifier: Environment :: Web Environment
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Information Technology
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
Classifier: Topic :: Software Development
Classifier: Topic :: Software Development :: Libraries
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
Requires-Python: <4.0,>=3.11
Requires-Dist: blinker<2,>=1.9
Provides-Extra: celery
Requires-Dist: celery>=5.3; extra == 'celery'
Provides-Extra: django
Requires-Dist: django>=4.2; extra == 'django'
Provides-Extra: flask
Requires-Dist: flask-sqlalchemy>=3.0; extra == 'flask'
Requires-Dist: flask>=2.0; extra == 'flask'
Provides-Extra: peewee
Requires-Dist: peewee>=3.15; extra == 'peewee'
Provides-Extra: sqlalchemy
Requires-Dist: sqlalchemy>=2.0; extra == 'sqlalchemy'
Description-Content-Type: text/markdown

# nplusone

Detect the N+1 queries problem in Python ORMs — SQLAlchemy, Peewee, and Django ORM.

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.

[![PyPI](https://img.shields.io/pypi/v/nplus1)](https://pypi.org/project/nplus1/)
[![Python](https://img.shields.io/pypi/pyversions/nplus1)](https://pypi.org/project/nplus1/)
[![Tests](https://github.com/huynguyengl99/nplus1/actions/workflows/test.yml/badge.svg)](https://github.com/huynguyengl99/nplus1/actions)
[![Coverage](https://codecov.io/gh/huynguyengl99/nplus1/branch/main/graph/badge.svg)](https://codecov.io/gh/huynguyengl99/nplus1)

## Installation

```bash
pip install nplus1
```

With optional ORM/framework dependencies:

```bash
pip install nplus1[django]           # Django 4.2+
pip install nplus1[sqlalchemy]       # SQLAlchemy 2.0+
pip install nplus1[flask]            # Flask + Flask-SQLAlchemy
pip install nplus1[peewee]           # Peewee 3.15+
```

## Quick Start

### Django

Add the middleware to your **dev** settings:

```python
# settings/dev.py
MIDDLEWARE = [
    ...
    "nplusone.ext.django.NPlusOneMiddleware",
    ...
]

NPLUSONE_ENABLED = True    # set False in prod (zero overhead)
NPLUSONE_LOG = True        # log detections (default)
NPLUSONE_RAISE = True      # raise exceptions in dev/test
```

### Flask + SQLAlchemy

```python
from nplusone.ext.flask_sqlalchemy import NPlusOne

app = Flask(__name__)
NPlusOne(app)
```

### Standalone (any code)

```python
from nplusone.core.profiler import Profiler

with Profiler():
    users = session.query(User).all()
    for user in users:
        user.addresses  # NPlusOneError raised
```

### Celery

```python
from nplusone.ext.celery import NPlusOneCelery

app = Celery("myapp")
NPlusOneCelery(app)
```

Or manually with signals:

```python
from celery.signals import task_prerun, task_postrun
from nplusone.core.profiler import setup, teardown

@task_prerun.connect()
def on_prerun(**kwargs):
    setup()

@task_postrun.connect()
def on_postrun(**kwargs):
    teardown()
```

## What It Detects

### N+1 lazy loads

```python
users = User.objects.all()          # 1 query
for user in users:
    print(user.addresses)           # N queries — flagged!
```

**Fix:** use `select_related` or `prefetch_related`:

```python
users = User.objects.select_related("addresses").all()
```

### Unnecessary eager loads

```python
users = User.objects.select_related("occupation").all()
for user in users:
    print(user.name)                # occupation never accessed — flagged!
```

## Configuration

All settings work across Django, Flask, and Celery:

| Setting | Default | Description |
|---------|---------|-------------|
| `NPLUSONE_ENABLED` | `True` | Master switch. Set `False` in prod for zero overhead. |
| `NPLUSONE_LOG` | `True` | Log detections to the `nplusone` logger. |
| `NPLUSONE_RAISE` | `False` | Raise `NPlusOneError` on detection. |
| `NPLUSONE_WHITELIST` | `[]` | List of rule dicts to suppress specific warnings. |
| `NPLUSONE_LOGGER` | `logging.getLogger("nplusone")` | Custom logger instance. |
| `NPLUSONE_LOG_LEVEL` | `DEBUG` | Log level for detections. |
| `NPLUSONE_DEBUG` | `False` | Verbose signal logging to `nplusone.debug` logger. |
| `NPLUSONE_REPORT_MODE` | `"immediate"` | `"immediate"` or `"batch"`. Batch collects all detections and reports at end of request. |
| `NPLUSONE_SKIP_EAGER_ON_ERROR` | `True` | Skip eager load checks on error responses (>= 400). |
| `NPLUSONE_EAGER_LOAD_SKIP` | `None` | Callable `(request, response) -> bool` for custom skip logic. |
| `NPLUSONE_SKIP_EMPTY_PREFETCH` | `False` | Skip flagging `prefetch_related` that returns zero rows. |
| `NPLUSONE_EXCLUDE_URLS` | `[]` | List of URL prefixes to skip all detection for (e.g. `["/admin/"]`). |

### Whitelisting

Suppress specific warnings by model, field, or pattern:

```python
NPLUSONE_WHITELIST = [
    {"model": "User", "field": "profile"},       # exact match
    {"model": "myapp.User"},                      # Django app_label.Model format
    {"model": "User*"},                           # fnmatch wildcard
    {"label": "unused_eager_load"},               # suppress all eager load warnings
]
```

### URL Exclusion

Skip detection entirely for URL prefixes where detections are unfixable
(e.g. Django admin internals):

```python
NPLUSONE_EXCLUDE_URLS = [
    "/admin/",
    "/debug/",
]
```

### Prod/Dev Split

Only add the middleware in dev/test settings — no need for it in production:

```python
# settings/dev.py (or settings/test.py)
MIDDLEWARE = [
    ...
    "nplusone.ext.django.NPlusOneMiddleware",
    ...
]
NPLUSONE_RAISE = True
```

No `INSTALLED_APPS` entry is needed — the ORM patches are applied
automatically when the middleware is imported.

For Celery, use `NPLUSONE_ENABLED` to control whether detection runs:

```python
# settings/base.py
NPLUSONE_ENABLED = False   # Celery setup() is a no-op

# settings/dev.py
NPLUSONE_ENABLED = True    # Celery detection active
```

## Debug Mode

Enable `NPLUSONE_DEBUG = True` to see every signal fire during a request:

```
[nplusone.debug] REQUEST START: GET /api/orders/
[nplusone.debug] EAGER_REGISTER: Order.customer (5 instances) at views.py:42 in get_queryset
[nplusone.debug] EAGER_ACCESS: Order.customer (1 instances) at serializers.py:18 in to_representation
[nplusone.debug] DETECTED: Potential unnecessary eager load on Order.shipping_address
[nplusone.debug] REQUEST END: GET /api/orders/ → 200
```

Detection messages include the registration site (inspired by
[django-zeal](https://github.com/taobojlen/django-zeal)'s `ZEAL_SHOW_ALL_CALLERS`):

```
Potential unnecessary eager load detected on `Order.shipping_address`
  Registered at: myapp/views.py:42 in get_queryset
                 qs.select_related("customer", "shipping_address")
```

## Comparison

### vs. [jmcarp/nplusone](https://github.com/jmcarp/nplusone) (original)

This library is a ground-up rewrite of the original nplusone, which has been
unmaintained since 2020. We preserve the same detection architecture
(blinker signals + ORM monkey-patching) but modernize everything else:

| | Original nplusone | This library |
|---|---|---|
| Python | 2.7+ / 3.3+ | 3.11+ |
| Type hints | None | Full (mypy strict + pyright strict) |
| SQLAlchemy | 1.x only | 2.0+ |
| Django | 1.8+ (compat code) | 4.2 – 5.2 (clean) |
| Nullable FK | False positive | Skipped (valid optimization) |
| MTI / Polymorphic | False positives | PK-based cross-model matching |
| Error responses | False positive | Skipped on 4xx/5xx (configurable) |
| Celery | Not supported | `NPlusOneCelery(app)` + `setup()`/`teardown()` |
| Debug/trace mode | Not available | `NPLUSONE_DEBUG` with full signal logging |
| Stack traces | Not in messages | Registration site in every detection |
| Batch reporting | Not available | `NPLUSONE_REPORT_MODE = "batch"` |
| Prod switch | Not available | `NPLUSONE_ENABLED = False` (zero overhead) |
| Dependencies | `six`, `blinker` | `blinker` only |

### vs. [django-zeal](https://github.com/taobojlen/django-zeal)

django-zeal is a Django-only N+1 detector with a different approach.

| | django-zeal | This library |
|---|---|---|
| ORMs | Django only | Django, SQLAlchemy, Peewee |
| Detect N+1 lazy loads | Yes | Yes |
| Detect unused eager loads | No | Yes |
| Detect `.defer()`/`.only()` issues | Yes | No |
| Configurable threshold | Yes (`ZEAL_NPLUSONE_THRESHOLD`) | No (flags on first repeat) |
| Non-invasive in prod | Yes (no patching when inactive) | Yes (`NPLUSONE_ENABLED = False` skips all setup) |
| Stack traces | Yes (`ZEAL_SHOW_ALL_CALLERS`) | Yes (always included) |
| Celery | Manual `setup()`/`teardown()` | `NPlusOneCelery(app)` or manual |
| Batch reporting | No | Yes |

**Choose nplusone** if you need multi-ORM support, unused eager load detection,
or work with complex Django patterns (MTI, polymorphic models, DRF).

**Choose django-zeal** if you only use Django and want `.defer()`/`.only()`
detection or configurable thresholds.

## Development

```bash
# Setup
uv sync

# Run tests
python -m pytest tests/

# Run tests for specific ORM
tox -e py311-django52
tox -e py311-sqlalchemy
tox -e py311-peewee
tox -e py311-flask

# Lint and type check
ruff check nplusone/ tests/
python -m mypy nplusone/
npx pyright

# Coverage
python -m pytest tests/ --cov=nplusone --cov-report=term-missing
```

### Multi-version Testing

```bash
# Full matrix
tox

# Specific Python + Django version
tox -e py312-django51
tox -e py313-django42
```

### Docker (PostgreSQL)

```bash
docker compose up -d
cp .env.EXAMPLE .env
python -m pytest tests/testapp/
```

## License

MIT. See [LICENSE](LICENSE).
