Skip to content

Commit 6c10870

Browse files
authored
Merge branch 'main' into ports_refactor
2 parents 83950fa + 8d77bd3 commit 6c10870

File tree

15 files changed

+172
-26
lines changed

15 files changed

+172
-26
lines changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
".": "4.8.2"
2+
".": "4.9.0"
33
}

.github/CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ You need to have the following tools available to you:
3939

4040
## Adding new containers
4141

42-
We have an [issue template](.github/ISSUE_TEMPLATE/new-container.md) for adding new containers, please refer to that for more information.
42+
We have an [issue template](./ISSUE_TEMPLATE/new-container.md) for adding new containers, please refer to that for more information.
4343
Once you've talked to the maintainers (we do our best to reply!) then you can proceed with contributing the new container.
4444

4545
> [!WARNING]

CHANGELOG.md

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

3+
## [4.9.0](https://github.com/testcontainers/testcontainers-python/compare/testcontainers-v4.8.2...testcontainers-v4.9.0) (2024-11-26)
4+
5+
6+
### Features
7+
8+
* **compose:** support for setting profiles ([#738](https://github.com/testcontainers/testcontainers-python/issues/738)) ([3e00e71](https://github.com/testcontainers/testcontainers-python/commit/3e00e71da4d2b5e7fd30315468d4e54c86ba6150))
9+
* **core:** Support working with env files ([#737](https://github.com/testcontainers/testcontainers-python/issues/737)) ([932ee30](https://github.com/testcontainers/testcontainers-python/commit/932ee307955e3591a63f194aee8e2f6d8e2f6bf9))
10+
11+
12+
### Bug Fixes
13+
14+
* allow running all tests ([#721](https://github.com/testcontainers/testcontainers-python/issues/721)) ([f958cf9](https://github.com/testcontainers/testcontainers-python/commit/f958cf9fe62a5f3ee2dc255713ec8b16de6a767d))
15+
* **core:** Avoid hanging upon bad docker host connection ([#742](https://github.com/testcontainers/testcontainers-python/issues/742)) ([4ced198](https://github.com/testcontainers/testcontainers-python/commit/4ced1983162914fe511a6e714f136b670e1dbdfb))
16+
* **core:** running testcontainer inside container ([#714](https://github.com/testcontainers/testcontainers-python/issues/714)) ([85a6666](https://github.com/testcontainers/testcontainers-python/commit/85a66667c23d76e87aecc6761bbb01429adb3dee))
17+
* **generic:** Also catch URLError waiting for ServerContainer ([#743](https://github.com/testcontainers/testcontainers-python/issues/743)) ([24e354f](https://github.com/testcontainers/testcontainers-python/commit/24e354f3bfa5912eaf7877da9442a885d7872f1a))
18+
* update wait_for_logs to not throw on 'created', and an optimization ([#719](https://github.com/testcontainers/testcontainers-python/issues/719)) ([271ca9a](https://github.com/testcontainers/testcontainers-python/commit/271ca9a0fef2e5f2b216457bfee44318e93990bf))
19+
* Vault health check ([#734](https://github.com/testcontainers/testcontainers-python/issues/734)) ([79434d6](https://github.com/testcontainers/testcontainers-python/commit/79434d6744b2918493884cf8fbf27aeadf78ecfd))
20+
21+
22+
### Documentation
23+
24+
* Documentation fix for ServerContainer ([#671](https://github.com/testcontainers/testcontainers-python/issues/671)) ([0303d47](https://github.com/testcontainers/testcontainers-python/commit/0303d47d7173e1c4ec1a4f565efee9b2fe694928))
25+
326
## [4.8.2](https://github.com/testcontainers/testcontainers-python/compare/testcontainers-v4.8.1...testcontainers-v4.8.2) (2024-09-27)
427

528

core/testcontainers/compose/compose.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ class DockerCompose:
171171
env_file: Optional[str] = None
172172
services: Optional[list[str]] = None
173173
docker_command_path: Optional[str] = None
174+
profiles: Optional[list[str]] = None
174175

175176
def __post_init__(self):
176177
if isinstance(self.compose_file_name, str):
@@ -198,6 +199,8 @@ def compose_command_property(self) -> list[str]:
198199
if self.compose_file_name:
199200
for file in self.compose_file_name:
200201
docker_compose_cmd += ["-f", file]
202+
if self.profiles:
203+
docker_compose_cmd += [item for profile in self.profiles for item in ["--profile", profile]]
201204
if self.env_file:
202205
docker_compose_cmd += ["--env-file", self.env_file]
203206
return docker_compose_cmd

core/testcontainers/core/container.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import contextlib
2+
from os import PathLike
23
from socket import socket
34
from typing import TYPE_CHECKING, Optional, Union
45

56
import docker.errors
67
from docker import version
78
from docker.types import EndpointConfig
9+
from dotenv import dotenv_values
810
from typing_extensions import Self, assert_never
911

1012
from testcontainers.core.config import ConnectionMode
1113
from testcontainers.core.config import testcontainers_config as c
1214
from testcontainers.core.docker_client import DockerClient
13-
from testcontainers.core.exceptions import ContainerStartException
15+
from testcontainers.core.exceptions import ContainerConnectException, ContainerStartException
1416
from testcontainers.core.labels import LABEL_SESSION_ID, SESSION_ID
1517
from testcontainers.core.network import Network
1618
from testcontainers.core.utils import is_arm, setup_logger
@@ -56,6 +58,12 @@ def __init__(
5658
def with_env(self, key: str, value: str) -> Self:
5759
self.env[key] = value
5860
return self
61+
62+
def with_env_file(self, env_file: Union[str, PathLike]) -> Self:
63+
env_values = dotenv_values(env_file)
64+
for key, value in env_values.items():
65+
self.with_env(key, value)
66+
return self
5967

6068
def with_bind_ports(self, container: Union[str, int], host: Optional[Union[str, int]] = None) -> Self:
6169
"""
@@ -72,7 +80,6 @@ def with_bind_ports(self, container: Union[str, int], host: Optional[Union[str,
7280
>>> container = container.with_bind_ports("8081/tcp", 8081)
7381
7482
"""
75-
7683
self.ports[container] = host
7784
return self
7885

@@ -248,15 +255,21 @@ def _create_instance(cls) -> "Reaper":
248255
.with_env("RYUK_RECONNECTION_TIMEOUT", c.ryuk_reconnection_timeout)
249256
.start()
250257
)
251-
wait_for_logs(Reaper._container, r".* Started!")
258+
wait_for_logs(Reaper._container, r".* Started!", timeout=20, raise_on_exit=True)
252259

253260
container_host = Reaper._container.get_container_host_ip()
254261
container_port = int(Reaper._container.get_exposed_port(8080))
255262

263+
if not container_host or not container_port:
264+
raise ContainerConnectException(
265+
f"Could not obtain network details for {Reaper._container._container.id}. Host: {container_host} Port: {container_port}"
266+
)
267+
256268
last_connection_exception: Optional[Exception] = None
257269
for _ in range(50):
258270
try:
259271
Reaper._socket = socket()
272+
Reaper._socket.settimeout(1)
260273
Reaper._socket.connect((container_host, container_port))
261274
last_connection_exception = None
262275
break

core/testcontainers/core/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ class ContainerStartException(RuntimeError):
1616
pass
1717

1818

19+
class ContainerConnectException(RuntimeError):
20+
pass
21+
22+
1923
class ContainerIsNotRunning(RuntimeError):
2024
pass
2125

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
services:
2+
runs-always: &simple-service
3+
image: alpine:latest
4+
init: true
5+
command:
6+
- sh
7+
- -c
8+
- 'while true; do sleep 0.1 ; date -Ins; done'
9+
runs-profile-a:
10+
<<: *simple-service
11+
profiles:
12+
- profile-a
13+
runs-profile-b:
14+
<<: *simple-service
15+
profiles:
16+
- profile-b

core/tests/test_compose.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from pathlib import Path
33
from re import split
44
from time import sleep
5-
from typing import Union
5+
from typing import Union, Optional
66
from urllib.request import urlopen, Request
77

88
import pytest
@@ -352,3 +352,27 @@ def fetch(req: Union[Request, str]):
352352
if 200 < res.getcode() >= 400:
353353
raise Exception(f"HTTP Error: {res.getcode()} - {res.reason}: {body}")
354354
return res.getcode(), body
355+
356+
357+
@pytest.mark.parametrize(
358+
argnames=["profiles", "running", "not_running"],
359+
argvalues=[
360+
pytest.param(None, ["runs-always"], ["runs-profile-a", "runs-profile-b"], id="default"),
361+
pytest.param(
362+
["profile-a"], ["runs-always", "runs-profile-a"], ["runs-profile-b"], id="one-additional-profile-via-str"
363+
),
364+
pytest.param(
365+
["profile-a", "profile-b"],
366+
["runs-always", "runs-profile-a", "runs-profile-b"],
367+
[],
368+
id="all-profiles-explicitly",
369+
),
370+
],
371+
)
372+
def test_compose_profile_support(profiles: Optional[list[str]], running: list[str], not_running: list[str]):
373+
with DockerCompose(context=FIXTURES / "profile_support", profiles=profiles) as compose:
374+
for service in running:
375+
assert compose.get_container(service) is not None
376+
for service in not_running:
377+
with pytest.raises(ContainerIsNotRunning):
378+
compose.get_container(service)

core/tests/test_core.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import tempfile
2+
from pathlib import Path
3+
14
from testcontainers.core.container import DockerContainer
25

36

@@ -17,3 +20,29 @@ def test_get_logs():
1720
assert isinstance(stdout, bytes)
1821
assert isinstance(stderr, bytes)
1922
assert "Hello from Docker".encode() in stdout, "There should be something on stdout"
23+
24+
25+
def test_docker_container_with_env_file():
26+
"""Test that environment variables can be loaded from a file"""
27+
with tempfile.TemporaryDirectory() as temp_directory:
28+
env_file_path = Path(temp_directory) / "env_file"
29+
with open(env_file_path, "w") as f:
30+
f.write(
31+
"""
32+
TEST_ENV_VAR=hello
33+
NUMBER=123
34+
DOMAIN=example.org
35+
ADMIN_EMAIL=admin@${DOMAIN}
36+
ROOT_URL=${DOMAIN}/app
37+
"""
38+
)
39+
container = DockerContainer("alpine").with_command("tail -f /dev/null") # Keep the container running
40+
container.with_env_file(env_file_path) # Load the environment variables from the file
41+
with container:
42+
output = container.exec("env").output.decode("utf-8").strip()
43+
assert "TEST_ENV_VAR=hello" in output
44+
assert "NUMBER=123" in output
45+
assert "DOMAIN=example.org" in output
46+
assert "[email protected]" in output
47+
assert "ROOT_URL=example.org/app" in output
48+
print(output)

modules/generic/testcontainers/generic/server.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from typing import Union
2-
from urllib.error import HTTPError
2+
from urllib.error import HTTPError, URLError
33
from urllib.request import urlopen
44

55
import httpx
@@ -31,16 +31,16 @@ class ServerContainer(DockerContainer):
3131
... delay = wait_for_logs(srv, "GET / HTTP/1.1")
3232
3333
34-
:param path: Path to the Dockerfile to build the image
35-
:param tag: Tag for the image to be built (default: None)
34+
:param port: Port to be exposed on the container.
35+
:param image: Docker image to be used for the container.
3636
"""
3737

3838
def __init__(self, port: int, image: Union[str, DockerImage]) -> None:
3939
super().__init__(str(image))
4040
self.internal_port = port
4141
self.with_exposed_ports(self.internal_port)
4242

43-
@wait_container_is_ready(HTTPError)
43+
@wait_container_is_ready(HTTPError, URLError)
4444
def _connect(self) -> None:
4545
# noinspection HttpUrlsUsage
4646
url = self._create_connection_url()

0 commit comments

Comments
 (0)