Skip to content

Commit 468211f

Browse files
Merge branch 'main' into main
2 parents 94564d5 + 73aeb43 commit 468211f

60 files changed

Lines changed: 2915 additions & 294 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
".": "4.14.1"
2+
".": "4.15.0-rc.1"
33
}

CHANGELOG.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,38 @@
11
# Changelog
22

3+
## [4.15.0-rc.1](https://github.com/testcontainers/testcontainers-python/compare/testcontainers-v4.14.2...testcontainers-v4.15.0-rc.1) (2026-04-07)
4+
5+
6+
### Features
7+
8+
* **compose:** add structured container inspect information ([#897](https://github.com/testcontainers/testcontainers-python/issues/897)) ([58459a1](https://github.com/testcontainers/testcontainers-python/commit/58459a13a1523c5dec8b21b0e16ae1afdce48156))
9+
* **core:** support SSH-based DOCKER_HOST ([#993](https://github.com/testcontainers/testcontainers-python/issues/993)) ([d48115d](https://github.com/testcontainers/testcontainers-python/commit/d48115def127644964d4d2b09a38e3f4492cc43c))
10+
* **generic:** Reintroducing the generic SQL module ([#892](https://github.com/testcontainers/testcontainers-python/issues/892)) ([2ca2321](https://github.com/testcontainers/testcontainers-python/commit/2ca2321ada12e09d491280c8ec855bf8511de7c2))
11+
* **keycloak:** support for relative path and management relative path ([#982](https://github.com/testcontainers/testcontainers-python/issues/982)) ([898faf6](https://github.com/testcontainers/testcontainers-python/commit/898faf6a5955698958be6e8cfd32b87323d62a44))
12+
* **mqtt:** MosquittoContainer: Add version 2.1.2 ([#978](https://github.com/testcontainers/testcontainers-python/issues/978)) ([af382f7](https://github.com/testcontainers/testcontainers-python/commit/af382f74e82bdcb14eac3f4e04a83432ae9beeba))
13+
14+
15+
### Bug Fixes
16+
17+
* **azurite:** make visible to type checkers ([#927](https://github.com/testcontainers/testcontainers-python/issues/927)) ([baa5668](https://github.com/testcontainers/testcontainers-python/commit/baa566814b22fa922094a625ff92037cbe8bd93f))
18+
* **clickhouse:** add `HttpWaitStrategy` instead of deprecated `wait_container_is_ready` ([#962](https://github.com/testcontainers/testcontainers-python/issues/962)) ([8034541](https://github.com/testcontainers/testcontainers-python/commit/803454147c03418b7b06601d251eb491a2cd79cf))
19+
* **compose:** return type in get_service_port docstring ([#939](https://github.com/testcontainers/testcontainers-python/issues/939)) ([fed65fe](https://github.com/testcontainers/testcontainers-python/commit/fed65fe14507020007c115c535364c90d4bbdde9))
20+
* **core:** Refactor copy file ([#996](https://github.com/testcontainers/testcontainers-python/issues/996)) ([0e0bb24](https://github.com/testcontainers/testcontainers-python/commit/0e0bb24a2bddfd8a03bebdfc3b9ff8cf8c78092b))
21+
* **core:** wait for ryuk more reliably, improve tests: long_running, filter logs ([#984](https://github.com/testcontainers/testcontainers-python/issues/984)) ([b12ae13](https://github.com/testcontainers/testcontainers-python/commit/b12ae13e589a4ffe326c162a38df56eb30521d69))
22+
* **generic:** Migrate ServerContainer from deprecated decorator to HttpWaitStrategy ([#971](https://github.com/testcontainers/testcontainers-python/issues/971)) ([460b0d8](https://github.com/testcontainers/testcontainers-python/commit/460b0d8a09635068815ea8c5c5a4e4cc1e3dfea7))
23+
* **kafka:** Use wait strategy instead of deprecated wait_for_logs ([#903](https://github.com/testcontainers/testcontainers-python/issues/903)) ([87332c1](https://github.com/testcontainers/testcontainers-python/commit/87332c1332a30b673aac919b48e296e21f2c1baf))
24+
* **postgres:** add py.typed marker to postgres module ([#849](https://github.com/testcontainers/testcontainers-python/issues/849)) ([c8a5bbd](https://github.com/testcontainers/testcontainers-python/commit/c8a5bbdbab137e6dc5af9a7224e65972665ec84d))
25+
* **qdrant:** migrate Qdrant from deprecated decorator. ([#963](https://github.com/testcontainers/testcontainers-python/issues/963)) ([407f798](https://github.com/testcontainers/testcontainers-python/commit/407f79825be97865010dc0119cdfe3498a609a08))
26+
* **redis:** Use wait strategy instead of deprecated decorator ([#914](https://github.com/testcontainers/testcontainers-python/issues/914)) ([e25713a](https://github.com/testcontainers/testcontainers-python/commit/e25713a300eda6a14973d2465590d2318dcc375d))
27+
* **sftp:** Avoid using wait_for_logs in module. ([#995](https://github.com/testcontainers/testcontainers-python/issues/995)) ([83157eb](https://github.com/testcontainers/testcontainers-python/commit/83157eb4acd931949cfec3d2a84db0a61685e739))
28+
29+
## [4.14.2](https://github.com/testcontainers/testcontainers-python/compare/testcontainers-v4.14.1...testcontainers-v4.14.2) (2026-03-18)
30+
31+
32+
### Features
33+
34+
* **kafka:** allow configurable listener name and security protocol ([#966](https://github.com/testcontainers/testcontainers-python/issues/966)) ([44dd40b](https://github.com/testcontainers/testcontainers-python/commit/44dd40b48c3a5020b487bae5d460124d9e594ac3))
35+
336
## [4.14.1](https://github.com/testcontainers/testcontainers-python/compare/testcontainers-v4.14.0...testcontainers-v4.14.1) (2026-01-31)
437

538

Makefile

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,13 @@ build: ## Build the python package
2020

2121
tests: ${TESTS} ## Run tests for each package
2222
${TESTS}: %/tests:
23-
uv run coverage run --parallel -m pytest -v $*/tests
23+
uv run coverage run --parallel -m pytest -v $*/tests
24+
25+
quick-core-tests: ## Run core tests excluding long_running
26+
uv run coverage run --parallel -m pytest -v -m "not long_running" core/tests
27+
28+
core-tests: ## Run tests for the core package
29+
uv run coverage run --parallel -m pytest -v core/tests
2430

2531
coverage: ## Target to combine and report coverage.
2632
uv run coverage combine
@@ -61,7 +67,7 @@ clean-all: clean ## Remove all generated files and reset the local virtual envir
6167
rm -rf .venv
6268

6369
# Targets that do not generate file-level artifacts.
64-
.PHONY: clean docs doctests image tests ${TESTS}
70+
.PHONY: clean docs doctests image tests quick-core-tests ${TESTS}
6571

6672

6773
# Implements this pattern for autodocumenting Makefiles:

conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,4 +168,5 @@
168168
nitpick_ignore = [
169169
("py:class", "typing_extensions.Self"),
170170
("py:class", "docker.models.containers.ExecResult"),
171+
("py:class", "testcontainers.core.docker_client.ContainerInspectInfo"),
171172
]

core/README.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ Testcontainers Core
1616

1717
.. autoclass:: testcontainers.core.wait_strategies.WaitStrategy
1818

19+
.. autoclass:: testcontainers.core.transferable.Transferable
20+
1921
.. raw:: html
2022

2123
<hr>
@@ -49,3 +51,20 @@ Using `DockerContainer` and `DockerImage` to create a container:
4951

5052
The `DockerImage` class is used to build the image from the specified path and tag.
5153
The `DockerContainer` class is then used to create a container from the image.
54+
55+
Copying a file from disk into a container:
56+
57+
.. doctest::
58+
59+
>>> import tempfile
60+
>>> from pathlib import Path
61+
>>> from testcontainers.core.container import DockerContainer
62+
63+
>>> with tempfile.TemporaryDirectory() as tmp:
64+
... my_file = Path(tmp) / "my_file.txt"
65+
... _ = my_file.write_text("file content")
66+
... with DockerContainer("bash", command="sleep infinity") as container:
67+
... container.copy_into_container(my_file, "/tmp/my_file.txt")
68+
... result = container.exec("cat /tmp/my_file.txt")
69+
... result.output
70+
b'file content'

core/testcontainers/compose/compose.py

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import sys
2-
from dataclasses import asdict, dataclass, field, fields, is_dataclass
2+
from dataclasses import asdict, dataclass, field
33
from functools import cached_property
44
from json import loads
55
from logging import getLogger, warning
@@ -11,28 +11,16 @@
1111
from types import TracebackType
1212
from typing import Any, Callable, Literal, Optional, TypeVar, Union, cast
1313

14+
from testcontainers.core.docker_client import DockerClient, get_docker_host_hostname
1415
from testcontainers.core.exceptions import ContainerIsNotRunning, NoSuchPortExposed
16+
from testcontainers.core.inspect import ContainerInspectInfo, _ignore_properties
1517
from testcontainers.core.waiting_utils import WaitStrategy
1618

17-
_IPT = TypeVar("_IPT")
1819
_WARNINGS = {"DOCKER_COMPOSE_GET_CONFIG": "get_config is experimental, see testcontainers/testcontainers-python#669"}
1920

2021
logger = getLogger(__name__)
2122

2223

23-
def _ignore_properties(cls: type[_IPT], dict_: Any) -> _IPT:
24-
"""omits extra fields like @JsonIgnoreProperties(ignoreUnknown = true)
25-
26-
https://gist.github.com/alexanderankin/2a4549ac03554a31bef6eaaf2eaf7fd5"""
27-
if isinstance(dict_, cls):
28-
return dict_
29-
if not is_dataclass(cls):
30-
raise TypeError(f"Expected a dataclass type, got {cls}")
31-
class_fields = {f.name for f in fields(cls)}
32-
filtered = {k: v for k, v in dict_.items() if k in class_fields}
33-
return cls(**filtered)
34-
35-
3624
@dataclass
3725
class PublishedPortModel:
3826
"""
@@ -45,10 +33,21 @@ class PublishedPortModel:
4533
Protocol: Optional[str] = None
4634

4735
def normalize(self) -> "PublishedPortModel":
48-
url_not_usable = system() == "Windows" and self.URL == "0.0.0.0"
49-
if url_not_usable:
36+
url = self.URL
37+
38+
# For SSH-based DOCKER_HOST, local addresses (0.0.0.0, 127.0.0.1, localhost, ::, ::1)
39+
# refer to the remote machine, not the local one.
40+
# Replace them with the actual remote hostname.
41+
ssh_host = get_docker_host_hostname()
42+
if ssh_host and url in ("0.0.0.0", "127.0.0.1", "localhost", "::", "::1"):
43+
url = ssh_host
44+
# On Windows, 0.0.0.0 is not usable — replace with 127.0.0.1
45+
elif system() == "Windows" and url == "0.0.0.0":
46+
url = "127.0.0.1"
47+
48+
if url != self.URL:
5049
self_dict = asdict(self)
51-
self_dict.update({"URL": "127.0.0.1"})
50+
self_dict.update({"URL": url})
5251
return PublishedPortModel(**self_dict)
5352
return self
5453

@@ -81,6 +80,7 @@ class ComposeContainer:
8180
ExitCode: Optional[int] = None
8281
Publishers: list[PublishedPortModel] = field(default_factory=list)
8382
_docker_compose: Optional["DockerCompose"] = field(default=None, init=False, repr=False)
83+
_cached_container_info: Optional[ContainerInspectInfo] = field(default=None, init=False, repr=False)
8484

8585
def __post_init__(self) -> None:
8686
if self.Publishers:
@@ -147,6 +147,28 @@ def reload(self) -> None:
147147
# each time through get_container(), but we need this method for compatibility
148148
pass
149149

150+
def get_container_info(self) -> Optional[ContainerInspectInfo]:
151+
"""Get container information via docker inspect (lazy loaded).
152+
153+
Returns:
154+
Container inspect information or None if container is not started.
155+
"""
156+
if self._cached_container_info is not None:
157+
return self._cached_container_info
158+
159+
if not self._docker_compose or not self.ID:
160+
return None
161+
162+
try:
163+
docker_client = self._docker_compose._get_docker_client()
164+
self._cached_container_info = docker_client.get_container_inspect_info(self.ID)
165+
166+
except Exception as e:
167+
logger.warning(f"Failed to get container info for {self.ID}: {e}")
168+
self._cached_container_info = None
169+
170+
return self._cached_container_info
171+
150172
@property
151173
def status(self) -> str:
152174
"""Get container status for compatibility with wait strategies."""
@@ -221,6 +243,7 @@ class DockerCompose:
221243
quiet_pull: bool = False
222244
quiet_build: bool = False
223245
_wait_strategies: Optional[dict[str, Any]] = field(default=None, init=False, repr=False)
246+
_docker_client: Optional[DockerClient] = field(default=None, init=False, repr=False)
224247

225248
def __post_init__(self) -> None:
226249
if isinstance(self.compose_file_name, str):
@@ -494,7 +517,7 @@ def get_service_port(
494517
495518
Returns
496519
-------
497-
str:
520+
int:
498521
The mapped port on the host
499522
"""
500523
normalize: PublishedPortModel = self.get_container(service_name).get_publisher(by_port=port).normalize()
@@ -585,3 +608,9 @@ def wait_for(self, url: str) -> "DockerCompose":
585608
with urlopen(url) as response:
586609
response.read()
587610
return self
611+
612+
def _get_docker_client(self) -> DockerClient:
613+
"""Get Docker client instance."""
614+
if self._docker_client is None:
615+
self._docker_client = DockerClient()
616+
return self._docker_client

core/testcontainers/core/config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ def read_tc_properties() -> dict[str, str]:
8888

8989
@dataclass
9090
class TestcontainersConfiguration:
91+
__test__ = False
92+
9193
def _render_bool(self, env_name: str, prop_name: str) -> bool:
9294
env_val = environ.get(env_name, None)
9395
if env_val is not None:

0 commit comments

Comments
 (0)