1+ # type: ignore
12import subprocess
23from typing import Iterable , List , Optional , Tuple , Union
34
45import requests
5-
66from testcontainers .core .exceptions import NoSuchPortExposed
77from testcontainers .core .waiting_utils import wait_container_is_ready
88
@@ -14,6 +14,9 @@ class DockerCompose:
1414 Args:
1515 filepath: Relative directory containing the docker compose configuration file.
1616 compose_file_name: File name of the docker compose configuration file.
17+ compose_command: The command to use for docker compose. If not specified, a call to
18+ docker compose --help will be made to determine the correct command to use.
19+ If docker compose is not installed, docker-compose will be used.
1720 pull: Pull images before launching environment.
1821 build: Build images referenced in the configuration file.
1922 env_file: Path to an env file containing environment variables to pass to docker compose.
@@ -45,21 +48,19 @@ def __init__(
4548 self ,
4649 filepath : str ,
4750 compose_file_name : Union [str , Iterable ] = "docker-compose.yml" ,
51+ compose_command : str = None ,
4852 pull : bool = False ,
4953 build : bool = False ,
5054 env_file : Optional [str ] = None ,
5155 services : Optional [List [str ]] = None ,
5256 ) -> None :
5357 self .filepath = filepath
54- self .compose_file_names = (
55- [compose_file_name ]
56- if isinstance (compose_file_name , str )
57- else list (compose_file_name )
58- )
58+ self .compose_file_names = [compose_file_name ] if isinstance (compose_file_name , str ) else list (compose_file_name )
5959 self .pull = pull
6060 self .build = build
6161 self .env_file = env_file
6262 self .services = services
63+ self .compose_command = self ._get_compose_command (compose_command )
6364
6465 def __enter__ (self ) -> "DockerCompose" :
6566 self .start ()
@@ -68,14 +69,37 @@ def __enter__(self) -> "DockerCompose":
6869 def __exit__ (self , exc_type , exc_val , exc_tb ) -> None :
6970 self .stop ()
7071
72+ def _get_compose_command (self , command ):
73+ """
74+ Returns the basecommand parts used for the docker compose commands
75+ depending on the docker compose api.
76+
77+ Returns
78+ -------
79+ list[str]
80+ The docker compose command parts
81+ """
82+ if command :
83+ return command .split (" " )
84+
85+ if (
86+ subprocess .run (
87+ ["docker" , "compose" , "--help" ], stdout = subprocess .DEVNULL , stderr = subprocess .STDOUT
88+ ).returncode
89+ == 0
90+ ):
91+ return ["docker" , "compose" ]
92+
93+ return ["docker-compose" ]
94+
7195 def docker_compose_command (self ) -> List [str ]:
7296 """
7397 Returns command parts used for the docker compose commands
7498
7599 Returns:
76100 cmd: Docker compose command parts.
77101 """
78- docker_compose_cmd = [ "docker-compose" ]
102+ docker_compose_cmd = self . compose_command . copy ()
79103 for file in self .compose_file_names :
80104 docker_compose_cmd += ["-f" , file ]
81105 if self .env_file :
@@ -95,7 +119,6 @@ def start(self) -> None:
95119 up_cmd .append ("--build" )
96120 if self .services :
97121 up_cmd .extend (self .services )
98-
99122 self ._call_command (cmd = up_cmd )
100123
101124 def stop (self ) -> None :
@@ -105,7 +128,7 @@ def stop(self) -> None:
105128 down_cmd = self .docker_compose_command () + ["down" , "-v" ]
106129 self ._call_command (cmd = down_cmd )
107130
108- def get_logs (self ) -> Tuple [bytes , bytes ]:
131+ def get_logs (self ) -> Tuple [str , str ]:
109132 """
110133 Returns all log output from stdout and stderr
111134
@@ -122,9 +145,7 @@ def get_logs(self) -> Tuple[bytes, bytes]:
122145 )
123146 return result .stdout , result .stderr
124147
125- def exec_in_container (
126- self , service_name : str , command : List [str ]
127- ) -> Tuple [str , str , int ]:
148+ def exec_in_container (self , service_name : str , command : List [str ]) -> Tuple [str , str ]:
128149 """
129150 Executes a command in the container of one of the services.
130151
@@ -136,20 +157,14 @@ def exec_in_container(
136157 stdout: Standard output stream.
137158 stderr: Standard error stream.
138159 """
139- exec_cmd = (
140- self .docker_compose_command () + ["exec" , "-T" , service_name ] + command
141- )
160+ exec_cmd = self .docker_compose_command () + ["exec" , "-T" , service_name ] + command
142161 result = subprocess .run (
143162 exec_cmd ,
144163 cwd = self .filepath ,
145164 stdout = subprocess .PIPE ,
146165 stderr = subprocess .PIPE ,
147166 )
148- return (
149- result .stdout .decode ("utf-8" ),
150- result .stderr .decode ("utf-8" ),
151- result .returncode ,
152- )
167+ return result .stdout .decode ("utf-8" ), result .stderr .decode ("utf-8" ), result .returncode
153168
154169 def get_service_port (self , service_name : str , port : int ) -> int :
155170 """
@@ -179,15 +194,16 @@ def get_service_host(self, service_name: str, port: int) -> str:
179194
180195 def _get_service_info (self , service : str , port : int ) -> List [str ]:
181196 port_cmd = self .docker_compose_command () + ["port" , service , str (port )]
182- output = subprocess .check_output (port_cmd , cwd = self .filepath ).decode ("utf-8" )
197+ try :
198+ output = subprocess .check_output (port_cmd , cwd = self .filepath ).decode ("utf-8" )
199+ except subprocess .CalledProcessError as e :
200+ raise NoSuchPortExposed (str (e .stderr ))
183201 result = str (output ).rstrip ().split (":" )
184202 if len (result ) != 2 or not all (result ):
185203 raise NoSuchPortExposed (f"port { port } is not exposed for service { service } " )
186204 return result
187205
188- def _call_command (
189- self , cmd : Union [str , List [str ]], filepath : Optional [str ] = None
190- ) -> None :
206+ def _call_command (self , cmd : Union [str , List [str ]], filepath : Optional [str ] = None ) -> None :
191207 if filepath is None :
192208 filepath = self .filepath
193209 subprocess .call (cmd , cwd = filepath )
0 commit comments