Skip to content

Commit 7a6145a

Browse files
authored
Rework filters to support passing multiple filters of the same type (#635)
1 parent de653e6 commit 7a6145a

File tree

14 files changed

+441
-136
lines changed

14 files changed

+441
-136
lines changed

python_on_whales/components/container/cli_wrapper.py

Lines changed: 50 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import json
55
import shlex
66
import textwrap
7+
import warnings
78
from datetime import datetime, timedelta
89
from pathlib import Path
910
from typing import (
@@ -16,12 +17,12 @@
1617
Optional,
1718
Sequence,
1819
Tuple,
19-
TypedDict,
2020
Union,
2121
overload,
2222
)
2323

2424
import pydantic
25+
from typing_extensions import TypeAlias
2526

2627
import python_on_whales.components.image.cli_wrapper
2728
import python_on_whales.components.network.cli_wrapper
@@ -46,7 +47,6 @@
4647
ValidPath,
4748
ValidPortMapping,
4849
custom_parse_object_as,
49-
format_mapping_for_cli,
5050
format_port_arg,
5151
format_signal_arg,
5252
format_time_arg,
@@ -58,29 +58,31 @@
5858
to_seconds,
5959
)
6060

61-
DockerContainerListFilters = TypedDict(
62-
"DockerContainerListFilters",
63-
{
64-
"id": str,
65-
"name": str,
66-
"label": str,
67-
"exited": int,
68-
"status": Literal[
61+
ContainerListFilter: TypeAlias = Union[
62+
Tuple[Literal["id"], str],
63+
Tuple[Literal["name"], str],
64+
Tuple[Literal["label"], str],
65+
Tuple[Literal["label!"], str],
66+
Tuple[Literal["exited"], int],
67+
Tuple[
68+
Literal["status"],
69+
Literal[
6970
"created", "restarting", "running", "removing", "paused", "exited", "dead"
7071
],
71-
"ancestor": str,
72-
"before": str, # TODO: allow datetime
73-
"since": str, # TODO: allow datetime
74-
"volume": str, # TODO: allow Volumes
75-
"network": str, # TODO: allow Network
76-
"publish": str,
77-
"expose": str,
78-
"health": Literal["starting", "healthy", "unhealthy", "none"],
79-
"isolation": Literal["default", "process", "hyperv"],
80-
"is-task": str, # TODO: allow bool
81-
},
82-
total=False,
83-
)
72+
],
73+
Tuple[Literal["ancestor"], str],
74+
Tuple[Literal["before"], str],
75+
Tuple[Literal["since"], str],
76+
Tuple[Literal["volume"], str], # TODO: allow Volumes
77+
Tuple[Literal["network"], str], # TODO: allow Network
78+
Tuple[Literal["pod"], str], # TODO: allow Pod
79+
Tuple[Literal["publish"], str],
80+
Tuple[Literal["expose"], str],
81+
Tuple[Literal["health"], Literal["starting", "healthy", "unhealthy", "none"]],
82+
Tuple[Literal["isolation"], Literal["default", "process", "hyperv"]],
83+
Tuple[Literal["is-task"], str], # TODO: allow bool
84+
Tuple[Literal["until"], str], # TODO: allow datetime
85+
]
8486

8587

8688
class Container(ReloadableObjectFromJson):
@@ -1179,24 +1181,33 @@ def logs(
11791181
return "".join(x[1].decode() for x in iterator)
11801182

11811183
def list(
1182-
self, all: bool = False, filters: DockerContainerListFilters = {}
1184+
self,
1185+
all: bool = False,
1186+
filters: Union[Iterable[ContainerListFilter], Mapping[str, Any]] = (),
11831187
) -> List[Container]:
11841188
"""List the containers on the host.
11851189
11861190
Alias: `docker.ps(...)`
11871191
11881192
Parameters:
11891193
all: If `True`, also returns containers that are not running.
1194+
filters: Filters to apply when listing containers.
11901195
11911196
# Returns
11921197
A `List[python_on_whales.Container]`
11931198
"""
1199+
if isinstance(filters, Mapping):
1200+
filters = filters.items()
1201+
warnings.warn(
1202+
"Passing filters as a mapping is deprecated, replace with an "
1203+
"iterable of tuples instead, as so:\n"
1204+
f"filters={list(filters)}",
1205+
DeprecationWarning,
1206+
)
11941207
full_cmd = self.docker_cmd
11951208
full_cmd += ["container", "list", "-q", "--no-trunc"]
1196-
full_cmd.add_args_iterable_or_single(
1197-
"--filter", format_mapping_for_cli(filters)
1198-
)
11991209
full_cmd.add_flag("--all", all)
1210+
full_cmd.add_args_iterable("--filter", (f"{f[0]}={f[1]}" for f in filters))
12001211

12011212
# TODO: add a test for the fix of is_immutable_id, without it, we get
12021213
# race conditions (we read the attributes of a container but it might not exist.
@@ -1230,41 +1241,41 @@ def pause(
12301241
@overload
12311242
def prune(
12321243
self,
1233-
filters: Dict[str, str] = {},
1244+
filters: Union[Iterable[ContainerListFilter], Mapping[str, Any]] = (),
12341245
stream_logs: Literal[True] = ...,
12351246
) -> Iterable[Tuple[str, bytes]]: ...
12361247

12371248
@overload
12381249
def prune(
12391250
self,
1240-
filters: Dict[str, str] = {},
1251+
filters: Union[Iterable[ContainerListFilter], Mapping[str, Any]] = (),
12411252
stream_logs: Literal[False] = ...,
12421253
) -> None: ...
12431254

12441255
def prune(
12451256
self,
1246-
filters: Dict[str, str] = {},
1257+
filters: Union[Iterable[ContainerListFilter], Mapping[str, Any]] = (),
12471258
stream_logs: bool = False,
12481259
):
12491260
"""Remove containers that are not running.
12501261
12511262
Parameters:
1252-
filters: Filters as strings or list of strings
1263+
filters: Filters to apply when pruning.
12531264
stream_logs: If `True` this function will return an iterator of strings.
12541265
You can then read the logs as they arrive. If `False` (the default value), then
12551266
the function returns `None`, but when it returns, then the prune operation has already been
12561267
done.
12571268
"""
1258-
if isinstance(filter, list):
1259-
raise TypeError(
1260-
"since python-on-whales 0.38.0, the filter argument is expected to be "
1261-
"a dict, not a list, please replace your function call by "
1262-
"docker.container.prune(filters={...})"
1269+
if isinstance(filters, Mapping):
1270+
filters = filters.items()
1271+
warnings.warn(
1272+
"Passing filters as a mapping is deprecated, replace with an "
1273+
"iterable of tuples instead, as so:\n"
1274+
f"filters={list(filters)}",
1275+
DeprecationWarning,
12631276
)
12641277
full_cmd = self.docker_cmd + ["container", "prune", "--force"]
1265-
full_cmd.add_args_iterable_or_single(
1266-
"--filter", format_mapping_for_cli(filters)
1267-
)
1278+
full_cmd.add_args_iterable("--filter", (f"{f[0]}={f[1]}" for f in filters))
12681279
if stream_logs:
12691280
return stream_stdout_and_stderr(full_cmd)
12701281
run(full_cmd)

python_on_whales/components/image/cli_wrapper.py

Lines changed: 51 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,20 @@
66
from multiprocessing.pool import ThreadPool
77
from pathlib import Path
88
from subprocess import PIPE, Popen
9-
from typing import Any, Iterable, Iterator, List, Mapping, Optional, Union, overload
9+
from typing import (
10+
Any,
11+
Iterable,
12+
Iterator,
13+
List,
14+
Literal,
15+
Mapping,
16+
Optional,
17+
Tuple,
18+
Union,
19+
overload,
20+
)
21+
22+
from typing_extensions import TypeAlias
1023

1124
import python_on_whales.components.buildx.cli_wrapper
1225
from python_on_whales.client_config import (
@@ -26,6 +39,21 @@
2639
from python_on_whales.exceptions import DockerException, NoSuchImage
2740
from python_on_whales.utils import ValidPath, run, stream_stdout_and_stderr, to_list
2841

42+
ImageListFilter: TypeAlias = Union[
43+
Tuple[Literal["id"], str],
44+
Tuple[Literal["reference"], str],
45+
Tuple[Literal["digest"], str],
46+
Tuple[Literal["label"], str],
47+
Tuple[Literal["label!"], str],
48+
Tuple[Literal["before"], str],
49+
Tuple[Literal["after", "since"], str],
50+
Tuple[Literal["until"], str], # TODO: allow datetime
51+
Tuple[Literal["dangling"], str], # TODO: allow bool
52+
Tuple[Literal["intermediate"], str], # TODO: allow bool
53+
Tuple[Literal["manifest"], str], # TODO: allow bool
54+
Tuple[Literal["readonly"], str], # TODO: allow bool
55+
]
56+
2957

3058
class Image(ReloadableObjectFromJson):
3159
def __init__(
@@ -406,7 +434,7 @@ def _load_from_generator(self, full_cmd: List[str], input: Iterator[bytes]):
406434
def list(
407435
self,
408436
repository_or_tag: Optional[str] = None,
409-
filters: Mapping[str, str] = {},
437+
filters: Union[Iterable[ImageListFilter], Mapping[str, Any]] = (),
410438
all: bool = False,
411439
) -> List[Image]:
412440
"""Returns the list of Docker images present on the machine.
@@ -418,29 +446,22 @@ def list(
418446
# Returns
419447
A `List[python_on_whales.Image]` object.
420448
"""
421-
# previously the signature was
422-
# def list(self,filters: Dict[str, str] = {}) -> List[Image]:
423-
# so to avoid breakages when people used positional arguments, we can check the types and send a warning
424-
if isinstance(repository_or_tag, dict):
425-
# after a while, we can convert that to an error. No hurry though.
449+
if isinstance(filters, Mapping):
450+
filters = filters.items()
426451
warnings.warn(
427-
f"You are calling docker.image.list({repository_or_tag}) with the filter as the first argument."
428-
f"Since Python-on-whales v0.51.0, the first argument has be changed to `repository_or_tag`."
429-
f"To fix this warning, please add the filters keyword argument, "
430-
f"like so: docker.image.list(filters={repository_or_tag}) ",
452+
"Passing filters as a mapping is deprecated, replace with an "
453+
"iterable of tuples instead, as so:\n"
454+
f"filters={list(filters)}",
431455
DeprecationWarning,
432456
)
433-
filters = repository_or_tag
434-
repository_or_tag = None
435-
436457
full_cmd = self.docker_cmd + [
437458
"image",
438459
"list",
439460
"--quiet",
440461
"--no-trunc",
441462
]
442-
full_cmd.add_args_mapping("--filter", filters)
443463
full_cmd.add_flag("--all", all)
464+
full_cmd.add_args_iterable("--filter", (f"{f[0]}={f[1]}" for f in filters))
444465

445466
if repository_or_tag is not None:
446467
full_cmd.append(repository_or_tag)
@@ -451,19 +472,31 @@ def list(
451472

452473
return [Image(self.client_config, x, is_immutable_id=True) for x in ids]
453474

454-
def prune(self, all: bool = False, filter: Mapping[str, str] = {}) -> str:
475+
def prune(
476+
self,
477+
all: bool = False,
478+
filters: Union[Iterable[ImageListFilter], Mapping[str, Any]] = (),
479+
) -> str:
455480
"""Remove unused images
456481
457482
Parameters:
458483
all: Remove all unused images, not just dangling ones
459-
filter: Provide filter values (e.g. `{"until": "<timestamp>"}`)
484+
filters: Provide filter values (e.g. `[("until", "<timestamp>")]`)
460485
461486
Returns:
462487
The output of the CLI (the layers removed).
463488
"""
489+
if isinstance(filters, Mapping):
490+
filters = filters.items()
491+
warnings.warn(
492+
"Passing filters as a mapping is deprecated, replace with an "
493+
"iterable of tuples instead, as so:\n"
494+
f"filters={list(filters)}",
495+
DeprecationWarning,
496+
)
464497
full_cmd = self.docker_cmd + ["image", "prune", "--force"]
465498
full_cmd.add_flag("--all", all)
466-
full_cmd.add_args_mapping("--filter", filter)
499+
full_cmd.add_args_iterable("--filter", (f"{f[0]}={f[1]}" for f in filters))
467500
return run(full_cmd)
468501

469502
def pull(

0 commit comments

Comments
 (0)