Skip to content

Commit 59ec1ce

Browse files
feat: support with_copy_to (#976)
This PR supports copying files into the container before startup time: ```python DockerContainer("alpine") .with_command(["cat", "/tmp/copied.txt"]) .with_copy_into_container(src, "/tmp/copied.txt") ``` To support this, I had to change how the container is started: `run` is not longer used, but instead `create` and `start` are used now, in order to be able to make the `copy` before the container is actually running. Inspired by testcontainers/testcontainers-rs#730 where I did exactly the same feature for the rust implementation of testcontainers :) ---- No tests are failing ```bash > uv run pytest -v core/tests ... ======================================= 316 passed, 1 skipped in 160.87s (0:02:40) ======================================== ``` Co-authored-by: David Ankin <daveankin@gmail.com>
1 parent 23aca0b commit 59ec1ce

3 files changed

Lines changed: 63 additions & 5 deletions

File tree

core/testcontainers/core/container.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -204,10 +204,9 @@ def start(self) -> Self:
204204
else {}
205205
)
206206

207-
self._container = docker_client.run(
207+
self._container = docker_client.create(
208208
self.image,
209209
command=self._command,
210-
detach=True,
211210
environment=self.env,
212211
ports=cast("dict[int, Optional[int]]", self.ports),
213212
name=self._name,
@@ -216,14 +215,16 @@ def start(self) -> Self:
216215
**{**network_kwargs, **self._kwargs},
217216
)
218217

218+
for t in self._transferable_specs:
219+
self._transfer_into_container(*t)
220+
221+
docker_client.start(self._container)
222+
219223
if self._wait_strategy is not None:
220224
self._wait_strategy.wait_until_ready(self)
221225

222226
logger.info("Container started: %s", self._container.short_id)
223227

224-
for t in self._transferable_specs:
225-
self._transfer_into_container(*t)
226-
227228
return self
228229

229230
def stop(self, force: bool = True, delete_volume: bool = True) -> None:
@@ -330,6 +331,13 @@ def exec(self, command: Union[str, list[str]]) -> ExecResult:
330331
raise ContainerStartException("Container should be started before executing a command")
331332
return self._container.exec_run(command)
332333

334+
def wait(self) -> int:
335+
"""Wait for the container to stop and return its exit code."""
336+
if not self._container:
337+
raise ContainerStartException("Container should be started before waiting")
338+
result = self._container.wait()
339+
return int(result["StatusCode"])
340+
333341
def get_container_info(self) -> Optional[ContainerInspectInfo]:
334342
"""Get container information via docker inspect (lazy loaded).
335343

core/testcontainers/core/docker_client.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,43 @@ def run(
118118
)
119119
return container
120120

121+
@_wrapped_container_collection
122+
def create(
123+
self,
124+
image: str,
125+
command: Optional[Union[str, list[str]]] = None,
126+
environment: Optional[dict[str, str]] = None,
127+
ports: Optional[dict[int, Optional[int]]] = None,
128+
labels: Optional[dict[str, str]] = None,
129+
**kwargs: Any,
130+
) -> Container:
131+
"""Create a container without starting it, pulling the image first if not present locally."""
132+
if "network" not in kwargs and not get_docker_host():
133+
host_network = self.find_host_network()
134+
if host_network:
135+
kwargs["network"] = host_network
136+
137+
try:
138+
# This is more or less a replication of what the self.client.containers.start does internally
139+
self.client.images.get(image)
140+
except docker.errors.ImageNotFound:
141+
self.client.images.pull(image)
142+
143+
container = self.client.containers.create(
144+
image,
145+
command=command,
146+
environment=environment,
147+
ports=ports,
148+
labels=create_labels(image, labels),
149+
**kwargs,
150+
)
151+
return container
152+
153+
@_wrapped_container_collection
154+
def start(self, container: Container) -> None:
155+
"""Start a previously created container."""
156+
container.start()
157+
121158
@_wrapped_image_collection
122159
def build(
123160
self, path: str, tag: Optional[str], rm: bool = True, **kwargs: Any

core/tests/test_transferable.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,19 @@ def test_copy_into_container_at_startup(transferable: Transferable):
104104
assert result.output == b"hello world"
105105

106106

107+
def test_copy_into_startup_file(transferable: Transferable):
108+
destination_in_container = "/tmp/my_file"
109+
110+
container = DockerContainer("bash", command=f"cat {destination_in_container}")
111+
container.with_copy_into_container(transferable, destination_in_container)
112+
113+
with container:
114+
exit_code = container.wait()
115+
stdout, _ = container.get_logs()
116+
assert exit_code == 0
117+
assert stdout.decode() == "hello world"
118+
119+
107120
def test_copy_into_container_via_initializer(transferable: Transferable):
108121
destination_in_container = "/tmp/my_file"
109122
transferables: list[TransferSpec] = [(transferable, destination_in_container, 0o644)]

0 commit comments

Comments
 (0)