77from re import split
88from subprocess import CompletedProcess
99from subprocess import run as subprocess_run
10+ from types import TracebackType
1011from typing import Any , Callable , Literal , Optional , TypeVar , Union , cast
1112from urllib .error import HTTPError , URLError
1213from urllib .request import urlopen
1819_WARNINGS = {"DOCKER_COMPOSE_GET_CONFIG" : "get_config is experimental, see testcontainers/testcontainers-python#669" }
1920
2021
21- def _ignore_properties (cls : type [_IPT ], dict_ : any ) -> _IPT :
22+ def _ignore_properties (cls : type [_IPT ], dict_ : Any ) -> _IPT :
2223 """omits extra fields like @JsonIgnoreProperties(ignoreUnknown = true)
2324
2425 https://gist.github.com/alexanderankin/2a4549ac03554a31bef6eaaf2eaf7fd5"""
@@ -30,23 +31,23 @@ def _ignore_properties(cls: type[_IPT], dict_: any) -> _IPT:
3031
3132
3233@dataclass
33- class PublishedPort :
34+ class PublishedPortModel :
3435 """
3536 Class that represents the response we get from compose when inquiring status
3637 via `DockerCompose.get_running_containers()`.
3738 """
3839
3940 URL : Optional [str ] = None
40- TargetPort : Optional [str ] = None
41- PublishedPort : Optional [str ] = None
41+ TargetPort : Optional [int ] = None
42+ PublishedPort : Optional [int ] = None
4243 Protocol : Optional [str ] = None
4344
44- def normalize (self ):
45+ def normalize (self ) -> "PublishedPortModel" :
4546 url_not_usable = system () == "Windows" and self .URL == "0.0.0.0"
4647 if url_not_usable :
4748 self_dict = asdict (self )
4849 self_dict .update ({"URL" : "127.0.0.1" })
49- return PublishedPort (** self_dict )
50+ return PublishedPortModel (** self_dict )
5051 return self
5152
5253
@@ -75,19 +76,19 @@ class ComposeContainer:
7576 Service : Optional [str ] = None
7677 State : Optional [str ] = None
7778 Health : Optional [str ] = None
78- ExitCode : Optional [str ] = None
79- Publishers : list [PublishedPort ] = field (default_factory = list )
79+ ExitCode : Optional [int ] = None
80+ Publishers : list [PublishedPortModel ] = field (default_factory = list )
8081
81- def __post_init__ (self ):
82+ def __post_init__ (self ) -> None :
8283 if self .Publishers :
83- self .Publishers = [_ignore_properties (PublishedPort , p ) for p in self .Publishers ]
84+ self .Publishers = [_ignore_properties (PublishedPortModel , p ) for p in self .Publishers ]
8485
8586 def get_publisher (
8687 self ,
8788 by_port : Optional [int ] = None ,
8889 by_host : Optional [str ] = None ,
89- prefer_ip_version : Literal ["IPV4 " , "IPv6" ] = "IPv4" ,
90- ) -> PublishedPort :
90+ prefer_ip_version : Literal ["IPv4 " , "IPv6" ] = "IPv4" ,
91+ ) -> PublishedPortModel :
9192 remaining_publishers = self .Publishers
9293
9394 remaining_publishers = [r for r in remaining_publishers if self ._matches_protocol (prefer_ip_version , r )]
@@ -109,8 +110,9 @@ def get_publisher(
109110 )
110111
111112 @staticmethod
112- def _matches_protocol (prefer_ip_version , r ):
113- return (":" in r .URL ) is (prefer_ip_version == "IPv6" )
113+ def _matches_protocol (prefer_ip_version : str , r : PublishedPortModel ) -> bool :
114+ r_url = r .URL
115+ return (r_url is not None and ":" in r_url ) is (prefer_ip_version == "IPv6" )
114116
115117
116118@dataclass
@@ -164,7 +166,7 @@ class DockerCompose:
164166 image: "hello-world"
165167 """
166168
167- context : Union [str , PathLike ]
169+ context : Union [str , PathLike [ str ] ]
168170 compose_file_name : Optional [Union [str , list [str ]]] = None
169171 pull : bool = False
170172 build : bool = False
@@ -175,15 +177,17 @@ class DockerCompose:
175177 docker_command_path : Optional [str ] = None
176178 profiles : Optional [list [str ]] = None
177179
178- def __post_init__ (self ):
180+ def __post_init__ (self ) -> None :
179181 if isinstance (self .compose_file_name , str ):
180182 self .compose_file_name = [self .compose_file_name ]
181183
182184 def __enter__ (self ) -> "DockerCompose" :
183185 self .start ()
184186 return self
185187
186- def __exit__ (self , exc_type , exc_val , exc_tb ) -> None :
188+ def __exit__ (
189+ self , exc_type : Optional [type [BaseException ]], exc_val : Optional [BaseException ], exc_tb : Optional [TracebackType ]
190+ ) -> None :
187191 self .stop (not self .keep_volumes )
188192
189193 def docker_compose_command (self ) -> list [str ]:
@@ -235,7 +239,7 @@ def start(self) -> None:
235239
236240 self ._run_command (cmd = up_cmd )
237241
238- def stop (self , down = True ) -> None :
242+ def stop (self , down : bool = True ) -> None :
239243 """
240244 Stops the docker compose environment.
241245 """
@@ -295,7 +299,7 @@ def get_config(
295299 cmd_output = self ._run_command (cmd = config_cmd ).stdout
296300 return cast (dict [str , Any ], loads (cmd_output )) # noqa: TC006
297301
298- def get_containers (self , include_all = False ) -> list [ComposeContainer ]:
302+ def get_containers (self , include_all : bool = False ) -> list [ComposeContainer ]:
299303 """
300304 Fetch information about running containers via `docker compose ps --format json`.
301305 Available only in V2 of compose.
@@ -370,17 +374,18 @@ def exec_in_container(
370374 """
371375 if not service_name :
372376 service_name = self .get_container ().Service
373- exec_cmd = [* self .compose_command_property , "exec" , "-T" , service_name , * command ]
377+ assert service_name
378+ exec_cmd : list [str ] = [* self .compose_command_property , "exec" , "-T" , service_name , * command ]
374379 result = self ._run_command (cmd = exec_cmd )
375380
376- return ( result .stdout .decode ("utf-8" ), result .stderr .decode ("utf-8" ), result .returncode )
381+ return result .stdout .decode ("utf-8" ), result .stderr .decode ("utf-8" ), result .returncode
377382
378383 def _run_command (
379384 self ,
380385 cmd : Union [str , list [str ]],
381386 context : Optional [str ] = None ,
382387 ) -> CompletedProcess [bytes ]:
383- context = context or self .context
388+ context = context or str ( self .context )
384389 return subprocess_run (
385390 cmd ,
386391 capture_output = True ,
@@ -392,7 +397,7 @@ def get_service_port(
392397 self ,
393398 service_name : Optional [str ] = None ,
394399 port : Optional [int ] = None ,
395- ):
400+ ) -> Optional [ int ] :
396401 """
397402 Returns the mapped port for one of the services.
398403
@@ -408,13 +413,14 @@ def get_service_port(
408413 str:
409414 The mapped port on the host
410415 """
411- return self .get_container (service_name ).get_publisher (by_port = port ).normalize ().PublishedPort
416+ normalize : PublishedPortModel = self .get_container (service_name ).get_publisher (by_port = port ).normalize ()
417+ return normalize .PublishedPort
412418
413419 def get_service_host (
414420 self ,
415421 service_name : Optional [str ] = None ,
416422 port : Optional [int ] = None ,
417- ):
423+ ) -> Optional [ str ] :
418424 """
419425 Returns the host for one of the services.
420426
@@ -430,13 +436,17 @@ def get_service_host(
430436 str:
431437 The hostname for the service
432438 """
433- return self .get_container (service_name ).get_publisher (by_port = port ).normalize ().URL
439+ container : ComposeContainer = self .get_container (service_name )
440+ publisher : PublishedPortModel = container .get_publisher (by_port = port )
441+ normalize : PublishedPortModel = publisher .normalize ()
442+ url : Optional [str ] = normalize .URL
443+ return url
434444
435445 def get_service_host_and_port (
436446 self ,
437447 service_name : Optional [str ] = None ,
438448 port : Optional [int ] = None ,
439- ):
449+ ) -> tuple [ Optional [ str ], Optional [ int ]] :
440450 publisher = self .get_container (service_name ).get_publisher (by_port = port ).normalize ()
441451 return publisher .URL , publisher .PublishedPort
442452
0 commit comments