pytest-testcontainers 0.2.0__tar.gz → 0.2.2__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.
Files changed (45) hide show
  1. {pytest_testcontainers-0.2.0 → pytest_testcontainers-0.2.2}/CHANGELOG.md +27 -0
  2. {pytest_testcontainers-0.2.0 → pytest_testcontainers-0.2.2}/PKG-INFO +80 -4
  3. {pytest_testcontainers-0.2.0 → pytest_testcontainers-0.2.2}/README.md +79 -3
  4. {pytest_testcontainers-0.2.0 → pytest_testcontainers-0.2.2}/pyproject.toml +1 -1
  5. {pytest_testcontainers-0.2.0 → pytest_testcontainers-0.2.2}/src/pytest_testcontainers/__init__.py +3 -1
  6. pytest_testcontainers-0.2.2/src/pytest_testcontainers/_internal/docker_health.py +105 -0
  7. pytest_testcontainers-0.2.2/src/pytest_testcontainers/_internal/git_identity.py +80 -0
  8. {pytest_testcontainers-0.2.0 → pytest_testcontainers-0.2.2}/src/pytest_testcontainers/errors.py +8 -0
  9. {pytest_testcontainers-0.2.0 → pytest_testcontainers-0.2.2}/src/pytest_testcontainers/makers.py +4 -5
  10. {pytest_testcontainers-0.2.0 → pytest_testcontainers-0.2.2}/src/pytest_testcontainers/plugin.py +18 -0
  11. pytest_testcontainers-0.2.2/src/pytest_testcontainers/reuse.py +319 -0
  12. {pytest_testcontainers-0.2.0 → pytest_testcontainers-0.2.2}/tests/conftest.py +17 -0
  13. pytest_testcontainers-0.2.2/tests/test_docker_context.py +113 -0
  14. {pytest_testcontainers-0.2.0 → pytest_testcontainers-0.2.2}/tests/test_errors.py +13 -0
  15. pytest_testcontainers-0.2.2/tests/test_git_identity.py +56 -0
  16. pytest_testcontainers-0.2.2/tests/test_makers_naming.py +71 -0
  17. pytest_testcontainers-0.2.2/tests/test_naming_docker.py +59 -0
  18. pytest_testcontainers-0.2.2/tests/test_plugin_options.py +39 -0
  19. pytest_testcontainers-0.2.2/tests/test_reuse.py +345 -0
  20. {pytest_testcontainers-0.2.0 → pytest_testcontainers-0.2.2}/tests/test_reuse_mode.py +2 -2
  21. pytest_testcontainers-0.2.0/src/pytest_testcontainers/_internal/docker_health.py +0 -59
  22. pytest_testcontainers-0.2.0/src/pytest_testcontainers/reuse.py +0 -209
  23. pytest_testcontainers-0.2.0/tests/test_reuse.py +0 -179
  24. {pytest_testcontainers-0.2.0 → pytest_testcontainers-0.2.2}/.gitignore +0 -0
  25. {pytest_testcontainers-0.2.0 → pytest_testcontainers-0.2.2}/LICENSE +0 -0
  26. {pytest_testcontainers-0.2.0 → pytest_testcontainers-0.2.2}/src/pytest_testcontainers/_internal/__init__.py +0 -0
  27. {pytest_testcontainers-0.2.0 → pytest_testcontainers-0.2.2}/src/pytest_testcontainers/_internal/clean_session_admin.py +0 -0
  28. {pytest_testcontainers-0.2.0 → pytest_testcontainers-0.2.2}/src/pytest_testcontainers/_internal/conn_info.py +0 -0
  29. {pytest_testcontainers-0.2.0 → pytest_testcontainers-0.2.2}/src/pytest_testcontainers/_internal/port_resolver.py +0 -0
  30. {pytest_testcontainers-0.2.0 → pytest_testcontainers-0.2.2}/src/pytest_testcontainers/containers.py +0 -0
  31. {pytest_testcontainers-0.2.0 → pytest_testcontainers-0.2.2}/src/pytest_testcontainers/fixtures.py +0 -0
  32. {pytest_testcontainers-0.2.0 → pytest_testcontainers-0.2.2}/tests/test_disabled.py +0 -0
  33. {pytest_testcontainers-0.2.0 → pytest_testcontainers-0.2.2}/tests/test_disabled_fixture.py +0 -0
  34. {pytest_testcontainers-0.2.0 → pytest_testcontainers-0.2.2}/tests/test_docker_not_running.py +0 -0
  35. {pytest_testcontainers-0.2.0 → pytest_testcontainers-0.2.2}/tests/test_fixtures_clean_session_mongo.py +0 -0
  36. {pytest_testcontainers-0.2.0 → pytest_testcontainers-0.2.2}/tests/test_fixtures_clean_session_psql.py +0 -0
  37. {pytest_testcontainers-0.2.0 → pytest_testcontainers-0.2.2}/tests/test_fixtures_clean_session_redis.py +0 -0
  38. {pytest_testcontainers-0.2.0 → pytest_testcontainers-0.2.2}/tests/test_fixtures_session.py +0 -0
  39. {pytest_testcontainers-0.2.0 → pytest_testcontainers-0.2.2}/tests/test_makers_generic.py +0 -0
  40. {pytest_testcontainers-0.2.0 → pytest_testcontainers-0.2.2}/tests/test_makers_mongo.py +0 -0
  41. {pytest_testcontainers-0.2.0 → pytest_testcontainers-0.2.2}/tests/test_makers_mysql.py +0 -0
  42. {pytest_testcontainers-0.2.0 → pytest_testcontainers-0.2.2}/tests/test_makers_no_docker.py +0 -0
  43. {pytest_testcontainers-0.2.0 → pytest_testcontainers-0.2.2}/tests/test_makers_postgres.py +0 -0
  44. {pytest_testcontainers-0.2.0 → pytest_testcontainers-0.2.2}/tests/test_makers_redis.py +0 -0
  45. {pytest_testcontainers-0.2.0 → pytest_testcontainers-0.2.2}/tests/test_port_resolver.py +0 -0
@@ -7,6 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.2] - 2026-06-29
11
+
12
+ ### Fixed
13
+
14
+ - Restored the `reuse_name_for` name in `pytest_testcontainers.reuse` as a
15
+ backward-compatible alias of `container_name_for`. The template-based
16
+ container-naming feature renamed the function, and 0.2.1 shipped that rename —
17
+ which broke `pytest-testcontainers-django` < 0.2.5 (it imports the old name)
18
+ at import time. The alias keeps existing installs working; the default
19
+ `reuse_mode=True` matches the old function's byte-stable behavior.
20
+
21
+ ## [0.2.1] - 2026-06-29
22
+
23
+ ### Fixed
24
+
25
+ - **`Docker daemon is not reachable` even though `docker ps` worked**, on setups
26
+ where the daemon lives on a non-default socket (OrbStack, colima, a stopped
27
+ Docker Desktop). `docker.from_env()` — used by docker-py, by
28
+ testcontainers' own client, and by Ryuk's socket bind-mount — honors
29
+ `DOCKER_HOST` but, unlike the `docker` CLI, ignores the active
30
+ `docker context`, so it fell back to `/var/run/docker.sock` (absent or a
31
+ dangling symlink) and crashed with `FileNotFoundError`. The plugin now
32
+ exports `DOCKER_HOST` from the active context before any client is built, so
33
+ the whole process resolves Docker the way the CLI does. An explicit
34
+ `DOCKER_HOST` still wins; with no resolvable context it falls back to the
35
+ platform default socket.
36
+
10
37
  ## [0.2.0] - 2026-06-18
11
38
 
12
39
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytest-testcontainers
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Named pytest fixtures and a maker convention on top of testcontainers-python.
5
5
  Project-URL: Homepage, https://github.com/iplweb/pytest-testcontainers
6
6
  Project-URL: Repository, https://github.com/iplweb/pytest-testcontainers
@@ -455,6 +455,53 @@ Every plumbing concern — daemon ping, reuse name, atexit cleanup,
455
455
  Ryuk-disable-when-reuse — applies. `args`/`kwargs` go to the upstream
456
456
  constructor verbatim.
457
457
 
458
+ ## Container names
459
+
460
+ Every container the plugin starts is named so you can find it in `docker ps`
461
+ (previously default-mode containers got random Docker names like
462
+ `heuristic_cannon`). The name is built from a template:
463
+
464
+ ```
465
+ {project}-tc-{service}-{branch}-{dirtag}-{worker}
466
+ ```
467
+
468
+ e.g. `myproj-tc-psql-feature-x-3f9ac1b2-master`.
469
+
470
+ Placeholders:
471
+
472
+ | Placeholder | Meaning |
473
+ |-------------|---------|
474
+ | `{project}` | Project name (see precedence below). |
475
+ | `{service}` | Service slug (`psql`, `redis`, `mysql`, `mongo`, `rabbitmq`, or derived from the container class). |
476
+ | `{branch}` | Current git branch (read from `.git/HEAD`, no subprocess). Detached HEAD → 12-char commit SHA. No repo → empty. |
477
+ | `{dir}` | Worktree-root basename. Readable, not collision-proof. |
478
+ | `{dirtag}` | 8-hex hash of the worktree-root absolute path. Disambiguates the same branch checked out in two directories. |
479
+ | `{worker}` | xdist worker id (`master` when not under xdist). |
480
+ | `{rand}` | 4 random hex chars. Empty in reuse mode; auto-appended in default mode when the template omits it. |
481
+
482
+ Empty placeholders collapse cleanly (no `--`, no dangling dashes). Names are
483
+ sanitized to Docker's charset and capped at 255 chars (trim + checksum).
484
+
485
+ Override the template (CLI > env > pyproject > built-in default):
486
+
487
+ ```bash
488
+ pytest --testcontainers-name-template='{project}-{service}-{branch}'
489
+ # or
490
+ export PYTEST_TESTCONTAINERS_NAME_TEMPLATE='{project}-{service}-{branch}'
491
+ ```
492
+
493
+ ```toml
494
+ # pyproject.toml
495
+ [tool.pytest_testcontainers]
496
+ name_template = "{project}-{service}-{branch}"
497
+ ```
498
+
499
+ The `{project}` component resolves as: `--testcontainers-project` →
500
+ `PYTEST_TESTCONTAINERS_PROJECT` → `PROJECT_NAME` → `COMPOSE_PROJECT_NAME` →
501
+ `pyproject.toml [project].name` → cwd basename.
502
+
503
+ An unknown placeholder raises `NameTemplateError` listing the valid ones.
504
+
458
505
  ## Reuse mode
459
506
 
460
507
  For iterative dev loops where you don't want to pay container-start
@@ -467,9 +514,11 @@ pytest --testcontainers-reuse tests/
467
514
  ```
468
515
 
469
516
  What changes:
470
- - Each container gets a stable name `<project>-tc-<service>-<worker>`
471
- (e.g. `myproject-tc-psql-master`). The project name comes from
472
- `pyproject.toml [project].name`.
517
+ - Each container gets a stable, identifiable name from the template
518
+ (default `{project}-tc-{service}-{branch}-{dirtag}-{worker}`), so reuse is
519
+ scoped per project **and** per git branch **and** per directory. Switching
520
+ branch or worktree yields a *new* container; the previous one lingers
521
+ (stopped) until you run `pytest --testcontainers-clean`.
473
522
  - Ryuk (testcontainers' reaper) is disabled so the named containers
474
523
  survive between runs.
475
524
  - On the next run we look up by name — found-and-running gets bound
@@ -491,6 +540,10 @@ own `PYTEST_TESTCONTAINERS_PROJECT` to avoid name collisions. The
491
540
  plugin doesn't auto-namespace by PID — that would defeat the "reuse
492
541
  across runs" point.
493
542
 
543
+ > **Upgrading from 0.1.0:** reuse-mode names now encode branch + directory, so
544
+ > containers reused from an older version are not found by name — a fresh one
545
+ > is created and the old one lingers until `pytest --testcontainers-clean`.
546
+
494
547
  ## Configuration
495
548
 
496
549
  No TOML config table. The handful of toggles read at maker-call time:
@@ -502,6 +555,9 @@ No TOML config table. The handful of toggles read at maker-call time:
502
555
  | `PYTEST_TESTCONTAINERS=0` | Disable plugin fixtures (raise UsageError). |
503
556
  | `PYTEST_TESTCONTAINERS_REUSE=1` | Reuse named containers across runs. |
504
557
  | `PYTEST_TESTCONTAINERS_PROJECT=<name>` | Override the `<project>` part of reuse names. |
558
+ | `PROJECT_NAME=<name>` | Generic project-name source (below the plugin var). |
559
+ | `COMPOSE_PROJECT_NAME=<name>` | docker-compose's project var (below `PROJECT_NAME`). |
560
+ | `PYTEST_TESTCONTAINERS_NAME_TEMPLATE=<t>` | Override the container-name template. |
505
561
  | `PYTEST_TESTCONTAINERS_NO_DAEMON_CHECK=1` | Skip Docker daemon ping (rare). |
506
562
  | `PYTEST_TESTCONTAINERS_QUIET=1` | Suppress one-shot informational advisories. |
507
563
 
@@ -513,6 +569,7 @@ No TOML config table. The handful of toggles read at maker-call time:
513
569
  | `--testcontainers-reuse` | `PYTEST_TESTCONTAINERS_REUSE=1` |
514
570
  | `--testcontainers-no-reuse` | force fresh-each-run mode |
515
571
  | `--testcontainers-project=NAME` | `PYTEST_TESTCONTAINERS_PROJECT=NAME` |
572
+ | `--testcontainers-name-template=T` | `PYTEST_TESTCONTAINERS_NAME_TEMPLATE=T` |
516
573
  | `--testcontainers-clean` | prune `<project>-tc-*` and exit 0 |
517
574
 
518
575
  CLI > env > defaults.
@@ -534,6 +591,25 @@ Options:
534
591
  Underlying error: ...
535
592
  ```
536
593
 
594
+ ### `docker ps` works but pytest says the daemon is unreachable
595
+
596
+ You're almost certainly on a non-default **Docker context** — OrbStack,
597
+ colima, or a Docker Desktop install where `/var/run/docker.sock` is missing
598
+ or a dangling symlink. `docker.from_env()` (used by docker-py *and* by
599
+ testcontainers internally) honors `DOCKER_HOST` but, unlike the `docker`
600
+ CLI, ignores the active `docker context`. The plugin now resolves the active
601
+ context's endpoint for you and exports `DOCKER_HOST` before any container is
602
+ started, so it talks to the same daemon the CLI does.
603
+
604
+ If it still fails, point it at the daemon explicitly — an explicit
605
+ `DOCKER_HOST` always wins:
606
+
607
+ ```bash
608
+ docker context inspect -f '{{.Endpoints.docker.Host}}' # see the endpoint
609
+ export DOCKER_HOST=unix://$HOME/.orbstack/run/docker.sock # e.g. OrbStack
610
+ export DOCKER_HOST=unix://$HOME/.colima/default/docker.sock # e.g. colima
611
+ ```
612
+
537
613
  When a stopped reused container can't be brought back (typically
538
614
  because the previously-mapped port is now held by something else):
539
615
 
@@ -383,6 +383,53 @@ Every plumbing concern — daemon ping, reuse name, atexit cleanup,
383
383
  Ryuk-disable-when-reuse — applies. `args`/`kwargs` go to the upstream
384
384
  constructor verbatim.
385
385
 
386
+ ## Container names
387
+
388
+ Every container the plugin starts is named so you can find it in `docker ps`
389
+ (previously default-mode containers got random Docker names like
390
+ `heuristic_cannon`). The name is built from a template:
391
+
392
+ ```
393
+ {project}-tc-{service}-{branch}-{dirtag}-{worker}
394
+ ```
395
+
396
+ e.g. `myproj-tc-psql-feature-x-3f9ac1b2-master`.
397
+
398
+ Placeholders:
399
+
400
+ | Placeholder | Meaning |
401
+ |-------------|---------|
402
+ | `{project}` | Project name (see precedence below). |
403
+ | `{service}` | Service slug (`psql`, `redis`, `mysql`, `mongo`, `rabbitmq`, or derived from the container class). |
404
+ | `{branch}` | Current git branch (read from `.git/HEAD`, no subprocess). Detached HEAD → 12-char commit SHA. No repo → empty. |
405
+ | `{dir}` | Worktree-root basename. Readable, not collision-proof. |
406
+ | `{dirtag}` | 8-hex hash of the worktree-root absolute path. Disambiguates the same branch checked out in two directories. |
407
+ | `{worker}` | xdist worker id (`master` when not under xdist). |
408
+ | `{rand}` | 4 random hex chars. Empty in reuse mode; auto-appended in default mode when the template omits it. |
409
+
410
+ Empty placeholders collapse cleanly (no `--`, no dangling dashes). Names are
411
+ sanitized to Docker's charset and capped at 255 chars (trim + checksum).
412
+
413
+ Override the template (CLI > env > pyproject > built-in default):
414
+
415
+ ```bash
416
+ pytest --testcontainers-name-template='{project}-{service}-{branch}'
417
+ # or
418
+ export PYTEST_TESTCONTAINERS_NAME_TEMPLATE='{project}-{service}-{branch}'
419
+ ```
420
+
421
+ ```toml
422
+ # pyproject.toml
423
+ [tool.pytest_testcontainers]
424
+ name_template = "{project}-{service}-{branch}"
425
+ ```
426
+
427
+ The `{project}` component resolves as: `--testcontainers-project` →
428
+ `PYTEST_TESTCONTAINERS_PROJECT` → `PROJECT_NAME` → `COMPOSE_PROJECT_NAME` →
429
+ `pyproject.toml [project].name` → cwd basename.
430
+
431
+ An unknown placeholder raises `NameTemplateError` listing the valid ones.
432
+
386
433
  ## Reuse mode
387
434
 
388
435
  For iterative dev loops where you don't want to pay container-start
@@ -395,9 +442,11 @@ pytest --testcontainers-reuse tests/
395
442
  ```
396
443
 
397
444
  What changes:
398
- - Each container gets a stable name `<project>-tc-<service>-<worker>`
399
- (e.g. `myproject-tc-psql-master`). The project name comes from
400
- `pyproject.toml [project].name`.
445
+ - Each container gets a stable, identifiable name from the template
446
+ (default `{project}-tc-{service}-{branch}-{dirtag}-{worker}`), so reuse is
447
+ scoped per project **and** per git branch **and** per directory. Switching
448
+ branch or worktree yields a *new* container; the previous one lingers
449
+ (stopped) until you run `pytest --testcontainers-clean`.
401
450
  - Ryuk (testcontainers' reaper) is disabled so the named containers
402
451
  survive between runs.
403
452
  - On the next run we look up by name — found-and-running gets bound
@@ -419,6 +468,10 @@ own `PYTEST_TESTCONTAINERS_PROJECT` to avoid name collisions. The
419
468
  plugin doesn't auto-namespace by PID — that would defeat the "reuse
420
469
  across runs" point.
421
470
 
471
+ > **Upgrading from 0.1.0:** reuse-mode names now encode branch + directory, so
472
+ > containers reused from an older version are not found by name — a fresh one
473
+ > is created and the old one lingers until `pytest --testcontainers-clean`.
474
+
422
475
  ## Configuration
423
476
 
424
477
  No TOML config table. The handful of toggles read at maker-call time:
@@ -430,6 +483,9 @@ No TOML config table. The handful of toggles read at maker-call time:
430
483
  | `PYTEST_TESTCONTAINERS=0` | Disable plugin fixtures (raise UsageError). |
431
484
  | `PYTEST_TESTCONTAINERS_REUSE=1` | Reuse named containers across runs. |
432
485
  | `PYTEST_TESTCONTAINERS_PROJECT=<name>` | Override the `<project>` part of reuse names. |
486
+ | `PROJECT_NAME=<name>` | Generic project-name source (below the plugin var). |
487
+ | `COMPOSE_PROJECT_NAME=<name>` | docker-compose's project var (below `PROJECT_NAME`). |
488
+ | `PYTEST_TESTCONTAINERS_NAME_TEMPLATE=<t>` | Override the container-name template. |
433
489
  | `PYTEST_TESTCONTAINERS_NO_DAEMON_CHECK=1` | Skip Docker daemon ping (rare). |
434
490
  | `PYTEST_TESTCONTAINERS_QUIET=1` | Suppress one-shot informational advisories. |
435
491
 
@@ -441,6 +497,7 @@ No TOML config table. The handful of toggles read at maker-call time:
441
497
  | `--testcontainers-reuse` | `PYTEST_TESTCONTAINERS_REUSE=1` |
442
498
  | `--testcontainers-no-reuse` | force fresh-each-run mode |
443
499
  | `--testcontainers-project=NAME` | `PYTEST_TESTCONTAINERS_PROJECT=NAME` |
500
+ | `--testcontainers-name-template=T` | `PYTEST_TESTCONTAINERS_NAME_TEMPLATE=T` |
444
501
  | `--testcontainers-clean` | prune `<project>-tc-*` and exit 0 |
445
502
 
446
503
  CLI > env > defaults.
@@ -462,6 +519,25 @@ Options:
462
519
  Underlying error: ...
463
520
  ```
464
521
 
522
+ ### `docker ps` works but pytest says the daemon is unreachable
523
+
524
+ You're almost certainly on a non-default **Docker context** — OrbStack,
525
+ colima, or a Docker Desktop install where `/var/run/docker.sock` is missing
526
+ or a dangling symlink. `docker.from_env()` (used by docker-py *and* by
527
+ testcontainers internally) honors `DOCKER_HOST` but, unlike the `docker`
528
+ CLI, ignores the active `docker context`. The plugin now resolves the active
529
+ context's endpoint for you and exports `DOCKER_HOST` before any container is
530
+ started, so it talks to the same daemon the CLI does.
531
+
532
+ If it still fails, point it at the daemon explicitly — an explicit
533
+ `DOCKER_HOST` always wins:
534
+
535
+ ```bash
536
+ docker context inspect -f '{{.Endpoints.docker.Host}}' # see the endpoint
537
+ export DOCKER_HOST=unix://$HOME/.orbstack/run/docker.sock # e.g. OrbStack
538
+ export DOCKER_HOST=unix://$HOME/.colima/default/docker.sock # e.g. colima
539
+ ```
540
+
465
541
  When a stopped reused container can't be brought back (typically
466
542
  because the previously-mapped port is now held by something else):
467
543
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pytest-testcontainers"
7
- version = "0.2.0"
7
+ version = "0.2.2"
8
8
  description = "Named pytest fixtures and a maker convention on top of testcontainers-python."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -7,6 +7,7 @@ from pytest_testcontainers.errors import (
7
7
  CleanSessionFixtureError,
8
8
  ContainerStartError,
9
9
  DockerNotRunningError,
10
+ NameTemplateError,
10
11
  PytestTestcontainersError,
11
12
  ReuseConflictError,
12
13
  )
@@ -24,6 +25,7 @@ __all__ = [
24
25
  "ContainerStartError",
25
26
  "DbConnInfo",
26
27
  "DockerNotRunningError",
28
+ "NameTemplateError",
27
29
  "PytestTestcontainersError",
28
30
  "ReuseConflictError",
29
31
  "make_container",
@@ -34,4 +36,4 @@ __all__ = [
34
36
  "make_redis",
35
37
  ]
36
38
 
37
- __version__ = "0.1.0"
39
+ __version__ = "0.2.2"
@@ -0,0 +1,105 @@
1
+ """Docker daemon health check — single source of truth for daemon ping.
2
+
3
+ The cache stores **only successful pings**. A successful ping flips a
4
+ process-wide flag; subsequent maker calls in the same process skip the
5
+ probe. A failed ping does NOT poison the cache, so a transient daemon
6
+ restart inside a long-running pytest session does not permanently
7
+ disable the plugin.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ import os
14
+ import threading
15
+
16
+ from pytest_testcontainers.errors import DockerNotRunningError
17
+
18
+ logger = logging.getLogger("pytest_testcontainers")
19
+
20
+ _lock = threading.Lock()
21
+ _ping_ok: bool = False
22
+
23
+
24
+ def _is_truthy(value: str | None) -> bool:
25
+ return (value or "").strip().lower() in {"1", "true", "yes", "on"}
26
+
27
+
28
+ def reset_cache() -> None:
29
+ """Reset the success cache. Used in tests."""
30
+ global _ping_ok
31
+ with _lock:
32
+ _ping_ok = False
33
+
34
+
35
+ def ensure_docker_host_env() -> None:
36
+ """Export ``DOCKER_HOST`` from the active ``docker context`` when unset.
37
+
38
+ ``docker.from_env()`` — used by docker-py, by testcontainers' own client,
39
+ and by Ryuk's socket bind-mount — honors ``DOCKER_HOST`` but, unlike the
40
+ ``docker`` CLI, ignores the active ``docker context``. On OrbStack /
41
+ colima / a stopped Docker Desktop the daemon lives on a non-default socket
42
+ and ``/var/run/docker.sock`` is absent or a dangling symlink, so container
43
+ launch crashes with ``FileNotFoundError`` even though ``docker ps`` works.
44
+
45
+ Exporting ``DOCKER_HOST`` once — before any client is built — makes the
46
+ whole process resolve Docker the way the CLI does. No-op when
47
+ ``DOCKER_HOST`` is already set (an explicit choice always wins) or when no
48
+ context endpoint resolves (fall back to the platform default socket).
49
+ """
50
+ if os.environ.get("DOCKER_HOST"):
51
+ return
52
+ ctx = _active_docker_context()
53
+ host = getattr(ctx, "Host", None) if ctx is not None else None
54
+ if host:
55
+ os.environ["DOCKER_HOST"] = host
56
+
57
+
58
+ def _active_docker_context(): # type: ignore[no-untyped-def]
59
+ """Return the active ``docker context`` object, or ``None``.
60
+
61
+ Resolution failures (older SDK without context support, malformed
62
+ ``~/.docker/config.json``, missing context) are logged and swallowed so
63
+ the caller falls back to the platform default socket.
64
+ """
65
+ try:
66
+ from docker.context import ContextAPI
67
+
68
+ return ContextAPI.get_current_context()
69
+ except Exception:
70
+ logger.debug("could not resolve active docker context", exc_info=True)
71
+ return None
72
+
73
+
74
+ def check_docker_daemon() -> None:
75
+ """Ping the Docker daemon once per process (on success).
76
+
77
+ Raises :class:`DockerNotRunningError` (chained from the underlying
78
+ docker-py exception) if the daemon cannot be reached.
79
+ """
80
+ global _ping_ok
81
+ if _ping_ok:
82
+ return
83
+ # Honor the active `docker context` before any client (ours, testcontainers'
84
+ # or Ryuk's) is built — even when the ping itself is skipped, so the launch
85
+ # path still resolves the right socket on OrbStack / colima / Docker Desktop.
86
+ ensure_docker_host_env()
87
+ if _is_truthy(os.environ.get("PYTEST_TESTCONTAINERS_NO_DAEMON_CHECK")):
88
+ return
89
+
90
+ try:
91
+ import docker # local import: never trigger at module-import time
92
+ except ImportError as exc: # pragma: no cover — docker is a hard dep
93
+ raise DockerNotRunningError(
94
+ "docker-py is not installed; cannot ping Docker daemon"
95
+ ) from exc
96
+
97
+ try:
98
+ client = docker.from_env()
99
+ client.ping()
100
+ except Exception as exc:
101
+ # Surface the underlying daemon failure with a concrete cause.
102
+ raise DockerNotRunningError(f"docker.from_env().ping() failed: {exc!r}") from exc
103
+
104
+ with _lock:
105
+ _ping_ok = True
@@ -0,0 +1,80 @@
1
+ """Filesystem-only git identity helpers: worktree root + current branch.
2
+
3
+ Pure reads — no subprocess, no docker, no import-time side effects. Used by
4
+ ``reuse.py`` to compute the ``{branch}``, ``{dir}`` and ``{dirtag}`` name
5
+ components. Handles both a normal clone (``.git`` is a directory) and a
6
+ linked worktree (``.git`` is a file containing ``gitdir: <path>``).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from pathlib import Path
12
+
13
+
14
+ def worktree_root(start: Path) -> Path:
15
+ """Return the directory that owns the working tree containing ``start``.
16
+
17
+ Walks upward from ``start`` looking for a ``.git`` entry (a directory in a
18
+ normal clone, a file in a linked worktree — either marks the root). Falls
19
+ back to ``start.resolve()`` when no ``.git`` is found.
20
+ """
21
+ current = start.resolve()
22
+ for candidate in [current, *current.parents]:
23
+ if (candidate / ".git").exists():
24
+ return candidate
25
+ return current
26
+
27
+
28
+ def _gitdir_for(root: Path) -> Path | None:
29
+ """Resolve the gitdir that holds ``HEAD`` for the worktree at ``root``.
30
+
31
+ Normal clone: ``<root>/.git`` is a directory → that directory.
32
+ Linked worktree: ``<root>/.git`` is a file ``gitdir: <path>`` → ``<path>``
33
+ (resolved relative to ``root`` when the recorded path is relative).
34
+ Returns ``None`` when neither shape is readable.
35
+ """
36
+ dot_git = root / ".git"
37
+ if dot_git.is_dir():
38
+ return dot_git
39
+ if dot_git.is_file():
40
+ try:
41
+ content = dot_git.read_text(encoding="utf-8", errors="replace").strip()
42
+ except OSError:
43
+ return None
44
+ prefix = "gitdir:"
45
+ if content.startswith(prefix):
46
+ target = content[len(prefix) :].strip()
47
+ if target:
48
+ resolved = Path(target)
49
+ if not resolved.is_absolute():
50
+ resolved = (root / resolved).resolve()
51
+ return resolved
52
+ return None
53
+
54
+
55
+ def current_branch(start: Path) -> str | None:
56
+ """Return the current git branch name, or ``None``.
57
+
58
+ Reads ``HEAD`` from the resolved gitdir without spawning git:
59
+ - ``ref: refs/heads/<b>`` → ``<b>``
60
+ - any other ``ref: .../<x>`` → last path component ``<x>``
61
+ - raw SHA (detached HEAD) → first 12 chars
62
+ - unreadable / missing / empty → ``None``
63
+ """
64
+ root = worktree_root(start)
65
+ gitdir = _gitdir_for(root)
66
+ if gitdir is None:
67
+ return None
68
+ try:
69
+ content = (gitdir / "HEAD").read_text(encoding="utf-8", errors="replace").strip()
70
+ except OSError:
71
+ return None
72
+ if not content:
73
+ return None
74
+ if content.startswith("ref:"):
75
+ ref = content[len("ref:") :].strip()
76
+ marker = "refs/heads/"
77
+ if marker in ref:
78
+ return ref.split(marker, 1)[1] or None
79
+ return ref.rsplit("/", 1)[-1] or None
80
+ return content[:12]
@@ -30,6 +30,14 @@ class ReuseConflictError(PytestTestcontainersError):
30
30
  """
31
31
 
32
32
 
33
+ class NameTemplateError(PytestTestcontainersError):
34
+ """The configured container-name template referenced an unknown placeholder.
35
+
36
+ Raised at name-resolution time (during fixture/maker setup, before any
37
+ container starts). The message enumerates the valid placeholders.
38
+ """
39
+
40
+
33
41
  class CleanSessionFixtureError(PytestTestcontainersError):
34
42
  """A clean-session fixture failed to issue an admin command.
35
43
 
@@ -27,8 +27,8 @@ from pytest_testcontainers.containers import (
27
27
  start_or_raise,
28
28
  )
29
29
  from pytest_testcontainers.reuse import (
30
+ container_name_for,
30
31
  is_reuse_enabled,
31
- reuse_name_for,
32
32
  sanitize_component,
33
33
  )
34
34
 
@@ -76,7 +76,7 @@ def _make_generic(
76
76
  check_docker_daemon()
77
77
 
78
78
  reuse_on = is_reuse_enabled() or reuse_name is not None
79
- name = reuse_name_for(service_slug, override=reuse_name) if reuse_on else None
79
+ name = container_name_for(service_slug, override=reuse_name, reuse_mode=reuse_on)
80
80
 
81
81
  if reuse_on:
82
82
  disable_ryuk_once()
@@ -84,7 +84,7 @@ def _make_generic(
84
84
  instance: DockerContainer | None = None
85
85
  is_bound_to_existing = False
86
86
 
87
- if reuse_on and name is not None:
87
+ if reuse_on:
88
88
  existing = find_existing_container(name)
89
89
  if existing is not None:
90
90
  status = getattr(existing, "status", "")
@@ -106,8 +106,7 @@ def _make_generic(
106
106
 
107
107
  if instance is None:
108
108
  instance = container_cls(*constructor_args, **constructor_kwargs)
109
- if name is not None:
110
- instance.with_name(name)
109
+ instance.with_name(name)
111
110
  _apply_env(instance, env)
112
111
 
113
112
  image_for_msg = constructor_kwargs.get("image") or (
@@ -50,6 +50,16 @@ def pytest_addoption(parser: pytest.Parser) -> None:
50
50
  dest="testcontainers_project",
51
51
  help="Override project name used in reuse-name prefix.",
52
52
  )
53
+ group.addoption(
54
+ "--testcontainers-name-template",
55
+ action="store",
56
+ default=None,
57
+ dest="testcontainers_name_template",
58
+ help=(
59
+ "Template for container names. Placeholders: {project} {service} "
60
+ "{branch} {dir} {dirtag} {worker} {rand}."
61
+ ),
62
+ )
53
63
  group.addoption(
54
64
  "--testcontainers-clean",
55
65
  action="store_true",
@@ -65,6 +75,9 @@ def pytest_configure(config: pytest.Config) -> None:
65
75
  project = config.getoption("testcontainers_project")
66
76
  if project:
67
77
  reuse.set_cli_project(project)
78
+ name_template = config.getoption("testcontainers_name_template")
79
+ if name_template:
80
+ reuse.set_cli_name_template(name_template)
68
81
  if config.getoption("testcontainers_no_reuse"):
69
82
  reuse.set_cli_reuse(False)
70
83
  elif config.getoption("testcontainers_reuse"):
@@ -82,6 +95,11 @@ def pytest_cmdline_main(config: pytest.Config) -> int | None:
82
95
  except ImportError:
83
96
  sys.stderr.write("[pytest-testcontainers] docker-py not installed\n")
84
97
  return 1
98
+ # Honor the active `docker context` (OrbStack / colima / Docker Desktop)
99
+ # before from_env(), same as the launch path.
100
+ from pytest_testcontainers._internal.docker_health import ensure_docker_host_env
101
+
102
+ ensure_docker_host_env()
85
103
  try:
86
104
  client = docker.from_env()
87
105
  client.ping()