Skip to content

Commit 71cb17a

Browse files
authored
Merge branch 'main' into fix-remaining-mypy
2 parents 89cd20c + ff6a32d commit 71cb17a

File tree

7 files changed

+107
-36
lines changed

7 files changed

+107
-36
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,20 @@ See [CONTRIBUTING.md](.github/CONTRIBUTING.md) for more details.
4141

4242
## Configuration
4343

44+
You can set environment variables to configure the library behaviour:
45+
4446
| Env Variable | Example | Description |
4547
| --------------------------------------- | --------------------------- | ---------------------------------------------------------------------------------- |
4648
| `TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE` | `/var/run/docker.sock` | Path to Docker's socket used by ryuk |
4749
| `TESTCONTAINERS_RYUK_PRIVILEGED` | `false` | Run ryuk as a privileged container |
4850
| `TESTCONTAINERS_RYUK_DISABLED` | `false` | Disable ryuk |
4951
| `RYUK_CONTAINER_IMAGE` | `testcontainers/ryuk:0.8.1` | Custom image for ryuk |
5052
| `RYUK_RECONNECTION_TIMEOUT` | `10s` | Reconnection timeout for Ryuk TCP socket before Ryuk reaps all dangling containers |
53+
54+
Alternatively you can set the configuration during runtime:
55+
56+
```python
57+
from testcontainers.core import testcontainers_config
58+
59+
testcontainers_config.ryuk_docker_socket = "/home/user/docker.sock"
60+
```
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .config import testcontainers_config
2+
3+
__all__ = ["testcontainers_config"]

core/testcontainers/core/config.py

Lines changed: 68 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
import types
2+
import warnings
3+
from collections.abc import Mapping
14
from dataclasses import dataclass, field
25
from enum import Enum
36
from logging import warning
47
from os import environ
58
from os.path import exists
69
from pathlib import Path
7-
from typing import Optional, Union, cast
10+
from typing import Final, Optional, Union, cast
811

912
import docker
1013

@@ -30,29 +33,28 @@ def get_docker_socket() -> str:
3033
3134
Using the docker api ensure we handle rootless docker properly
3235
"""
33-
if socket_path := environ.get("TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE"):
36+
if socket_path := environ.get("TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE", ""):
3437
return socket_path
3538

36-
client = docker.from_env()
3739
try:
40+
client = docker.from_env()
3841
socket_path = client.api.get_adapter(client.api.base_url).socket_path
3942
socket_path = cast("str", socket_path)
4043
# return the normalized path as string
4144
return str(Path(socket_path).absolute())
42-
except AttributeError:
45+
except Exception:
4346
return "/var/run/docker.sock"
4447

4548

46-
MAX_TRIES = int(environ.get("TC_MAX_TRIES", 120))
47-
SLEEP_TIME = int(environ.get("TC_POOLING_INTERVAL", 1))
48-
TIMEOUT = MAX_TRIES * SLEEP_TIME
49+
def get_bool_env(name: str) -> bool:
50+
"""
51+
Get environment variable named `name` and convert it to bool.
52+
53+
Defaults to False.
54+
"""
55+
value = environ.get(name, "")
56+
return value.lower() in ("yes", "true", "t", "y", "1")
4957

50-
RYUK_IMAGE: str = environ.get("RYUK_CONTAINER_IMAGE", "testcontainers/ryuk:0.8.1")
51-
RYUK_PRIVILEGED: bool = environ.get("TESTCONTAINERS_RYUK_PRIVILEGED", "false") == "true"
52-
RYUK_DISABLED: bool = environ.get("TESTCONTAINERS_RYUK_DISABLED", "false") == "true"
53-
RYUK_DOCKER_SOCKET: str = get_docker_socket()
54-
RYUK_RECONNECTION_TIMEOUT: str = environ.get("RYUK_RECONNECTION_TIMEOUT", "10s")
55-
TC_HOST_OVERRIDE: Optional[str] = environ.get("TC_HOST", environ.get("TESTCONTAINERS_HOST_OVERRIDE"))
5658

5759
TC_FILE = ".testcontainers.properties"
5860
TC_GLOBAL = Path.home() / TC_FILE
@@ -95,16 +97,16 @@ def read_tc_properties() -> dict[str, str]:
9597

9698
@dataclass
9799
class TestcontainersConfiguration:
98-
max_tries: int = MAX_TRIES
99-
sleep_time: int = SLEEP_TIME
100-
ryuk_image: str = RYUK_IMAGE
101-
ryuk_privileged: bool = RYUK_PRIVILEGED
102-
ryuk_disabled: bool = RYUK_DISABLED
103-
ryuk_docker_socket: str = RYUK_DOCKER_SOCKET
104-
ryuk_reconnection_timeout: str = RYUK_RECONNECTION_TIMEOUT
100+
max_tries: int = int(environ.get("TC_MAX_TRIES", "120"))
101+
sleep_time: int = int(environ.get("TC_POOLING_INTERVAL", "1"))
102+
ryuk_image: str = environ.get("RYUK_CONTAINER_IMAGE", "testcontainers/ryuk:0.8.1")
103+
ryuk_privileged: bool = get_bool_env("TESTCONTAINERS_RYUK_PRIVILEGED")
104+
ryuk_disabled: bool = get_bool_env("TESTCONTAINERS_RYUK_DISABLED")
105+
_ryuk_docker_socket: str = ""
106+
ryuk_reconnection_timeout: str = environ.get("RYUK_RECONNECTION_TIMEOUT", "10s")
105107
tc_properties: dict[str, str] = field(default_factory=read_tc_properties)
106108
_docker_auth_config: Optional[str] = field(default_factory=lambda: environ.get("DOCKER_AUTH_CONFIG"))
107-
tc_host_override: Optional[str] = TC_HOST_OVERRIDE
109+
tc_host_override: Optional[str] = environ.get("TC_HOST", environ.get("TESTCONTAINERS_HOST_OVERRIDE"))
108110
connection_mode_override: Optional[ConnectionMode] = field(default_factory=get_user_overwritten_connection_mode)
109111

110112
"""
@@ -132,20 +134,55 @@ def tc_properties_get_tc_host(self) -> Union[str, None]:
132134
def timeout(self) -> int:
133135
return self.max_tries * self.sleep_time
134136

137+
@property
138+
def ryuk_docker_socket(self) -> str:
139+
if not self._ryuk_docker_socket:
140+
self.ryuk_docker_socket = get_docker_socket()
141+
return self._ryuk_docker_socket
135142

136-
testcontainers_config = TestcontainersConfiguration()
143+
@ryuk_docker_socket.setter
144+
def ryuk_docker_socket(self, value: str) -> None:
145+
self._ryuk_docker_socket = value
146+
147+
148+
testcontainers_config: Final = TestcontainersConfiguration()
137149

138150
__all__ = [
139-
# Legacy things that are deprecated:
140-
"MAX_TRIES",
141-
"RYUK_DISABLED",
142-
"RYUK_DOCKER_SOCKET",
143-
"RYUK_IMAGE",
144-
"RYUK_PRIVILEGED",
145-
"RYUK_RECONNECTION_TIMEOUT",
146-
"SLEEP_TIME",
147-
"TIMEOUT",
148151
# Public API of this module:
149152
"ConnectionMode",
150153
"testcontainers_config",
151154
]
155+
156+
_deprecated_attribute_mapping: Final[Mapping[str, str]] = types.MappingProxyType(
157+
{
158+
"MAX_TRIES": "max_tries",
159+
"RYUK_DISABLED": "ryuk_disabled",
160+
"RYUK_DOCKER_SOCKET": "ryuk_docker_socket",
161+
"RYUK_IMAGE": "ryuk_image",
162+
"RYUK_PRIVILEGED": "ryuk_privileged",
163+
"RYUK_RECONNECTION_TIMEOUT": "ryuk_reconnection_timeout",
164+
"SLEEP_TIME": "sleep_time",
165+
"TIMEOUT": "timeout",
166+
}
167+
)
168+
169+
170+
def __dir__() -> list[str]:
171+
return __all__ + list(_deprecated_attribute_mapping.keys())
172+
173+
174+
def __getattr__(name: str) -> object:
175+
"""
176+
Allow getting deprecated legacy settings.
177+
"""
178+
module = f"{__name__!r}"
179+
180+
if name in _deprecated_attribute_mapping:
181+
attrib = _deprecated_attribute_mapping[name]
182+
warnings.warn(
183+
f"{module}.{name} is deprecated. Use {module}.testcontainers_config.{attrib} instead.",
184+
DeprecationWarning,
185+
stacklevel=2,
186+
)
187+
return getattr(testcontainers_config, attrib)
188+
raise AttributeError(f"module {module} has no attribute {name!r}")

core/testcontainers/core/waiting_utils.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ def wait_for(condition: Callable[..., bool]) -> bool:
8383
def wait_for_logs(
8484
container: "DockerContainer",
8585
predicate: Union[Callable[..., bool], str],
86-
timeout: float = config.timeout,
86+
timeout: Union[float, None] = None,
8787
interval: float = 1,
8888
predicate_streams_and: bool = False,
8989
raise_on_exit: bool = False,
@@ -105,6 +105,8 @@ def wait_for_logs(
105105
duration: Number of seconds until the predicate was satisfied.
106106
"""
107107
re_predicate: Optional[Callable[[str], Any]] = None
108+
if timeout is None:
109+
timeout = config.timeout
108110
if isinstance(predicate, str):
109111
re_predicate = re.compile(predicate, re.MULTILINE).search
110112
elif callable(predicate):

core/tests/test_config.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,23 @@ def test_get_docker_host_root(monkeypatch: pytest.MonkeyPatch) -> None:
150150
# Define a Root like Docker Client
151151
monkeypatch.setenv("DOCKER_HOST", "unix://")
152152
assert get_docker_socket() == "/var/run/docker.sock"
153+
154+
155+
def test_deprecated_settings() -> None:
156+
"""
157+
Getting deprecated settings raises a DepcrationWarning
158+
"""
159+
from testcontainers.core import config
160+
161+
with pytest.warns(DeprecationWarning):
162+
assert config.TIMEOUT
163+
164+
165+
def test_attribut_error() -> None:
166+
"""
167+
Accessing a not existing attribute raises an AttributeError
168+
"""
169+
from testcontainers.core import config
170+
171+
with pytest.raises(AttributeError):
172+
config.missing

core/tests/test_labels.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
TESTCONTAINERS_NAMESPACE,
88
)
99
import pytest
10-
from testcontainers.core.config import RYUK_IMAGE
10+
from testcontainers.core.config import testcontainers_config as config
1111

1212

1313
def assert_in_with_value(labels: dict[str, str], label: str, value: str, known_before_test_time: bool):
@@ -43,7 +43,7 @@ def test_containers_respect_custom_labels_if_no_collision():
4343

4444

4545
def test_if_ryuk_no_session():
46-
actual_labels = create_labels(RYUK_IMAGE, None)
46+
actual_labels = create_labels(config.ryuk_image, None)
4747
assert LABEL_SESSION_ID not in actual_labels
4848

4949

modules/scylla/testcontainers/scylla/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from testcontainers.core.config import MAX_TRIES
21
from testcontainers.core.generic import DockerContainer
32
from testcontainers.core.waiting_utils import wait_container_is_ready, wait_for_logs
43

@@ -29,7 +28,7 @@ def __init__(self, image="scylladb/scylla:latest", ports_to_expose=(9042,)):
2928

3029
@wait_container_is_ready(OSError)
3130
def _connect(self):
32-
wait_for_logs(self, predicate="Starting listening for CQL clients", timeout=MAX_TRIES)
31+
wait_for_logs(self, predicate="Starting listening for CQL clients")
3332
cluster = self.get_cluster()
3433
cluster.connect()
3534

0 commit comments

Comments
 (0)