Skip to content

Commit d4754e5

Browse files
authored
Merge branch 'main' into with_env_file
2 parents a5a6d5e + 8f1165d commit d4754e5

File tree

17 files changed

+289
-97
lines changed

17 files changed

+289
-97
lines changed

.github/.release-please-manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
".": "4.8.0"
2+
".": "4.8.1"
33
}

.github/workflows/ci-core.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ jobs:
3333
uses: actions/upload-artifact@v4
3434
with:
3535
name: "coverage-artifact-${{ matrix.python-version}}"
36+
include-hidden-files: true
3637
path: ".coverage.*"
3738
retention-days: 1
3839
- name: Run doctests

CHANGELOG.md

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

3+
## [4.8.1](https://github.com/testcontainers/testcontainers-python/compare/testcontainers-v4.8.0...testcontainers-v4.8.1) (2024-08-18)
4+
5+
6+
### Bug Fixes
7+
8+
* **generic:** Update the FastAPI install on genric module doctest samples ([#686](https://github.com/testcontainers/testcontainers-python/issues/686)) ([5216b02](https://github.com/testcontainers/testcontainers-python/commit/5216b0241a27afe3419f5c4a6d500dc27154ddd4))
9+
* **mssql:** use glob to find mssql-tools folder since it moves ([#685](https://github.com/testcontainers/testcontainers-python/issues/685)) ([4912725](https://github.com/testcontainers/testcontainers-python/commit/4912725c2a54a9edce046416fbf11e089cc03cb0)), closes [#666](https://github.com/testcontainers/testcontainers-python/issues/666)
10+
* wait_for_logs can now fail early when the container stops ([#682](https://github.com/testcontainers/testcontainers-python/issues/682)) ([925329d](https://github.com/testcontainers/testcontainers-python/commit/925329d8d2df78437a491a29b707d5ac97e7b734))
11+
12+
13+
### Documentation
14+
15+
* Add a more advance usecase documentation for ServerContainer ([#688](https://github.com/testcontainers/testcontainers-python/issues/688)) ([2cf5a9f](https://github.com/testcontainers/testcontainers-python/commit/2cf5a9fbe6db3fa4254a5bb54e67412ec2d08488))
16+
317
## [4.8.0](https://github.com/testcontainers/testcontainers-python/compare/testcontainers-v4.7.2...testcontainers-v4.8.0) (2024-08-14)
418

519

core/testcontainers/core/auth.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import json as json
33
from collections import namedtuple
44
from logging import warning
5-
from typing import Optional
5+
from typing import Any, Optional
66

77
DockerAuthInfo = namedtuple("DockerAuthInfo", ["registry", "username", "password"])
88

@@ -12,7 +12,7 @@
1212
}
1313

1414

15-
def process_docker_auth_config_encoded(auth_config_dict: dict) -> list[DockerAuthInfo]:
15+
def process_docker_auth_config_encoded(auth_config_dict: dict[str, dict[str, dict[str, Any]]]) -> list[DockerAuthInfo]:
1616
"""
1717
Process the auths config.
1818
@@ -30,16 +30,19 @@ def process_docker_auth_config_encoded(auth_config_dict: dict) -> list[DockerAut
3030
auth_info: list[DockerAuthInfo] = []
3131

3232
auths = auth_config_dict.get("auths")
33+
if not auths:
34+
raise KeyError("No auths found in the docker auth config")
35+
3336
for registry, auth in auths.items():
34-
auth_str = auth.get("auth")
37+
auth_str = str(auth.get("auth"))
3538
auth_str = base64.b64decode(auth_str).decode("utf-8")
3639
username, password = auth_str.split(":")
3740
auth_info.append(DockerAuthInfo(registry, username, password))
3841

3942
return auth_info
4043

4144

42-
def process_docker_auth_config_cred_helpers(auth_config_dict: dict) -> None:
45+
def process_docker_auth_config_cred_helpers(auth_config_dict: dict[str, Any]) -> None:
4346
"""
4447
Process the credHelpers config.
4548
@@ -56,7 +59,7 @@ def process_docker_auth_config_cred_helpers(auth_config_dict: dict) -> None:
5659
warning(_AUTH_WARNINGS.pop("credHelpers"))
5760

5861

59-
def process_docker_auth_config_store(auth_config_dict: dict) -> None:
62+
def process_docker_auth_config_store(auth_config_dict: dict[str, Any]) -> None:
6063
"""
6164
Process the credsStore config.
6265
@@ -74,7 +77,7 @@ def process_docker_auth_config_store(auth_config_dict: dict) -> None:
7477
def parse_docker_auth_config(auth_config: str) -> Optional[list[DockerAuthInfo]]:
7578
"""Parse the docker auth config from a string and handle the different formats."""
7679
try:
77-
auth_config_dict: dict = json.loads(auth_config)
80+
auth_config_dict: dict[str, Any] = json.loads(auth_config)
7881
if "credHelpers" in auth_config:
7982
process_docker_auth_config_cred_helpers(auth_config_dict)
8083
if "credsStore" in auth_config:

core/testcontainers/core/config.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def read_tc_properties() -> dict[str, str]:
3030
tc_files = [item for item in [TC_GLOBAL] if exists(item)]
3131
if not tc_files:
3232
return {}
33-
settings = {}
33+
settings: dict[str, str] = {}
3434

3535
for file in tc_files:
3636
with open(file) as contents:
@@ -60,14 +60,14 @@ class TestcontainersConfiguration:
6060
"""
6161

6262
@property
63-
def docker_auth_config(self):
63+
def docker_auth_config(self) -> Optional[str]:
6464
config = self._docker_auth_config
6565
if config and "DOCKER_AUTH_CONFIG" in _WARNINGS:
6666
warning(_WARNINGS.pop("DOCKER_AUTH_CONFIG"))
6767
return config
6868

6969
@docker_auth_config.setter
70-
def docker_auth_config(self, value: str):
70+
def docker_auth_config(self, value: str) -> None:
7171
if "DOCKER_AUTH_CONFIG" in _WARNINGS:
7272
warning(_WARNINGS.pop("DOCKER_AUTH_CONFIG"))
7373
self._docker_auth_config = value
@@ -76,7 +76,7 @@ def tc_properties_get_tc_host(self) -> Union[str, None]:
7676
return self.tc_properties.get("tc.host")
7777

7878
@property
79-
def timeout(self):
79+
def timeout(self) -> int:
8080
return self.max_tries * self.sleep_time
8181

8282

core/testcontainers/core/utils.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import platform
44
import subprocess
55
import sys
6+
from typing import Any, Optional
67

78
LINUX = "linux"
89
MAC = "mac"
@@ -18,14 +19,15 @@ def setup_logger(name: str) -> logging.Logger:
1819
return logger
1920

2021

21-
def os_name() -> str:
22+
def os_name() -> Optional[str]:
2223
pl = sys.platform
2324
if pl == "linux" or pl == "linux2":
2425
return LINUX
2526
elif pl == "darwin":
2627
return MAC
2728
elif pl == "win32":
2829
return WIN
30+
return None
2931

3032

3133
def is_mac() -> bool:
@@ -53,7 +55,7 @@ def inside_container() -> bool:
5355
return os.path.exists("/.dockerenv")
5456

5557

56-
def default_gateway_ip() -> str:
58+
def default_gateway_ip() -> Optional[str]:
5759
"""
5860
Returns gateway IP address of the host that testcontainer process is
5961
running on
@@ -66,11 +68,12 @@ def default_gateway_ip() -> str:
6668
ip_address = process.communicate()[0]
6769
if ip_address and process.returncode == 0:
6870
return ip_address.decode("utf-8").strip().strip("\n")
71+
return None
6972
except subprocess.SubprocessError:
7073
return None
7174

7275

73-
def raise_for_deprecated_parameter(kwargs: dict, name: str, replacement: str) -> dict:
76+
def raise_for_deprecated_parameter(kwargs: dict[Any, Any], name: str, replacement: str) -> dict[Any, Any]:
7477
"""
7578
Raise an error if a dictionary of keyword arguments contains a key and suggest the replacement.
7679
"""

core/tests/test_auth.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
def test_parse_docker_auth_config_encoded():
88
auth_config_json = '{"auths":{"https://index.docker.io/v1/":{"auth":"dXNlcm5hbWU6cGFzc3dvcmQ="}}}'
99
auth_info = parse_docker_auth_config(auth_config_json)
10+
assert auth_info
1011
assert len(auth_info) == 1
1112
assert auth_info[0] == DockerAuthInfo(
1213
registry="https://index.docker.io/v1/",
@@ -37,6 +38,7 @@ def test_parse_docker_auth_config_encoded_multiple():
3738
}
3839
auth_config_json = json.dumps(auth_dict)
3940
auth_info = parse_docker_auth_config(auth_config_json)
41+
assert auth_info
4042
assert len(auth_info) == 3
4143
assert auth_info[0] == DockerAuthInfo(
4244
registry="localhost:5000",

core/tests/test_config.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
from testcontainers.core.config import TestcontainersConfiguration as TCC, TC_FILE
2+
3+
from pytest import MonkeyPatch, mark, LogCaptureFixture
4+
5+
import logging
6+
import tempfile
7+
8+
9+
def test_read_tc_properties(monkeypatch: MonkeyPatch) -> None:
10+
with tempfile.TemporaryDirectory() as tmpdirname:
11+
file = f"{tmpdirname}/{TC_FILE}"
12+
with open(file, "w") as f:
13+
f.write("tc.host=some_value\n")
14+
15+
monkeypatch.setattr("testcontainers.core.config.TC_GLOBAL", file)
16+
17+
config = TCC()
18+
assert config.tc_properties == {"tc.host": "some_value"}
19+
20+
21+
@mark.parametrize("docker_auth_config_env", ["key=value", ""])
22+
@mark.parametrize("warning_dict", [{}, {"key": "value"}, {"DOCKER_AUTH_CONFIG": "TEST"}])
23+
@mark.parametrize("warning_dict_post", [{}, {"key": "value"}, {"DOCKER_AUTH_CONFIG": "TEST"}])
24+
def test_docker_auth_config(
25+
caplog: LogCaptureFixture,
26+
monkeypatch: MonkeyPatch,
27+
docker_auth_config_env: str,
28+
warning_dict: dict[str, str],
29+
warning_dict_post: dict[str, str],
30+
) -> None:
31+
monkeypatch.setattr("testcontainers.core.config._WARNINGS", warning_dict)
32+
monkeypatch.setenv("DOCKER_AUTH_CONFIG", docker_auth_config_env)
33+
caplog.set_level(logging.WARNING)
34+
35+
config = TCC()
36+
if not docker_auth_config_env:
37+
assert config.docker_auth_config == ""
38+
assert caplog.text == ""
39+
else:
40+
assert config.docker_auth_config == docker_auth_config_env
41+
42+
if "DOCKER_AUTH_CONFIG" in warning_dict:
43+
assert warning_dict["DOCKER_AUTH_CONFIG"] in caplog.text
44+
45+
if warning_dict == {}:
46+
monkeypatch.setattr("testcontainers.core.config._WARNINGS", warning_dict_post)
47+
48+
config.docker_auth_config = "new_value"
49+
assert config.docker_auth_config == "new_value"
50+
51+
52+
def test_tc_properties_get_tc_host() -> None:
53+
config = TCC()
54+
config.tc_properties = {"tc.host": "some_value"}
55+
assert config.tc_properties_get_tc_host() == "some_value"
56+
57+
58+
def test_timeout() -> None:
59+
config = TCC()
60+
config.max_tries = 2
61+
config.sleep_time = 3
62+
assert config.timeout == 6

core/tests/test_core.py

Lines changed: 2 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,4 @@
1-
import pytest
2-
import tempfile
3-
import random
4-
import os
5-
6-
from pathlib import Path
7-
from typing import Optional
8-
91
from testcontainers.core.container import DockerContainer
10-
from testcontainers.core.image import DockerImage
11-
from testcontainers.core.waiting_utils import wait_for_logs
12-
13-
14-
def test_timeout_is_raised_when_waiting_for_logs():
15-
with pytest.raises(TimeoutError), DockerContainer("alpine").with_command("sleep 2") as container:
16-
wait_for_logs(container, "Hello from Docker!", timeout=1e-3)
172

183

194
def test_garbage_collection_is_defensive():
@@ -26,72 +11,12 @@ def test_garbage_collection_is_defensive():
2611
del container
2712

2813

29-
def test_wait_for_hello():
14+
def test_get_logs():
3015
with DockerContainer("hello-world") as container:
31-
wait_for_logs(container, "Hello from Docker!")
32-
33-
34-
def test_can_get_logs():
35-
with DockerContainer("hello-world") as container:
36-
wait_for_logs(container, "Hello from Docker!")
3716
stdout, stderr = container.get_logs()
3817
assert isinstance(stdout, bytes)
3918
assert isinstance(stderr, bytes)
40-
assert stdout, "There should be something on stdout"
41-
42-
43-
@pytest.mark.parametrize("test_cleanup", [True, False])
44-
@pytest.mark.parametrize("test_image_tag", [None, "test-image:latest"])
45-
def test_docker_image(test_image_tag: Optional[str], test_cleanup: bool, check_for_image):
46-
with tempfile.TemporaryDirectory() as temp_directory:
47-
# It's important to use a random string to avoid image caching
48-
random_string = "Hello from Docker Image! " + str(random.randint(0, 1000))
49-
with open(f"{temp_directory}/Dockerfile", "w") as f:
50-
f.write(
51-
f"""
52-
FROM alpine:latest
53-
CMD echo "{random_string}"
54-
"""
55-
)
56-
with DockerImage(path=temp_directory, tag=test_image_tag, clean_up=test_cleanup) as image:
57-
image_short_id = image.short_id
58-
assert image.tag is test_image_tag, f"Expected {test_image_tag}, got {image.tag}"
59-
assert image.short_id is not None, "Short ID should not be None"
60-
logs = image.get_logs()
61-
assert isinstance(logs, list), "Logs should be a list"
62-
assert logs[0] == {"stream": "Step 1/2 : FROM alpine:latest"}
63-
assert logs[3] == {"stream": f'Step 2/2 : CMD echo "{random_string}"'}
64-
with DockerContainer(str(image)) as container:
65-
assert container._container.image.short_id.endswith(image_short_id), "Image ID mismatch"
66-
assert container.get_logs() == ((random_string + "\n").encode(), b""), "Container logs mismatch"
67-
68-
check_for_image(image_short_id, test_cleanup)
69-
70-
71-
@pytest.mark.parametrize("dockerfile_path", [None, Path("subdir/my.Dockerfile")])
72-
def test_docker_image_with_custom_dockerfile_path(dockerfile_path: Optional[Path]):
73-
with tempfile.TemporaryDirectory() as temp_directory:
74-
temp_dir_path = Path(temp_directory)
75-
if dockerfile_path:
76-
os.makedirs(temp_dir_path / dockerfile_path.parent, exist_ok=True)
77-
dockerfile_rel_path = dockerfile_path
78-
dockerfile_kwargs = {"dockerfile_path": dockerfile_path}
79-
else:
80-
dockerfile_rel_path = Path("Dockerfile") # default
81-
dockerfile_kwargs = {}
82-
83-
with open(temp_dir_path / dockerfile_rel_path, "x") as f:
84-
f.write(
85-
f"""
86-
FROM alpine:latest
87-
CMD echo "Hello world!"
88-
"""
89-
)
90-
with DockerImage(path=temp_directory, tag="test", clean_up=True, no_cache=True, **dockerfile_kwargs) as image:
91-
image_short_id = image.short_id
92-
with DockerContainer(str(image)) as container:
93-
assert container._container.image.short_id.endswith(image_short_id), "Image ID mismatch"
94-
assert container.get_logs() == (("Hello world!\n").encode(), b""), "Container logs mismatch"
19+
assert "Hello from Docker".encode() in stdout, "There should be something on stdout"
9520

9621

9722
def test_docker_container_with_env_file():

0 commit comments

Comments
 (0)