Skip to content

Commit dda48e6

Browse files
authored
Improve type annotations in core functions (#850)
1 parent 7b3d86f commit dda48e6

34 files changed

+687
-358
lines changed

.github/workflows/ci-cd.yml

+19
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,22 @@ jobs:
113113
run: |
114114
python -m pip install --upgrade pip
115115
python -m pip install -e .[ci,dev]
116+
# NOTE: Unfortunately the gain of image caching is too small,
117+
# and even it increases the latency of Linux jobs. :(
118+
# - name: Cache Docker images
119+
# uses: ScribeMD/docker-cache@0.5.0
120+
# with:
121+
# key: docker-${{ runner.os }}
122+
- name: Prepare Docker images (Linux)
123+
if: ${{ runner.os == 'Linux' }}
124+
run: |
125+
docker pull python:3.12-alpine
126+
- name: Prepare Docker images (Windows)
127+
if: ${{ runner.os == 'Windows' }}
128+
# Unfortunately, there is no slim version for Windows.
129+
# This may take more than 10 minutes as the image size is a few gigabytes.
130+
run: |
131+
docker pull python:3.12
116132
- name: Start Docker services
117133
if: ${{ matrix.registry == '1' }}
118134
run: |
@@ -134,6 +150,9 @@ jobs:
134150
path: coverage-unit-${{ matrix.python-version }}-${{ matrix.os }}-${{ matrix.registry }}.xml
135151
if-no-files-found: error
136152
retention-days: 1
153+
- name: Clean up Docker images produced during tests
154+
run: |
155+
docker image list --filter 'reference=aiodocker-*' --format '{{.Repository}}:{{.Tag}}' | xargs -r docker rmi
137156
138157
check: # This job does nothing and is only used for the branch protection
139158
name: ✅ Ensure the required checks passing

CHANGES/850.misc

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add more type annotations to the core APIs and retire codes for Python 3.7 compatibility

aiodocker/channel.py

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
import asyncio
24

35

aiodocker/configs.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
from __future__ import annotations
2+
13
import json
24
from base64 import b64encode
3-
from typing import Any, List, Mapping, Optional
5+
from typing import Any, List, Mapping, Optional, Sequence
46

57
from .utils import clean_filters, clean_map
68

@@ -9,7 +11,11 @@ class DockerConfigs:
911
def __init__(self, docker):
1012
self.docker = docker
1113

12-
async def list(self, *, filters: Optional[Mapping] = None) -> List[Mapping]:
14+
async def list(
15+
self,
16+
*,
17+
filters: Optional[Mapping[str, str | Sequence[str]]] = None,
18+
) -> List[Mapping]:
1319
"""
1420
Return a list of configs
1521

aiodocker/containers.py

+110-35
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,58 @@
1+
from __future__ import annotations
2+
13
import json
24
import shlex
35
import tarfile
4-
from typing import Any, Dict, Mapping, Optional, Sequence, Tuple, Union
5-
6+
from typing import (
7+
TYPE_CHECKING,
8+
Any,
9+
AsyncIterator,
10+
Dict,
11+
List,
12+
Literal,
13+
Mapping,
14+
Optional,
15+
Sequence,
16+
Tuple,
17+
Union,
18+
overload,
19+
)
20+
21+
from aiohttp import ClientWebSocketResponse
622
from multidict import MultiDict
723
from yarl import URL
824

25+
from aiodocker.types import JSONObject
26+
927
from .exceptions import DockerContainerError, DockerError
1028
from .execs import Exec
1129
from .jsonstream import json_stream_list, json_stream_stream
1230
from .logs import DockerLog
1331
from .multiplexed import multiplexed_result_list, multiplexed_result_stream
1432
from .stream import Stream
33+
from .types import PortInfo
1534
from .utils import identical, parse_result
1635

1736

37+
if TYPE_CHECKING:
38+
from .docker import Docker
39+
40+
1841
class DockerContainers:
19-
def __init__(self, docker):
42+
def __init__(self, docker: Docker) -> None:
2043
self.docker = docker
2144

22-
async def list(self, **kwargs):
45+
async def list(self, **kwargs) -> List[DockerContainer]:
2346
data = await self.docker._query_json(
2447
"containers/json", method="GET", params=kwargs
2548
)
2649
return [DockerContainer(self.docker, **x) for x in data]
2750

28-
async def create_or_replace(self, name, config):
51+
async def create_or_replace(
52+
self,
53+
name: str,
54+
config: JSONObject,
55+
) -> DockerContainer:
2956
container = None
3057

3158
try:
@@ -44,25 +71,29 @@ async def create_or_replace(self, name, config):
4471

4572
return container
4673

47-
async def create(self, config, *, name=None):
74+
async def create(
75+
self,
76+
config: JSONObject,
77+
*,
78+
name: Optional[str] = None,
79+
) -> DockerContainer:
4880
url = "containers/create"
49-
50-
config = json.dumps(config, sort_keys=True).encode("utf-8")
81+
encoded_config = json.dumps(config, sort_keys=True).encode("utf-8")
5182
kwargs = {}
5283
if name:
5384
kwargs["name"] = name
5485
data = await self.docker._query_json(
55-
url, method="POST", data=config, params=kwargs
86+
url, method="POST", data=encoded_config, params=kwargs
5687
)
5788
return DockerContainer(self.docker, id=data["Id"])
5889

5990
async def run(
6091
self,
61-
config,
92+
config: JSONObject,
6293
*,
6394
auth: Optional[Union[Mapping, str, bytes]] = None,
6495
name: Optional[str] = None,
65-
):
96+
) -> DockerContainer:
6697
"""
6798
Create and start a container.
6899
@@ -77,7 +108,7 @@ async def run(
77108
except DockerError as err:
78109
# image not found, try pulling it
79110
if err.status == 404 and "Image" in config:
80-
await self.docker.pull(config["Image"], auth=auth)
111+
await self.docker.pull(str(config["Image"]), auth=auth)
81112
container = await self.create(config, name=name)
82113
else:
83114
raise err
@@ -91,26 +122,28 @@ async def run(
91122

92123
return container
93124

94-
async def get(self, container, **kwargs):
125+
async def get(self, container_id: str, **kwargs) -> DockerContainer:
95126
data = await self.docker._query_json(
96-
f"containers/{container}/json",
127+
f"containers/{container_id}/json",
97128
method="GET",
98129
params=kwargs,
99130
)
100131
return DockerContainer(self.docker, **data)
101132

102-
def container(self, container_id, **kwargs):
133+
def container(self, container_id: str, **kwargs) -> DockerContainer:
103134
data = {"id": container_id}
104135
data.update(kwargs)
105136
return DockerContainer(self.docker, **data)
106137

107138
def exec(self, exec_id: str) -> Exec:
108139
"""Return Exec instance for already created exec object."""
109-
return Exec(self.docker, exec_id, None)
140+
return Exec(self.docker, exec_id)
110141

111142

112143
class DockerContainer:
113-
def __init__(self, docker, **kwargs):
144+
_container: Dict[str, Any]
145+
146+
def __init__(self, docker: Docker, **kwargs) -> None:
114147
self.docker = docker
115148
self._container = kwargs
116149
self._id = self._container.get(
@@ -122,17 +155,42 @@ def __init__(self, docker, **kwargs):
122155
def id(self) -> str:
123156
return self._id
124157

125-
def log(self, *, stdout=False, stderr=False, follow=False, **kwargs):
158+
@overload
159+
async def log(
160+
self,
161+
*,
162+
stdout: bool = False,
163+
stderr: bool = False,
164+
follow: Literal[False] = False,
165+
**kwargs,
166+
) -> List[str]: ...
167+
168+
@overload
169+
def log(
170+
self,
171+
*,
172+
stdout: bool = False,
173+
stderr: bool = False,
174+
follow: Literal[True],
175+
**kwargs,
176+
) -> AsyncIterator[str]: ...
177+
178+
def log(
179+
self,
180+
*,
181+
stdout: bool = False,
182+
stderr: bool = False,
183+
follow: bool = False,
184+
**kwargs,
185+
) -> Any:
126186
if stdout is False and stderr is False:
127187
raise TypeError("Need one of stdout or stderr")
128188

129189
params = {"stdout": stdout, "stderr": stderr, "follow": follow}
130190
params.update(kwargs)
131-
132191
cm = self.docker._query(
133192
f"containers/{self._id}/logs", method="GET", params=params
134193
)
135-
136194
if follow:
137195
return self._logs_stream(cm)
138196
else:
@@ -154,7 +212,6 @@ async def _logs_list(self, cm):
154212
try:
155213
inspect_info = await self.show()
156214
except DockerError:
157-
cm.cancel()
158215
raise
159216
is_tty = inspect_info["Config"]["Tty"]
160217

@@ -181,20 +238,20 @@ async def put_archive(self, path, data):
181238
data = await parse_result(response)
182239
return data
183240

184-
async def show(self, **kwargs):
241+
async def show(self, **kwargs) -> Dict[str, Any]:
185242
data = await self.docker._query_json(
186243
f"containers/{self._id}/json", method="GET", params=kwargs
187244
)
188245
self._container = data
189246
return data
190247

191-
async def stop(self, **kwargs):
248+
async def stop(self, **kwargs) -> None:
192249
async with self.docker._query(
193250
f"containers/{self._id}/stop", method="POST", params=kwargs
194251
):
195252
pass
196253

197-
async def start(self, **kwargs):
254+
async def start(self, **kwargs) -> None:
198255
async with self.docker._query(
199256
f"containers/{self._id}/start",
200257
method="POST",
@@ -203,7 +260,7 @@ async def start(self, **kwargs):
203260
):
204261
pass
205262

206-
async def restart(self, timeout=None):
263+
async def restart(self, timeout=None) -> None:
207264
params = {}
208265
if timeout is not None:
209266
params["t"] = timeout
@@ -214,13 +271,13 @@ async def restart(self, timeout=None):
214271
):
215272
pass
216273

217-
async def kill(self, **kwargs):
274+
async def kill(self, **kwargs) -> None:
218275
async with self.docker._query(
219276
f"containers/{self._id}/kill", method="POST", params=kwargs
220277
):
221278
pass
222279

223-
async def wait(self, *, timeout=None, **kwargs):
280+
async def wait(self, *, timeout=None, **kwargs) -> Dict[str, Any]:
224281
data = await self.docker._query_json(
225282
f"containers/{self._id}/wait",
226283
method="POST",
@@ -229,13 +286,13 @@ async def wait(self, *, timeout=None, **kwargs):
229286
)
230287
return data
231288

232-
async def delete(self, **kwargs):
289+
async def delete(self, **kwargs) -> None:
233290
async with self.docker._query(
234291
f"containers/{self._id}", method="DELETE", params=kwargs
235292
):
236293
pass
237294

238-
async def rename(self, newname):
295+
async def rename(self, newname) -> None:
239296
async with self.docker._query(
240297
f"containers/{self._id}/rename",
241298
method="POST",
@@ -244,7 +301,7 @@ async def rename(self, newname):
244301
):
245302
pass
246303

247-
async def websocket(self, **params):
304+
async def websocket(self, **params) -> ClientWebSocketResponse:
248305
if not params:
249306
params = {"stdin": True, "stdout": True, "stderr": True, "stream": True}
250307
path = f"containers/{self._id}/attach/ws"
@@ -280,7 +337,7 @@ async def setup() -> Tuple[URL, Optional[bytes], bool]:
280337

281338
return Stream(self.docker, setup, None)
282339

283-
async def port(self, private_port):
340+
async def port(self, private_port: int | str) -> List[PortInfo] | None:
284341
if "NetworkSettings" not in self._container:
285342
await self.show()
286343

@@ -302,7 +359,25 @@ async def port(self, private_port):
302359

303360
return h_ports
304361

305-
def stats(self, *, stream=True):
362+
@overload
363+
def stats(
364+
self,
365+
*,
366+
stream: Literal[True] = True,
367+
) -> AsyncIterator[Dict[str, Any]]: ...
368+
369+
@overload
370+
async def stats(
371+
self,
372+
*,
373+
stream: Literal[False],
374+
) -> List[Dict[str, Any]]: ...
375+
376+
def stats(
377+
self,
378+
*,
379+
stream: bool = True,
380+
) -> Any:
306381
cm = self.docker._query(
307382
f"containers/{self._id}/stats",
308383
params={"stream": "1" if stream else "0"},
@@ -333,7 +408,7 @@ async def exec(
333408
environment: Optional[Union[Mapping[str, str], Sequence[str]]] = None,
334409
workdir: Optional[str] = None,
335410
detach_keys: Optional[str] = None,
336-
):
411+
) -> Exec:
337412
if isinstance(cmd, str):
338413
cmd = shlex.split(cmd)
339414
if environment is None:
@@ -418,8 +493,8 @@ async def unpause(self) -> None:
418493
async with self.docker._query(f"containers/{self._id}/unpause", method="POST"):
419494
pass
420495

421-
def __getitem__(self, key):
496+
def __getitem__(self, key: str) -> Any:
422497
return self._container[key]
423498

424-
def __hasitem__(self, key):
499+
def __hasitem__(self, key: str) -> bool:
425500
return key in self._container

0 commit comments

Comments
 (0)