Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: testcontainers/testcontainers-python
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: testcontainers-v4.3.1
Choose a base ref
...
head repository: testcontainers/testcontainers-python
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: testcontainers-v4.3.2
Choose a head ref
  • 5 commits
  • 28 files changed
  • 6 contributors

Commits on Apr 3, 2024

  1. fix(core): Improve typing for common container usage scenarios (#523)

    Improves type hints for type checking in a common use cases:
    
    ```python
    with MySqlContainer("mysql:8").with_env("some", "value") as mysql:
        url = mysql.get_connection_url()  # get_connection_url would previously be an unknown member here
    ```
    And, also improved type hinting for the custom `DockerClient`'s `run`
    command, where the linter no longer reports an error due to missing
    parameter types:
    ```python
    DockerClient.run("nginx")  # Previously this would report "Argument missing for parameter "image"
    ```
    santi authored Apr 3, 2024

    Unverified

    This user has not yet uploaded their public signing key.
    Copy the full SHA
    d5b8553 View commit details

Commits on Apr 5, 2024

  1. chore(codestyle): switch to ruff from black for code formatting (#…

    …529)
    
    Changed pre-commit config. Some files became re-formatted.
    max-pfeiffer authored Apr 5, 2024

    Unverified

    This user has not yet uploaded their public signing key.
    Copy the full SHA
    9a89748 View commit details
  2. fix(vault): add support for HashiCorp Vault container (#366)

    Add support for a Vault container.
    f4z3r authored Apr 5, 2024
    Copy the full SHA
    1326278 View commit details

Commits on Apr 8, 2024

  1. fix(core): make config editable to avoid monkeypatching.1 (#532)

    see #531:
    
    I am using testcontainers within a library that provides some
    pytest-fixtures.
    In order for this to work I have change some settings.
    
    As I can not guarantee that that my lib is imported before
    testcontainers I need to monkeypatch the settings.
    This is much easier if I only need to monkeypatch the config file and
    not all modules that use configurations.
    
    I would argue that for a potential library as this, this is a better
    design.
    
    Also one can easier see that the given UPERCASE variable is not a
    constant but rather a setting.
    
    Co-authored-by: Carli* Freudenberg <carli.freudenberg@energymeteo.de>
    alexanderankin and CarliJoy authored Apr 8, 2024
    Copy the full SHA
    3be6da3 View commit details
  2. chore(main): release testcontainers 4.3.2 (#530)

    🤖 I have created a release *beep* *boop*
    ---
    
    
    ##
    [4.3.2](testcontainers-v4.3.1...testcontainers-v4.3.2)
    (2024-04-08)
    
    
    ### Bug Fixes
    
    * **core:** Improve typing for common container usage scenarios
    ([#523](#523))
    ([d5b8553](d5b8553))
    * **core:** make config editable to avoid monkeypatching.1
    ([#532](#532))
    ([3be6da3](3be6da3))
    * **vault:** add support for HashiCorp Vault container
    ([#366](#366))
    ([1326278](1326278))
    
    ---
    This PR was generated with [Release
    Please](https://github.com/googleapis/release-please). See
    [documentation](https://github.com/googleapis/release-please#release-please).
    
    Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
    github-actions[bot] authored Apr 8, 2024
    Copy the full SHA
    fe22758 View commit details
2 changes: 1 addition & 1 deletion .github/.release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "4.3.1"
".": "4.3.2"
}
10 changes: 3 additions & 7 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -9,18 +9,14 @@ repos:
- id: trailing-whitespace
- id: end-of-file-fixer

- repo: https://github.com/psf/black-pre-commit-mirror
rev: '24.2.0'
hooks:
- id: black
args: [ '--config', 'pyproject.toml' ]

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: 'v0.3.0'
rev: 'v0.3.5'
hooks:
- id: ruff
# Explicitly setting config to prevent Ruff from using `pyproject.toml` in sub packages.
args: [ '--fix', '--exit-non-zero-on-fix', '--config', 'pyproject.toml' ]
- id: ruff-format
args: [ '--config', 'pyproject.toml' ]

# - repo: local
# hooks:
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## [4.3.2](https://github.com/testcontainers/testcontainers-python/compare/testcontainers-v4.3.1...testcontainers-v4.3.2) (2024-04-08)


### Bug Fixes

* **core:** Improve typing for common container usage scenarios ([#523](https://github.com/testcontainers/testcontainers-python/issues/523)) ([d5b8553](https://github.com/testcontainers/testcontainers-python/commit/d5b855323be06f8d1395dd480a347f0efef75703))
* **core:** make config editable to avoid monkeypatching.1 ([#532](https://github.com/testcontainers/testcontainers-python/issues/532)) ([3be6da3](https://github.com/testcontainers/testcontainers-python/commit/3be6da335ba2026b4800dfd6a19cda4ca8e52be8))
* **vault:** add support for HashiCorp Vault container ([#366](https://github.com/testcontainers/testcontainers-python/issues/366)) ([1326278](https://github.com/testcontainers/testcontainers-python/commit/13262785dedf32a97e392afc1a758616995dc9d9))

## [4.3.1](https://github.com/testcontainers/testcontainers-python/compare/testcontainers-v4.3.0...testcontainers-v4.3.1) (2024-04-02)


61 changes: 61 additions & 0 deletions core/testcontainers/core/config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from dataclasses import dataclass, field
from os import environ
from os.path import exists
from pathlib import Path

MAX_TRIES = int(environ.get("TC_MAX_TRIES", 120))
SLEEP_TIME = int(environ.get("TC_POOLING_INTERVAL", 1))
@@ -9,3 +12,61 @@
RYUK_DISABLED: bool = environ.get("TESTCONTAINERS_RYUK_DISABLED", "false") == "true"
RYUK_DOCKER_SOCKET: str = environ.get("TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE", "/var/run/docker.sock")
RYUK_RECONNECTION_TIMEOUT: str = environ.get("RYUK_RECONNECTION_TIMEOUT", "10s")

TC_FILE = ".testcontainers.properties"
TC_GLOBAL = Path.home() / TC_FILE


def read_tc_properties() -> dict[str, str]:
"""
Read the .testcontainers.properties for settings. (see the Java implementation for details)
Currently we only support the ~/.testcontainers.properties but may extend to per-project variables later.
:return: the merged properties from the sources.
"""
tc_files = [item for item in [TC_GLOBAL] if exists(item)]
if not tc_files:
return {}
settings = {}

for file in tc_files:
with open(file) as contents:
tuples = [line.split("=") for line in contents.readlines() if "=" in line]
settings = {**settings, **{item[0].strip(): item[1].strip() for item in tuples}}
return settings


@dataclass
class TestcontainersConfiguration:
max_tries: int = MAX_TRIES
sleep_time: int = SLEEP_TIME
ryuk_image: str = RYUK_IMAGE
ryuk_privileged: bool = RYUK_PRIVILEGED
ryuk_disabled: bool = RYUK_DISABLED
ryuk_docker_socket: str = RYUK_DOCKER_SOCKET
ryuk_reconnection_timeout: str = RYUK_RECONNECTION_TIMEOUT
tc_properties: dict[str, str] = field(default_factory=read_tc_properties)

def tc_properties_get_tc_host(self):
return self.tc_properties.get("tc.host")

@property
def timeout(self):
return self.max_tries * self.sleep_time


testcontainers_config = TestcontainersConfiguration()

__all__ = [
# the public API of this module
"testcontainers_config",
# and all the legacy things that are deprecated:
"MAX_TRIES",
"SLEEP_TIME",
"TIMEOUT",
"RYUK_IMAGE",
"RYUK_PRIVILEGED",
"RYUK_DISABLED",
"RYUK_DOCKER_SOCKET",
"RYUK_RECONNECTION_TIMEOUT",
]
42 changes: 19 additions & 23 deletions core/testcontainers/core/container.py
Original file line number Diff line number Diff line change
@@ -4,14 +4,9 @@
from typing import TYPE_CHECKING, Optional

import docker.errors
from typing_extensions import Self

from testcontainers.core.config import (
RYUK_DISABLED,
RYUK_DOCKER_SOCKET,
RYUK_IMAGE,
RYUK_PRIVILEGED,
RYUK_RECONNECTION_TIMEOUT,
)
from testcontainers.core.config import testcontainers_config as c
from testcontainers.core.docker_client import DockerClient
from testcontainers.core.exceptions import ContainerStartException
from testcontainers.core.labels import LABEL_SESSION_ID, SESSION_ID
@@ -53,30 +48,30 @@ def __init__(
self._name = None
self._kwargs = kwargs

def with_env(self, key: str, value: str) -> "DockerContainer":
def with_env(self, key: str, value: str) -> Self:
self.env[key] = value
return self

def with_bind_ports(self, container: int, host: Optional[int] = None) -> "DockerContainer":
def with_bind_ports(self, container: int, host: Optional[int] = None) -> Self:
self.ports[container] = host
return self

def with_exposed_ports(self, *ports: int) -> "DockerContainer":
def with_exposed_ports(self, *ports: int) -> Self:
for port in ports:
self.ports[port] = None
return self

def with_kwargs(self, **kwargs) -> "DockerContainer":
def with_kwargs(self, **kwargs) -> Self:
self._kwargs = kwargs
return self

def maybe_emulate_amd64(self) -> "DockerContainer":
def maybe_emulate_amd64(self) -> Self:
if is_arm():
return self.with_kwargs(platform="linux/amd64")
return self

def start(self):
if not RYUK_DISABLED and self.image != RYUK_IMAGE:
def start(self) -> Self:
if not c.ryuk_disabled and self.image != c.ryuk_image:
logger.debug("Creating Ryuk container")
Reaper.get_instance()
logger.info("Pulling image %s", self.image)
@@ -95,10 +90,11 @@ def start(self):
return self

def stop(self, force=True, delete_volume=True) -> None:
self._container.remove(force=force, v=delete_volume)
if self._container:
self._container.remove(force=force, v=delete_volume)
self.get_docker_client().client.close()

def __enter__(self):
def __enter__(self) -> Self:
return self.start()

def __exit__(self, exc_type, exc_val, exc_tb) -> None:
@@ -138,15 +134,15 @@ def get_exposed_port(self, port: int) -> str:
return port
return mapped_port

def with_command(self, command: str) -> "DockerContainer":
def with_command(self, command: str) -> Self:
self._command = command
return self

def with_name(self, name: str) -> "DockerContainer":
def with_name(self, name: str) -> Self:
self._name = name
return self

def with_volume_mapping(self, host: str, container: str, mode: str = "ro") -> "DockerContainer":
def with_volume_mapping(self, host: str, container: str, mode: str = "ro") -> Self:
mapping = {"bind": container, "mode": mode}
self.volumes[host] = mapping
return self
@@ -199,12 +195,12 @@ def _create_instance(cls) -> "Reaper":
logger.debug(f"Creating new Reaper for session: {SESSION_ID}")

Reaper._container = (
DockerContainer(RYUK_IMAGE)
DockerContainer(c.ryuk_image)
.with_name(f"testcontainers-ryuk-{SESSION_ID}")
.with_exposed_ports(8080)
.with_volume_mapping(RYUK_DOCKER_SOCKET, "/var/run/docker.sock", "rw")
.with_kwargs(privileged=RYUK_PRIVILEGED, auto_remove=True)
.with_env("RYUK_RECONNECTION_TIMEOUT", RYUK_RECONNECTION_TIMEOUT)
.with_volume_mapping(c.ryuk_docker_socket, "/var/run/docker.sock", "rw")
.with_kwargs(privileged=c.ryuk_privileged, auto_remove=True)
.with_env("RYUK_RECONNECTION_TIMEOUT", c.ryuk_reconnection_timeout)
.start()
)
wait_for_logs(Reaper._container, r".* Started!")
44 changes: 16 additions & 28 deletions core/testcontainers/core/docker_client.py
Original file line number Diff line number Diff line change
@@ -16,19 +16,28 @@
import os
import urllib
import urllib.parse
from os.path import exists
from pathlib import Path
from typing import Optional, Union
from typing import Callable, Optional, TypeVar, Union

import docker
from docker.models.containers import Container, ContainerCollection
from typing_extensions import ParamSpec

from testcontainers.core.config import testcontainers_config as c
from testcontainers.core.labels import SESSION_ID, create_labels
from testcontainers.core.utils import default_gateway_ip, inside_container, setup_logger

LOGGER = setup_logger(__name__)
TC_FILE = ".testcontainers.properties"
TC_GLOBAL = Path.home() / TC_FILE

_P = ParamSpec("_P")
_T = TypeVar("_T")


def _wrapped_container_collection(function: Callable[_P, _T]) -> Callable[_P, _T]:
@ft.wraps(ContainerCollection.run)
def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _T:
return function(*args, **kwargs)

return wrapper


class DockerClient:
@@ -48,7 +57,7 @@ def __init__(self, **kwargs) -> None:
self.client.api.headers["x-tc-sid"] = SESSION_ID
self.client.api.headers["User-Agent"] = "tc-python/" + importlib.metadata.version("testcontainers")

@ft.wraps(ContainerCollection.run)
@_wrapped_container_collection
def run(
self,
image: str,
@@ -173,26 +182,5 @@ def host(self) -> str:
return "localhost"


@ft.cache
def read_tc_properties() -> dict[str, str]:
"""
Read the .testcontainers.properties for settings. (see the Java implementation for details)
Currently we only support the ~/.testcontainers.properties but may extend to per-project variables later.
:return: the merged properties from the sources.
"""
tc_files = [item for item in [TC_GLOBAL] if exists(item)]
if not tc_files:
return {}
settings = {}

for file in tc_files:
tuples = []
with open(file) as contents:
tuples = [line.split("=") for line in contents.readlines() if "=" in line]
settings = {**settings, **{item[0].strip(): item[1].strip() for item in tuples}}
return settings


def get_docker_host() -> Optional[str]:
return read_tc_properties().get("tc.host") or os.getenv("DOCKER_HOST")
return c.tc_properties_get_tc_host() or os.getenv("DOCKER_HOST")
4 changes: 2 additions & 2 deletions core/testcontainers/core/labels.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from typing import Optional
from uuid import uuid4

from testcontainers.core.config import RYUK_IMAGE
from testcontainers.core.config import testcontainers_config as c

SESSION_ID: str = str(uuid4())
LABEL_SESSION_ID = "org.testcontainers.session-id"
@@ -13,7 +13,7 @@ def create_labels(image: str, labels: Optional[dict[str, str]]) -> dict[str, str
labels = {}
labels[LABEL_LANG] = "python"

if image == RYUK_IMAGE:
if image == c.ryuk_image:
return labels

labels[LABEL_SESSION_ID] = SESSION_ID
10 changes: 5 additions & 5 deletions core/testcontainers/core/waiting_utils.py
Original file line number Diff line number Diff line change
@@ -19,7 +19,7 @@

import wrapt

from testcontainers.core import config
from testcontainers.core.config import testcontainers_config as config
from testcontainers.core.utils import setup_logger

if TYPE_CHECKING:
@@ -54,18 +54,18 @@ def wrapper(wrapped: Callable, instance: Any, args: list, kwargs: dict) -> Any:
logger.info("Waiting for %s to be ready ...", instance)

exception = None
for attempt_no in range(config.MAX_TRIES):
for attempt_no in range(config.max_tries):
try:
return wrapped(*args, **kwargs)
except transient_exceptions as e:
logger.debug(
f"Connection attempt '{attempt_no + 1}' of '{config.MAX_TRIES + 1}' "
f"Connection attempt '{attempt_no + 1}' of '{config.max_tries + 1}' "
f"failed: {traceback.format_exc()}"
)
time.sleep(config.SLEEP_TIME)
time.sleep(config.sleep_time)
exception = e
raise TimeoutError(
f"Wait time ({config.TIMEOUT}s) exceeded for {wrapped.__name__}(args: {args}, kwargs: "
f"Wait time ({config.timeout}s) exceeded for {wrapped.__name__}(args: {args}, kwargs: "
f"{kwargs}). Exception: {exception}"
)

6 changes: 3 additions & 3 deletions core/tests/test_ryuk.py
Original file line number Diff line number Diff line change
@@ -5,15 +5,15 @@
from docker import DockerClient
from docker.errors import NotFound

from testcontainers.core import container as container_module
from testcontainers.core.config import testcontainers_config
from testcontainers.core.container import Reaper
from testcontainers.core.container import DockerContainer
from testcontainers.core.waiting_utils import wait_for_logs


def test_wait_for_reaper(monkeypatch: MonkeyPatch):
Reaper.delete_instance()
monkeypatch.setattr(container_module, "RYUK_RECONNECTION_TIMEOUT", "0.1s")
monkeypatch.setattr(testcontainers_config, "ryuk_reconnection_timeout", "0.1s")
docker_client = DockerClient()
container = DockerContainer("hello-world").start()

@@ -40,7 +40,7 @@ def test_wait_for_reaper(monkeypatch: MonkeyPatch):

def test_container_without_ryuk(monkeypatch: MonkeyPatch):
Reaper.delete_instance()
monkeypatch.setattr(container_module, "RYUK_DISABLED", True)
monkeypatch.setattr(testcontainers_config, "ryuk_disabled", True)
with DockerContainer("hello-world") as container:
wait_for_logs(container, "Hello from Docker!")
assert Reaper._instance is None
1 change: 1 addition & 0 deletions index.rst
Original file line number Diff line number Diff line change
@@ -42,6 +42,7 @@ testcontainers-python facilitates the use of Docker containers for functional an
modules/redis/README
modules/registry/README
modules/selenium/README
modules/vault/README
modules/weaviate/README

Getting Started
4 changes: 2 additions & 2 deletions modules/arangodb/testcontainers/arangodb/__init__.py
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@
import typing
from os import environ

from testcontainers.core.config import TIMEOUT
from testcontainers.core.config import testcontainers_config as c
from testcontainers.core.generic import DbContainer
from testcontainers.core.utils import raise_for_deprecated_parameter
from testcontainers.core.waiting_utils import wait_for_logs
@@ -90,4 +90,4 @@ def get_connection_url(self) -> str:
return f"http://{self.get_container_host_ip()}:{port}"

def _connect(self) -> None:
wait_for_logs(self, predicate="is ready for business", timeout=TIMEOUT)
wait_for_logs(self, predicate="is ready for business", timeout=c.timeout)
1 change: 1 addition & 0 deletions modules/influxdb/testcontainers/influxdb.py
Original file line number Diff line number Diff line change
@@ -26,6 +26,7 @@
- because the InfluxDB clients are different for 1.x and 2.x versions,
so you won't have to install dependencies that you do not need
"""

from typing import Optional

from requests import get
Loading