Skip to content

Commit 0b89c76

Browse files
authored
feat: add types for mypy (#101)
1 parent eed92e9 commit 0b89c76

File tree

12 files changed

+148
-128
lines changed

12 files changed

+148
-128
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ run in CI.
180180

181181
Use [black](https://pypi.org/project/black/) with default settings for
182182
formatting. You can also use `pylint` with `setup.cfg` as the configuration
183-
file.
183+
file as well as `mypy` for type checking.
184184

185185
# Contributing
186186

setup.cfg

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[metadata]
22
name = pytest-docker
3-
version = 3.0.0
3+
version = 3.1.0
44
description = Simple pytest fixtures for Docker and Docker Compose based tests
55
long_description = file: README.md
66
long_description_content_type = text/markdown
@@ -41,29 +41,27 @@ docker-compose-v1 =
4141
docker-compose >=1.27.3, <2.0
4242
tests =
4343
requests >=2.22.0, <3.0
44+
mypy >=0.500, <2.000
4445
pytest-pylint >=0.14.1, <1.0
4546
pytest-pycodestyle >=2.0.0, <3.0
47+
pytest-mypy >=0.10, <1.0
48+
types-requests >=2.31, <3.0
49+
types-setuptools >=69.0, <70.0
4650

4751
[options.entry_points]
4852
pytest11 =
4953
docker = pytest_docker
5054

5155
[tool:pytest]
52-
addopts = --verbose --pylint-rcfile=setup.cfg
53-
# --pylint --pycodestyle
54-
55-
[pycodestyle]
56-
max-line-length=120
57-
ignore=E4,E7,W3
56+
addopts = --verbose --mypy --pycodestyle --pylint-rcfile=setup.cfg --pylint
5857

5958
# Configuration for pylint
60-
[MASTER]
61-
ignore=CVS
62-
good-names=logger,e,i,j,n,m,f,_
59+
[pylint.MASTER]
60+
good-names = "logger,e,i,j,n,m,f,_"
6361

64-
[MESSAGES CONTROL]
65-
disable=all
66-
enable=unused-import,
62+
[pylint]
63+
disable = all
64+
enable = unused-import,
6765
fixme,
6866
useless-object-inheritance,
6967
unused-variable,
@@ -73,4 +71,15 @@ enable=unused-import,
7371
unreachable,
7472
invalid-name,
7573
logging-not-lazy,
76-
unnecesary-pass
74+
unnecesary-pass,
75+
broad-except
76+
77+
[pycodestyle]
78+
max-line-length=120
79+
ignore=E4,E7,W3
80+
81+
[mypy]
82+
strict = true
83+
mypy_path = "src/pytest_docker,tests"
84+
namespace_packages = true
85+
warn_unused_ignores = true

src/pytest_docker/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import pytest
2+
23
from .plugin import (
4+
docker_cleanup,
35
docker_compose_command,
46
docker_compose_file,
57
docker_compose_project_name,
68
docker_ip,
7-
docker_setup,
8-
docker_cleanup,
99
docker_services,
10+
docker_setup,
1011
)
1112

1213
__all__ = [

src/pytest_docker/plugin.py

Lines changed: 47 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,24 @@
44
import subprocess
55
import time
66
import timeit
7+
from typing import Any, Dict, Iterable, Iterator, List, Tuple, Union
78

89
import attr
9-
1010
import pytest
11+
from _pytest.config import Config
12+
from _pytest.fixtures import FixtureRequest
1113

1214

1315
@pytest.fixture
14-
def container_scope_fixture(request):
16+
def container_scope_fixture(request: FixtureRequest) -> Any:
1517
return request.config.getoption("--container-scope")
1618

17-
def containers_scope(fixture_name, config):
19+
20+
def containers_scope(fixture_name: str, config: Config) -> Any: # pylint: disable=unused-argument
1821
return config.getoption("--container-scope", "session")
1922

20-
def execute(command, success_codes=(0,)):
23+
24+
def execute(command: str, success_codes: Iterable[int] = (0,)) -> Union[bytes, Any]:
2125
"""Run a shell command."""
2226
try:
2327
output = subprocess.check_output(command, stderr=subprocess.STDOUT, shell=True)
@@ -29,14 +33,12 @@ def execute(command, success_codes=(0,)):
2933

3034
if status not in success_codes:
3135
raise Exception(
32-
'Command {} returned {}: """{}""".'.format(
33-
command, status, output.decode("utf-8")
34-
)
36+
'Command {} returned {}: """{}""".'.format(command, status, output.decode("utf-8"))
3537
)
3638
return output
3739

3840

39-
def get_docker_ip():
41+
def get_docker_ip() -> Union[str, Any]:
4042
# When talking to the Docker daemon via a UNIX socket, route all TCP
4143
# traffic to docker containers via the TCP loopback interface.
4244
docker_host = os.environ.get("DOCKER_HOST", "").strip()
@@ -50,19 +52,18 @@ def get_docker_ip():
5052

5153

5254
@pytest.fixture(scope=containers_scope)
53-
def docker_ip():
55+
def docker_ip() -> Union[str, Any]:
5456
"""Determine the IP address for TCP connections to Docker containers."""
5557

5658
return get_docker_ip()
5759

5860

5961
@attr.s(frozen=True)
6062
class Services:
63+
_docker_compose: Any = attr.ib()
64+
_services: Dict[Any, Dict[Any, Any]] = attr.ib(init=False, default=attr.Factory(dict))
6165

62-
_docker_compose = attr.ib()
63-
_services = attr.ib(init=False, default=attr.Factory(dict))
64-
65-
def port_for(self, service, container_port):
66+
def port_for(self, service: str, container_port: int) -> int:
6667
"""Return the "host" port for `service` and `container_port`.
6768
6869
E.g. If the service is defined like this:
@@ -78,16 +79,14 @@ def port_for(self, service, container_port):
7879
"""
7980

8081
# Lookup in the cache.
81-
cache = self._services.get(service, {}).get(container_port, None)
82+
cache: int = self._services.get(service, {}).get(container_port, None)
8283
if cache is not None:
8384
return cache
8485

8586
output = self._docker_compose.execute("port %s %d" % (service, container_port))
8687
endpoint = output.strip().decode("utf-8")
8788
if not endpoint:
88-
raise ValueError(
89-
'Could not detect port for "%s:%d".' % (service, container_port)
90-
)
89+
raise ValueError('Could not detect port for "%s:%d".' % (service, container_port))
9190

9291
# This handles messy output that might contain warnings or other text
9392
if len(endpoint.split("\n")) > 1:
@@ -101,7 +100,13 @@ def port_for(self, service, container_port):
101100

102101
return match
103102

104-
def wait_until_responsive(self, check, timeout, pause, clock=timeit.default_timer):
103+
def wait_until_responsive(
104+
self,
105+
check: Any,
106+
timeout: float,
107+
pause: float,
108+
clock: Any = timeit.default_timer,
109+
) -> None:
105110
"""Wait until a service is responsive."""
106111

107112
ref = clock()
@@ -115,20 +120,19 @@ def wait_until_responsive(self, check, timeout, pause, clock=timeit.default_time
115120
raise Exception("Timeout reached while waiting on service!")
116121

117122

118-
def str_to_list(arg):
123+
def str_to_list(arg: Union[str, List[Any], Tuple[Any]]) -> Union[List[Any], Tuple[Any]]:
119124
if isinstance(arg, (list, tuple)):
120125
return arg
121126
return [arg]
122127

123128

124129
@attr.s(frozen=True)
125130
class DockerComposeExecutor:
131+
_compose_command: str = attr.ib()
132+
_compose_files: Any = attr.ib(converter=str_to_list)
133+
_compose_project_name: str = attr.ib()
126134

127-
_compose_command = attr.ib()
128-
_compose_files = attr.ib(converter=str_to_list)
129-
_compose_project_name = attr.ib()
130-
131-
def execute(self, subcommand):
135+
def execute(self, subcommand: str) -> Union[bytes, Any]:
132136
command = self._compose_command
133137
for compose_file in self._compose_files:
134138
command += ' -f "{}"'.format(compose_file)
@@ -137,7 +141,7 @@ def execute(self, subcommand):
137141

138142

139143
@pytest.fixture(scope=containers_scope)
140-
def docker_compose_command():
144+
def docker_compose_command() -> str:
141145
"""Docker Compose command to use, it could be either `docker compose`
142146
for Docker Compose V2 or `docker-compose` for Docker Compose
143147
V1."""
@@ -146,40 +150,40 @@ def docker_compose_command():
146150

147151

148152
@pytest.fixture(scope=containers_scope)
149-
def docker_compose_file(pytestconfig):
153+
def docker_compose_file(pytestconfig: Any) -> str:
150154
"""Get an absolute path to the `docker-compose.yml` file. Override this
151155
fixture in your tests if you need a custom location."""
152156

153157
return os.path.join(str(pytestconfig.rootdir), "tests", "docker-compose.yml")
154158

155159

156160
@pytest.fixture(scope=containers_scope)
157-
def docker_compose_project_name():
161+
def docker_compose_project_name() -> str:
158162
"""Generate a project name using the current process PID. Override this
159163
fixture in your tests if you need a particular project name."""
160164

161165
return "pytest{}".format(os.getpid())
162166

163167

164-
def get_cleanup_command():
168+
def get_cleanup_command() -> Union[List[str], str]:
165169
return ["down -v"]
166170

167171

168172
@pytest.fixture(scope=containers_scope)
169-
def docker_cleanup():
173+
def docker_cleanup() -> Union[List[str], str]:
170174
"""Get the docker_compose command to be executed for test clean-up actions.
171175
Override this fixture in your tests if you need to change clean-up actions.
172176
Returning anything that would evaluate to False will skip this command."""
173177

174178
return get_cleanup_command()
175179

176180

177-
def get_setup_command():
181+
def get_setup_command() -> Union[List[str], str]:
178182
return ["up --build -d"]
179183

180184

181185
@pytest.fixture(scope=containers_scope)
182-
def docker_setup():
186+
def docker_setup() -> Union[List[str], str]:
183187
"""Get the docker_compose command to be executed for test setup actions.
184188
Override this fixture in your tests if you need to change setup actions.
185189
Returning anything that would evaluate to False will skip this command."""
@@ -189,12 +193,12 @@ def docker_setup():
189193

190194
@contextlib.contextmanager
191195
def get_docker_services(
192-
docker_compose_command,
193-
docker_compose_file,
194-
docker_compose_project_name,
195-
docker_setup,
196-
docker_cleanup,
197-
):
196+
docker_compose_command: str,
197+
docker_compose_file: str,
198+
docker_compose_project_name: str,
199+
docker_setup: Union[List[str], str],
200+
docker_cleanup: Union[List[str], str],
201+
) -> Iterator[Services]:
198202
docker_compose = DockerComposeExecutor(
199203
docker_compose_command, docker_compose_file, docker_compose_project_name
200204
)
@@ -222,12 +226,12 @@ def get_docker_services(
222226

223227
@pytest.fixture(scope=containers_scope)
224228
def docker_services(
225-
docker_compose_command,
226-
docker_compose_file,
227-
docker_compose_project_name,
228-
docker_setup,
229-
docker_cleanup,
230-
):
229+
docker_compose_command: str,
230+
docker_compose_file: str,
231+
docker_compose_project_name: str,
232+
docker_setup: str,
233+
docker_cleanup: str,
234+
) -> Iterator[Services]:
231235
"""Start all services from a docker compose file (`docker-compose up`).
232236
After test are finished, shutdown all services (`docker-compose down`)."""
233237

src/pytest_docker/py.typed

Whitespace-only changes.

tests/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
pytest_plugins = ["pytester"] # pylint: disable=invalid-name
1+
pytest_plugins = ["pytester"]

tests/containers/hello/server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from wsgiref.simple_server import make_server
33

44

5-
def test_app(_, start_response):
5+
def test_app(_, start_response): # type: ignore
66
# This path is set up as a volume in the test's docker-compose.yml,
77
# so we make sure that we really work with Docker Compose.
88
if path.exists("/test_volume"):

tests/test_docker_ip.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,32 @@
1+
from typing import Dict
12
from unittest import mock
23

34
import pytest
45
from pytest_docker.plugin import get_docker_ip
56

67

7-
def test_docker_ip_native():
8-
environ = {}
8+
def test_docker_ip_native() -> None:
9+
environ: Dict[str, str] = {}
910
with mock.patch("os.environ", environ):
1011
assert get_docker_ip() == "127.0.0.1"
1112

1213

13-
def test_docker_ip_remote():
14+
def test_docker_ip_remote() -> None:
1415
environ = {"DOCKER_HOST": "tcp://1.2.3.4:2376"}
1516
with mock.patch("os.environ", environ):
1617
assert get_docker_ip() == "1.2.3.4"
1718

1819

19-
def test_docker_ip_unix():
20+
def test_docker_ip_unix() -> None:
2021
environ = {"DOCKER_HOST": "unix:///run/user/1000/podman/podman.sock"}
2122
with mock.patch("os.environ", environ):
2223
assert get_docker_ip() == "127.0.0.1"
2324

2425

2526
@pytest.mark.parametrize("docker_host", ["http://1.2.3.4:2376"])
26-
def test_docker_ip_remote_invalid(docker_host):
27+
def test_docker_ip_remote_invalid(docker_host: str) -> None:
2728
environ = {"DOCKER_HOST": docker_host}
2829
with mock.patch("os.environ", environ):
2930
with pytest.raises(ValueError) as exc:
3031
print(get_docker_ip())
31-
assert str(exc.value) == (
32-
'Invalid value for DOCKER_HOST: "%s".' % (docker_host,)
33-
)
32+
assert str(exc.value) == ('Invalid value for DOCKER_HOST: "%s".' % (docker_host,))

0 commit comments

Comments
 (0)