pytest-testcontainers-django 0.2.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.
@@ -0,0 +1,27 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ *.egg
5
+ build/
6
+ dist/
7
+ .eggs/
8
+ .pytest_cache/
9
+ .tox/
10
+ .coverage
11
+ .coverage.*
12
+ htmlcov/
13
+ coverage.xml
14
+ .mypy_cache/
15
+ .ruff_cache/
16
+ .venv/
17
+ venv/
18
+ env/
19
+ .envrc
20
+ .env
21
+ .env.local
22
+ *.swp
23
+ *.swo
24
+ .DS_Store
25
+ .idea/
26
+ .vscode/
27
+ *.log
@@ -0,0 +1,62 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.2.0] - 2026-05-08
11
+
12
+ ### Added
13
+
14
+ - New `[redis]` install extra so `redis_enabled = true` works without a
15
+ separate `pip install redis` (`testcontainers.redis` imports the Python
16
+ redis client at module load).
17
+ - `ReuseStaleContainerError` and matching `pytest.UsageError` for the
18
+ reuse-mode edge case where a pre-existing container is in `dead` /
19
+ `removing` state — surfaced with the exact `docker rm -f <name>`
20
+ command instead of letting Docker fail the start with a 409 name
21
+ conflict.
22
+
23
+ ### Changed
24
+
25
+ - `__version__` now reads from `importlib.metadata` so it stays in sync
26
+ with `pyproject.toml`.
27
+ - README Django support matrix marks 4.2 LTS as past EOL (Apr 2026).
28
+
29
+ ### Fixed
30
+
31
+ - The plugin's own test suite now disables eager-start via a *root*
32
+ `conftest.py` rather than `tests/conftest.py`. The root file is
33
+ preloaded by the plugin's `tryfirst` hook; `tests/conftest.py` is not
34
+ — meaning the previous setup silently ran every "unit" test against
35
+ a real Docker daemon when one was available locally (CI was unaffected
36
+ because it sets the env var explicitly).
37
+
38
+ ## [0.1.0] - 2026-05-08
39
+
40
+ Initial release.
41
+
42
+ ### Added
43
+
44
+ - `pytest_load_initial_conftests(tryfirst=True)` hook that starts Postgres
45
+ (and optionally Redis) containers and injects connection details into
46
+ `os.environ` **before** pytest-django imports settings.
47
+ - Configuration via `[tool.pytest-testcontainers-django]` in `pyproject.toml`
48
+ or programmatically via `register(DjangoContainerConfig(...))` from
49
+ `conftest.py`.
50
+ - Postgres init-script mounting (`/docker-entrypoint-initdb.d/NN-name.sql`)
51
+ with automatic `postgres_template = postgres_database` defaulting when
52
+ init scripts are present (SPEC §10.6).
53
+ - Reuse mode via `PYTEST_TESTCONTAINERS_REUSE=1`, with a stderr warning
54
+ when init scripts wouldn't be replayed against a pre-existing container
55
+ (SPEC §10.7).
56
+ - pytest-xdist worker handling: workers inherit env from the controller and
57
+ only set the `*_SKIP_DOTENV` flag (SPEC §7).
58
+ - Optional integration with `django-pg-baseline` via the
59
+ `use_django_pg_baseline = true` flag.
60
+ - atexit safety net for abrupt-exit paths that skip `pytest_unconfigure`.
61
+ - Custom `*_SKIP_DOTENV` env-var injection so projects using django-environ
62
+ don't have their just-injected ports clobbered by `.env` reload (SPEC §9).
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Michał Pasternak
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.
@@ -0,0 +1,371 @@
1
+ Metadata-Version: 2.4
2
+ Name: pytest-testcontainers-django
3
+ Version: 0.2.0
4
+ Summary: Bridge between pytest-testcontainers and pytest-django: starts the DB container before Django imports settings.
5
+ Project-URL: Homepage, https://github.com/iplweb/pytest-testcontainers-django
6
+ Project-URL: Repository, https://github.com/iplweb/pytest-testcontainers-django
7
+ Project-URL: Issues, https://github.com/iplweb/pytest-testcontainers-django/issues
8
+ Project-URL: Changelog, https://github.com/iplweb/pytest-testcontainers-django/blob/main/CHANGELOG.md
9
+ Author-email: Michał Pasternak <michal.dtz@gmail.com>
10
+ License: MIT License
11
+
12
+ Copyright (c) 2026 Michał Pasternak
13
+
14
+ Permission is hereby granted, free of charge, to any person obtaining a copy
15
+ of this software and associated documentation files (the "Software"), to deal
16
+ in the Software without restriction, including without limitation the rights
17
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
+ copies of the Software, and to permit persons to whom the Software is
19
+ furnished to do so, subject to the following conditions:
20
+
21
+ The above copyright notice and this permission notice shall be included in all
22
+ copies or substantial portions of the Software.
23
+
24
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
+ SOFTWARE.
31
+ License-File: LICENSE
32
+ Keywords: django,docker,integration-testing,postgres,pytest,pytest-django,pytest-plugin,redis,testcontainers
33
+ Classifier: Development Status :: 4 - Beta
34
+ Classifier: Framework :: Django
35
+ Classifier: Framework :: Django :: 4.2
36
+ Classifier: Framework :: Django :: 5.0
37
+ Classifier: Framework :: Django :: 5.1
38
+ Classifier: Framework :: Django :: 5.2
39
+ Classifier: Framework :: Pytest
40
+ Classifier: Intended Audience :: Developers
41
+ Classifier: License :: OSI Approved :: MIT License
42
+ Classifier: Operating System :: OS Independent
43
+ Classifier: Programming Language :: Python
44
+ Classifier: Programming Language :: Python :: 3
45
+ Classifier: Programming Language :: Python :: 3.10
46
+ Classifier: Programming Language :: Python :: 3.11
47
+ Classifier: Programming Language :: Python :: 3.12
48
+ Classifier: Programming Language :: Python :: 3.13
49
+ Classifier: Topic :: Software Development :: Testing
50
+ Requires-Python: >=3.10
51
+ Requires-Dist: pytest-django>=4
52
+ Requires-Dist: pytest-testcontainers<2,>=0.1
53
+ Requires-Dist: pytest<9,>=7.4
54
+ Requires-Dist: testcontainers<5,>=4.7
55
+ Requires-Dist: tomli>=2; python_version < '3.11'
56
+ Provides-Extra: baseline
57
+ Requires-Dist: django-pg-baseline; extra == 'baseline'
58
+ Provides-Extra: dev
59
+ Requires-Dist: django-environ; extra == 'dev'
60
+ Requires-Dist: django>=4.2; extra == 'dev'
61
+ Requires-Dist: pre-commit; extra == 'dev'
62
+ Requires-Dist: pytest-xdist; extra == 'dev'
63
+ Requires-Dist: redis>=4; extra == 'dev'
64
+ Requires-Dist: ruff; extra == 'dev'
65
+ Provides-Extra: redis
66
+ Requires-Dist: redis>=4; extra == 'redis'
67
+ Description-Content-Type: text/markdown
68
+
69
+ # pytest-testcontainers-django
70
+
71
+ [![CI](https://github.com/iplweb/pytest-testcontainers-django/actions/workflows/ci.yml/badge.svg)](https://github.com/iplweb/pytest-testcontainers-django/actions/workflows/ci.yml)
72
+ [![PyPI](https://img.shields.io/pypi/v/pytest-testcontainers-django.svg)](https://pypi.org/project/pytest-testcontainers-django/)
73
+ [![Python versions](https://img.shields.io/pypi/pyversions/pytest-testcontainers-django.svg)](https://pypi.org/project/pytest-testcontainers-django/)
74
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
75
+
76
+ Bridge between [`pytest-testcontainers`][pt] and [`pytest-django`][pd]:
77
+ starts a Postgres (and optionally Redis) container **before** Django imports
78
+ its settings, so your tests run against a real, ephemeral DB without any
79
+ docker-compose orchestration — and without "Connection refused" against
80
+ port 5432 because Django read `os.environ` too early.
81
+
82
+ [pt]: https://github.com/iplweb/pytest-testcontainers
83
+ [pd]: https://github.com/pytest-dev/pytest-django
84
+
85
+ ## Why this package exists
86
+
87
+ Django evaluates `DATABASES` at **module-import time**. pytest-django
88
+ imports settings during its `pytest_load_initial_conftests` hook. Any
89
+ fixture-based testcontainer setup runs *after* that — so by the time the
90
+ container has a port, Django has already opened a connection (or failed
91
+ to) against whatever your `.env` had at pytest startup.
92
+
93
+ The only correct hook for "start a container, write its port to
94
+ `os.environ`, before Django imports settings" is
95
+ `pytest_load_initial_conftests` itself, registered with
96
+ `@pytest.hookimpl(tryfirst=True)`. That single detail is the core IP of
97
+ this package; the rest is plumbing — xdist worker propagation, dotenv
98
+ suppression, init-script mounting, TEST TEMPLATE wiring, cleanup
99
+ ordering.
100
+
101
+ See [`SPEC.md`](SPEC.md) for the full design rationale (especially §6 on
102
+ the timing dance).
103
+
104
+ ## Features
105
+
106
+ - `pytest_load_initial_conftests(tryfirst=True)` hook that runs **before**
107
+ pytest-django imports your `settings.py`.
108
+ - Zero-config defaults — works out of the box for projects whose
109
+ `settings.py` reads `DJANGO_DB_HOST` / `DJANGO_DB_PORT` / etc. from
110
+ `os.environ`.
111
+ - Declarative configuration in `[tool.pytest-testcontainers-django]` or
112
+ programmatic configuration via `register(DjangoContainerConfig(...))`
113
+ from `conftest.py`.
114
+ - Postgres init-script mounting (`/docker-entrypoint-initdb.d/`) with
115
+ automatic `DATABASES['TEST']['TEMPLATE']` defaulting — so
116
+ `pytest --create-db` finishes in seconds.
117
+ - Optional Redis container with the same timing-safe injection pattern.
118
+ - pytest-xdist support — workers inherit the controller's environment,
119
+ no port-fight.
120
+ - `--no-testcontainers` / `PYTEST_TESTCONTAINERS_DISABLE=1` to delegate
121
+ to docker-compose; `PYTEST_TESTCONTAINERS_REUSE=1` for fast local
122
+ iteration.
123
+ - atexit safety net for abrupt-exit paths that skip `pytest_unconfigure`.
124
+ - Optional integration with [`django-pg-baseline`](https://github.com/iplweb/django-pg-baseline)
125
+ for managed baseline SQL artifacts.
126
+
127
+ ## Supported versions
128
+
129
+ ### Python
130
+
131
+ | Python | 3.10 | 3.11 | 3.12 | 3.13 |
132
+ |--------|------|------|------|------|
133
+ | | ✓ | ✓ | ✓ | ✓ |
134
+
135
+ ### Django
136
+
137
+ Authoritative upstream: <https://docs.djangoproject.com/en/dev/faq/install/#what-python-version-can-i-use-with-django>
138
+
139
+ | Django | 3.10 | 3.11 | 3.12 | 3.13 | Status |
140
+ |---------|------|------|------|------|------------------------------|
141
+ | 4.2 LTS | ✓ | ✓ | ✓ | — | EOL Apr 2026 (still works) |
142
+ | 5.2 LTS | ✓ | ✓ | ✓ | ✓ | Active LTS |
143
+
144
+ EOL Django releases (4.2, 5.0, 5.1) are not actively tested but should
145
+ still work — this package only consumes pytest-django's hook surface,
146
+ not Django internals. Open an issue if you need an LTS-only reassurance.
147
+
148
+ ## Install
149
+
150
+ ### Using uv (recommended)
151
+
152
+ ```bash
153
+ uv add pytest-testcontainers-django
154
+ ```
155
+
156
+ ### Using pip
157
+
158
+ ```bash
159
+ pip install pytest-testcontainers-django
160
+ ```
161
+
162
+ You also need a working Docker daemon on the host running pytest. No
163
+ extra system libraries are required — the package is pure Python.
164
+
165
+ ## Quick start
166
+
167
+ For most projects, configuration lives in `pyproject.toml`. Zero
168
+ `conftest.py` needed:
169
+
170
+ ```toml
171
+ [tool.pytest-testcontainers-django]
172
+ postgres_image = "postgres:16"
173
+ postgres_user = "myapp"
174
+ postgres_password = "myapp"
175
+ postgres_database = "myapp"
176
+
177
+ # Env-var names this plugin writes into os.environ.
178
+ # These are the same names your settings.py reads.
179
+ db_host_env = "DJANGO_DB_HOST"
180
+ db_port_env = "DJANGO_DB_PORT"
181
+ db_name_env = "DJANGO_DB_NAME"
182
+ db_user_env = "DJANGO_DB_USER"
183
+ db_password_env = "DJANGO_DB_PASSWORD"
184
+ db_test_template_env = "DJANGO_DB_TEST_TEMPLATE"
185
+ skip_dotenv_env = "DJANGO_SKIP_DOTENV"
186
+ ```
187
+
188
+ Your `settings.py` reads these env vars exactly as you'd expect:
189
+
190
+ ```python
191
+ import environ
192
+ import os
193
+
194
+ env = environ.Env()
195
+
196
+ # Skip .env loading when the plugin already populated os.environ —
197
+ # otherwise read_env(overwrite=True) would clobber our injected port.
198
+ if not os.environ.get("DJANGO_SKIP_DOTENV"):
199
+ environ.Env.read_env(".env", overwrite=True)
200
+
201
+ DATABASES = {
202
+ "default": {
203
+ "ENGINE": "django.db.backends.postgresql",
204
+ "NAME": env("DJANGO_DB_NAME"),
205
+ "USER": env("DJANGO_DB_USER"),
206
+ "PASSWORD": env("DJANGO_DB_PASSWORD"),
207
+ "HOST": env("DJANGO_DB_HOST"),
208
+ "PORT": env("DJANGO_DB_PORT"),
209
+ },
210
+ }
211
+
212
+ # Wire the TEST TEMPLATE env var (optional but recommended when you
213
+ # load init scripts — see "Init scripts / baseline" below).
214
+ _test_template = env("DJANGO_DB_TEST_TEMPLATE", default="")
215
+ if _test_template:
216
+ DATABASES["default"]["TEST"] = {"TEMPLATE": _test_template}
217
+ ```
218
+
219
+ That's it — `pytest` will start a Postgres container, inject the port,
220
+ let pytest-django import settings, and tear the container down at exit.
221
+
222
+ ## Init scripts / baseline
223
+
224
+ Mount SQL files into the Postgres container's
225
+ `/docker-entrypoint-initdb.d/` so they're replayed once on cluster init —
226
+ significantly faster than running `psql -f` from the host:
227
+
228
+ ```toml
229
+ [tool.pytest-testcontainers-django]
230
+ postgres_database = "myapp"
231
+ postgres_init_scripts = [
232
+ "tests/fixtures/baseline.sql",
233
+ "tests/fixtures/extensions.sql",
234
+ ]
235
+ # postgres_template defaults to postgres_database when init_scripts is
236
+ # set, so the test DB will be created via fast in-server CREATE DATABASE
237
+ # test_<X> WITH TEMPLATE myapp instead of replaying migrations.
238
+ ```
239
+
240
+ Combine with the `DATABASES['default']['TEST']['TEMPLATE']` snippet
241
+ above to make `pytest --create-db` finish in seconds.
242
+
243
+ ## Optional Redis
244
+
245
+ `testcontainers`'s `RedisContainer` imports the `redis` Python client at
246
+ module load, so install it alongside this package when you enable Redis:
247
+
248
+ ```bash
249
+ uv add 'pytest-testcontainers-django[redis]'
250
+ # or
251
+ pip install 'pytest-testcontainers-django[redis]'
252
+ ```
253
+
254
+ ```toml
255
+ [tool.pytest-testcontainers-django]
256
+ redis_enabled = true
257
+ redis_image = "redis:7-alpine"
258
+ redis_host_env = "DJANGO_REDIS_HOST"
259
+ redis_port_env = "DJANGO_REDIS_PORT"
260
+ ```
261
+
262
+ Your settings reads `DJANGO_REDIS_HOST` / `DJANGO_REDIS_PORT` and folds
263
+ them into a `redis://...` URL however your stack prefers.
264
+
265
+ ## Programmatic configuration
266
+
267
+ For projects that need conditional configuration or want to wire in
268
+ [`django-pg-baseline`](https://github.com/iplweb/django-pg-baseline) for
269
+ baseline-managed seed data, register from `conftest.py`:
270
+
271
+ ```python
272
+ # conftest.py at the project root
273
+ from pathlib import Path
274
+
275
+ from pytest_testcontainers_django import (
276
+ DjangoContainerConfig,
277
+ PostgresService,
278
+ RedisService,
279
+ register,
280
+ )
281
+
282
+ register(
283
+ DjangoContainerConfig(
284
+ postgres=PostgresService(
285
+ image="postgres:16",
286
+ user="myapp",
287
+ password="myapp",
288
+ database="myapp",
289
+ init_scripts=[Path("tests/fixtures/baseline.sql")],
290
+ template="myapp",
291
+ ),
292
+ redis=RedisService(),
293
+ )
294
+ )
295
+ ```
296
+
297
+ `register()` overrides any `pyproject.toml` table. This works because
298
+ the plugin force-imports the rootdir `conftest.py` from inside its
299
+ `tryfirst` hook, so top-level `register(...)` calls run before
300
+ configuration is read.
301
+
302
+ ## Disable / reuse
303
+
304
+ ```bash
305
+ # Delegate to docker-compose / pre-existing services:
306
+ pytest --no-testcontainers
307
+ PYTEST_TESTCONTAINERS_DISABLE=1 pytest
308
+
309
+ # Keep containers alive between runs for fast local iteration:
310
+ PYTEST_TESTCONTAINERS_REUSE=1 pytest
311
+ ```
312
+
313
+ ## pytest-xdist
314
+
315
+ Workers inherit the controller's environment on fork, so they don't
316
+ start new containers — they only re-set the `*_SKIP_DOTENV` flag so
317
+ django-environ doesn't re-read `.env` on settings re-import.
318
+
319
+ ## Coexistence with other testcontainers
320
+
321
+ Django projects that need additional services (Elasticsearch, MinIO,
322
+ Kafka, etc.) declare plain pytest fixtures using
323
+ `pytest-testcontainers`'s maker functions directly — no special
324
+ integration with this package needed:
325
+
326
+ ```python
327
+ # conftest.py
328
+ import pytest
329
+ from pytest_testcontainers import make_container
330
+
331
+ @pytest.fixture(scope="session")
332
+ def minio():
333
+ with make_container("minio/minio:latest", ports={"9000/tcp": None}) as c:
334
+ yield c
335
+ ```
336
+
337
+ Late resolution is fine for non-DB services — their host:port is read
338
+ at *connection time*, not import time. Only Django's `DATABASES` has
339
+ the import-time-read race that this package solves.
340
+
341
+ ## Configuration reference
342
+
343
+ | Pyproject key | Default | Purpose |
344
+ | ----------------------------- | -------------------------------- | ------------------------------------------------ |
345
+ | `postgres_image` | `postgres:16` | Image used for the DB container |
346
+ | `postgres_user` | `postgres` | `POSTGRES_USER` |
347
+ | `postgres_password` | `postgres` | `POSTGRES_PASSWORD` |
348
+ | `postgres_database` | `postgres` | `POSTGRES_DB` |
349
+ | `postgres_internal_port` | `5432` | Image's internal port |
350
+ | `postgres_template` | (= `postgres_database` when init scripts set, else unset) | Value injected as `DATABASES['TEST']['TEMPLATE']` |
351
+ | `postgres_init_scripts` | `[]` | Paths mounted into `/docker-entrypoint-initdb.d/`|
352
+ | `postgres_env` | `{}` | Image-specific env (e.g. tuning flags) |
353
+ | `db_host_env` | `DJANGO_DB_HOST` | Env var written with the resolved host |
354
+ | `db_port_env` | `DJANGO_DB_PORT` | Env var written with the resolved port |
355
+ | `db_name_env` | `DJANGO_DB_NAME` | Env var written with `postgres_database` |
356
+ | `db_user_env` | `DJANGO_DB_USER` | Env var written with `postgres_user` |
357
+ | `db_password_env` | `DJANGO_DB_PASSWORD` | Env var written with `postgres_password` |
358
+ | `db_test_template_env` | `DJANGO_DB_TEST_TEMPLATE` | Env var written with `postgres_template` |
359
+ | `skip_dotenv_env` | `DJANGO_SKIP_DOTENV` | Env var your settings checks before reading .env |
360
+ | `disable_env` | `PYTEST_TESTCONTAINERS_DISABLE` | Env var that disables the plugin |
361
+ | `reuse_env` | `PYTEST_TESTCONTAINERS_REUSE` | Env var that enables reuse mode |
362
+ | `redis_enabled` | `false` | Enable Redis |
363
+ | `redis_image` | `redis:7-alpine` | Image used for Redis |
364
+ | `redis_internal_port` | `6379` | |
365
+ | `redis_host_env` | `DJANGO_REDIS_HOST` | |
366
+ | `redis_port_env` | `DJANGO_REDIS_PORT` | |
367
+ | `use_django_pg_baseline` | `false` | Auto-prepend `django-pg-baseline`'s artifact |
368
+
369
+ ## License
370
+
371
+ MIT — see [`LICENSE`](LICENSE).