Skip to content

Commit b1cf47a

Browse files
committed
Merge remote-tracking branch 'upstream/main' into support-arm-local-development
2 parents c0653d0 + 408f5c2 commit b1cf47a

File tree

31 files changed

+4134
-2029
lines changed

31 files changed

+4134
-2029
lines changed

.github/workflows/ci-lint.yml

+6-1
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,9 @@ jobs:
2020
- name: Install Python dependencies
2121
run: poetry install --no-interaction
2222
- name: Execute pre-commit handler
23-
run: poetry run pre-commit run -a
23+
run: |
24+
poetry run pre-commit run check-toml
25+
poetry run pre-commit run trailing-whitespace
26+
poetry run pre-commit run end-of-file-fixer
27+
poetry run pre-commit run ruff
28+
poetry run pre-commit run ruff-format

.pre-commit-config.yaml

+11-11
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,21 @@ repos:
1010
- id: end-of-file-fixer
1111

1212
- repo: https://github.com/astral-sh/ruff-pre-commit
13-
rev: 'v0.3.5'
13+
rev: 'v0.11.5'
1414
hooks:
1515
- id: ruff
1616
# Explicitly setting config to prevent Ruff from using `pyproject.toml` in sub packages.
1717
args: [ '--fix', '--exit-non-zero-on-fix', '--config', 'pyproject.toml' ]
1818
- id: ruff-format
1919
args: [ '--config', 'pyproject.toml' ]
2020

21-
# - repo: local
22-
# hooks:
23-
# - id: mypy
24-
# name: mypy
25-
# entry: poetry run mypy
26-
# args: ["--config-file", "pyproject.toml"]
27-
# files: "core" # start with the core being type checked
28-
# language: system
29-
# types: [ python ]
30-
# require_serial: true
21+
- repo: local
22+
hooks:
23+
- id: mypy
24+
name: mypy
25+
entry: poetry run mypy
26+
args: ["--config-file", "pyproject.toml"]
27+
files: "core" # start with the core being type checked
28+
language: system
29+
types: [ python ]
30+
require_serial: true

Makefile

+3
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ coverage: ## Target to combine and report coverage.
3030
lint: ## Lint all files in the project, which we also run in pre-commit
3131
poetry run pre-commit run -a
3232

33+
mypy-core-report:
34+
poetry run mypy --config-file pyproject.toml core | poetry run python scripts/mypy_report.py
35+
3336
docs: ## Build the docs for the project
3437
poetry run sphinx-build -nW . docs/_build
3538

conf.py

+5
Original file line numberDiff line numberDiff line change
@@ -161,4 +161,9 @@
161161
intersphinx_mapping = {
162162
"python": ("https://docs.python.org/3", None),
163163
"selenium": ("https://seleniumhq.github.io/selenium/docs/api/py/", None),
164+
"typing_extensions": ("https://typing-extensions.readthedocs.io/en/latest/", None),
164165
}
166+
167+
nitpick_ignore = [
168+
("py:class", "typing_extensions.Self"),
169+
]

core/README.rst

+9
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@ Testcontainers Core
44
:code:`testcontainers-core` is the core functionality for spinning up Docker containers in test environments.
55

66
.. autoclass:: testcontainers.core.container.DockerContainer
7+
:members: with_bind_ports, with_exposed_ports
8+
9+
.. note::
10+
When using `with_bind_ports` or `with_exposed_ports`
11+
you can specify the port in the following formats: :code:`{private_port}/{protocol}`
12+
13+
e.g. `8080/tcp` or `8125/udp` or just `8080` (default protocol is tcp)
14+
15+
For legacy reasons, the port can be an *integer*
716

817
.. autoclass:: testcontainers.core.image.DockerImage
918

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
# flake8: noqa
1+
# flake8: noqa: F401
22
from testcontainers.compose.compose import (
3+
ComposeContainer,
34
ContainerIsNotRunning,
5+
DockerCompose,
46
NoSuchPortExposed,
57
PublishedPort,
6-
ComposeContainer,
7-
DockerCompose,
88
)

core/testcontainers/compose/compose.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ def get_config(
293293
config_cmd.append("--no-interpolate")
294294

295295
cmd_output = self._run_command(cmd=config_cmd).stdout
296-
return cast(dict[str, Any], loads(cmd_output))
296+
return cast(dict[str, Any], loads(cmd_output)) # noqa: TC006
297297

298298
def get_containers(self, include_all=False) -> list[ComposeContainer]:
299299
"""

core/testcontainers/core/config.py

+11-13
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,11 @@ class ConnectionMode(Enum):
1717
@property
1818
def use_mapped_port(self) -> bool:
1919
"""
20-
Return true if we need to use mapped port for this connection
20+
Return True if mapped ports should be used for this connection mode.
2121
22-
This is true for everything but bridge mode.
22+
Mapped ports are used for all connection modes except 'bridge_ip'.
2323
"""
24-
if self == self.bridge_ip:
25-
return False
26-
return True
24+
return self != ConnectionMode.bridge_ip
2725

2826

2927
def get_docker_socket() -> str:
@@ -63,7 +61,7 @@ def get_user_overwritten_connection_mode() -> Optional[ConnectionMode]:
6361
"""
6462
Return the user overwritten connection mode.
6563
"""
66-
connection_mode: str | None = environ.get("TESTCONTAINERS_CONNECTION_MODE")
64+
connection_mode: Union[str, None] = environ.get("TESTCONTAINERS_CONNECTION_MODE")
6765
if connection_mode:
6866
try:
6967
return ConnectionMode(connection_mode)
@@ -137,15 +135,15 @@ def timeout(self) -> int:
137135
testcontainers_config = TestcontainersConfiguration()
138136

139137
__all__ = [
140-
# the public API of this module
141-
"testcontainers_config",
142-
# and all the legacy things that are deprecated:
138+
# Legacy things that are deprecated:
143139
"MAX_TRIES",
144-
"SLEEP_TIME",
145-
"TIMEOUT",
146-
"RYUK_IMAGE",
147-
"RYUK_PRIVILEGED",
148140
"RYUK_DISABLED",
149141
"RYUK_DOCKER_SOCKET",
142+
"RYUK_IMAGE",
143+
"RYUK_PRIVILEGED",
150144
"RYUK_RECONNECTION_TIMEOUT",
145+
"SLEEP_TIME",
146+
"TIMEOUT",
147+
# Public API of this module:
148+
"testcontainers_config",
151149
]

core/testcontainers/core/container.py

+29-2
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,38 @@ def with_env_file(self, env_file: Union[str, PathLike]) -> Self:
6565
self.with_env(key, value)
6666
return self
6767

68-
def with_bind_ports(self, container: int, host: Optional[int] = None) -> Self:
68+
def with_bind_ports(self, container: Union[str, int], host: Optional[Union[str, int]] = None) -> Self:
69+
"""
70+
Bind container port to host port
71+
72+
:param container: container port
73+
:param host: host port
74+
75+
:doctest:
76+
77+
>>> from testcontainers.core.container import DockerContainer
78+
>>> container = DockerContainer("nginx")
79+
>>> container = container.with_bind_ports("8080/tcp", 8080)
80+
>>> container = container.with_bind_ports("8081/tcp", 8081)
81+
82+
"""
6983
self.ports[container] = host
7084
return self
7185

72-
def with_exposed_ports(self, *ports: int) -> Self:
86+
def with_exposed_ports(self, *ports: Union[str, int]) -> Self:
87+
"""
88+
Expose ports from the container without binding them to the host.
89+
90+
:param ports: ports to expose
91+
92+
:doctest:
93+
94+
>>> from testcontainers.core.container import DockerContainer
95+
>>> container = DockerContainer("nginx")
96+
>>> container = container.with_exposed_ports("8080/tcp", "8081/tcp")
97+
98+
"""
99+
73100
for port in ports:
74101
self.ports[port] = None
75102
return self

core/testcontainers/core/docker_client.py

+24-20
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import urllib
2020
import urllib.parse
2121
from collections.abc import Iterable
22-
from typing import Callable, Optional, TypeVar, Union
22+
from typing import Any, Callable, Optional, TypeVar, Union, cast
2323

2424
import docker
2525
from docker.models.containers import Container, ContainerCollection
@@ -59,7 +59,7 @@ class DockerClient:
5959
Thin wrapper around :class:`docker.DockerClient` for a more functional interface.
6060
"""
6161

62-
def __init__(self, **kwargs) -> None:
62+
def __init__(self, **kwargs: Any) -> None:
6363
docker_host = get_docker_host()
6464

6565
if docker_host:
@@ -82,14 +82,14 @@ def run(
8282
self,
8383
image: str,
8484
command: Optional[Union[str, list[str]]] = None,
85-
environment: Optional[dict] = None,
86-
ports: Optional[dict] = None,
85+
environment: Optional[dict[str, str]] = None,
86+
ports: Optional[dict[int, Optional[int]]] = None,
8787
labels: Optional[dict[str, str]] = None,
8888
detach: bool = False,
8989
stdout: bool = True,
9090
stderr: bool = False,
9191
remove: bool = False,
92-
**kwargs,
92+
**kwargs: Any,
9393
) -> Container:
9494
# If the user has specified a network, we'll assume the user knows best
9595
if "network" not in kwargs and not get_docker_host():
@@ -112,7 +112,7 @@ def run(
112112
return container
113113

114114
@_wrapped_image_collection
115-
def build(self, path: str, tag: str, rm: bool = True, **kwargs) -> tuple[Image, Iterable[dict]]:
115+
def build(self, path: str, tag: str, rm: bool = True, **kwargs: Any) -> tuple[Image, Iterable[dict[str, Any]]]:
116116
"""
117117
Build a Docker image from a directory containing the Dockerfile.
118118
@@ -151,43 +151,43 @@ def find_host_network(self) -> Optional[str]:
151151
except ipaddress.AddressValueError:
152152
continue
153153
if docker_host in subnet:
154-
return network.name
154+
return cast(str, network.name)
155155
except (ipaddress.AddressValueError, OSError):
156156
pass
157157
return None
158158

159-
def port(self, container_id: str, port: int) -> int:
159+
def port(self, container_id: str, port: int) -> str:
160160
"""
161161
Lookup the public-facing port that is NAT-ed to :code:`port`.
162162
"""
163163
port_mappings = self.client.api.port(container_id, port)
164164
if not port_mappings:
165-
raise ConnectionError(f"Port mapping for container {container_id} and port {port} is " "not available")
166-
return port_mappings[0]["HostPort"]
165+
raise ConnectionError(f"Port mapping for container {container_id} and port {port} is not available")
166+
return cast(str, port_mappings[0]["HostPort"])
167167

168-
def get_container(self, container_id: str) -> Container:
168+
def get_container(self, container_id: str) -> dict[str, Any]:
169169
"""
170170
Get the container with a given identifier.
171171
"""
172172
containers = self.client.api.containers(filters={"id": container_id})
173173
if not containers:
174174
raise RuntimeError(f"Could not get container with id {container_id}")
175-
return containers[0]
175+
return cast(dict[str, Any], containers[0])
176176

177177
def bridge_ip(self, container_id: str) -> str:
178178
"""
179179
Get the bridge ip address for a container.
180180
"""
181181
container = self.get_container(container_id)
182182
network_name = self.network_name(container_id)
183-
return container["NetworkSettings"]["Networks"][network_name]["IPAddress"]
183+
return str(container["NetworkSettings"]["Networks"][network_name]["IPAddress"])
184184

185185
def network_name(self, container_id: str) -> str:
186186
"""
187187
Get the name of the network this container runs on
188188
"""
189189
container = self.get_container(container_id)
190-
name = container["HostConfig"]["NetworkMode"]
190+
name = str(container["HostConfig"]["NetworkMode"])
191191
if name == "default":
192192
return "bridge"
193193
return name
@@ -198,7 +198,7 @@ def gateway_ip(self, container_id: str) -> str:
198198
"""
199199
container = self.get_container(container_id)
200200
network_name = self.network_name(container_id)
201-
return container["NetworkSettings"]["Networks"][network_name]["Gateway"]
201+
return str(container["NetworkSettings"]["Networks"][network_name]["Gateway"])
202202

203203
def get_connection_mode(self) -> ConnectionMode:
204204
"""
@@ -233,11 +233,15 @@ def host(self) -> str:
233233
url = urllib.parse.urlparse(self.client.api.base_url)
234234
except ValueError:
235235
return "localhost"
236-
if "http" in url.scheme or "tcp" in url.scheme and url.hostname:
236+
237+
is_http_scheme = "http" in url.scheme
238+
is_tcp_scheme_with_hostname = "tcp" in url.scheme and url.hostname
239+
if is_http_scheme or is_tcp_scheme_with_hostname:
237240
# see https://github.com/testcontainers/testcontainers-python/issues/415
238-
if url.hostname == "localnpipe" and utils.is_windows():
241+
hostname = url.hostname
242+
if not hostname or (hostname == "localnpipe" and utils.is_windows()):
239243
return "localhost"
240-
return url.hostname
244+
return cast(str, url.hostname)
241245
if utils.inside_container() and ("unix" in url.scheme or "npipe" in url.scheme):
242246
ip_address = utils.default_gateway_ip()
243247
if ip_address:
@@ -251,9 +255,9 @@ def login(self, auth_config: DockerAuthInfo) -> None:
251255
login_info = self.client.login(**auth_config._asdict())
252256
LOGGER.debug(f"logged in using {login_info}")
253257

254-
def client_networks_create(self, name: str, param: dict):
258+
def client_networks_create(self, name: str, param: dict[str, Any]) -> dict[str, Any]:
255259
labels = create_labels("", param.get("labels"))
256-
return self.client.networks.create(name, **{**param, "labels": labels})
260+
return cast(dict[str, Any], self.client.networks.create(name, **{**param, "labels": labels}))
257261

258262

259263
def get_docker_host() -> Optional[str]:

core/testcontainers/core/generic.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
1111
# License for the specific language governing permissions and limitations
1212
# under the License.
13-
from typing import Optional
13+
from typing import Any, Optional
1414
from urllib.parse import quote
1515

1616
from testcontainers.core.container import DockerContainer
@@ -55,7 +55,7 @@ def _create_connection_url(
5555
host: Optional[str] = None,
5656
port: Optional[int] = None,
5757
dbname: Optional[str] = None,
58-
**kwargs,
58+
**kwargs: Any,
5959
) -> str:
6060
if raise_for_deprecated_parameter(kwargs, "db_name", "dbname"):
6161
raise ValueError(f"Unexpected arguments: {','.join(kwargs)}")

core/testcontainers/core/network.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
# License for the specific language governing permissions and limitations
1212
# under the License.
1313
import uuid
14-
from typing import Optional
14+
from typing import Any, Optional
1515

1616
from testcontainers.core.docker_client import DockerClient
1717

@@ -21,12 +21,14 @@ class Network:
2121
Network context manager for programmatically connecting containers.
2222
"""
2323

24-
def __init__(self, docker_client_kw: Optional[dict] = None, docker_network_kw: Optional[dict] = None) -> None:
24+
def __init__(
25+
self, docker_client_kw: Optional[dict[str, Any]] = None, docker_network_kw: Optional[dict[str, Any]] = None
26+
):
2527
self.name = str(uuid.uuid4())
2628
self._docker = DockerClient(**(docker_client_kw or {}))
2729
self._docker_network_kw = docker_network_kw or {}
2830

29-
def connect(self, container_id: str, network_aliases: Optional[list] = None):
31+
def connect(self, container_id: str, network_aliases: Optional[list[str]] = None) -> None:
3032
self._network.connect(container_id, aliases=network_aliases)
3133

3234
def remove(self) -> None:
@@ -40,5 +42,5 @@ def create(self) -> "Network":
4042
def __enter__(self) -> "Network":
4143
return self.create()
4244

43-
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
45+
def __exit__(self, exc_type, exc_val, exc_tb) -> None: # type: ignore[no-untyped-def]
4446
self.remove()

0 commit comments

Comments
 (0)