From 1d697680d2286153448fb6fe6085251aeb3fad1e Mon Sep 17 00:00:00 2001 From: Mariano Scazzariello Date: Sat, 6 May 2023 13:49:01 +0200 Subject: [PATCH 01/45] Full support to networking config during container creation Signed-off-by: Mariano Scazzariello --- docker/models/containers.py | 44 +++++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/docker/models/containers.py b/docker/models/containers.py index 2eeefda1ee..5ba6297bd5 100644 --- a/docker/models/containers.py +++ b/docker/models/containers.py @@ -8,7 +8,7 @@ ContainerError, DockerException, ImageNotFound, NotFound, create_unexpected_kwargs_error ) -from ..types import HostConfig +from ..types import EndpointConfig, HostConfig, NetworkingConfig from ..utils import version_gte from .images import Image from .resource import Collection, Model @@ -680,10 +680,32 @@ def run(self, image, command=None, stdout=True, stderr=False, This mode is incompatible with ``ports``. Incompatible with ``network``. - network_driver_opt (dict): A dictionary of options to provide - to the network driver. Defaults to ``None``. Used in - conjuction with ``network``. Incompatible - with ``network_mode``. + network_config (dict): A dictionary containing options that are + passed to the network driver during the connection. + Defaults to ``None``. + The dictionary contains the following keys: + + - ``aliases`` (:py:class:`list`): A list of aliases for + the network endpoint. + Names in that list can be used within the network to + reach this container. Defaults to ``None``. + - ``links`` (:py:class:`list`): A list of links for + the network endpoint endpoint. + Containers declared in this list will be linked to this + container. Defaults to ``None``. + - ``ipv4_address`` (str): The IP address to assign to + this container on the network, using the IPv4 protocol. + Defaults to ``None``. + - ``ipv6_address`` (str): The IP address to assign to + this container on the network, using the IPv6 protocol. + Defaults to ``None``. + - ``link_local_ips`` (:py:class:`list`): A list of link-local + (IPv4/IPv6) addresses. + - ``driver_opt`` (dict): A dictionary of options to provide to + the network driver. Defaults to ``None``. + + Used in conjuction with ``network``. + Incompatible with ``network_mode``. oom_kill_disable (bool): Whether to disable OOM killer. oom_score_adj (int): An integer value containing the score given to the container in order to tune OOM killer preferences. @@ -1124,12 +1146,16 @@ def _create_container_args(kwargs): host_config_kwargs['binds'] = volumes network = kwargs.pop('network', None) - network_driver_opt = kwargs.pop('network_driver_opt', None) + network_config = kwargs.pop('network_config', None) if network: - network_configuration = {'driver_opt': network_driver_opt} \ - if network_driver_opt else None + network_configuration = EndpointConfig( + host_config_kwargs['version'], + **network_config + ) if network_config else None - create_kwargs['networking_config'] = {network: network_configuration} + create_kwargs['networking_config'] = NetworkingConfig( + {network: network_configuration} + ) host_config_kwargs['network_mode'] = network # All kwargs should have been consumed by this point, so raise From a662d5a3051e49ac12caef967245d9e718eb1cb3 Mon Sep 17 00:00:00 2001 From: Mariano Scazzariello Date: Sun, 7 May 2023 11:42:23 +0200 Subject: [PATCH 02/45] Fix pytests Signed-off-by: Mariano Scazzariello --- docker/models/containers.py | 4 +- tests/unit/models_containers_test.py | 55 +++++++++++++++++----------- 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/docker/models/containers.py b/docker/models/containers.py index 5ba6297bd5..1d2e58c641 100644 --- a/docker/models/containers.py +++ b/docker/models/containers.py @@ -870,9 +870,9 @@ def run(self, image, command=None, stdout=True, stderr=False, 'together.' ) - if kwargs.get('network_driver_opt') and not kwargs.get('network'): + if kwargs.get('network_config') and not kwargs.get('network'): raise RuntimeError( - 'The options "network_driver_opt" can not be used ' + 'The option "network_config" can not be used ' 'without "network".' ) diff --git a/tests/unit/models_containers_test.py b/tests/unit/models_containers_test.py index 0592af5e04..240b592fb6 100644 --- a/tests/unit/models_containers_test.py +++ b/tests/unit/models_containers_test.py @@ -1,12 +1,12 @@ +import pytest +import unittest + import docker -from docker.constants import DEFAULT_DATA_CHUNK_SIZE +from docker.constants import DEFAULT_DATA_CHUNK_SIZE, DEFAULT_DOCKER_API_VERSION from docker.models.containers import Container, _create_container_args from docker.models.images import Image -import unittest - from .fake_api import FAKE_CONTAINER_ID, FAKE_IMAGE_ID, FAKE_EXEC_ID from .fake_api_client import make_fake_client -import pytest class ContainerCollectionTest(unittest.TestCase): @@ -74,7 +74,7 @@ def test_create_container_args(self): name='somename', network_disabled=False, network='foo', - network_driver_opt={'key1': 'a'}, + network_config={'aliases': ['test'], 'driver_opt': {'key1': 'a'}}, oom_kill_disable=True, oom_score_adj=5, pid_mode='host', @@ -99,7 +99,7 @@ def test_create_container_args(self): user='bob', userns_mode='host', uts_mode='host', - version='1.23', + version=DEFAULT_DOCKER_API_VERSION, volume_driver='some_driver', volumes=[ '/home/user1/:/mnt/vol2', @@ -189,7 +189,9 @@ def test_create_container_args(self): mac_address='abc123', name='somename', network_disabled=False, - networking_config={'foo': {'driver_opt': {'key1': 'a'}}}, + networking_config={'EndpointsConfig': { + 'foo': {'Aliases': ['test'], 'DriverOpts': {'key1': 'a'}}} + }, platform='linux', ports=[('1111', 'tcp'), ('2222', 'tcp')], stdin_open=True, @@ -346,39 +348,44 @@ def test_run_platform(self): host_config={'NetworkMode': 'default'}, ) - def test_run_network_driver_opts_without_network(self): + def test_run_network_config_without_network(self): client = make_fake_client() with pytest.raises(RuntimeError): client.containers.run( image='alpine', - network_driver_opt={'key1': 'a'} + network_config={'aliases': ['test'], + 'driver_opt': {'key1': 'a'}} ) - def test_run_network_driver_opts_with_network_mode(self): + def test_run_network_config_with_network_mode(self): client = make_fake_client() with pytest.raises(RuntimeError): client.containers.run( image='alpine', network_mode='none', - network_driver_opt={'key1': 'a'} + network_config={'aliases': ['test'], + 'driver_opt': {'key1': 'a'}} ) - def test_run_network_driver_opts(self): + def test_run_network_config(self): client = make_fake_client() client.containers.run( image='alpine', network='foo', - network_driver_opt={'key1': 'a'} + network_config={'aliases': ['test'], + 'driver_opt': {'key1': 'a'}} ) client.api.create_container.assert_called_with( detach=False, image='alpine', command=None, - networking_config={'foo': {'driver_opt': {'key1': 'a'}}}, + networking_config={'EndpointsConfig': { + 'foo': {'Aliases': ['test'], 'DriverOpts': {'key1': 'a'}}} + }, host_config={'NetworkMode': 'foo'} ) @@ -409,12 +416,13 @@ def test_create_with_image_object(self): host_config={'NetworkMode': 'default'} ) - def test_create_network_driver_opts_without_network(self): + def test_create_network_config_without_network(self): client = make_fake_client() client.containers.create( image='alpine', - network_driver_opt={'key1': 'a'} + network_config={'aliases': ['test'], + 'driver_opt': {'key1': 'a'}} ) client.api.create_container.assert_called_with( @@ -423,13 +431,14 @@ def test_create_network_driver_opts_without_network(self): host_config={'NetworkMode': 'default'} ) - def test_create_network_driver_opts_with_network_mode(self): + def test_create_network_config_with_network_mode(self): client = make_fake_client() client.containers.create( image='alpine', network_mode='none', - network_driver_opt={'key1': 'a'} + network_config={'aliases': ['test'], + 'driver_opt': {'key1': 'a'}} ) client.api.create_container.assert_called_with( @@ -438,19 +447,22 @@ def test_create_network_driver_opts_with_network_mode(self): host_config={'NetworkMode': 'none'} ) - def test_create_network_driver_opts(self): + def test_create_network_config(self): client = make_fake_client() client.containers.create( image='alpine', network='foo', - network_driver_opt={'key1': 'a'} + network_config={'aliases': ['test'], + 'driver_opt': {'key1': 'a'}} ) client.api.create_container.assert_called_with( image='alpine', command=None, - networking_config={'foo': {'driver_opt': {'key1': 'a'}}}, + networking_config={'EndpointsConfig': { + 'foo': {'Aliases': ['test'], 'DriverOpts': {'key1': 'a'}}} + }, host_config={'NetworkMode': 'foo'} ) @@ -479,6 +491,7 @@ def test_list(self): def test_list_ignore_removed(self): def side_effect(*args, **kwargs): raise docker.errors.NotFound('Container not found') + client = make_fake_client({ 'inspect_container.side_effect': side_effect }) From a18f91bf08b4dca8dcf7627c8477a12ff2c1ca6a Mon Sep 17 00:00:00 2001 From: Mariano Scazzariello Date: Sun, 7 May 2023 11:49:59 +0200 Subject: [PATCH 03/45] Fix long line Signed-off-by: Mariano Scazzariello --- tests/unit/models_containers_test.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/unit/models_containers_test.py b/tests/unit/models_containers_test.py index 240b592fb6..f721bedbe3 100644 --- a/tests/unit/models_containers_test.py +++ b/tests/unit/models_containers_test.py @@ -2,7 +2,8 @@ import unittest import docker -from docker.constants import DEFAULT_DATA_CHUNK_SIZE, DEFAULT_DOCKER_API_VERSION +from docker.constants import DEFAULT_DATA_CHUNK_SIZE, \ + DEFAULT_DOCKER_API_VERSION from docker.models.containers import Container, _create_container_args from docker.models.images import Image from .fake_api import FAKE_CONTAINER_ID, FAKE_IMAGE_ID, FAKE_EXEC_ID From 7870503c523a130a2c8731df292eb904cd1a7345 Mon Sep 17 00:00:00 2001 From: Mariano Scazzariello Date: Sun, 7 May 2023 12:15:32 +0200 Subject: [PATCH 04/45] Fix case when "network_config" is not passed Signed-off-by: Mariano Scazzariello --- docker/models/containers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/models/containers.py b/docker/models/containers.py index 1d2e58c641..bc2ed011fc 100644 --- a/docker/models/containers.py +++ b/docker/models/containers.py @@ -1148,14 +1148,14 @@ def _create_container_args(kwargs): network = kwargs.pop('network', None) network_config = kwargs.pop('network_config', None) if network: - network_configuration = EndpointConfig( + endpoint_config = EndpointConfig( host_config_kwargs['version'], **network_config ) if network_config else None create_kwargs['networking_config'] = NetworkingConfig( - {network: network_configuration} - ) + {network: endpoint_config} + ) if endpoint_config else {network: None} host_config_kwargs['network_mode'] = network # All kwargs should have been consumed by this point, so raise From e011ff5be89f84f999847d73d73ff695b9c8c4d4 Mon Sep 17 00:00:00 2001 From: Mariano Scazzariello Date: Sun, 7 May 2023 12:40:08 +0200 Subject: [PATCH 05/45] More sanity checking of EndpointConfig params Signed-off-by: Mariano Scazzariello --- docker/models/containers.py | 29 ++++++-- tests/integration/models_containers_test.py | 57 ++++++++++++++++ tests/unit/models_containers_test.py | 75 +++++++++++++++++++++ 3 files changed, 157 insertions(+), 4 deletions(-) diff --git a/docker/models/containers.py b/docker/models/containers.py index bc2ed011fc..3312b0e2d8 100644 --- a/docker/models/containers.py +++ b/docker/models/containers.py @@ -703,6 +703,8 @@ def run(self, image, command=None, stdout=True, stderr=False, (IPv4/IPv6) addresses. - ``driver_opt`` (dict): A dictionary of options to provide to the network driver. Defaults to ``None``. + - ``mac_address`` (str): MAC Address to assign to the network + interface. Defaults to ``None``. Requires API >= 1.25. Used in conjuction with ``network``. Incompatible with ``network_mode``. @@ -1122,6 +1124,17 @@ def prune(self, filters=None): ] +NETWORKING_CONFIG_ARGS = [ + 'aliases', + 'links', + 'ipv4_address', + 'ipv6_address', + 'link_local_ips', + 'driver_opt', + 'mac_address' +] + + def _create_container_args(kwargs): """ Convert arguments to create() to arguments to create_container(). @@ -1148,10 +1161,18 @@ def _create_container_args(kwargs): network = kwargs.pop('network', None) network_config = kwargs.pop('network_config', None) if network: - endpoint_config = EndpointConfig( - host_config_kwargs['version'], - **network_config - ) if network_config else None + endpoint_config = None + + if network_config: + clean_endpoint_args = {} + for arg_name in NETWORKING_CONFIG_ARGS: + if arg_name in network_config: + clean_endpoint_args[arg_name] = network_config[arg_name] + + if clean_endpoint_args: + endpoint_config = EndpointConfig( + host_config_kwargs['version'], **clean_endpoint_args + ) create_kwargs['networking_config'] = NetworkingConfig( {network: endpoint_config} diff --git a/tests/integration/models_containers_test.py b/tests/integration/models_containers_test.py index eac4c97909..050efa01ca 100644 --- a/tests/integration/models_containers_test.py +++ b/tests/integration/models_containers_test.py @@ -104,6 +104,63 @@ def test_run_with_network(self): assert 'Networks' in attrs['NetworkSettings'] assert list(attrs['NetworkSettings']['Networks'].keys()) == [net_name] + def test_run_with_network_config(self): + net_name = random_name() + client = docker.from_env(version=TEST_API_VERSION) + client.networks.create(net_name) + self.tmp_networks.append(net_name) + + test_aliases = ['hello'] + test_driver_opt = {'key1': 'a'} + + container = client.containers.run( + 'alpine', 'echo hello world', network=net_name, + network_config={'aliases': test_aliases, + 'driver_opt': test_driver_opt}, + detach=True + ) + self.tmp_containers.append(container.id) + + attrs = container.attrs + + assert 'NetworkSettings' in attrs + assert 'Networks' in attrs['NetworkSettings'] + assert list(attrs['NetworkSettings']['Networks'].keys()) == [net_name] + assert attrs['NetworkSettings']['Networks'][net_name]['Aliases'] == \ + test_aliases + assert attrs['NetworkSettings']['Networks'][net_name]['DriverOpts'] \ + == test_driver_opt + + def test_run_with_network_config_undeclared_params(self): + net_name = random_name() + client = docker.from_env(version=TEST_API_VERSION) + client.networks.create(net_name) + self.tmp_networks.append(net_name) + + test_aliases = ['hello'] + test_driver_opt = {'key1': 'a'} + + container = client.containers.run( + 'alpine', 'echo hello world', network=net_name, + network_config={'aliases': test_aliases, + 'driver_opt': test_driver_opt, + 'undeclared_param': 'random_value'}, + detach=True + ) + self.tmp_containers.append(container.id) + + attrs = container.attrs + + assert 'NetworkSettings' in attrs + assert 'Networks' in attrs['NetworkSettings'] + assert list(attrs['NetworkSettings']['Networks'].keys()) == [net_name] + assert attrs['NetworkSettings']['Networks'][net_name]['Aliases'] == \ + test_aliases + assert attrs['NetworkSettings']['Networks'][net_name]['DriverOpts'] \ + == test_driver_opt + assert 'undeclared_param' not in \ + attrs['NetworkSettings']['Networks'][net_name] + def test_run_with_none_driver(self): client = docker.from_env(version=TEST_API_VERSION) diff --git a/tests/unit/models_containers_test.py b/tests/unit/models_containers_test.py index f721bedbe3..3425ea897b 100644 --- a/tests/unit/models_containers_test.py +++ b/tests/unit/models_containers_test.py @@ -390,6 +390,44 @@ def test_run_network_config(self): host_config={'NetworkMode': 'foo'} ) + def test_run_network_config_undeclared_params(self): + client = make_fake_client() + + client.containers.run( + image='alpine', + network='foo', + network_config={'aliases': ['test'], + 'driver_opt': {'key1': 'a'}, + 'undeclared_param': 'random_value'} + ) + + client.api.create_container.assert_called_with( + detach=False, + image='alpine', + command=None, + networking_config={'EndpointsConfig': { + 'foo': {'Aliases': ['test'], 'DriverOpts': {'key1': 'a'}}} + }, + host_config={'NetworkMode': 'foo'} + ) + + def test_run_network_config_only_undeclared_params(self): + client = make_fake_client() + + client.containers.run( + image='alpine', + network='foo', + network_config={'undeclared_param': 'random_value'} + ) + + client.api.create_container.assert_called_with( + detach=False, + image='alpine', + command=None, + networking_config={'foo': None}, + host_config={'NetworkMode': 'foo'} + ) + def test_create(self): client = make_fake_client() container = client.containers.create( @@ -467,6 +505,43 @@ def test_create_network_config(self): host_config={'NetworkMode': 'foo'} ) + def test_create_network_config_undeclared_params(self): + client = make_fake_client() + + client.containers.create( + image='alpine', + network='foo', + network_config={'aliases': ['test'], + 'driver_opt': {'key1': 'a'}, + 'undeclared_param': 'random_value'} + ) + + client.api.create_container.assert_called_with( + image='alpine', + command=None, + networking_config={'EndpointsConfig': { + 'foo': {'Aliases': ['test'], 'DriverOpts': {'key1': 'a'}}} + }, + host_config={'NetworkMode': 'foo'} + ) + + def test_create_network_config_only_undeclared_params(self): + client = make_fake_client() + + client.containers.create( + image='alpine', + network='foo', + network_config={'undeclared_param': 'random_value'} + ) + + client.api.create_container.assert_called_with( + image='alpine', + command=None, + networking_config={'foo': None}, + host_config={'NetworkMode': 'foo'} + ) + + def test_get(self): client = make_fake_client() container = client.containers.get(FAKE_CONTAINER_ID) From 0318ad8e7ee67c9ef0fbffaaf70029f255963012 Mon Sep 17 00:00:00 2001 From: Mariano Scazzariello Date: Mon, 15 May 2023 14:49:55 +0200 Subject: [PATCH 06/45] Fix blank line Signed-off-by: Mariano Scazzariello --- tests/unit/models_containers_test.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit/models_containers_test.py b/tests/unit/models_containers_test.py index 3425ea897b..f6dccaaba1 100644 --- a/tests/unit/models_containers_test.py +++ b/tests/unit/models_containers_test.py @@ -541,7 +541,6 @@ def test_create_network_config_only_undeclared_params(self): host_config={'NetworkMode': 'foo'} ) - def test_get(self): client = make_fake_client() container = client.containers.get(FAKE_CONTAINER_ID) From 84414e343e526cf93f285284dd2c2c40f703e4a9 Mon Sep 17 00:00:00 2001 From: Hao Yu <129033897+Longin-Yu@users.noreply.github.com> Date: Wed, 7 Jun 2023 02:28:15 +0800 Subject: [PATCH 07/45] fix user_guides/multiplex.rst (#3130) Signed-off-by: Longin-Yu --- docs/user_guides/multiplex.rst | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/docs/user_guides/multiplex.rst b/docs/user_guides/multiplex.rst index 78d7e3728d..7add69b121 100644 --- a/docs/user_guides/multiplex.rst +++ b/docs/user_guides/multiplex.rst @@ -16,10 +16,13 @@ Prepare the command we are going to use. It prints "hello stdout" in `stdout`, followed by "hello stderr" in `stderr`: >>> cmd = '/bin/sh -c "echo hello stdout ; echo hello stderr >&2"' + We'll run this command with all four the combinations of ``stream`` and ``demux``. + With ``stream=False`` and ``demux=False``, the output is a string that contains both the `stdout` and the `stderr` output: + >>> res = container.exec_run(cmd, stream=False, demux=False) >>> res.output b'hello stderr\nhello stdout\n' @@ -52,15 +55,8 @@ Traceback (most recent call last): File "", line 1, in StopIteration -Finally, with ``stream=False`` and ``demux=True``, the whole output -is returned, but the streams are still separated: +Finally, with ``stream=False`` and ``demux=True``, the output is a tuple ``(stdout, stderr)``: ->>> res = container.exec_run(cmd, stream=True, demux=True) ->>> next(res.output) -(b'hello stdout\n', None) ->>> next(res.output) -(None, b'hello stderr\n') ->>> next(res.output) -Traceback (most recent call last): - File "", line 1, in -StopIteration +>>> res = container.exec_run(cmd, stream=False, demux=True) +>>> res.output +(b'hello stdout\n', b'hello stderr\n') \ No newline at end of file From f0d38fb7f40e01904bc3f788b7c29545f0c9fab2 Mon Sep 17 00:00:00 2001 From: Jay Turner Date: Tue, 27 Jun 2023 12:51:40 +0100 Subject: [PATCH 08/45] Add health property to Containers model Signed-off-by: Jay Turner --- docker/models/containers.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docker/models/containers.py b/docker/models/containers.py index 2eeefda1ee..6febb19ebd 100644 --- a/docker/models/containers.py +++ b/docker/models/containers.py @@ -62,6 +62,13 @@ def status(self): return self.attrs['State']['Status'] return self.attrs['State'] + @property + def health(self): + """ + The healthcheck status of the container. For example, ``healthy`, or ``unhealthy`. + """ + return self.attrs.get('State', {}).get('Health', {}).get('Status', 'unknown') + @property def ports(self): """ From fb974de27a2ad4295807ab61e47a781c5518d384 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Mon, 14 Aug 2023 10:26:36 +0200 Subject: [PATCH 09/45] tests/integration: fix flake8 failures (E721 do not compare types) Run flake8 docker/ tests/ flake8 docker/ tests/ shell: /usr/bin/bash -e {0} env: DOCKER_BUILDKIT: 1 pythonLocation: /opt/hostedtoolcache/Python/3.11.4/x64 PKG_CONFIG_PATH: /opt/hostedtoolcache/Python/3.11.4/x64/lib/pkgconfig Python_ROOT_DIR: /opt/hostedtoolcache/Python/3.11.4/x64 Python2_ROOT_DIR: /opt/hostedtoolcache/Python/3.11.4/x64 Python3_ROOT_DIR: /opt/hostedtoolcache/Python/3.11.4/x64 LD_LIBRARY_PATH: /opt/hostedtoolcache/Python/3.11.4/x64/lib tests/integration/api_container_test.py:1395:16: E721 do not compare types, for exact checks use `is` / `is not`, for instance checks use `isinstance()` tests/integration/api_container_test.py:1408:24: E721 do not compare types, for exact checks use `is` / `is not`, for instance checks use `isinstance()` tests/integration/api_image_test.py:35:16: E721 do not compare types, for exact checks use `is` / `is not`, for instance checks use `isinstance()` tests/integration/api_image_test.py:46:16: E721 do not compare types, for exact checks use `is` / `is not`, for instance checks use `isinstance()` Error: Process completed with exit code 1. Signed-off-by: Sebastiaan van Stijn --- tests/integration/api_container_test.py | 4 ++-- tests/integration/api_image_test.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/integration/api_container_test.py b/tests/integration/api_container_test.py index 0cb8fec68b..0782b12cc8 100644 --- a/tests/integration/api_container_test.py +++ b/tests/integration/api_container_test.py @@ -1392,7 +1392,7 @@ def test_get_container_stats_no_stream(self): response = self.client.stats(container, stream=0) self.client.kill(container) - assert type(response) == dict + assert isinstance(response, dict) for key in ['read', 'networks', 'precpu_stats', 'cpu_stats', 'memory_stats', 'blkio_stats']: assert key in response @@ -1405,7 +1405,7 @@ def test_get_container_stats_stream(self): self.client.start(container) stream = self.client.stats(container) for chunk in stream: - assert type(chunk) == dict + assert isinstance(chunk, dict) for key in ['read', 'network', 'precpu_stats', 'cpu_stats', 'memory_stats', 'blkio_stats']: assert key in chunk diff --git a/tests/integration/api_image_test.py b/tests/integration/api_image_test.py index 6a6686e377..cb3d667112 100644 --- a/tests/integration/api_image_test.py +++ b/tests/integration/api_image_test.py @@ -32,7 +32,7 @@ def test_images(self): def test_images_quiet(self): res1 = self.client.images(quiet=True) - assert type(res1[0]) == str + assert isinstance(res1[0], str) class PullImageTest(BaseAPIIntegrationTest): @@ -43,7 +43,7 @@ def test_pull(self): pass res = self.client.pull('hello-world') self.tmp_imgs.append('hello-world') - assert type(res) == str + assert isinstance(res, str) assert len(self.client.images('hello-world')) >= 1 img_info = self.client.inspect_image('hello-world') assert 'Id' in img_info From 83e93228ea79d899c38cd7f75e688f634454318a Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Mon, 14 Aug 2023 10:32:04 +0200 Subject: [PATCH 10/45] tests/Dockerfile: fix Dockerfile for debian bookworm The Dockerfile failed to build due to the base-image having switched to "bookworm"; Dockerfile:8 -------------------- 7 | ARG APT_MIRROR 8 | >>> RUN sed -ri "s/(httpredir|deb).debian.org/${APT_MIRROR:-deb.debian.org}/g" /etc/apt/sources.list \ 9 | >>> && sed -ri "s/(security).debian.org/${APT_MIRROR:-security.debian.org}/g" /etc/apt/sources.list 10 | -------------------- ERROR: failed to solve: process "/bin/sh -c sed -ri \"s/(httpredir|deb).debian.org/${APT_MIRROR:-deb.debian.org}/g\" /etc/apt/sources.list && sed -ri \"s/(security).debian.org/${APT_MIRROR:-security.debian.org}/g\" /etc/apt/sources.list" did not complete successfully: exit code: 2 Signed-off-by: Sebastiaan van Stijn --- tests/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Dockerfile b/tests/Dockerfile index bf95cd6a3c..4705acca54 100644 --- a/tests/Dockerfile +++ b/tests/Dockerfile @@ -5,8 +5,8 @@ ARG PYTHON_VERSION=3.10 FROM python:${PYTHON_VERSION} ARG APT_MIRROR -RUN sed -ri "s/(httpredir|deb).debian.org/${APT_MIRROR:-deb.debian.org}/g" /etc/apt/sources.list \ - && sed -ri "s/(security).debian.org/${APT_MIRROR:-security.debian.org}/g" /etc/apt/sources.list +RUN sed -ri "s/(httpredir|deb).debian.org/${APT_MIRROR:-deb.debian.org}/g" /etc/apt/sources.list.d/debian.sources \ + && sed -ri "s/(security).debian.org/${APT_MIRROR:-security.debian.org}/g" /etc/apt/sources.list.d/debian.sources RUN apt-get update && apt-get -y install --no-install-recommends \ gnupg2 \ From 5064995bc40768ad28af93d017d3c032312e41ec Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Mon, 14 Aug 2023 10:17:25 +0200 Subject: [PATCH 11/45] tests/integration: update some tests for updated error-messages I was in the process of cleaning up some error-messages, and it looks like the docker-py tests were depending on strings that will be removed; =================================== FAILURES =================================== _____________ CreateContainerTest.test_create_with_restart_policy ______________ tests/integration/api_container_test.py:126: in test_create_with_restart_policy assert 'You cannot remove ' in err E AssertionError: assert 'You cannot remove ' in 'cannot remove container d11580f6078108691096ec8a23404a6bda9ad1d1b2bafe88b17d127a67728833: container is restarting: stop the container before removing or force remove' ____________________ ErrorsTest.test_api_error_parses_json _____________________ tests/integration/errors_test.py:13: in test_api_error_parses_json assert 'You cannot remove a running container' in explanation E AssertionError: assert 'You cannot remove a running container' in 'cannot remove container 4b90ce2e907dd0f99d0f561619b803e7a2a31809ced366c537874dd13f8a47ec: container is running: stop the container before removing or force remove' This updates the tests to match on a string that will be present in both the old and new error-messages, but added a "lower()", so that matching will be done case-insensitive (Go errors generally should be lowercase). Signed-off-by: Sebastiaan van Stijn --- tests/integration/api_container_test.py | 4 ++-- tests/integration/errors_test.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/integration/api_container_test.py b/tests/integration/api_container_test.py index 0782b12cc8..b510979de0 100644 --- a/tests/integration/api_container_test.py +++ b/tests/integration/api_container_test.py @@ -122,8 +122,8 @@ def test_create_with_restart_policy(self): self.client.wait(id) with pytest.raises(docker.errors.APIError) as exc: self.client.remove_container(id) - err = exc.value.explanation - assert 'You cannot remove ' in err + err = exc.value.explanation.lower() + assert 'stop the container before' in err self.client.remove_container(id, force=True) def test_create_container_with_volumes_from(self): diff --git a/tests/integration/errors_test.py b/tests/integration/errors_test.py index 7bf156afb0..e2fce48b0f 100644 --- a/tests/integration/errors_test.py +++ b/tests/integration/errors_test.py @@ -9,7 +9,7 @@ def test_api_error_parses_json(self): self.client.start(container['Id']) with pytest.raises(APIError) as cm: self.client.remove_container(container['Id']) - explanation = cm.value.explanation - assert 'You cannot remove a running container' in explanation + explanation = cm.value.explanation.lower() + assert 'stop the container before' in explanation assert '{"message":' not in explanation self.client.remove_container(container['Id'], force=True) From 62b4bb8489001a8907046b8f4b68e6d68eca1d2b Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Mon, 14 Aug 2023 14:58:34 +0200 Subject: [PATCH 12/45] README: fix link for CI status badge The default branch was renamed from master to main, but the badge was still linking to the status for the master branch. Remove the branch-name so that the badge always refers to the "default" branch Signed-off-by: Sebastiaan van Stijn --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2db678dccc..921ffbcb88 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Docker SDK for Python -[![Build Status](https://github.com/docker/docker-py/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/docker/docker-py/actions/workflows/ci.yml/) +[![Build Status](https://github.com/docker/docker-py/actions/workflows/ci.yml/badge.svg)](https://github.com/docker/docker-py/actions/workflows/ci.yml) A Python library for the Docker Engine API. It lets you do anything the `docker` command does, but from within Python apps – run containers, manage containers, manage Swarms, etc. From 0618951093bf3116af482c6dfb983219c5684343 Mon Sep 17 00:00:00 2001 From: Mehmet Nuri Deveci <5735811+mndeveci@users.noreply.github.com> Date: Mon, 14 Aug 2023 21:43:31 +0300 Subject: [PATCH 13/45] fix: use response.text to get string rather than bytes (#3156) Signed-off-by: Mehmet Nuri Deveci <5735811+mndeveci@users.noreply.github.com> Co-authored-by: Milas Bowman --- docker/errors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/errors.py b/docker/errors.py index 8cf8670baf..75e30a8c94 100644 --- a/docker/errors.py +++ b/docker/errors.py @@ -27,7 +27,7 @@ def create_api_error_from_http_exception(e): try: explanation = response.json()['message'] except ValueError: - explanation = (response.content or '').strip() + explanation = (response.text or '').strip() cls = APIError if response.status_code == 404: explanation_msg = (explanation or '').lower() From 4571f7f9b4044e87303bb2215f8a308b7ddc2e3d Mon Sep 17 00:00:00 2001 From: VincentLeeMax Date: Tue, 15 Aug 2023 02:52:38 +0800 Subject: [PATCH 14/45] feat: add pause option to commit api (#3159) add commit pause option Signed-off-by: VincentLeeMax Co-authored-by: Milas Bowman --- docker/api/container.py | 4 +++- docker/models/containers.py | 1 + tests/unit/api_image_test.py | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docker/api/container.py b/docker/api/container.py index 40607e79a3..b8d5957b62 100644 --- a/docker/api/container.py +++ b/docker/api/container.py @@ -112,7 +112,7 @@ def attach_socket(self, container, params=None, ws=False): @utils.check_resource('container') def commit(self, container, repository=None, tag=None, message=None, - author=None, changes=None, conf=None): + author=None, pause=True, changes=None, conf=None): """ Commit a container to an image. Similar to the ``docker commit`` command. @@ -123,6 +123,7 @@ def commit(self, container, repository=None, tag=None, message=None, tag (str): The tag to push message (str): A commit message author (str): The name of the author + pause (bool): Whether to pause the container before committing changes (str): Dockerfile instructions to apply while committing conf (dict): The configuration for the container. See the `Engine API documentation @@ -139,6 +140,7 @@ def commit(self, container, repository=None, tag=None, message=None, 'tag': tag, 'comment': message, 'author': author, + 'pause': pause, 'changes': changes } u = self._url("/commit") diff --git a/docker/models/containers.py b/docker/models/containers.py index 2eeefda1ee..64838397a6 100644 --- a/docker/models/containers.py +++ b/docker/models/containers.py @@ -121,6 +121,7 @@ def commit(self, repository=None, tag=None, **kwargs): tag (str): The tag to push message (str): A commit message author (str): The name of the author + pause (bool): Whether to pause the container before committing changes (str): Dockerfile instructions to apply while committing conf (dict): The configuration for the container. See the `Engine API documentation diff --git a/tests/unit/api_image_test.py b/tests/unit/api_image_test.py index e285932941..b3428aa169 100644 --- a/tests/unit/api_image_test.py +++ b/tests/unit/api_image_test.py @@ -102,6 +102,7 @@ def test_commit(self): 'tag': None, 'container': fake_api.FAKE_CONTAINER_ID, 'author': None, + 'pause': True, 'changes': None }, timeout=DEFAULT_TIMEOUT_SECONDS From dbc061f4fab49a67994ca4de26cdab989c356247 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Aug 2023 15:08:38 -0400 Subject: [PATCH 15/45] build(deps): Bump requests from 2.28.1 to 2.31.0 (#3136) Bumps [requests](https://github.com/psf/requests) from 2.28.1 to 2.31.0. - [Release notes](https://github.com/psf/requests/releases) - [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md) - [Commits](https://github.com/psf/requests/compare/v2.28.1...v2.31.0) --- updated-dependencies: - dependency-name: requests dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 36660b660c..897cdbd5ef 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ packaging==21.3 paramiko==2.11.0 pywin32==304; sys_platform == 'win32' -requests==2.28.1 +requests==2.31.0 urllib3==1.26.11 websocket-client==1.3.3 From ee2310595d1362428f2826afac2e17077231473a Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Mon, 14 Aug 2023 21:12:44 +0200 Subject: [PATCH 16/45] test: remove APT_MIRROR from Dockerfile (#3145) The official Python images on Docker Hub switched to debian bookworm, which is now the current stable version of Debian. However, the location of the apt repository config file changed, which causes the Dockerfile build to fail; Loaded image: emptyfs:latest Loaded image ID: sha256:0df1207206e5288f4a989a2f13d1f5b3c4e70467702c1d5d21dfc9f002b7bd43 INFO: Building docker-sdk-python3:5.0.3... tests/Dockerfile:6 -------------------- 5 | ARG APT_MIRROR 6 | >>> RUN sed -ri "s/(httpredir|deb).debian.org/${APT_MIRROR:-deb.debian.org}/g" /etc/apt/sources.list \ 7 | >>> && sed -ri "s/(security).debian.org/${APT_MIRROR:-security.debian.org}/g" /etc/apt/sources.list 8 | -------------------- ERROR: failed to solve: process "/bin/sh -c sed -ri \"s/(httpredir|deb).debian.org/${APT_MIRROR:-deb.debian.org}/g\" /etc/apt/sources.list && sed -ri \"s/(security).debian.org/${APT_MIRROR:-security.debian.org}/g\" /etc/apt/sources.list" did not complete successfully: exit code: 2 The APT_MIRROR build-arg was originally added when the Debian package repositories were known to be unreliable, but that hasn't been the case for quite a while, so let's remove this altogether. Signed-off-by: Sebastiaan van Stijn Signed-off-by: Milas Bowman Co-authored-by: Milas Bowman --- tests/Dockerfile | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/Dockerfile b/tests/Dockerfile index 4705acca54..366abe23bb 100644 --- a/tests/Dockerfile +++ b/tests/Dockerfile @@ -4,10 +4,6 @@ ARG PYTHON_VERSION=3.10 FROM python:${PYTHON_VERSION} -ARG APT_MIRROR -RUN sed -ri "s/(httpredir|deb).debian.org/${APT_MIRROR:-deb.debian.org}/g" /etc/apt/sources.list.d/debian.sources \ - && sed -ri "s/(security).debian.org/${APT_MIRROR:-security.debian.org}/g" /etc/apt/sources.list.d/debian.sources - RUN apt-get update && apt-get -y install --no-install-recommends \ gnupg2 \ pass From 8a3402c049437bf10066c0527fafa06de8bca73f Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Thu, 11 May 2023 16:12:41 +0300 Subject: [PATCH 17/45] Replace string formatting with f-strings Signed-off-by: Aarni Koskela --- docker/api/build.py | 14 ++---- docker/api/client.py | 19 ++++---- docker/api/container.py | 10 ++-- docker/api/service.py | 4 +- docker/auth.py | 27 ++++------- docker/context/api.py | 4 +- docker/context/context.py | 10 ++-- docker/credentials/errors.py | 12 +---- docker/credentials/store.py | 18 ++------ docker/errors.py | 22 +++++---- docker/models/images.py | 11 ++--- docker/models/resource.py | 10 ++-- docker/transport/unixconn.py | 2 +- docker/types/containers.py | 18 ++++---- docker/types/services.py | 12 ++--- docker/utils/build.py | 4 +- docker/utils/decorators.py | 4 +- docker/utils/fnmatch.py | 16 +++---- docker/utils/ports.py | 2 +- docker/utils/proxy.py | 8 +++- docker/utils/utils.py | 37 +++++++-------- docs/conf.py | 2 +- tests/helpers.py | 2 +- tests/integration/api_client_test.py | 4 +- tests/integration/api_container_test.py | 16 +++---- tests/integration/base.py | 3 +- tests/integration/credentials/store_test.py | 2 +- tests/integration/models_containers_test.py | 4 +- tests/ssh/base.py | 3 +- tests/unit/api_build_test.py | 10 ++-- tests/unit/api_exec_test.py | 14 ++---- tests/unit/api_image_test.py | 48 +++++++++---------- tests/unit/api_network_test.py | 12 ++--- tests/unit/api_test.py | 51 +++++++++------------ tests/unit/api_volume_test.py | 8 ++-- tests/unit/auth_test.py | 7 +-- tests/unit/client_test.py | 20 ++------ tests/unit/fake_api.py | 28 +++-------- tests/unit/swarm_test.py | 6 +-- tests/unit/utils_test.py | 4 +- 40 files changed, 214 insertions(+), 294 deletions(-) diff --git a/docker/api/build.py b/docker/api/build.py index 3a1a3d9642..439f4dc351 100644 --- a/docker/api/build.py +++ b/docker/api/build.py @@ -314,9 +314,8 @@ def _set_auth_headers(self, headers): auth_data[auth.INDEX_URL] = auth_data.get(auth.INDEX_NAME, {}) log.debug( - 'Sending auth config ({})'.format( - ', '.join(repr(k) for k in auth_data.keys()) - ) + "Sending auth config (%s)", + ', '.join(repr(k) for k in auth_data), ) if auth_data: @@ -336,12 +335,9 @@ def process_dockerfile(dockerfile, path): abs_dockerfile = os.path.join(path, dockerfile) if constants.IS_WINDOWS_PLATFORM and path.startswith( constants.WINDOWS_LONGPATH_PREFIX): - abs_dockerfile = '{}{}'.format( - constants.WINDOWS_LONGPATH_PREFIX, - os.path.normpath( - abs_dockerfile[len(constants.WINDOWS_LONGPATH_PREFIX):] - ) - ) + normpath = os.path.normpath( + abs_dockerfile[len(constants.WINDOWS_LONGPATH_PREFIX):]) + abs_dockerfile = f'{constants.WINDOWS_LONGPATH_PREFIX}{normpath}' if (os.path.splitdrive(path)[0] != os.path.splitdrive(abs_dockerfile)[0] or os.path.relpath(abs_dockerfile, path).startswith('..')): # Dockerfile not in context - read data to insert into tar later diff --git a/docker/api/client.py b/docker/api/client.py index 65b9d9d198..8633025f3f 100644 --- a/docker/api/client.py +++ b/docker/api/client.py @@ -199,14 +199,13 @@ def __init__(self, base_url=None, version=None, self._version = version if not isinstance(self._version, str): raise DockerException( - 'Version parameter must be a string or None. Found {}'.format( - type(version).__name__ - ) + 'Version parameter must be a string or None. ' + f'Found {type(version).__name__}' ) if utils.version_lt(self._version, MINIMUM_DOCKER_API_VERSION): raise InvalidVersion( - 'API versions below {} are no longer supported by this ' - 'library.'.format(MINIMUM_DOCKER_API_VERSION) + f'API versions below {MINIMUM_DOCKER_API_VERSION} are ' + f'no longer supported by this library.' ) def _retrieve_server_version(self): @@ -248,19 +247,17 @@ def _url(self, pathfmt, *args, **kwargs): for arg in args: if not isinstance(arg, str): raise ValueError( - 'Expected a string but found {} ({}) ' - 'instead'.format(arg, type(arg)) + f'Expected a string but found {arg} ({type(arg)}) instead' ) quote_f = partial(urllib.parse.quote, safe="/:") args = map(quote_f, args) + formatted_path = pathfmt.format(*args) if kwargs.get('versioned_api', True): - return '{}/v{}{}'.format( - self.base_url, self._version, pathfmt.format(*args) - ) + return f'{self.base_url}/v{self._version}{formatted_path}' else: - return f'{self.base_url}{pathfmt.format(*args)}' + return f'{self.base_url}{formatted_path}' def _raise_for_status(self, response): """Raises stored :class:`APIError`, if one occurred.""" diff --git a/docker/api/container.py b/docker/api/container.py index b8d5957b62..ec28fd581b 100644 --- a/docker/api/container.py +++ b/docker/api/container.py @@ -863,8 +863,8 @@ def logs(self, container, stdout=True, stderr=True, stream=False, params['since'] = since else: raise errors.InvalidArgument( - 'since value should be datetime or positive int/float, ' - 'not {}'.format(type(since)) + 'since value should be datetime or positive int/float,' + f' not {type(since)}' ) if until is not None: @@ -880,8 +880,8 @@ def logs(self, container, stdout=True, stderr=True, stream=False, params['until'] = until else: raise errors.InvalidArgument( - 'until value should be datetime or positive int/float, ' - 'not {}'.format(type(until)) + f'until value should be datetime or positive int/float, ' + f'not {type(until)}' ) url = self._url("/containers/{0}/logs", container) @@ -953,7 +953,7 @@ def port(self, container, private_port): return port_settings.get(private_port) for protocol in ['tcp', 'udp', 'sctp']: - h_ports = port_settings.get(private_port + '/' + protocol) + h_ports = port_settings.get(f"{private_port}/{protocol}") if h_ports: break diff --git a/docker/api/service.py b/docker/api/service.py index 652b7c2458..3aed065175 100644 --- a/docker/api/service.py +++ b/docker/api/service.py @@ -7,9 +7,7 @@ def _check_api_features(version, task_template, update_config, endpoint_spec, def raise_version_error(param, min_version): raise errors.InvalidVersion( - '{} is not supported in API version < {}'.format( - param, min_version - ) + f'{param} is not supported in API version < {min_version}' ) if update_config is not None: diff --git a/docker/auth.py b/docker/auth.py index cb3885548f..4bce788701 100644 --- a/docker/auth.py +++ b/docker/auth.py @@ -22,15 +22,15 @@ def resolve_repository_name(repo_name): index_name, remote_name = split_repo_name(repo_name) if index_name[0] == '-' or index_name[-1] == '-': raise errors.InvalidRepository( - 'Invalid index name ({}). Cannot begin or end with a' - ' hyphen.'.format(index_name) + f'Invalid index name ({index_name}). ' + 'Cannot begin or end with a hyphen.' ) return resolve_index_name(index_name), remote_name def resolve_index_name(index_name): index_name = convert_to_hostname(index_name) - if index_name == 'index.' + INDEX_NAME: + if index_name == f"index.{INDEX_NAME}": index_name = INDEX_NAME return index_name @@ -99,9 +99,7 @@ def parse_auth(cls, entries, raise_on_error=False): for registry, entry in entries.items(): if not isinstance(entry, dict): log.debug( - 'Config entry for key {} is not auth config'.format( - registry - ) + f'Config entry for key {registry} is not auth config' ) # We sometimes fall back to parsing the whole config as if it # was the auth config by itself, for legacy purposes. In that @@ -109,17 +107,11 @@ def parse_auth(cls, entries, raise_on_error=False): # keys is not formatted properly. if raise_on_error: raise errors.InvalidConfigFile( - 'Invalid configuration for registry {}'.format( - registry - ) + f'Invalid configuration for registry {registry}' ) return {} if 'identitytoken' in entry: - log.debug( - 'Found an IdentityToken entry for registry {}'.format( - registry - ) - ) + log.debug(f'Found an IdentityToken entry for registry {registry}') conf[registry] = { 'IdentityToken': entry['identitytoken'] } @@ -130,16 +122,15 @@ def parse_auth(cls, entries, raise_on_error=False): # a valid value in the auths config. # https://github.com/docker/compose/issues/3265 log.debug( - 'Auth data for {} is absent. Client might be using a ' - 'credentials store instead.'.format(registry) + f'Auth data for {registry} is absent. ' + f'Client might be using a credentials store instead.' ) conf[registry] = {} continue username, password = decode_auth(entry['auth']) log.debug( - 'Found entry (registry={}, username={})' - .format(repr(registry), repr(username)) + f'Found entry (registry={registry!r}, username={username!r})' ) conf[registry] = { diff --git a/docker/context/api.py b/docker/context/api.py index 380e8c4c4f..e340fb6dd9 100644 --- a/docker/context/api.py +++ b/docker/context/api.py @@ -113,8 +113,8 @@ def contexts(cls): names.append(data["Name"]) except Exception as e: raise errors.ContextException( - "Failed to load metafile {}: {}".format( - filename, e)) + f"Failed to load metafile {filename}: {e}", + ) contexts = [cls.DEFAULT_CONTEXT] for name in names: diff --git a/docker/context/context.py b/docker/context/context.py index dbaa01cb5b..b607b77148 100644 --- a/docker/context/context.py +++ b/docker/context/context.py @@ -42,8 +42,9 @@ def __init__(self, name, orchestrator=None, host=None, endpoints=None, for k, v in endpoints.items(): if not isinstance(v, dict): # unknown format - raise ContextException("""Unknown endpoint format for - context {}: {}""".format(name, v)) + raise ContextException( + f"Unknown endpoint format for context {name}: {v}", + ) self.endpoints[k] = v if k != "docker": @@ -96,8 +97,9 @@ def _load_meta(cls, name): metadata = json.load(f) except (OSError, KeyError, ValueError) as e: # unknown format - raise Exception("""Detected corrupted meta file for - context {} : {}""".format(name, e)) + raise Exception( + f"Detected corrupted meta file for context {name} : {e}" + ) # for docker endpoints, set defaults for # Host and SkipTLSVerify fields diff --git a/docker/credentials/errors.py b/docker/credentials/errors.py index 42a1bc1a50..d059fd9fbb 100644 --- a/docker/credentials/errors.py +++ b/docker/credentials/errors.py @@ -13,13 +13,5 @@ class InitializationError(StoreError): def process_store_error(cpe, program): message = cpe.output.decode('utf-8') if 'credentials not found in native keychain' in message: - return CredentialsNotFound( - 'No matching credentials in {}'.format( - program - ) - ) - return StoreError( - 'Credentials store {} exited with "{}".'.format( - program, cpe.output.decode('utf-8').strip() - ) - ) + return CredentialsNotFound(f'No matching credentials in {program}') + return StoreError(f'Credentials store {program} exited with "{message}".') diff --git a/docker/credentials/store.py b/docker/credentials/store.py index b7ab53fbad..37c703e78c 100644 --- a/docker/credentials/store.py +++ b/docker/credentials/store.py @@ -20,9 +20,7 @@ def __init__(self, program, environment=None): self.environment = environment if self.exe is None: warnings.warn( - '{} not installed or not available in PATH'.format( - self.program - ) + f'{self.program} not installed or not available in PATH' ) def get(self, server): @@ -73,10 +71,8 @@ def list(self): def _execute(self, subcmd, data_input): if self.exe is None: raise errors.StoreError( - '{} not installed or not available in PATH'.format( - self.program - ) - ) + f'{self.program} not installed or not available in PATH' + ) output = None env = create_environment_dict(self.environment) try: @@ -88,14 +84,10 @@ def _execute(self, subcmd, data_input): except OSError as e: if e.errno == errno.ENOENT: raise errors.StoreError( - '{} not installed or not available in PATH'.format( - self.program - ) + f'{self.program} not installed or not available in PATH' ) else: raise errors.StoreError( - 'Unexpected OS error "{}", errno={}'.format( - e.strerror, e.errno - ) + f'Unexpected OS error "{e.strerror}", errno={e.errno}' ) return output diff --git a/docker/errors.py b/docker/errors.py index 75e30a8c94..d03e10f693 100644 --- a/docker/errors.py +++ b/docker/errors.py @@ -54,14 +54,16 @@ def __str__(self): message = super().__str__() if self.is_client_error(): - message = '{} Client Error for {}: {}'.format( - self.response.status_code, self.response.url, - self.response.reason) + message = ( + f'{self.response.status_code} Client Error for ' + f'{self.response.url}: {self.response.reason}' + ) elif self.is_server_error(): - message = '{} Server Error for {}: {}'.format( - self.response.status_code, self.response.url, - self.response.reason) + message = ( + f'{self.response.status_code} Server Error for ' + f'{self.response.url}: {self.response.reason}' + ) if self.explanation: message = f'{message} ("{self.explanation}")' @@ -142,10 +144,10 @@ def __init__(self, container, exit_status, command, image, stderr): self.stderr = stderr err = f": {stderr}" if stderr is not None else "" - msg = ("Command '{}' in image '{}' returned non-zero exit " - "status {}{}").format(command, image, exit_status, err) - - super().__init__(msg) + super().__init__( + f"Command '{command}' in image '{image}' " + f"returned non-zero exit status {exit_status}{err}" + ) class StreamParseError(RuntimeError): diff --git a/docker/models/images.py b/docker/models/images.py index e3ec39d28d..abb4b12b50 100644 --- a/docker/models/images.py +++ b/docker/models/images.py @@ -15,10 +15,8 @@ class Image(Model): An image on the server. """ def __repr__(self): - return "<{}: '{}'>".format( - self.__class__.__name__, - "', '".join(self.tags), - ) + tag_str = "', '".join(self.tags) + return f"<{self.__class__.__name__}: '{tag_str}'>" @property def labels(self): @@ -471,9 +469,8 @@ def pull(self, repository, tag=None, all_tags=False, **kwargs): # to be pulled. pass if not all_tags: - return self.get('{0}{2}{1}'.format( - repository, tag, '@' if tag.startswith('sha256:') else ':' - )) + sep = '@' if tag.startswith('sha256:') else ':' + return self.get(f'{repository}{sep}{tag}') return self.list(repository) def push(self, repository, tag=None, **kwargs): diff --git a/docker/models/resource.py b/docker/models/resource.py index 89030e592e..d3a35e84be 100644 --- a/docker/models/resource.py +++ b/docker/models/resource.py @@ -64,9 +64,10 @@ def __init__(self, client=None): def __call__(self, *args, **kwargs): raise TypeError( - "'{}' object is not callable. You might be trying to use the old " - "(pre-2.0) API - use docker.APIClient if so." - .format(self.__class__.__name__)) + f"'{self.__class__.__name__}' object is not callable. " + "You might be trying to use the old (pre-2.0) API - " + "use docker.APIClient if so." + ) def list(self): raise NotImplementedError @@ -88,5 +89,4 @@ def prepare_model(self, attrs): elif isinstance(attrs, dict): return self.model(attrs=attrs, client=self.client, collection=self) else: - raise Exception("Can't create %s from %s" % - (self.model.__name__, attrs)) + raise Exception(f"Can't create {self.model.__name__} from {attrs}") diff --git a/docker/transport/unixconn.py b/docker/transport/unixconn.py index fae10f2664..09d373dd6d 100644 --- a/docker/transport/unixconn.py +++ b/docker/transport/unixconn.py @@ -55,7 +55,7 @@ def __init__(self, socket_url, timeout=60, max_pool_size=constants.DEFAULT_MAX_POOL_SIZE): socket_path = socket_url.replace('http+unix://', '') if not socket_path.startswith('/'): - socket_path = '/' + socket_path + socket_path = f"/{socket_path}" self.socket_path = socket_path self.timeout = timeout self.max_pool_size = max_pool_size diff --git a/docker/types/containers.py b/docker/types/containers.py index 84df0f7e61..6d54aa65cc 100644 --- a/docker/types/containers.py +++ b/docker/types/containers.py @@ -652,25 +652,25 @@ def __init__(self, version, binds=None, port_bindings=None, def host_config_type_error(param, param_value, expected): - error_msg = 'Invalid type for {0} param: expected {1} but found {2}' - return TypeError(error_msg.format(param, expected, type(param_value))) + return TypeError( + f'Invalid type for {param} param: expected {expected} ' + f'but found {type(param_value)}' + ) def host_config_version_error(param, version, less_than=True): operator = '<' if less_than else '>' - error_msg = '{0} param is not supported in API versions {1} {2}' - return errors.InvalidVersion(error_msg.format(param, operator, version)) - + return errors.InvalidVersion( + f'{param} param is not supported in API versions {operator} {version}', + ) def host_config_value_error(param, param_value): - error_msg = 'Invalid value for {0} param: {1}' - return ValueError(error_msg.format(param, param_value)) + return ValueError(f'Invalid value for {param} param: {param_value}') def host_config_incompatible_error(param, param_value, incompatible_param): - error_msg = '\"{1}\" {0} is incompatible with {2}' return errors.InvalidArgument( - error_msg.format(param, param_value, incompatible_param) + f'\"{param_value}\" {param} is incompatible with {incompatible_param}' ) diff --git a/docker/types/services.py b/docker/types/services.py index a3383ef75b..0b07c350ee 100644 --- a/docker/types/services.py +++ b/docker/types/services.py @@ -370,8 +370,8 @@ def _convert_generic_resources_dict(generic_resources): return generic_resources if not isinstance(generic_resources, dict): raise errors.InvalidArgument( - 'generic_resources must be a dict or a list' - ' (found {})'.format(type(generic_resources)) + 'generic_resources must be a dict or a list ' + f'(found {type(generic_resources)})' ) resources = [] for kind, value in generic_resources.items(): @@ -381,9 +381,9 @@ def _convert_generic_resources_dict(generic_resources): elif isinstance(value, str): resource_type = 'NamedResourceSpec' else: + kv = {kind: value} raise errors.InvalidArgument( - 'Unsupported generic resource reservation ' - 'type: {}'.format({kind: value}) + f'Unsupported generic resource reservation type: {kv}' ) resources.append({ resource_type: {'Kind': kind, 'Value': value} @@ -764,8 +764,8 @@ class PlacementPreference(dict): def __init__(self, strategy, descriptor): if strategy != 'spread': raise errors.InvalidArgument( - 'PlacementPreference strategy value is invalid ({}):' - ' must be "spread".'.format(strategy) + f'PlacementPreference strategy value is invalid ({strategy}): ' + 'must be "spread".' ) self['Spread'] = {'SpreadDescriptor': descriptor} diff --git a/docker/utils/build.py b/docker/utils/build.py index 59564c4cda..6b38eacdb2 100644 --- a/docker/utils/build.py +++ b/docker/utils/build.py @@ -42,7 +42,7 @@ def exclude_paths(root, patterns, dockerfile=None): if dockerfile is None: dockerfile = 'Dockerfile' - patterns.append('!' + dockerfile) + patterns.append(f"!{dockerfile}") pm = PatternMatcher(patterns) return set(pm.walk(root)) @@ -180,7 +180,7 @@ def rec_walk(current_dir): fpath = os.path.join( os.path.relpath(current_dir, root), f ) - if fpath.startswith('.' + os.path.sep): + if fpath.startswith(f".{os.path.sep}"): fpath = fpath[2:] match = self.matches(fpath) if not match: diff --git a/docker/utils/decorators.py b/docker/utils/decorators.py index cf1baf496c..5aab98cd46 100644 --- a/docker/utils/decorators.py +++ b/docker/utils/decorators.py @@ -27,9 +27,7 @@ def decorator(f): def wrapper(self, *args, **kwargs): if utils.version_lt(self._version, version): raise errors.InvalidVersion( - '{} is not available for version < {}'.format( - f.__name__, version - ) + f'{f.__name__} is not available for version < {version}', ) return f(self, *args, **kwargs) return wrapper diff --git a/docker/utils/fnmatch.py b/docker/utils/fnmatch.py index 90e9f60f59..be745381e4 100644 --- a/docker/utils/fnmatch.py +++ b/docker/utils/fnmatch.py @@ -79,18 +79,18 @@ def translate(pat): i = i + 1 if i >= n: # is "**EOF" - to align with .gitignore just accept all - res = res + '.*' + res = f"{res}.*" else: # is "**" # Note that this allows for any # of /'s (even 0) because # the .* will eat everything, even /'s - res = res + '(.*/)?' + res = f"{res}(.*/)?" else: # is "*" so map it to anything but "/" - res = res + '[^/]*' + res = f"{res}[^/]*" elif c == '?': # "?" is any char except "/" - res = res + '[^/]' + res = f"{res}[^/]" elif c == '[': j = i if j < n and pat[j] == '!': @@ -100,16 +100,16 @@ def translate(pat): while j < n and pat[j] != ']': j = j + 1 if j >= n: - res = res + '\\[' + res = f"{res}\\[" else: stuff = pat[i:j].replace('\\', '\\\\') i = j + 1 if stuff[0] == '!': - stuff = '^' + stuff[1:] + stuff = f"^{stuff[1:]}" elif stuff[0] == '^': - stuff = '\\' + stuff + stuff = f"\\{stuff}" res = f'{res}[{stuff}]' else: res = res + re.escape(c) - return res + '$' + return f"{res}$" diff --git a/docker/utils/ports.py b/docker/utils/ports.py index e813936602..9fd6e8f6b8 100644 --- a/docker/utils/ports.py +++ b/docker/utils/ports.py @@ -49,7 +49,7 @@ def port_range(start, end, proto, randomly_available_port=False): if not end: return [start + proto] if randomly_available_port: - return [f'{start}-{end}' + proto] + return [f"{start}-{end}{proto}"] return [str(port) + proto for port in range(int(start), int(end) + 1)] diff --git a/docker/utils/proxy.py b/docker/utils/proxy.py index 49e98ed912..e7164b6cea 100644 --- a/docker/utils/proxy.py +++ b/docker/utils/proxy.py @@ -69,5 +69,9 @@ def inject_proxy_environment(self, environment): return proxy_env + environment def __str__(self): - return 'ProxyConfig(http={}, https={}, ftp={}, no_proxy={})'.format( - self.http, self.https, self.ftp, self.no_proxy) + return ( + 'ProxyConfig(' + f'http={self.http}, https={self.https}, ' + f'ftp={self.ftp}, no_proxy={self.no_proxy}' + ')' + ) diff --git a/docker/utils/utils.py b/docker/utils/utils.py index 7b2bbf4ba1..15e3869000 100644 --- a/docker/utils/utils.py +++ b/docker/utils/utils.py @@ -127,8 +127,7 @@ def convert_volume_binds(binds): if isinstance(v, dict): if 'ro' in v and 'mode' in v: raise ValueError( - 'Binding cannot contain both "ro" and "mode": {}' - .format(repr(v)) + f'Binding cannot contain both "ro" and "mode": {v!r}' ) bind = v['bind'] @@ -160,8 +159,8 @@ def convert_tmpfs_mounts(tmpfs): if not isinstance(tmpfs, list): raise ValueError( - 'Expected tmpfs value to be either a list or a dict, found: {}' - .format(type(tmpfs).__name__) + 'Expected tmpfs value to be either a list or a dict, ' + f'found: {type(tmpfs).__name__}' ) result = {} @@ -175,8 +174,8 @@ def convert_tmpfs_mounts(tmpfs): else: raise ValueError( - "Expected item in tmpfs list to be a string, found: {}" - .format(type(mount).__name__) + "Expected item in tmpfs list to be a string, " + f"found: {type(mount).__name__}" ) result[name] = options @@ -218,9 +217,9 @@ def parse_host(addr, is_win32=False, tls=False): parsed_url = urlparse(addr) proto = parsed_url.scheme - if not proto or any([x not in string.ascii_letters + '+' for x in proto]): + if not proto or any([x not in f"{string.ascii_letters}+" for x in proto]): # https://bugs.python.org/issue754016 - parsed_url = urlparse('//' + addr, 'tcp') + parsed_url = urlparse(f"//{addr}", 'tcp') proto = 'tcp' if proto == 'fd': @@ -256,15 +255,14 @@ def parse_host(addr, is_win32=False, tls=False): if parsed_url.path and proto == 'ssh': raise errors.DockerException( - 'Invalid bind address format: no path allowed for this protocol:' - ' {}'.format(addr) + f'Invalid bind address format: no path allowed for this protocol: {addr}' ) else: path = parsed_url.path if proto == 'unix' and parsed_url.hostname is not None: # For legacy reasons, we consider unix://path # to be valid and equivalent to unix:///path - path = '/'.join((parsed_url.hostname, path)) + path = f"{parsed_url.hostname}/{path}" netloc = parsed_url.netloc if proto in ('tcp', 'ssh'): @@ -272,8 +270,7 @@ def parse_host(addr, is_win32=False, tls=False): if port <= 0: if proto != 'ssh': raise errors.DockerException( - 'Invalid bind address format: port is required:' - ' {}'.format(addr) + f'Invalid bind address format: port is required: {addr}' ) port = 22 netloc = f'{parsed_url.netloc}:{port}' @@ -283,7 +280,7 @@ def parse_host(addr, is_win32=False, tls=False): # Rewrite schemes to fit library internals (requests adapters) if proto == 'tcp': - proto = 'http{}'.format('s' if tls else '') + proto = f"http{'s' if tls else ''}" elif proto == 'unix': proto = 'http+unix' @@ -419,17 +416,16 @@ def parse_bytes(s): digits = float(digits_part) except ValueError: raise errors.DockerException( - 'Failed converting the string value for memory ({}) to' - ' an integer.'.format(digits_part) + 'Failed converting the string value for memory ' + f'({digits_part}) to an integer.' ) # Reconvert to long for the final result s = int(digits * units[suffix]) else: raise errors.DockerException( - 'The specified value for memory ({}) should specify the' - ' units. The postfix should be one of the `b` `k` `m` `g`' - ' characters'.format(s) + f'The specified value for memory ({s}) should specify the units. ' + 'The postfix should be one of the `b` `k` `m` `g` characters' ) return s @@ -465,8 +461,7 @@ def parse_env_file(env_file): environment[k] = v else: raise errors.DockerException( - 'Invalid line in environment file {}:\n{}'.format( - env_file, line)) + f'Invalid line in environment file {env_file}:\n{line}') return environment diff --git a/docs/conf.py b/docs/conf.py index dc3b37cc8a..e9971e0d2e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -56,7 +56,7 @@ # General information about the project. project = 'Docker SDK for Python' year = datetime.datetime.now().year -copyright = '%d Docker Inc' % year +copyright = f'{year} Docker Inc' author = 'Docker Inc' # The version info for the project you're documenting, acts as replacement for diff --git a/tests/helpers.py b/tests/helpers.py index bdb07f96b9..e0785774b3 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -80,7 +80,7 @@ def wait_on_condition(condition, delay=0.1, timeout=40): start_time = time.time() while not condition(): if time.time() - start_time > timeout: - raise AssertionError("Timeout: %s" % condition) + raise AssertionError(f"Timeout: {condition}") time.sleep(delay) diff --git a/tests/integration/api_client_test.py b/tests/integration/api_client_test.py index d1622fa88d..d7a22a04af 100644 --- a/tests/integration/api_client_test.py +++ b/tests/integration/api_client_test.py @@ -72,6 +72,4 @@ def test_resource_warnings(self): client.close() del client - assert len(w) == 0, "No warnings produced: {}".format( - w[0].message - ) + assert len(w) == 0, f"No warnings produced: {w[0].message}" diff --git a/tests/integration/api_container_test.py b/tests/integration/api_container_test.py index b510979de0..590c4fa0ce 100644 --- a/tests/integration/api_container_test.py +++ b/tests/integration/api_container_test.py @@ -666,9 +666,7 @@ def test_copy_file_to_container(self): test_file.seek(0) ctnr = self.client.create_container( TEST_IMG, - 'cat {}'.format( - os.path.join('/vol1/', os.path.basename(test_file.name)) - ), + f"cat {os.path.join('/vol1/', os.path.basename(test_file.name))}", volumes=['/vol1'] ) self.tmp_containers.append(ctnr) @@ -826,7 +824,7 @@ def test_logs(self): exitcode = self.client.wait(id)['StatusCode'] assert exitcode == 0 logs = self.client.logs(id) - assert logs == (snippet + '\n').encode(encoding='ascii') + assert logs == f"{snippet}\n".encode(encoding='ascii') def test_logs_tail_option(self): snippet = '''Line1 @@ -857,7 +855,7 @@ def test_logs_streaming_and_follow(self): exitcode = self.client.wait(id)['StatusCode'] assert exitcode == 0 - assert logs == (snippet + '\n').encode(encoding='ascii') + assert logs == f"{snippet}\n".encode(encoding='ascii') @pytest.mark.timeout(5) @pytest.mark.skipif(os.environ.get('DOCKER_HOST', '').startswith('ssh://'), @@ -878,7 +876,7 @@ def test_logs_streaming_and_follow_and_cancel(self): for chunk in generator: logs += chunk - assert logs == (snippet + '\n').encode(encoding='ascii') + assert logs == f"{snippet}\n".encode(encoding='ascii') def test_logs_with_dict_instead_of_id(self): snippet = 'Flowering Nights (Sakuya Iyazoi)' @@ -891,7 +889,7 @@ def test_logs_with_dict_instead_of_id(self): exitcode = self.client.wait(id)['StatusCode'] assert exitcode == 0 logs = self.client.logs(container) - assert logs == (snippet + '\n').encode(encoding='ascii') + assert logs == f"{snippet}\n".encode(encoding='ascii') def test_logs_with_tail_0(self): snippet = 'Flowering Nights (Sakuya Iyazoi)' @@ -920,7 +918,7 @@ def test_logs_with_until(self): logs_until_1 = self.client.logs(container, until=1) assert logs_until_1 == b'' logs_until_now = self.client.logs(container, datetime.now()) - assert logs_until_now == (snippet + '\n').encode(encoding='ascii') + assert logs_until_now == f"{snippet}\n".encode(encoding='ascii') class DiffTest(BaseAPIIntegrationTest): @@ -1086,7 +1084,7 @@ def test_port(self): ip, host_port = port_binding['HostIp'], port_binding['HostPort'] - port_binding = port if not protocol else port + "/" + protocol + port_binding = port if not protocol else f"{port}/{protocol}" assert ip == port_bindings[port_binding][0] assert host_port == port_bindings[port_binding][1] diff --git a/tests/integration/base.py b/tests/integration/base.py index 031079c917..e4073757ee 100644 --- a/tests/integration/base.py +++ b/tests/integration/base.py @@ -103,8 +103,7 @@ def run_container(self, *args, **kwargs): if exitcode != 0: output = self.client.logs(container) raise Exception( - "Container exited with code {}:\n{}" - .format(exitcode, output)) + f"Container exited with code {exitcode}:\n{output}") return container diff --git a/tests/integration/credentials/store_test.py b/tests/integration/credentials/store_test.py index 16f4d60ab4..82ea84741d 100644 --- a/tests/integration/credentials/store_test.py +++ b/tests/integration/credentials/store_test.py @@ -22,7 +22,7 @@ def teardown_method(self): def setup_method(self): self.tmp_keys = [] if sys.platform.startswith('linux'): - if shutil.which('docker-credential-' + DEFAULT_LINUX_STORE): + if shutil.which(f"docker-credential-{DEFAULT_LINUX_STORE}"): self.store = Store(DEFAULT_LINUX_STORE) elif shutil.which('docker-credential-pass'): self.store = Store('pass') diff --git a/tests/integration/models_containers_test.py b/tests/integration/models_containers_test.py index eac4c97909..5b0470b937 100644 --- a/tests/integration/models_containers_test.py +++ b/tests/integration/models_containers_test.py @@ -49,7 +49,7 @@ def test_run_with_volume(self): container = client.containers.run( "alpine", "sh -c 'echo \"hello\" > /insidecontainer/test'", - volumes=["%s:/insidecontainer" % path], + volumes=[f"{path}:/insidecontainer"], detach=True ) self.tmp_containers.append(container.id) @@ -58,7 +58,7 @@ def test_run_with_volume(self): name = "container_volume_test" out = client.containers.run( "alpine", "cat /insidecontainer/test", - volumes=["%s:/insidecontainer" % path], + volumes=[f"{path}:/insidecontainer"], name=name ) self.tmp_containers.append(name) diff --git a/tests/ssh/base.py b/tests/ssh/base.py index 4b91add4be..d6ff130a1d 100644 --- a/tests/ssh/base.py +++ b/tests/ssh/base.py @@ -110,8 +110,7 @@ def run_container(self, *args, **kwargs): if exitcode != 0: output = self.client.logs(container) raise Exception( - "Container exited with code {}:\n{}" - .format(exitcode, output)) + f"Container exited with code {exitcode}:\n{output}") return container diff --git a/tests/unit/api_build_test.py b/tests/unit/api_build_test.py index 7e07a2695e..cbecd1e544 100644 --- a/tests/unit/api_build_test.py +++ b/tests/unit/api_build_test.py @@ -89,7 +89,7 @@ def test_build_remote_with_registry_auth(self): fake_request.assert_called_with( 'POST', - url_prefix + 'build', + f"{url_prefix}build", stream=True, data=None, headers=expected_headers, @@ -193,10 +193,10 @@ def pre(path): 'foo/Dockerfile.foo', None ) assert process_dockerfile( - '../Dockerfile', pre(base + '\\foo') + '../Dockerfile', pre(f"{base}\\foo") )[1] is not None assert process_dockerfile( - '../baz/Dockerfile.baz', pre(base + '/baz') + '../baz/Dockerfile.baz', pre(f"{base}/baz") ) == ('../baz/Dockerfile.baz', None) def test_process_dockerfile(self): @@ -218,8 +218,8 @@ def test_process_dockerfile(self): 'foo/Dockerfile.foo', None ) assert process_dockerfile( - '../Dockerfile', base + '/foo' + '../Dockerfile', f"{base}/foo" )[1] is not None - assert process_dockerfile('../baz/Dockerfile.baz', base + '/baz') == ( + assert process_dockerfile('../baz/Dockerfile.baz', f"{base}/baz") == ( '../baz/Dockerfile.baz', None ) diff --git a/tests/unit/api_exec_test.py b/tests/unit/api_exec_test.py index 4504250846..1760239fd6 100644 --- a/tests/unit/api_exec_test.py +++ b/tests/unit/api_exec_test.py @@ -32,9 +32,7 @@ def test_exec_start(self): self.client.exec_start(fake_api.FAKE_EXEC_ID) args = fake_request.call_args - assert args[0][1] == url_prefix + 'exec/{}/start'.format( - fake_api.FAKE_EXEC_ID - ) + assert args[0][1] == f"{url_prefix}exec/{fake_api.FAKE_EXEC_ID}/start" assert json.loads(args[1]['data']) == { 'Tty': False, @@ -51,9 +49,7 @@ def test_exec_start_detached(self): self.client.exec_start(fake_api.FAKE_EXEC_ID, detach=True) args = fake_request.call_args - assert args[0][1] == url_prefix + 'exec/{}/start'.format( - fake_api.FAKE_EXEC_ID - ) + assert args[0][1] == f"{url_prefix}exec/{fake_api.FAKE_EXEC_ID}/start" assert json.loads(args[1]['data']) == { 'Tty': False, @@ -68,16 +64,14 @@ def test_exec_inspect(self): self.client.exec_inspect(fake_api.FAKE_EXEC_ID) args = fake_request.call_args - assert args[0][1] == url_prefix + 'exec/{}/json'.format( - fake_api.FAKE_EXEC_ID - ) + assert args[0][1] == f"{url_prefix}exec/{fake_api.FAKE_EXEC_ID}/json" def test_exec_resize(self): self.client.exec_resize(fake_api.FAKE_EXEC_ID, height=20, width=60) fake_request.assert_called_with( 'POST', - url_prefix + f'exec/{fake_api.FAKE_EXEC_ID}/resize', + f"{url_prefix}exec/{fake_api.FAKE_EXEC_ID}/resize", params={'h': 20, 'w': 60}, timeout=DEFAULT_TIMEOUT_SECONDS ) diff --git a/tests/unit/api_image_test.py b/tests/unit/api_image_test.py index b3428aa169..aea3a0e136 100644 --- a/tests/unit/api_image_test.py +++ b/tests/unit/api_image_test.py @@ -21,7 +21,7 @@ def test_images(self): fake_request.assert_called_with( 'GET', - url_prefix + 'images/json', + f"{url_prefix}images/json", params={'only_ids': 0, 'all': 1}, timeout=DEFAULT_TIMEOUT_SECONDS ) @@ -31,7 +31,7 @@ def test_images_name(self): fake_request.assert_called_with( 'GET', - url_prefix + 'images/json', + f"{url_prefix}images/json", params={'only_ids': 0, 'all': 0, 'filters': '{"reference": ["foo:bar"]}'}, timeout=DEFAULT_TIMEOUT_SECONDS @@ -42,7 +42,7 @@ def test_images_quiet(self): fake_request.assert_called_with( 'GET', - url_prefix + 'images/json', + f"{url_prefix}images/json", params={'only_ids': 1, 'all': 1}, timeout=DEFAULT_TIMEOUT_SECONDS ) @@ -52,7 +52,7 @@ def test_image_ids(self): fake_request.assert_called_with( 'GET', - url_prefix + 'images/json', + f"{url_prefix}images/json", params={'only_ids': 1, 'all': 0}, timeout=DEFAULT_TIMEOUT_SECONDS ) @@ -62,7 +62,7 @@ def test_images_filters(self): fake_request.assert_called_with( 'GET', - url_prefix + 'images/json', + f"{url_prefix}images/json", params={'only_ids': 0, 'all': 0, 'filters': '{"dangling": ["true"]}'}, timeout=DEFAULT_TIMEOUT_SECONDS @@ -72,7 +72,7 @@ def test_pull(self): self.client.pull('joffrey/test001') args = fake_request.call_args - assert args[0][1] == url_prefix + 'images/create' + assert args[0][1] == f"{url_prefix}images/create" assert args[1]['params'] == { 'tag': 'latest', 'fromImage': 'joffrey/test001' } @@ -82,7 +82,7 @@ def test_pull_stream(self): self.client.pull('joffrey/test001', stream=True) args = fake_request.call_args - assert args[0][1] == url_prefix + 'images/create' + assert args[0][1] == f"{url_prefix}images/create" assert args[1]['params'] == { 'tag': 'latest', 'fromImage': 'joffrey/test001' } @@ -93,7 +93,7 @@ def test_commit(self): fake_request.assert_called_with( 'POST', - url_prefix + 'commit', + f"{url_prefix}commit", data='{}', headers={'Content-Type': 'application/json'}, params={ @@ -113,7 +113,7 @@ def test_remove_image(self): fake_request.assert_called_with( 'DELETE', - url_prefix + 'images/' + fake_api.FAKE_IMAGE_ID, + f"{url_prefix}images/{fake_api.FAKE_IMAGE_ID}", params={'force': False, 'noprune': False}, timeout=DEFAULT_TIMEOUT_SECONDS ) @@ -123,7 +123,7 @@ def test_image_history(self): fake_request.assert_called_with( 'GET', - url_prefix + 'images/test_image/history', + f"{url_prefix}images/test_image/history", timeout=DEFAULT_TIMEOUT_SECONDS ) @@ -136,7 +136,7 @@ def test_import_image(self): fake_request.assert_called_with( 'POST', - url_prefix + 'images/create', + f"{url_prefix}images/create", params={ 'repo': fake_api.FAKE_REPO_NAME, 'tag': fake_api.FAKE_TAG_NAME, @@ -157,7 +157,7 @@ def test_import_image_from_bytes(self): fake_request.assert_called_with( 'POST', - url_prefix + 'images/create', + f"{url_prefix}images/create", params={ 'repo': fake_api.FAKE_REPO_NAME, 'tag': fake_api.FAKE_TAG_NAME, @@ -179,7 +179,7 @@ def test_import_image_from_image(self): fake_request.assert_called_with( 'POST', - url_prefix + 'images/create', + f"{url_prefix}images/create", params={ 'repo': fake_api.FAKE_REPO_NAME, 'tag': fake_api.FAKE_TAG_NAME, @@ -194,7 +194,7 @@ def test_inspect_image(self): fake_request.assert_called_with( 'GET', - url_prefix + 'images/test_image/json', + f"{url_prefix}images/test_image/json", timeout=DEFAULT_TIMEOUT_SECONDS ) @@ -212,7 +212,7 @@ def test_push_image(self): fake_request.assert_called_with( 'POST', - url_prefix + 'images/test_image/push', + f"{url_prefix}images/test_image/push", params={ 'tag': None }, @@ -231,7 +231,7 @@ def test_push_image_with_tag(self): fake_request.assert_called_with( 'POST', - url_prefix + 'images/test_image/push', + f"{url_prefix}images/test_image/push", params={ 'tag': fake_api.FAKE_TAG_NAME, }, @@ -255,7 +255,7 @@ def test_push_image_with_auth(self): fake_request.assert_called_with( 'POST', - url_prefix + 'images/test_image/push', + f"{url_prefix}images/test_image/push", params={ 'tag': fake_api.FAKE_TAG_NAME, }, @@ -273,7 +273,7 @@ def test_push_image_stream(self): fake_request.assert_called_with( 'POST', - url_prefix + 'images/test_image/push', + f"{url_prefix}images/test_image/push", params={ 'tag': None }, @@ -288,7 +288,7 @@ def test_tag_image(self): fake_request.assert_called_with( 'POST', - url_prefix + 'images/' + fake_api.FAKE_IMAGE_ID + '/tag', + f"{url_prefix}images/{fake_api.FAKE_IMAGE_ID}/tag", params={ 'tag': None, 'repo': 'repo', @@ -306,7 +306,7 @@ def test_tag_image_tag(self): fake_request.assert_called_with( 'POST', - url_prefix + 'images/' + fake_api.FAKE_IMAGE_ID + '/tag', + f"{url_prefix}images/{fake_api.FAKE_IMAGE_ID}/tag", params={ 'tag': 'tag', 'repo': 'repo', @@ -321,7 +321,7 @@ def test_tag_image_force(self): fake_request.assert_called_with( 'POST', - url_prefix + 'images/' + fake_api.FAKE_IMAGE_ID + '/tag', + f"{url_prefix}images/{fake_api.FAKE_IMAGE_ID}/tag", params={ 'tag': None, 'repo': 'repo', @@ -335,7 +335,7 @@ def test_get_image(self): fake_request.assert_called_with( 'GET', - url_prefix + 'images/' + fake_api.FAKE_IMAGE_ID + '/get', + f"{url_prefix}images/{fake_api.FAKE_IMAGE_ID}/get", stream=True, timeout=DEFAULT_TIMEOUT_SECONDS ) @@ -345,7 +345,7 @@ def test_load_image(self): fake_request.assert_called_with( 'POST', - url_prefix + 'images/load', + f"{url_prefix}images/load", data='Byte Stream....', stream=True, params={}, @@ -357,7 +357,7 @@ def test_load_image_quiet(self): fake_request.assert_called_with( 'POST', - url_prefix + 'images/load', + f"{url_prefix}images/load", data='Byte Stream....', stream=True, params={'quiet': True}, diff --git a/tests/unit/api_network_test.py b/tests/unit/api_network_test.py index 8afab7379d..d3daa44c41 100644 --- a/tests/unit/api_network_test.py +++ b/tests/unit/api_network_test.py @@ -28,7 +28,7 @@ def test_list_networks(self): with mock.patch('docker.api.client.APIClient.get', get): assert self.client.networks() == networks - assert get.call_args[0][0] == url_prefix + 'networks' + assert get.call_args[0][0] == f"{url_prefix}networks" filters = json.loads(get.call_args[1]['params']['filters']) assert not filters @@ -54,7 +54,7 @@ def test_create_network(self): result = self.client.create_network('foo') assert result == network_data - assert post.call_args[0][0] == url_prefix + 'networks/create' + assert post.call_args[0][0] == f"{url_prefix}networks/create" assert json.loads(post.call_args[1]['data']) == {"Name": "foo"} @@ -97,7 +97,7 @@ def test_remove_network(self): self.client.remove_network(network_id) args = delete.call_args - assert args[0][0] == url_prefix + f'networks/{network_id}' + assert args[0][0] == f"{url_prefix}networks/{network_id}" def test_inspect_network(self): network_id = 'abc12345' @@ -117,7 +117,7 @@ def test_inspect_network(self): assert result == network_data args = get.call_args - assert args[0][0] == url_prefix + f'networks/{network_id}' + assert args[0][0] == f"{url_prefix}networks/{network_id}" def test_connect_container_to_network(self): network_id = 'abc12345' @@ -135,7 +135,7 @@ def test_connect_container_to_network(self): ) assert post.call_args[0][0] == ( - url_prefix + f'networks/{network_id}/connect' + f"{url_prefix}networks/{network_id}/connect" ) assert json.loads(post.call_args[1]['data']) == { @@ -158,7 +158,7 @@ def test_disconnect_container_from_network(self): container={'Id': container_id}, net_id=network_id) assert post.call_args[0][0] == ( - url_prefix + f'networks/{network_id}/disconnect' + f"{url_prefix}networks/{network_id}/disconnect" ) assert json.loads(post.call_args[1]['data']) == { 'Container': container_id diff --git a/tests/unit/api_test.py b/tests/unit/api_test.py index 4b6099c904..78c0bab12e 100644 --- a/tests/unit/api_test.py +++ b/tests/unit/api_test.py @@ -86,9 +86,7 @@ def fake_read_from_socket(self, response, stream, tty=False, demux=False): url_base = f'{fake_api.prefix}/' -url_prefix = '{}v{}/'.format( - url_base, - docker.constants.DEFAULT_DOCKER_API_VERSION) +url_prefix = f'{url_base}v{docker.constants.DEFAULT_DOCKER_API_VERSION}/' class BaseAPIClientTest(unittest.TestCase): @@ -130,22 +128,18 @@ def test_ctor(self): def test_url_valid_resource(self): url = self.client._url('/hello/{0}/world', 'somename') - assert url == '{}{}'.format(url_prefix, 'hello/somename/world') + assert url == f"{url_prefix}hello/somename/world" url = self.client._url( '/hello/{0}/world/{1}', 'somename', 'someothername' ) - assert url == '{}{}'.format( - url_prefix, 'hello/somename/world/someothername' - ) + assert url == f"{url_prefix}hello/somename/world/someothername" url = self.client._url('/hello/{0}/world', 'some?name') - assert url == '{}{}'.format(url_prefix, 'hello/some%3Fname/world') + assert url == f"{url_prefix}hello/some%3Fname/world" url = self.client._url("/images/{0}/push", "localhost:5000/image") - assert url == '{}{}'.format( - url_prefix, 'images/localhost:5000/image/push' - ) + assert url == f"{url_prefix}images/localhost:5000/image/push" def test_url_invalid_resource(self): with pytest.raises(ValueError): @@ -153,20 +147,20 @@ def test_url_invalid_resource(self): def test_url_no_resource(self): url = self.client._url('/simple') - assert url == '{}{}'.format(url_prefix, 'simple') + assert url == f"{url_prefix}simple" def test_url_unversioned_api(self): url = self.client._url( '/hello/{0}/world', 'somename', versioned_api=False ) - assert url == '{}{}'.format(url_base, 'hello/somename/world') + assert url == f"{url_base}hello/somename/world" def test_version(self): self.client.version() fake_request.assert_called_with( 'GET', - url_prefix + 'version', + f"{url_prefix}version", timeout=DEFAULT_TIMEOUT_SECONDS ) @@ -175,7 +169,7 @@ def test_version_no_api_version(self): fake_request.assert_called_with( 'GET', - url_base + 'version', + f"{url_base}version", timeout=DEFAULT_TIMEOUT_SECONDS ) @@ -194,7 +188,7 @@ def test_info(self): fake_request.assert_called_with( 'GET', - url_prefix + 'info', + f"{url_prefix}info", timeout=DEFAULT_TIMEOUT_SECONDS ) @@ -203,7 +197,7 @@ def test_search(self): fake_request.assert_called_with( 'GET', - url_prefix + 'images/search', + f"{url_prefix}images/search", params={'term': 'busybox'}, timeout=DEFAULT_TIMEOUT_SECONDS ) @@ -212,7 +206,7 @@ def test_login(self): self.client.login('sakuya', 'izayoi') args = fake_request.call_args assert args[0][0] == 'POST' - assert args[0][1] == url_prefix + 'auth' + assert args[0][1] == f"{url_prefix}auth" assert json.loads(args[1]['data']) == { 'username': 'sakuya', 'password': 'izayoi' } @@ -229,7 +223,7 @@ def test_events(self): fake_request.assert_called_with( 'GET', - url_prefix + 'events', + f"{url_prefix}events", params={'since': None, 'until': None, 'filters': None}, stream=True, timeout=None @@ -245,7 +239,7 @@ def test_events_with_since_until(self): fake_request.assert_called_with( 'GET', - url_prefix + 'events', + f"{url_prefix}events", params={ 'since': ts - 10, 'until': ts + 10, @@ -264,7 +258,7 @@ def test_events_with_filters(self): expected_filters = docker.utils.convert_filters(filters) fake_request.assert_called_with( 'GET', - url_prefix + 'events', + f"{url_prefix}events", params={ 'since': None, 'until': None, @@ -318,7 +312,7 @@ def test_remove_link(self): fake_request.assert_called_with( 'DELETE', - url_prefix + 'containers/' + fake_api.FAKE_CONTAINER_ID, + f"{url_prefix}containers/{fake_api.FAKE_CONTAINER_ID}", params={'v': False, 'link': True, 'force': False}, timeout=DEFAULT_TIMEOUT_SECONDS ) @@ -332,7 +326,7 @@ def test_create_host_config_secopt(self): self.client.create_host_config(security_opt='wrong') def test_stream_helper_decoding(self): - status_code, content = fake_api.fake_responses[url_prefix + 'events']() + status_code, content = fake_api.fake_responses[f"{url_prefix}events"]() content_str = json.dumps(content) content_str = content_str.encode('utf-8') body = io.BytesIO(content_str) @@ -443,7 +437,7 @@ def test_early_stream_response(self): lines = [] for i in range(0, 50): line = str(i).encode() - lines += [('%x' % len(line)).encode(), line] + lines += [f'{len(line):x}'.encode(), line] lines.append(b'0') lines.append(b'') @@ -454,7 +448,7 @@ def test_early_stream_response(self): ) + b'\r\n'.join(lines) with APIClient( - base_url="http+unix://" + self.socket_file, + base_url=f"http+unix://{self.socket_file}", version=DEFAULT_DOCKER_API_VERSION) as client: for i in range(5): try: @@ -490,8 +484,7 @@ def setup_class(cls): cls.thread = threading.Thread(target=cls.server.serve_forever) cls.thread.daemon = True cls.thread.start() - cls.address = 'http://{}:{}'.format( - socket.gethostname(), cls.server.server_address[1]) + cls.address = f'http://{socket.gethostname()}:{cls.server.server_address[1]}' @classmethod def teardown_class(cls): @@ -600,7 +593,7 @@ def setUp(self): self.patcher = mock.patch.object( APIClient, 'send', - return_value=fake_resp("GET", "%s/version" % fake_api.prefix) + return_value=fake_resp("GET", f"{fake_api.prefix}/version") ) self.mock_send = self.patcher.start() @@ -613,7 +606,7 @@ def test_default_user_agent(self): assert self.mock_send.call_count == 1 headers = self.mock_send.call_args[0][0].headers - expected = 'docker-sdk-python/%s' % docker.__version__ + expected = f'docker-sdk-python/{docker.__version__}' assert headers['User-Agent'] == expected def test_custom_user_agent(self): diff --git a/tests/unit/api_volume_test.py b/tests/unit/api_volume_test.py index a8d9193f75..0a97ca5150 100644 --- a/tests/unit/api_volume_test.py +++ b/tests/unit/api_volume_test.py @@ -14,7 +14,7 @@ def test_list_volumes(self): args = fake_request.call_args assert args[0][0] == 'GET' - assert args[0][1] == url_prefix + 'volumes' + assert args[0][1] == f"{url_prefix}volumes" def test_list_volumes_and_filters(self): volumes = self.client.volumes(filters={'dangling': True}) @@ -23,7 +23,7 @@ def test_list_volumes_and_filters(self): args = fake_request.call_args assert args[0][0] == 'GET' - assert args[0][1] == url_prefix + 'volumes' + assert args[0][1] == f"{url_prefix}volumes" assert args[1] == {'params': {'filters': '{"dangling": ["true"]}'}, 'timeout': 60} @@ -37,7 +37,7 @@ def test_create_volume(self): args = fake_request.call_args assert args[0][0] == 'POST' - assert args[0][1] == url_prefix + 'volumes/create' + assert args[0][1] == f"{url_prefix}volumes/create" assert json.loads(args[1]['data']) == {'Name': name} @requires_api_version('1.23') @@ -63,7 +63,7 @@ def test_create_volume_with_driver(self): args = fake_request.call_args assert args[0][0] == 'POST' - assert args[0][1] == url_prefix + 'volumes/create' + assert args[0][1] == f"{url_prefix}volumes/create" data = json.loads(args[1]['data']) assert 'Driver' in data assert data['Driver'] == driver_name diff --git a/tests/unit/auth_test.py b/tests/unit/auth_test.py index dd5b5f8b57..26254fadde 100644 --- a/tests/unit/auth_test.py +++ b/tests/unit/auth_test.py @@ -290,9 +290,10 @@ def test_load_config_with_random_name(self): folder = tempfile.mkdtemp() self.addCleanup(shutil.rmtree, folder) - dockercfg_path = os.path.join(folder, - '.{}.dockercfg'.format( - random.randrange(100000))) + dockercfg_path = os.path.join( + folder, + f'.{random.randrange(100000)}.dockercfg', + ) registry = 'https://your.private.registry.io' auth_ = base64.b64encode(b'sakuya:izayoi').decode('ascii') config = { diff --git a/tests/unit/client_test.py b/tests/unit/client_test.py index e7c7eec827..1148d7ac1c 100644 --- a/tests/unit/client_test.py +++ b/tests/unit/client_test.py @@ -85,10 +85,7 @@ def test_default_pool_size_unix(self, mock_obj): mock_obj.return_value.urlopen.return_value.status = 200 client.ping() - base_url = "{base_url}/v{version}/_ping".format( - base_url=client.api.base_url, - version=client.api._version - ) + base_url = f"{client.api.base_url}/v{client.api._version}/_ping" mock_obj.assert_called_once_with(base_url, "/var/run/docker.sock", @@ -124,10 +121,7 @@ def test_pool_size_unix(self, mock_obj): mock_obj.return_value.urlopen.return_value.status = 200 client.ping() - base_url = "{base_url}/v{version}/_ping".format( - base_url=client.api.base_url, - version=client.api._version - ) + base_url = f"{client.api.base_url}/v{client.api._version}/_ping" mock_obj.assert_called_once_with(base_url, "/var/run/docker.sock", @@ -198,10 +192,7 @@ def test_default_pool_size_from_env_unix(self, mock_obj): mock_obj.return_value.urlopen.return_value.status = 200 client.ping() - base_url = "{base_url}/v{version}/_ping".format( - base_url=client.api.base_url, - version=client.api._version - ) + base_url = f"{client.api.base_url}/v{client.api._version}/_ping" mock_obj.assert_called_once_with(base_url, "/var/run/docker.sock", @@ -235,10 +226,7 @@ def test_pool_size_from_env_unix(self, mock_obj): mock_obj.return_value.urlopen.return_value.status = 200 client.ping() - base_url = "{base_url}/v{version}/_ping".format( - base_url=client.api.base_url, - version=client.api._version - ) + base_url = f"{client.api.base_url}/v{client.api._version}/_ping" mock_obj.assert_called_once_with(base_url, "/var/run/docker.sock", diff --git a/tests/unit/fake_api.py b/tests/unit/fake_api.py index 6acfb64b8c..133a99f80e 100644 --- a/tests/unit/fake_api.py +++ b/tests/unit/fake_api.py @@ -617,17 +617,11 @@ def post_fake_secret(): get_fake_volume_list, (f'{prefix}/{CURRENT_VERSION}/volumes/create', 'POST'): get_fake_volume, - ('{1}/{0}/volumes/{2}'.format( - CURRENT_VERSION, prefix, FAKE_VOLUME_NAME - ), 'GET'): + (f'{prefix}/{CURRENT_VERSION}/volumes/{FAKE_VOLUME_NAME}', 'GET'): get_fake_volume, - ('{1}/{0}/volumes/{2}'.format( - CURRENT_VERSION, prefix, FAKE_VOLUME_NAME - ), 'DELETE'): + (f'{prefix}/{CURRENT_VERSION}/volumes/{FAKE_VOLUME_NAME}', 'DELETE'): fake_remove_volume, - ('{1}/{0}/nodes/{2}/update?version=1'.format( - CURRENT_VERSION, prefix, FAKE_NODE_ID - ), 'POST'): + (f'{prefix}/{CURRENT_VERSION}/nodes/{FAKE_NODE_ID}/update?version=1', 'POST'): post_fake_update_node, (f'{prefix}/{CURRENT_VERSION}/swarm/join', 'POST'): post_fake_join_swarm, @@ -635,21 +629,13 @@ def post_fake_secret(): get_fake_network_list, (f'{prefix}/{CURRENT_VERSION}/networks/create', 'POST'): post_fake_network, - ('{1}/{0}/networks/{2}'.format( - CURRENT_VERSION, prefix, FAKE_NETWORK_ID - ), 'GET'): + (f'{prefix}/{CURRENT_VERSION}/networks/{FAKE_NETWORK_ID}', 'GET'): get_fake_network, - ('{1}/{0}/networks/{2}'.format( - CURRENT_VERSION, prefix, FAKE_NETWORK_ID - ), 'DELETE'): + (f'{prefix}/{CURRENT_VERSION}/networks/{FAKE_NETWORK_ID}', 'DELETE'): delete_fake_network, - ('{1}/{0}/networks/{2}/connect'.format( - CURRENT_VERSION, prefix, FAKE_NETWORK_ID - ), 'POST'): + (f'{prefix}/{CURRENT_VERSION}/networks/{FAKE_NETWORK_ID}/connect', 'POST'): post_fake_network_connect, - ('{1}/{0}/networks/{2}/disconnect'.format( - CURRENT_VERSION, prefix, FAKE_NETWORK_ID - ), 'POST'): + (f'{prefix}/{CURRENT_VERSION}/networks/{FAKE_NETWORK_ID}/disconnect', 'POST'): post_fake_network_disconnect, f'{prefix}/{CURRENT_VERSION}/secrets/create': post_fake_secret, diff --git a/tests/unit/swarm_test.py b/tests/unit/swarm_test.py index aee1b9e802..3fc7c68cd5 100644 --- a/tests/unit/swarm_test.py +++ b/tests/unit/swarm_test.py @@ -20,7 +20,7 @@ def test_node_update(self): ) args = fake_request.call_args assert args[0][1] == ( - url_prefix + 'nodes/24ifsmvkjbyhk/update?version=1' + f"{url_prefix}nodes/24ifsmvkjbyhk/update?version=1" ) assert json.loads(args[1]['data']) == node_spec assert args[1]['headers']['Content-Type'] == 'application/json' @@ -45,7 +45,7 @@ def test_join_swarm(self): args = fake_request.call_args - assert (args[0][1] == url_prefix + 'swarm/join') + assert (args[0][1] == f"{url_prefix}swarm/join") assert (json.loads(args[1]['data']) == data) assert (args[1]['headers']['Content-Type'] == 'application/json') @@ -64,6 +64,6 @@ def test_join_swarm_no_listen_address_takes_default(self): args = fake_request.call_args - assert (args[0][1] == url_prefix + 'swarm/join') + assert (args[0][1] == f"{url_prefix}swarm/join") assert (json.loads(args[1]['data']) == data) assert (args[1]['headers']['Content-Type'] == 'application/json') diff --git a/tests/unit/utils_test.py b/tests/unit/utils_test.py index 12cb7bd657..9c8a55bd52 100644 --- a/tests/unit/utils_test.py +++ b/tests/unit/utils_test.py @@ -486,9 +486,9 @@ def test_split_port_with_host_ip(self): def test_split_port_with_protocol(self): for protocol in ['tcp', 'udp', 'sctp']: internal_port, external_port = split_port( - "127.0.0.1:1000:2000/" + protocol + f"127.0.0.1:1000:2000/{protocol}" ) - assert internal_port == ["2000/" + protocol] + assert internal_port == [f"2000/{protocol}"] assert external_port == [("127.0.0.1", "1000")] def test_split_port_with_host_ip_no_port(self): From 9313536601064f8e7654af655604e4de24483b09 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Thu, 11 May 2023 15:45:07 +0300 Subject: [PATCH 18/45] Switch linting from flake8 to ruff Signed-off-by: Aarni Koskela --- .github/workflows/ci.yml | 10 +++++----- CONTRIBUTING.md | 2 +- Makefile | 8 ++++---- docker/__init__.py | 1 - docker/api/__init__.py | 1 - docker/context/__init__.py | 1 - docker/credentials/__init__.py | 4 ++-- docker/transport/__init__.py | 1 - docker/types/__init__.py | 1 - docker/utils/__init__.py | 2 +- pyproject.toml | 3 +++ test-requirements.txt | 2 +- tox.ini | 6 +++--- 13 files changed, 20 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f23873f0e8..dfbcc701eb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,16 +6,16 @@ env: DOCKER_BUILDKIT: '1' jobs: - flake8: + lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: - python-version: '3.x' - - run: pip install -U flake8 - - name: Run flake8 - run: flake8 docker/ tests/ + python-version: '3.11' + - run: pip install -U ruff==0.0.284 + - name: Run ruff + run: ruff docker tests unit-tests: runs-on: ubuntu-latest diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 861731188c..acf22ef7ca 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,7 +44,7 @@ paragraph in the Docker contribution guidelines. Before we can review your pull request, please ensure that nothing has been broken by your changes by running the test suite. You can do so simply by running `make test` in the project root. This also includes coding style using -`flake8` +`ruff` ### 3. Write clear, self-contained commits diff --git a/Makefile b/Makefile index ae6ae34ef2..79486e3ec2 100644 --- a/Makefile +++ b/Makefile @@ -46,7 +46,7 @@ build-dind-certs: docker build -t dpy-dind-certs -f tests/Dockerfile-dind-certs . .PHONY: test -test: flake8 unit-test-py3 integration-dind integration-dind-ssl +test: ruff unit-test-py3 integration-dind integration-dind-ssl .PHONY: unit-test-py3 unit-test-py3: build-py3 @@ -163,9 +163,9 @@ integration-dind-ssl: build-dind-certs build-py3 setup-network docker rm -vf dpy-dind-ssl dpy-dind-certs -.PHONY: flake8 -flake8: build-py3 - docker run -t --rm docker-sdk-python3 flake8 docker tests +.PHONY: ruff +ruff: build-py3 + docker run -t --rm docker-sdk-python3 ruff docker tests .PHONY: docs docs: build-docs diff --git a/docker/__init__.py b/docker/__init__.py index 46beb532a7..c1c518c56d 100644 --- a/docker/__init__.py +++ b/docker/__init__.py @@ -1,4 +1,3 @@ -# flake8: noqa from .api import APIClient from .client import DockerClient, from_env from .context import Context diff --git a/docker/api/__init__.py b/docker/api/__init__.py index ff5184414b..7260e9537e 100644 --- a/docker/api/__init__.py +++ b/docker/api/__init__.py @@ -1,2 +1 @@ -# flake8: noqa from .client import APIClient diff --git a/docker/context/__init__.py b/docker/context/__init__.py index 0a6707f997..dbf172fdac 100644 --- a/docker/context/__init__.py +++ b/docker/context/__init__.py @@ -1,3 +1,2 @@ -# flake8: noqa from .context import Context from .api import ContextAPI diff --git a/docker/credentials/__init__.py b/docker/credentials/__init__.py index 31ad28e34d..db3e1fbfd0 100644 --- a/docker/credentials/__init__.py +++ b/docker/credentials/__init__.py @@ -1,4 +1,4 @@ -# flake8: noqa + from .store import Store from .errors import StoreError, CredentialsNotFound -from .constants import * +from .constants import * # noqa: F403 diff --git a/docker/transport/__init__.py b/docker/transport/__init__.py index e37fc3ba21..54492c11ac 100644 --- a/docker/transport/__init__.py +++ b/docker/transport/__init__.py @@ -1,4 +1,3 @@ -# flake8: noqa from .unixconn import UnixHTTPAdapter from .ssladapter import SSLHTTPAdapter try: diff --git a/docker/types/__init__.py b/docker/types/__init__.py index b425746e78..89f2238934 100644 --- a/docker/types/__init__.py +++ b/docker/types/__init__.py @@ -1,4 +1,3 @@ -# flake8: noqa from .containers import ( ContainerConfig, HostConfig, LogConfig, Ulimit, DeviceRequest ) diff --git a/docker/utils/__init__.py b/docker/utils/__init__.py index 81c8186c84..944c6e65e0 100644 --- a/docker/utils/__init__.py +++ b/docker/utils/__init__.py @@ -1,4 +1,4 @@ -# flake8: noqa + from .build import create_archive, exclude_paths, mkbuildcontext, tar from .decorators import check_resource, minimum_version, update_headers from .utils import ( diff --git a/pyproject.toml b/pyproject.toml index 9554358e56..eb87cefbbf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,3 +3,6 @@ requires = ["setuptools>=45", "setuptools_scm[toml]>=6.2"] [tool.setuptools_scm] write_to = 'docker/_version.py' + +[tool.ruff.per-file-ignores] +"**/__init__.py" = ["F401"] diff --git a/test-requirements.txt b/test-requirements.txt index b7457fa773..951b3be9fc 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,6 +1,6 @@ setuptools==65.5.1 coverage==6.4.2 -flake8==4.0.1 +ruff==0.0.284 pytest==7.1.2 pytest-cov==3.0.0 pytest-timeout==2.1.0 diff --git a/tox.ini b/tox.ini index 9edc15c54e..2028dd3957 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py{37,38,39,310,311}, flake8 +envlist = py{37,38,39,310,311}, ruff skipsdist=True [testenv] @@ -10,7 +10,7 @@ deps = -r{toxinidir}/test-requirements.txt -r{toxinidir}/requirements.txt -[testenv:flake8] -commands = flake8 docker tests setup.py +[testenv:ruff] +commands = ruff docker tests setup.py deps = -r{toxinidir}/test-requirements.txt From fad792bfc76852a17fb7d717b23122aeb7b0bbd6 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Thu, 11 May 2023 15:50:13 +0300 Subject: [PATCH 19/45] Get rid of star import Signed-off-by: Aarni Koskela --- docker/credentials/__init__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docker/credentials/__init__.py b/docker/credentials/__init__.py index db3e1fbfd0..a1247700d3 100644 --- a/docker/credentials/__init__.py +++ b/docker/credentials/__init__.py @@ -1,4 +1,8 @@ - from .store import Store from .errors import StoreError, CredentialsNotFound -from .constants import * # noqa: F403 +from .constants import ( + DEFAULT_LINUX_STORE, + DEFAULT_OSX_STORE, + DEFAULT_WIN32_STORE, + PROGRAM_PREFIX, +) From ec58856ee3145eef2b0d02e72d3ee4548b241b0e Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Thu, 11 May 2023 15:52:37 +0300 Subject: [PATCH 20/45] Clean up unnecessary noqa comments Signed-off-by: Aarni Koskela --- docker/types/containers.py | 7 +++++-- docs/conf.py | 2 +- setup.py | 2 +- tests/integration/api_client_test.py | 2 +- tests/unit/fake_api.py | 6 +++--- 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/docker/types/containers.py b/docker/types/containers.py index 6d54aa65cc..a28061383d 100644 --- a/docker/types/containers.py +++ b/docker/types/containers.py @@ -48,8 +48,11 @@ class LogConfig(DictType): >>> container = client.create_container('busybox', 'true', ... host_config=hc) >>> client.inspect_container(container)['HostConfig']['LogConfig'] - {'Type': 'json-file', 'Config': {'labels': 'production_status,geo', 'max-size': '1g'}} - """ # noqa: E501 + { + 'Type': 'json-file', + 'Config': {'labels': 'production_status,geo', 'max-size': '1g'} + } + """ types = LogConfigTypesEnum def __init__(self, **kwargs): diff --git a/docs/conf.py b/docs/conf.py index e9971e0d2e..a529f8be82 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,6 +18,7 @@ import datetime import os import sys +from importlib.metadata import version sys.path.insert(0, os.path.abspath('..')) @@ -64,7 +65,6 @@ # built documents. # # see https://github.com/pypa/setuptools_scm#usage-from-sphinx -from importlib.metadata import version release = version('docker') # for example take major/minor version = '.'.join(release.split('.')[:2]) diff --git a/setup.py b/setup.py index ff6da71419..d0145d235e 100644 --- a/setup.py +++ b/setup.py @@ -48,7 +48,7 @@ url='https://github.com/docker/docker-py', project_urls={ 'Documentation': 'https://docker-py.readthedocs.io', - 'Changelog': 'https://docker-py.readthedocs.io/en/stable/change-log.html', # noqa: E501 + 'Changelog': 'https://docker-py.readthedocs.io/en/stable/change-log.html', 'Source': 'https://github.com/docker/docker-py', 'Tracker': 'https://github.com/docker/docker-py/issues', }, diff --git a/tests/integration/api_client_test.py b/tests/integration/api_client_test.py index d7a22a04af..ae71a57bf9 100644 --- a/tests/integration/api_client_test.py +++ b/tests/integration/api_client_test.py @@ -47,7 +47,7 @@ def test_timeout(self): # This call isn't supposed to complete, and it should fail fast. try: res = self.client.inspect_container('id') - except: # noqa: E722 + except Exception: pass end = time.time() assert res is None diff --git a/tests/unit/fake_api.py b/tests/unit/fake_api.py index 133a99f80e..87d8927578 100644 --- a/tests/unit/fake_api.py +++ b/tests/unit/fake_api.py @@ -4,10 +4,10 @@ CURRENT_VERSION = f'v{constants.DEFAULT_DOCKER_API_VERSION}' -FAKE_CONTAINER_ID = '81cf499cc928ce3fedc250a080d2b9b978df20e4517304c45211e8a68b33e254' # noqa: E501 +FAKE_CONTAINER_ID = '81cf499cc928ce3fedc250a080d2b9b978df20e4517304c45211e8a68b33e254' FAKE_IMAGE_ID = 'sha256:fe7a8fc91d3f17835cbb3b86a1c60287500ab01a53bc79c4497d09f07a3f0688' # noqa: E501 -FAKE_EXEC_ID = 'b098ec855f10434b5c7c973c78484208223a83f663ddaefb0f02a242840cb1c7' # noqa: E501 -FAKE_NETWORK_ID = '1999cfb42e414483841a125ade3c276c3cb80cb3269b14e339354ac63a31b02c' # noqa: E501 +FAKE_EXEC_ID = 'b098ec855f10434b5c7c973c78484208223a83f663ddaefb0f02a242840cb1c7' +FAKE_NETWORK_ID = '1999cfb42e414483841a125ade3c276c3cb80cb3269b14e339354ac63a31b02c' FAKE_IMAGE_NAME = 'test_image' FAKE_TARBALL_PATH = '/path/to/tarball' FAKE_REPO_NAME = 'repo' From 601476733c26aeff303b27c9da59701c86d49742 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Thu, 11 May 2023 15:55:09 +0300 Subject: [PATCH 21/45] Enable Ruff C rules and autofix Signed-off-by: Aarni Koskela --- docker/api/client.py | 2 +- docker/utils/utils.py | 2 +- pyproject.toml | 11 ++ setup.py | 2 +- tests/integration/api_build_test.py | 6 +- tests/integration/api_healthcheck_test.py | 40 ++--- tests/integration/api_image_test.py | 6 +- tests/integration/api_plugin_test.py | 4 +- tests/integration/context_api_test.py | 2 +- tests/integration/models_containers_test.py | 8 +- tests/integration/models_images_test.py | 4 +- tests/integration/models_networks_test.py | 4 +- tests/ssh/api_build_test.py | 6 +- tests/unit/models_containers_test.py | 190 ++++++++++---------- 14 files changed, 147 insertions(+), 140 deletions(-) diff --git a/docker/api/client.py b/docker/api/client.py index 8633025f3f..ce1b3a307b 100644 --- a/docker/api/client.py +++ b/docker/api/client.py @@ -476,7 +476,7 @@ def _get_result_tty(self, stream, res, is_tty): return self._multiplexed_response_stream_helper(res) else: return sep.join( - [x for x in self._multiplexed_buffer_helper(res)] + list(self._multiplexed_buffer_helper(res)) ) def _unmount(self, *args): diff --git a/docker/utils/utils.py b/docker/utils/utils.py index 15e3869000..234be32076 100644 --- a/docker/utils/utils.py +++ b/docker/utils/utils.py @@ -217,7 +217,7 @@ def parse_host(addr, is_win32=False, tls=False): parsed_url = urlparse(addr) proto = parsed_url.scheme - if not proto or any([x not in f"{string.ascii_letters}+" for x in proto]): + if not proto or any(x not in f"{string.ascii_letters}+" for x in proto): # https://bugs.python.org/issue754016 parsed_url = urlparse(f"//{addr}", 'tcp') proto = 'tcp' diff --git a/pyproject.toml b/pyproject.toml index eb87cefbbf..82d4869006 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,5 +4,16 @@ requires = ["setuptools>=45", "setuptools_scm[toml]>=6.2"] [tool.setuptools_scm] write_to = 'docker/_version.py' +[tool.ruff] +target-version = "py37" +extend-select = [ + "C", + "F", + "W", +] +ignore = [ + "C901", # too complex (there's a whole bunch of these) +] + [tool.ruff.per-file-ignores] "**/__init__.py" = ["F401"] diff --git a/setup.py b/setup.py index d0145d235e..866aa23c8d 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ } with open('./test-requirements.txt') as test_reqs_txt: - test_requirements = [line for line in test_reqs_txt] + test_requirements = list(test_reqs_txt) long_description = '' diff --git a/tests/integration/api_build_test.py b/tests/integration/api_build_test.py index 606c3b7e11..e5e7904d8f 100644 --- a/tests/integration/api_build_test.py +++ b/tests/integration/api_build_test.py @@ -142,7 +142,7 @@ def test_build_with_dockerignore(self): logs = logs.decode('utf-8') - assert sorted(list(filter(None, logs.split('\n')))) == sorted([ + assert sorted(filter(None, logs.split('\n'))) == sorted([ '/test/#file.txt', '/test/ignored/subdir/excepted-with-spaces', '/test/ignored/subdir/excepted-file', @@ -312,7 +312,7 @@ def test_build_with_network_mode(self): ) self.tmp_imgs.append('dockerpytest_nonebuild') - logs = [chunk for chunk in stream] + logs = list(stream) assert 'errorDetail' in logs[-1] assert logs[-1]['errorDetail']['code'] == 1 @@ -392,7 +392,7 @@ def test_build_stderr_data(self): expected = '{0}{2}\n{1}'.format( control_chars[0], control_chars[1], snippet ) - assert any([line == expected for line in lines]) + assert any(line == expected for line in lines) def test_build_gzip_encoding(self): base_dir = tempfile.mkdtemp() diff --git a/tests/integration/api_healthcheck_test.py b/tests/integration/api_healthcheck_test.py index c54583b0be..9ecdcd86a6 100644 --- a/tests/integration/api_healthcheck_test.py +++ b/tests/integration/api_healthcheck_test.py @@ -16,7 +16,7 @@ class HealthcheckTest(BaseAPIIntegrationTest): @helpers.requires_api_version('1.24') def test_healthcheck_shell_command(self): container = self.client.create_container( - TEST_IMG, 'top', healthcheck=dict(test='echo "hello world"')) + TEST_IMG, 'top', healthcheck={'test': 'echo "hello world"'}) self.tmp_containers.append(container) res = self.client.inspect_container(container) @@ -27,12 +27,12 @@ def test_healthcheck_shell_command(self): @helpers.requires_api_version('1.24') def test_healthcheck_passes(self): container = self.client.create_container( - TEST_IMG, 'top', healthcheck=dict( - test="true", - interval=1 * SECOND, - timeout=1 * SECOND, - retries=1, - )) + TEST_IMG, 'top', healthcheck={ + 'test': "true", + 'interval': 1 * SECOND, + 'timeout': 1 * SECOND, + 'retries': 1, + }) self.tmp_containers.append(container) self.client.start(container) wait_on_health_status(self.client, container, "healthy") @@ -40,12 +40,12 @@ def test_healthcheck_passes(self): @helpers.requires_api_version('1.24') def test_healthcheck_fails(self): container = self.client.create_container( - TEST_IMG, 'top', healthcheck=dict( - test="false", - interval=1 * SECOND, - timeout=1 * SECOND, - retries=1, - )) + TEST_IMG, 'top', healthcheck={ + 'test': "false", + 'interval': 1 * SECOND, + 'timeout': 1 * SECOND, + 'retries': 1, + }) self.tmp_containers.append(container) self.client.start(container) wait_on_health_status(self.client, container, "unhealthy") @@ -53,14 +53,14 @@ def test_healthcheck_fails(self): @helpers.requires_api_version('1.29') def test_healthcheck_start_period(self): container = self.client.create_container( - TEST_IMG, 'top', healthcheck=dict( - test="echo 'x' >> /counter.txt && " + TEST_IMG, 'top', healthcheck={ + 'test': "echo 'x' >> /counter.txt && " "test `cat /counter.txt | wc -l` -ge 3", - interval=1 * SECOND, - timeout=1 * SECOND, - retries=1, - start_period=3 * SECOND - ) + 'interval': 1 * SECOND, + 'timeout': 1 * SECOND, + 'retries': 1, + 'start_period': 3 * SECOND + } ) self.tmp_containers.append(container) diff --git a/tests/integration/api_image_test.py b/tests/integration/api_image_test.py index cb3d667112..7081b53b8f 100644 --- a/tests/integration/api_image_test.py +++ b/tests/integration/api_image_test.py @@ -263,10 +263,8 @@ def test_get_load_image(self): data = self.client.get_image(test_img) assert data output = self.client.load_image(data) - assert any([ - line for line in output - if f'Loaded image: {test_img}' in line.get('stream', '') - ]) + assert any(line for line in output + if f'Loaded image: {test_img}' in line.get('stream', '')) @contextlib.contextmanager def temporary_http_file_server(self, stream): diff --git a/tests/integration/api_plugin_test.py b/tests/integration/api_plugin_test.py index 3ecb028346..a35c30d3e9 100644 --- a/tests/integration/api_plugin_test.py +++ b/tests/integration/api_plugin_test.py @@ -118,7 +118,7 @@ def test_install_plugin(self): pass prv = self.client.plugin_privileges(SSHFS) - logs = [d for d in self.client.pull_plugin(SSHFS, prv)] + logs = list(self.client.pull_plugin(SSHFS, prv)) assert filter(lambda x: x['status'] == 'Download complete', logs) assert self.client.inspect_plugin(SSHFS) assert self.client.enable_plugin(SSHFS) @@ -128,7 +128,7 @@ def test_upgrade_plugin(self): pl_data = self.ensure_plugin_installed(SSHFS) assert pl_data['Enabled'] is False prv = self.client.plugin_privileges(SSHFS) - logs = [d for d in self.client.upgrade_plugin(SSHFS, SSHFS, prv)] + logs = list(self.client.upgrade_plugin(SSHFS, SSHFS, prv)) assert filter(lambda x: x['status'] == 'Download complete', logs) assert self.client.inspect_plugin(SSHFS) assert self.client.enable_plugin(SSHFS) diff --git a/tests/integration/context_api_test.py b/tests/integration/context_api_test.py index a2a12a5cb0..1a13f2817e 100644 --- a/tests/integration/context_api_test.py +++ b/tests/integration/context_api_test.py @@ -29,7 +29,7 @@ def test_lifecycle(self): "test", tls_cfg=docker_tls) # check for a context 'test' in the context store - assert any([ctx.Name == "test" for ctx in ContextAPI.contexts()]) + assert any(ctx.Name == "test" for ctx in ContextAPI.contexts()) # retrieve a context object for 'test' assert ContextAPI.get_context("test") # remove context diff --git a/tests/integration/models_containers_test.py b/tests/integration/models_containers_test.py index 5b0470b937..3cf74cbc59 100644 --- a/tests/integration/models_containers_test.py +++ b/tests/integration/models_containers_test.py @@ -109,7 +109,7 @@ def test_run_with_none_driver(self): out = client.containers.run( "alpine", "echo hello", - log_config=dict(type='none') + log_config={"type": 'none'} ) assert out is None @@ -118,7 +118,7 @@ def test_run_with_json_file_driver(self): out = client.containers.run( "alpine", "echo hello", - log_config=dict(type='json-file') + log_config={"type": 'json-file'} ) assert out == b'hello\n' @@ -150,7 +150,7 @@ def test_run_with_streamed_logs(self): out = client.containers.run( 'alpine', 'sh -c "echo hello && echo world"', stream=True ) - logs = [line for line in out] + logs = list(out) assert logs[0] == b'hello\n' assert logs[1] == b'world\n' @@ -165,7 +165,7 @@ def test_run_with_streamed_logs_and_cancel(self): threading.Timer(1, out.close).start() - logs = [line for line in out] + logs = list(out) assert len(logs) == 2 assert logs[0] == b'hello\n' diff --git a/tests/integration/models_images_test.py b/tests/integration/models_images_test.py index 94aa201004..d335da4a71 100644 --- a/tests/integration/models_images_test.py +++ b/tests/integration/models_images_test.py @@ -88,9 +88,7 @@ def test_pull_multiple(self): client = docker.from_env(version=TEST_API_VERSION) images = client.images.pull('hello-world', all_tags=True) assert len(images) >= 1 - assert any([ - 'hello-world:latest' in img.attrs['RepoTags'] for img in images - ]) + assert any('hello-world:latest' in img.attrs['RepoTags'] for img in images) def test_load_error(self): client = docker.from_env(version=TEST_API_VERSION) diff --git a/tests/integration/models_networks_test.py b/tests/integration/models_networks_test.py index 08d7ad2955..f4052e4ba1 100644 --- a/tests/integration/models_networks_test.py +++ b/tests/integration/models_networks_test.py @@ -59,11 +59,11 @@ def test_connect_disconnect(self): network.connect(container) container.start() assert client.networks.get(network.id).containers == [container] - network_containers = list( + network_containers = [ c for net in client.networks.list(ids=[network.id], greedy=True) for c in net.containers - ) + ] assert network_containers == [container] network.disconnect(container) assert network.containers == [] diff --git a/tests/ssh/api_build_test.py b/tests/ssh/api_build_test.py index ef48e12ed3..160d53f1e5 100644 --- a/tests/ssh/api_build_test.py +++ b/tests/ssh/api_build_test.py @@ -134,7 +134,7 @@ def test_build_with_dockerignore(self): logs = logs.decode('utf-8') - assert sorted(list(filter(None, logs.split('\n')))) == sorted([ + assert sorted(filter(None, logs.split('\n'))) == sorted([ '/test/#file.txt', '/test/ignored/subdir/excepted-file', '/test/not-ignored' @@ -303,7 +303,7 @@ def test_build_with_network_mode(self): ) self.tmp_imgs.append('dockerpytest_nonebuild') - logs = [chunk for chunk in stream] + logs = list(stream) assert 'errorDetail' in logs[-1] assert logs[-1]['errorDetail']['code'] == 1 @@ -383,7 +383,7 @@ def test_build_stderr_data(self): expected = '{0}{2}\n{1}'.format( control_chars[0], control_chars[1], snippet ) - assert any([line == expected for line in lines]) + assert any(line == expected for line in lines) def test_build_gzip_encoding(self): base_dir = tempfile.mkdtemp() diff --git a/tests/unit/models_containers_test.py b/tests/unit/models_containers_test.py index 0592af5e04..2eabd26859 100644 --- a/tests/unit/models_containers_test.py +++ b/tests/unit/models_containers_test.py @@ -31,77 +31,77 @@ def test_run(self): ) def test_create_container_args(self): - create_kwargs = _create_container_args(dict( - image='alpine', - command='echo hello world', - blkio_weight_device=[{'Path': 'foo', 'Weight': 3}], - blkio_weight=2, - cap_add=['foo'], - cap_drop=['bar'], - cgroup_parent='foobar', - cgroupns='host', - cpu_period=1, - cpu_quota=2, - cpu_shares=5, - cpuset_cpus='0-3', - detach=False, - device_read_bps=[{'Path': 'foo', 'Rate': 3}], - device_read_iops=[{'Path': 'foo', 'Rate': 3}], - device_write_bps=[{'Path': 'foo', 'Rate': 3}], - device_write_iops=[{'Path': 'foo', 'Rate': 3}], - devices=['/dev/sda:/dev/xvda:rwm'], - dns=['8.8.8.8'], - domainname='example.com', - dns_opt=['foo'], - dns_search=['example.com'], - entrypoint='/bin/sh', - environment={'FOO': 'BAR'}, - extra_hosts={'foo': '1.2.3.4'}, - group_add=['blah'], - ipc_mode='foo', - kernel_memory=123, - labels={'key': 'value'}, - links={'foo': 'bar'}, - log_config={'Type': 'json-file', 'Config': {}}, - lxc_conf={'foo': 'bar'}, - healthcheck={'test': 'true'}, - hostname='somehost', - mac_address='abc123', - mem_limit=123, - mem_reservation=123, - mem_swappiness=2, - memswap_limit=456, - name='somename', - network_disabled=False, - network='foo', - network_driver_opt={'key1': 'a'}, - oom_kill_disable=True, - oom_score_adj=5, - pid_mode='host', - pids_limit=500, - platform='linux', - ports={ + create_kwargs = _create_container_args({ + "image": 'alpine', + "command": 'echo hello world', + "blkio_weight_device": [{'Path': 'foo', 'Weight': 3}], + "blkio_weight": 2, + "cap_add": ['foo'], + "cap_drop": ['bar'], + "cgroup_parent": 'foobar', + "cgroupns": 'host', + "cpu_period": 1, + "cpu_quota": 2, + "cpu_shares": 5, + "cpuset_cpus": '0-3', + "detach": False, + "device_read_bps": [{'Path': 'foo', 'Rate': 3}], + "device_read_iops": [{'Path': 'foo', 'Rate': 3}], + "device_write_bps": [{'Path': 'foo', 'Rate': 3}], + "device_write_iops": [{'Path': 'foo', 'Rate': 3}], + "devices": ['/dev/sda:/dev/xvda:rwm'], + "dns": ['8.8.8.8'], + "domainname": 'example.com', + "dns_opt": ['foo'], + "dns_search": ['example.com'], + "entrypoint": '/bin/sh', + "environment": {'FOO': 'BAR'}, + "extra_hosts": {'foo': '1.2.3.4'}, + "group_add": ['blah'], + "ipc_mode": 'foo', + "kernel_memory": 123, + "labels": {'key': 'value'}, + "links": {'foo': 'bar'}, + "log_config": {'Type': 'json-file', 'Config': {}}, + "lxc_conf": {'foo': 'bar'}, + "healthcheck": {'test': 'true'}, + "hostname": 'somehost', + "mac_address": 'abc123', + "mem_limit": 123, + "mem_reservation": 123, + "mem_swappiness": 2, + "memswap_limit": 456, + "name": 'somename', + "network_disabled": False, + "network": 'foo', + "network_driver_opt": {'key1': 'a'}, + "oom_kill_disable": True, + "oom_score_adj": 5, + "pid_mode": 'host', + "pids_limit": 500, + "platform": 'linux', + "ports": { 1111: 4567, 2222: None }, - privileged=True, - publish_all_ports=True, - read_only=True, - restart_policy={'Name': 'always'}, - security_opt=['blah'], - shm_size=123, - stdin_open=True, - stop_signal=9, - sysctls={'foo': 'bar'}, - tmpfs={'/blah': ''}, - tty=True, - ulimits=[{"Name": "nofile", "Soft": 1024, "Hard": 2048}], - user='bob', - userns_mode='host', - uts_mode='host', - version='1.23', - volume_driver='some_driver', - volumes=[ + "privileged": True, + "publish_all_ports": True, + "read_only": True, + "restart_policy": {'Name': 'always'}, + "security_opt": ['blah'], + "shm_size": 123, + "stdin_open": True, + "stop_signal": 9, + "sysctls": {'foo': 'bar'}, + "tmpfs": {'/blah': ''}, + "tty": True, + "ulimits": [{"Name": "nofile", "Soft": 1024, "Hard": 2048}], + "user": 'bob', + "userns_mode": 'host', + "uts_mode": 'host', + "version": '1.23', + "volume_driver": 'some_driver', + "volumes": [ '/home/user1/:/mnt/vol2', '/var/www:/mnt/vol1:ro', 'volumename:/mnt/vol3r', @@ -109,18 +109,18 @@ def test_create_container_args(self): '/anothervolumewithnohostpath:ro', 'C:\\windows\\path:D:\\hello\\world:rw' ], - volumes_from=['container'], - working_dir='/code' - )) + "volumes_from": ['container'], + "working_dir": '/code' + }) - expected = dict( - image='alpine', - command='echo hello world', - domainname='example.com', - detach=False, - entrypoint='/bin/sh', - environment={'FOO': 'BAR'}, - host_config={ + expected = { + "image": 'alpine', + "command": 'echo hello world', + "domainname": 'example.com', + "detach": False, + "entrypoint": '/bin/sh', + "environment": {'FOO': 'BAR'}, + "host_config": { 'Binds': [ '/home/user1/:/mnt/vol2', '/var/www:/mnt/vol1:ro', @@ -183,20 +183,20 @@ def test_create_container_args(self): 'VolumeDriver': 'some_driver', 'VolumesFrom': ['container'], }, - healthcheck={'test': 'true'}, - hostname='somehost', - labels={'key': 'value'}, - mac_address='abc123', - name='somename', - network_disabled=False, - networking_config={'foo': {'driver_opt': {'key1': 'a'}}}, - platform='linux', - ports=[('1111', 'tcp'), ('2222', 'tcp')], - stdin_open=True, - stop_signal=9, - tty=True, - user='bob', - volumes=[ + "healthcheck": {'test': 'true'}, + "hostname": 'somehost', + "labels": {'key': 'value'}, + "mac_address": 'abc123', + "name": 'somename', + "network_disabled": False, + "networking_config": {'foo': {'driver_opt': {'key1': 'a'}}}, + "platform": 'linux', + "ports": [('1111', 'tcp'), ('2222', 'tcp')], + "stdin_open": True, + "stop_signal": 9, + "tty": True, + "user": 'bob', + "volumes": [ '/mnt/vol2', '/mnt/vol1', '/mnt/vol3r', @@ -204,8 +204,8 @@ def test_create_container_args(self): '/anothervolumewithnohostpath', 'D:\\hello\\world' ], - working_dir='/code' - ) + "working_dir": '/code' + } assert create_kwargs == expected From 8447f7b0f02d10399916b285f2fea4284f5a3005 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Thu, 11 May 2023 15:57:01 +0300 Subject: [PATCH 22/45] Enable Ruff B rules and autofix Signed-off-by: Aarni Koskela --- docker/models/plugins.py | 2 +- docker/utils/socket.py | 2 +- pyproject.toml | 1 + tests/integration/api_build_test.py | 18 +++++++++--------- tests/integration/api_plugin_test.py | 2 +- tests/integration/regression_test.py | 2 +- tests/ssh/api_build_test.py | 18 +++++++++--------- tests/unit/api_test.py | 6 +++--- 8 files changed, 26 insertions(+), 25 deletions(-) diff --git a/docker/models/plugins.py b/docker/models/plugins.py index 16f5245e9e..85d768c935 100644 --- a/docker/models/plugins.py +++ b/docker/models/plugins.py @@ -187,7 +187,7 @@ def install(self, remote_name, local_name=None): """ privileges = self.client.api.plugin_privileges(remote_name) it = self.client.api.pull_plugin(remote_name, privileges, local_name) - for data in it: + for _data in it: pass return self.get(local_name or remote_name) diff --git a/docker/utils/socket.py b/docker/utils/socket.py index cdc485ea3a..2306ed0736 100644 --- a/docker/utils/socket.py +++ b/docker/utils/socket.py @@ -42,7 +42,7 @@ def read(socket, n=4096): try: if hasattr(socket, 'recv'): return socket.recv(n) - if isinstance(socket, getattr(pysocket, 'SocketIO')): + if isinstance(socket, pysocket.SocketIO): return socket.read(n) return os.read(socket.fileno(), n) except OSError as e: diff --git a/pyproject.toml b/pyproject.toml index 82d4869006..0a67279661 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,7 @@ write_to = 'docker/_version.py' [tool.ruff] target-version = "py37" extend-select = [ + "B", "C", "F", "W", diff --git a/tests/integration/api_build_test.py b/tests/integration/api_build_test.py index e5e7904d8f..2add2d87af 100644 --- a/tests/integration/api_build_test.py +++ b/tests/integration/api_build_test.py @@ -132,7 +132,7 @@ def test_build_with_dockerignore(self): path=base_dir, tag=tag, ) - for chunk in stream: + for _chunk in stream: pass c = self.client.create_container(tag, ['find', '/test', '-type', 'f']) @@ -160,7 +160,7 @@ def test_build_with_buildargs(self): fileobj=script, tag='buildargs', buildargs={'test': 'OK'} ) self.tmp_imgs.append('buildargs') - for chunk in stream: + for _chunk in stream: pass info = self.client.inspect_image('buildargs') @@ -180,7 +180,7 @@ def test_build_shmsize(self): fileobj=script, tag=tag, shmsize=shmsize ) self.tmp_imgs.append(tag) - for chunk in stream: + for _chunk in stream: pass # There is currently no way to get the shmsize @@ -198,7 +198,7 @@ def test_build_isolation(self): isolation='default' ) - for chunk in stream: + for _chunk in stream: pass @requires_api_version('1.23') @@ -213,7 +213,7 @@ def test_build_labels(self): fileobj=script, tag='labels', labels=labels ) self.tmp_imgs.append('labels') - for chunk in stream: + for _chunk in stream: pass info = self.client.inspect_image('labels') @@ -230,7 +230,7 @@ def test_build_with_cache_from(self): stream = self.client.build(fileobj=script, tag='build1') self.tmp_imgs.append('build1') - for chunk in stream: + for _chunk in stream: pass stream = self.client.build( @@ -271,7 +271,7 @@ def test_build_container_with_target(self): fileobj=script, target='first', tag='build1' ) self.tmp_imgs.append('build1') - for chunk in stream: + for _chunk in stream: pass info = self.client.inspect_image('build1') @@ -300,7 +300,7 @@ def test_build_with_network_mode(self): ) self.tmp_imgs.append('dockerpytest_customnetbuild') - for chunk in stream: + for _chunk in stream: pass assert self.client.inspect_image('dockerpytest_customnetbuild') @@ -365,7 +365,7 @@ def build_squashed(squash): fileobj=script, tag=tag, squash=squash ) self.tmp_imgs.append(tag) - for chunk in stream: + for _chunk in stream: pass return self.client.inspect_image(tag) diff --git a/tests/integration/api_plugin_test.py b/tests/integration/api_plugin_test.py index a35c30d3e9..3f1633900d 100644 --- a/tests/integration/api_plugin_test.py +++ b/tests/integration/api_plugin_test.py @@ -39,7 +39,7 @@ def ensure_plugin_installed(self, plugin_name): return self.client.inspect_plugin(plugin_name) except docker.errors.NotFound: prv = self.client.plugin_privileges(plugin_name) - for d in self.client.pull_plugin(plugin_name, prv): + for _d in self.client.pull_plugin(plugin_name, prv): pass return self.client.inspect_plugin(plugin_name) diff --git a/tests/integration/regression_test.py b/tests/integration/regression_test.py index 10313a637c..7d2b228cc9 100644 --- a/tests/integration/regression_test.py +++ b/tests/integration/regression_test.py @@ -12,7 +12,7 @@ class TestRegressions(BaseAPIIntegrationTest): def test_443_handle_nonchunked_response_in_stream(self): dfile = io.BytesIO() with pytest.raises(docker.errors.APIError) as exc: - for line in self.client.build(fileobj=dfile, tag="a/b/c"): + for _line in self.client.build(fileobj=dfile, tag="a/b/c"): pass assert exc.value.is_error() dfile.close() diff --git a/tests/ssh/api_build_test.py b/tests/ssh/api_build_test.py index 160d53f1e5..d060f465f2 100644 --- a/tests/ssh/api_build_test.py +++ b/tests/ssh/api_build_test.py @@ -124,7 +124,7 @@ def test_build_with_dockerignore(self): path=base_dir, tag=tag, ) - for chunk in stream: + for _chunk in stream: pass c = self.client.create_container(tag, ['find', '/test', '-type', 'f']) @@ -151,7 +151,7 @@ def test_build_with_buildargs(self): fileobj=script, tag='buildargs', buildargs={'test': 'OK'} ) self.tmp_imgs.append('buildargs') - for chunk in stream: + for _chunk in stream: pass info = self.client.inspect_image('buildargs') @@ -171,7 +171,7 @@ def test_build_shmsize(self): fileobj=script, tag=tag, shmsize=shmsize ) self.tmp_imgs.append(tag) - for chunk in stream: + for _chunk in stream: pass # There is currently no way to get the shmsize @@ -189,7 +189,7 @@ def test_build_isolation(self): isolation='default' ) - for chunk in stream: + for _chunk in stream: pass @requires_api_version('1.23') @@ -204,7 +204,7 @@ def test_build_labels(self): fileobj=script, tag='labels', labels=labels ) self.tmp_imgs.append('labels') - for chunk in stream: + for _chunk in stream: pass info = self.client.inspect_image('labels') @@ -221,7 +221,7 @@ def test_build_with_cache_from(self): stream = self.client.build(fileobj=script, tag='build1') self.tmp_imgs.append('build1') - for chunk in stream: + for _chunk in stream: pass stream = self.client.build( @@ -262,7 +262,7 @@ def test_build_container_with_target(self): fileobj=script, target='first', tag='build1' ) self.tmp_imgs.append('build1') - for chunk in stream: + for _chunk in stream: pass info = self.client.inspect_image('build1') @@ -291,7 +291,7 @@ def test_build_with_network_mode(self): ) self.tmp_imgs.append('dockerpytest_customnetbuild') - for chunk in stream: + for _chunk in stream: pass assert self.client.inspect_image('dockerpytest_customnetbuild') @@ -356,7 +356,7 @@ def build_squashed(squash): fileobj=script, tag=tag, squash=squash ) self.tmp_imgs.append(tag) - for chunk in stream: + for _chunk in stream: pass return self.client.inspect_image(tag) diff --git a/tests/unit/api_test.py b/tests/unit/api_test.py index 78c0bab12e..99aa23bc20 100644 --- a/tests/unit/api_test.py +++ b/tests/unit/api_test.py @@ -333,8 +333,8 @@ def test_stream_helper_decoding(self): # mock a stream interface raw_resp = urllib3.HTTPResponse(body=body) - setattr(raw_resp._fp, 'chunked', True) - setattr(raw_resp._fp, 'chunk_left', len(body.getvalue()) - 1) + raw_resp._fp.chunked = True + raw_resp._fp.chunk_left = len(body.getvalue()) - 1 # pass `decode=False` to the helper raw_resp._fp.seek(0) @@ -349,7 +349,7 @@ def test_stream_helper_decoding(self): assert result == content # non-chunked response, pass `decode=False` to the helper - setattr(raw_resp._fp, 'chunked', False) + raw_resp._fp.chunked = False raw_resp._fp.seek(0) resp = response(status_code=status_code, content=content, raw=raw_resp) result = next(self.client._stream_helper(resp)) From 6aec90a41bc07e1757f28304d3c9e068245afdb9 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Thu, 11 May 2023 15:58:14 +0300 Subject: [PATCH 23/45] Fix Ruff B904s (be explicit about exception causes) Signed-off-by: Aarni Koskela --- docker/api/client.py | 14 +++++++------- docker/auth.py | 2 +- docker/context/api.py | 2 +- docker/context/context.py | 2 +- docker/credentials/store.py | 6 +++--- docker/models/containers.py | 4 ++-- docker/tls.py | 2 +- docker/transport/npipeconn.py | 7 +++---- docker/transport/sshconn.py | 6 +++--- docker/types/daemon.py | 4 ++-- docker/utils/build.py | 4 ++-- docker/utils/json_stream.py | 2 +- docker/utils/utils.py | 4 ++-- tests/unit/auth_test.py | 4 ++-- 14 files changed, 31 insertions(+), 32 deletions(-) diff --git a/docker/api/client.py b/docker/api/client.py index ce1b3a307b..a2cb459de0 100644 --- a/docker/api/client.py +++ b/docker/api/client.py @@ -160,10 +160,10 @@ def __init__(self, base_url=None, version=None, base_url, timeout, pool_connections=num_pools, max_pool_size=max_pool_size ) - except NameError: + except NameError as err: raise DockerException( 'Install pypiwin32 package to enable npipe:// support' - ) + ) from err self.mount('http+docker://', self._custom_adapter) self.base_url = 'http+docker://localnpipe' elif base_url.startswith('ssh://'): @@ -172,10 +172,10 @@ def __init__(self, base_url=None, version=None, base_url, timeout, pool_connections=num_pools, max_pool_size=max_pool_size, shell_out=use_ssh_client ) - except NameError: + except NameError as err: raise DockerException( 'Install paramiko package to enable ssh:// support' - ) + ) from err self.mount('http+docker://ssh', self._custom_adapter) self._unmount('http://', 'https://') self.base_url = 'http+docker://ssh' @@ -211,15 +211,15 @@ def __init__(self, base_url=None, version=None, def _retrieve_server_version(self): try: return self.version(api_version=False)["ApiVersion"] - except KeyError: + except KeyError as ke: raise DockerException( 'Invalid response from docker daemon: key "ApiVersion"' ' is missing.' - ) + ) from ke except Exception as e: raise DockerException( f'Error while fetching server API version: {e}' - ) + ) from e def _set_request_timeout(self, kwargs): """Prepare the kwargs for an HTTP request by inserting the timeout diff --git a/docker/auth.py b/docker/auth.py index 4bce788701..7a301ba407 100644 --- a/docker/auth.py +++ b/docker/auth.py @@ -268,7 +268,7 @@ def _resolve_authconfig_credstore(self, registry, credstore_name): except credentials.StoreError as e: raise errors.DockerException( f'Credentials store error: {repr(e)}' - ) + ) from e def _get_store_instance(self, name): if name not in self._stores: diff --git a/docker/context/api.py b/docker/context/api.py index e340fb6dd9..493f470e5d 100644 --- a/docker/context/api.py +++ b/docker/context/api.py @@ -114,7 +114,7 @@ def contexts(cls): except Exception as e: raise errors.ContextException( f"Failed to load metafile {filename}: {e}", - ) + ) from e contexts = [cls.DEFAULT_CONTEXT] for name in names: diff --git a/docker/context/context.py b/docker/context/context.py index b607b77148..4faf8e7017 100644 --- a/docker/context/context.py +++ b/docker/context/context.py @@ -99,7 +99,7 @@ def _load_meta(cls, name): # unknown format raise Exception( f"Detected corrupted meta file for context {name} : {e}" - ) + ) from e # for docker endpoints, set defaults for # Host and SkipTLSVerify fields diff --git a/docker/credentials/store.py b/docker/credentials/store.py index 37c703e78c..5edeaa7f63 100644 --- a/docker/credentials/store.py +++ b/docker/credentials/store.py @@ -80,14 +80,14 @@ def _execute(self, subcmd, data_input): [self.exe, subcmd], input=data_input, env=env, ) except subprocess.CalledProcessError as e: - raise errors.process_store_error(e, self.program) + raise errors.process_store_error(e, self.program) from e except OSError as e: if e.errno == errno.ENOENT: raise errors.StoreError( f'{self.program} not installed or not available in PATH' - ) + ) from e else: raise errors.StoreError( f'Unexpected OS error "{e.strerror}", errno={e.errno}' - ) + ) from e return output diff --git a/docker/models/containers.py b/docker/models/containers.py index 64838397a6..44bb92a0fc 100644 --- a/docker/models/containers.py +++ b/docker/models/containers.py @@ -47,11 +47,11 @@ def labels(self): try: result = self.attrs['Config'].get('Labels') return result or {} - except KeyError: + except KeyError as ke: raise DockerException( 'Label data is not available for sparse objects. Call reload()' ' to retrieve all information' - ) + ) from ke @property def status(self): diff --git a/docker/tls.py b/docker/tls.py index f4dffb2e25..a4dd002091 100644 --- a/docker/tls.py +++ b/docker/tls.py @@ -55,7 +55,7 @@ def __init__(self, client_cert=None, ca_cert=None, verify=None, raise errors.TLSParameterError( 'client_cert must be a tuple of' ' (client certificate, key file)' - ) + ) from None if not (tls_cert and tls_key) or (not os.path.isfile(tls_cert) or not os.path.isfile(tls_key)): diff --git a/docker/transport/npipeconn.py b/docker/transport/npipeconn.py index 45988b2df1..d335d8718f 100644 --- a/docker/transport/npipeconn.py +++ b/docker/transport/npipeconn.py @@ -46,9 +46,8 @@ def _get_conn(self, timeout): conn = None try: conn = self.pool.get(block=self.block, timeout=timeout) - - except AttributeError: # self.pool is None - raise urllib3.exceptions.ClosedPoolError(self, "Pool is closed.") + except AttributeError as ae: # self.pool is None + raise urllib3.exceptions.ClosedPoolError(self, "Pool is closed.") from ae except queue.Empty: if self.block: @@ -56,7 +55,7 @@ def _get_conn(self, timeout): self, "Pool reached maximum size and no more " "connections are allowed." - ) + ) from None # Oh well, we'll create a new connection then return conn or self._new_conn() diff --git a/docker/transport/sshconn.py b/docker/transport/sshconn.py index a92beb621f..6e1d0ee723 100644 --- a/docker/transport/sshconn.py +++ b/docker/transport/sshconn.py @@ -141,8 +141,8 @@ def _get_conn(self, timeout): try: conn = self.pool.get(block=self.block, timeout=timeout) - except AttributeError: # self.pool is None - raise urllib3.exceptions.ClosedPoolError(self, "Pool is closed.") + except AttributeError as ae: # self.pool is None + raise urllib3.exceptions.ClosedPoolError(self, "Pool is closed.") from ae except queue.Empty: if self.block: @@ -150,7 +150,7 @@ def _get_conn(self, timeout): self, "Pool reached maximum size and no more " "connections are allowed." - ) + ) from None # Oh well, we'll create a new connection then return conn or self._new_conn() diff --git a/docker/types/daemon.py b/docker/types/daemon.py index 096b2cc169..04e6ccb2d7 100644 --- a/docker/types/daemon.py +++ b/docker/types/daemon.py @@ -28,9 +28,9 @@ def __next__(self): try: return next(self._stream) except urllib3.exceptions.ProtocolError: - raise StopIteration + raise StopIteration from None except OSError: - raise StopIteration + raise StopIteration from None next = __next__ diff --git a/docker/utils/build.py b/docker/utils/build.py index 6b38eacdb2..8d18c2be71 100644 --- a/docker/utils/build.py +++ b/docker/utils/build.py @@ -93,10 +93,10 @@ def create_archive(root, files=None, fileobj=None, gzip=False, try: with open(full_path, 'rb') as f: t.addfile(i, f) - except OSError: + except OSError as oe: raise OSError( f'Can not read file in context: {full_path}' - ) + ) from oe else: # Directories, FIFOs, symlinks... don't need to be read. t.addfile(i, None) diff --git a/docker/utils/json_stream.py b/docker/utils/json_stream.py index f384175f75..266193e567 100644 --- a/docker/utils/json_stream.py +++ b/docker/utils/json_stream.py @@ -72,4 +72,4 @@ def split_buffer(stream, splitter=None, decoder=lambda a: a): try: yield decoder(buffered) except Exception as e: - raise StreamParseError(e) + raise StreamParseError(e) from e diff --git a/docker/utils/utils.py b/docker/utils/utils.py index 234be32076..4affeb3396 100644 --- a/docker/utils/utils.py +++ b/docker/utils/utils.py @@ -414,11 +414,11 @@ def parse_bytes(s): if suffix in units.keys() or suffix.isdigit(): try: digits = float(digits_part) - except ValueError: + except ValueError as ve: raise errors.DockerException( 'Failed converting the string value for memory ' f'({digits_part}) to an integer.' - ) + ) from ve # Reconvert to long for the final result s = int(digits * units[suffix]) diff --git a/tests/unit/auth_test.py b/tests/unit/auth_test.py index 26254fadde..0ed890fdf3 100644 --- a/tests/unit/auth_test.py +++ b/tests/unit/auth_test.py @@ -778,8 +778,8 @@ def __init__(self, *args, **kwargs): def get(self, server): try: return self.__store[server] - except KeyError: - raise credentials.errors.CredentialsNotFound() + except KeyError as ke: + raise credentials.errors.CredentialsNotFound() from ke def store(self, server, username, secret): self.__store[server] = { From 09f12f20460e5d0d063cda68c825f926953f388c Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Thu, 11 May 2023 16:18:13 +0300 Subject: [PATCH 24/45] Fix B005 (probably an actual bug too) Signed-off-by: Aarni Koskela --- docker/context/config.py | 3 ++- tests/unit/context_test.py | 8 +++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/docker/context/config.py b/docker/context/config.py index d761aef13c..8c3fe25007 100644 --- a/docker/context/config.py +++ b/docker/context/config.py @@ -77,5 +77,6 @@ def get_context_host(path=None, tls=False): host = utils.parse_host(path, IS_WINDOWS_PLATFORM, tls) if host == DEFAULT_UNIX_SOCKET: # remove http+ from default docker socket url - return host.strip("http+") + if host.startswith("http+"): + host = host[5:] return host diff --git a/tests/unit/context_test.py b/tests/unit/context_test.py index 6d6d6726bc..25f0d8c6ba 100644 --- a/tests/unit/context_test.py +++ b/tests/unit/context_test.py @@ -13,7 +13,7 @@ class BaseContextTest(unittest.TestCase): ) def test_url_compatibility_on_linux(self): c = Context("test") - assert c.Host == DEFAULT_UNIX_SOCKET.strip("http+") + assert c.Host == DEFAULT_UNIX_SOCKET[5:] @pytest.mark.skipif( not IS_WINDOWS_PLATFORM, reason='Windows specific path check' @@ -45,5 +45,7 @@ def test_context_inspect_without_params(self): ctx = ContextAPI.inspect_context() assert ctx["Name"] == "default" assert ctx["Metadata"]["StackOrchestrator"] == "swarm" - assert ctx["Endpoints"]["docker"]["Host"] in [ - DEFAULT_NPIPE, DEFAULT_UNIX_SOCKET.strip("http+")] + assert ctx["Endpoints"]["docker"]["Host"] in ( + DEFAULT_NPIPE, + DEFAULT_UNIX_SOCKET[5:], + ) From cc76c9c20d2af71f759abe02b02d5c96f14e6fdf Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Thu, 11 May 2023 16:20:27 +0300 Subject: [PATCH 25/45] Fix B082 (no explicit stacklevel for warnings) Signed-off-by: Aarni Koskela --- docker/credentials/store.py | 3 ++- docker/models/images.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docker/credentials/store.py b/docker/credentials/store.py index 5edeaa7f63..4e63a5ba60 100644 --- a/docker/credentials/store.py +++ b/docker/credentials/store.py @@ -20,7 +20,8 @@ def __init__(self, program, environment=None): self.environment = environment if self.exe is None: warnings.warn( - f'{self.program} not installed or not available in PATH' + f'{self.program} not installed or not available in PATH', + stacklevel=1, ) def get(self, server): diff --git a/docker/models/images.py b/docker/models/images.py index abb4b12b50..b4777d8da9 100644 --- a/docker/models/images.py +++ b/docker/models/images.py @@ -456,7 +456,8 @@ def pull(self, repository, tag=None, all_tags=False, **kwargs): if 'stream' in kwargs: warnings.warn( '`stream` is not a valid parameter for this method' - ' and will be overridden' + ' and will be overridden', + stacklevel=1, ) del kwargs['stream'] From 0566f1260cd6f89588df2128ec4e86e8266e5d74 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Thu, 11 May 2023 16:21:05 +0300 Subject: [PATCH 26/45] Fix missing asserts or assignments Signed-off-by: Aarni Koskela --- tests/integration/api_swarm_test.py | 4 ++-- tests/integration/models_containers_test.py | 2 +- tests/unit/api_test.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/integration/api_swarm_test.py b/tests/integration/api_swarm_test.py index cffe12fc24..d6aab9665f 100644 --- a/tests/integration/api_swarm_test.py +++ b/tests/integration/api_swarm_test.py @@ -127,11 +127,11 @@ def test_leave_swarm(self): assert self.init_swarm() with pytest.raises(docker.errors.APIError) as exc_info: self.client.leave_swarm() - exc_info.value.response.status_code == 500 + assert exc_info.value.response.status_code == 500 assert self.client.leave_swarm(force=True) with pytest.raises(docker.errors.APIError) as exc_info: self.client.inspect_swarm() - exc_info.value.response.status_code == 406 + assert exc_info.value.response.status_code == 406 assert self.client.leave_swarm(force=True) @requires_api_version('1.24') diff --git a/tests/integration/models_containers_test.py b/tests/integration/models_containers_test.py index 3cf74cbc59..4d33e622e8 100644 --- a/tests/integration/models_containers_test.py +++ b/tests/integration/models_containers_test.py @@ -221,7 +221,7 @@ def test_list_sparse(self): assert container.status == 'running' assert container.image == client.images.get('alpine') with pytest.raises(docker.errors.DockerException): - container.labels + _ = container.labels container.kill() container.remove() diff --git a/tests/unit/api_test.py b/tests/unit/api_test.py index 99aa23bc20..7bc2ea8cda 100644 --- a/tests/unit/api_test.py +++ b/tests/unit/api_test.py @@ -581,7 +581,7 @@ def test_read_from_socket_no_stream_tty_demux(self): def test_read_from_socket_no_stream_no_tty(self): res = self.request(stream=False, tty=False, demux=False) - res == self.stdout_data + self.stderr_data + assert res == self.stdout_data + self.stderr_data def test_read_from_socket_no_stream_no_tty_demux(self): res = self.request(stream=False, tty=False, demux=True) From 3948540c89fc3a58508580bf550e39a883745700 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Thu, 11 May 2023 16:22:43 +0300 Subject: [PATCH 27/45] Fix or noqa B003 (assigning to os.environ doesn't do what you expect) Signed-off-by: Aarni Koskela --- tests/integration/credentials/utils_test.py | 2 +- tests/unit/client_test.py | 3 ++- tests/unit/utils_test.py | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/integration/credentials/utils_test.py b/tests/integration/credentials/utils_test.py index acf018d2ff..4644039793 100644 --- a/tests/integration/credentials/utils_test.py +++ b/tests/integration/credentials/utils_test.py @@ -7,7 +7,7 @@ @mock.patch.dict(os.environ) def test_create_environment_dict(): base = {'FOO': 'bar', 'BAZ': 'foobar'} - os.environ = base + os.environ = base # noqa: B003 assert create_environment_dict({'FOO': 'baz'}) == { 'FOO': 'baz', 'BAZ': 'foobar', } diff --git a/tests/unit/client_test.py b/tests/unit/client_test.py index 1148d7ac1c..7012b21236 100644 --- a/tests/unit/client_test.py +++ b/tests/unit/client_test.py @@ -153,7 +153,8 @@ def setUp(self): self.os_environ = os.environ.copy() def tearDown(self): - os.environ = self.os_environ + os.environ.clear() + os.environ.update(self.os_environ) def test_from_env(self): """Test that environment variables are passed through to diff --git a/tests/unit/utils_test.py b/tests/unit/utils_test.py index 9c8a55bd52..b47cb0c62f 100644 --- a/tests/unit/utils_test.py +++ b/tests/unit/utils_test.py @@ -59,7 +59,8 @@ def setUp(self): self.os_environ = os.environ.copy() def tearDown(self): - os.environ = self.os_environ + os.environ.clear() + os.environ.update(self.os_environ) def test_kwargs_from_env_empty(self): os.environ.update(DOCKER_HOST='', From a9a3775b15e7557b9a7f3db6e27d70b400e91d7e Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Thu, 11 May 2023 16:25:31 +0300 Subject: [PATCH 28/45] Noqa pytest.raises(Exception) Signed-off-by: Aarni Koskela --- tests/unit/api_image_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/api_image_test.py b/tests/unit/api_image_test.py index aea3a0e136..22b27fe0da 100644 --- a/tests/unit/api_image_test.py +++ b/tests/unit/api_image_test.py @@ -12,7 +12,7 @@ class ImageTest(BaseAPIClientTest): def test_image_viz(self): - with pytest.raises(Exception): + with pytest.raises(Exception): # noqa: B017 self.client.images('busybox', viz=True) self.fail('Viz output should not be supported!') From c68d532f540906b366aa9ec657208bbb47bc51ae Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 15 Aug 2023 13:31:10 +0300 Subject: [PATCH 29/45] Fix duplicate dict key literal (ruff F601) Signed-off-by: Aarni Koskela --- tests/unit/fake_api.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/tests/unit/fake_api.py b/tests/unit/fake_api.py index 87d8927578..0524becdc7 100644 --- a/tests/unit/fake_api.py +++ b/tests/unit/fake_api.py @@ -111,13 +111,6 @@ def get_fake_image_history(): return status_code, response -def post_fake_import_image(): - status_code = 200 - response = 'Import messages...' - - return status_code, response - - def get_fake_containers(): status_code = 200 response = [{ @@ -542,8 +535,6 @@ def post_fake_secret(): get_fake_images, f'{prefix}/{CURRENT_VERSION}/images/test_image/history': get_fake_image_history, - f'{prefix}/{CURRENT_VERSION}/images/create': - post_fake_import_image, f'{prefix}/{CURRENT_VERSION}/containers/json': get_fake_containers, f'{prefix}/{CURRENT_VERSION}/containers/{FAKE_CONTAINER_ID}/start': From bea63224e028226085c85caecace6480fe0aa6b0 Mon Sep 17 00:00:00 2001 From: Janne Jakob Fleischer Date: Wed, 9 Aug 2023 10:03:52 +0200 Subject: [PATCH 30/45] volume: added support for bind propagation https://docs.docker.com/storage/bind-mounts/#configure-bind-propagation Signed-off-by: Janne Jakob Fleischer Signed-off-by: Milas Bowman --- docker/api/container.py | 8 +++++- docker/utils/utils.py | 17 ++++++++++- tests/integration/api_container_test.py | 38 ++++++++++++++++++++++++- 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/docker/api/container.py b/docker/api/container.py index ec28fd581b..5a267d13f1 100644 --- a/docker/api/container.py +++ b/docker/api/container.py @@ -319,6 +319,11 @@ def create_container(self, image, command=None, hostname=None, user=None, '/var/www': { 'bind': '/mnt/vol1', 'mode': 'ro', + }, + '/autofs/user1': { + 'bind': '/mnt/vol3', + 'mode': 'rw', + 'propagation': 'shared' } }) ) @@ -329,10 +334,11 @@ def create_container(self, image, command=None, hostname=None, user=None, .. code-block:: python container_id = client.api.create_container( - 'busybox', 'ls', volumes=['/mnt/vol1', '/mnt/vol2'], + 'busybox', 'ls', volumes=['/mnt/vol1', '/mnt/vol2', '/mnt/vol3'], host_config=client.api.create_host_config(binds=[ '/home/user1/:/mnt/vol2', '/var/www:/mnt/vol1:ro', + '/autofs/user1:/mnt/vol3:rw,shared', ]) ) diff --git a/docker/utils/utils.py b/docker/utils/utils.py index 4affeb3396..0f28afb116 100644 --- a/docker/utils/utils.py +++ b/docker/utils/utils.py @@ -17,7 +17,6 @@ from urllib.parse import urlparse, urlunparse - URLComponents = collections.namedtuple( 'URLComponents', 'scheme netloc url params query fragment', @@ -141,6 +140,22 @@ def convert_volume_binds(binds): else: mode = 'rw' + # NOTE: this is only relevant for Linux hosts + # (doesn't apply in Docker Desktop) + propagation_modes = [ + 'rshared', + 'shared', + 'rslave', + 'slave', + 'rprivate', + 'private', + ] + if 'propagation' in v and v['propagation'] in propagation_modes: + if mode: + mode = ','.join([mode, v['propagation']]) + else: + mode = v['propagation'] + result.append( f'{k}:{bind}:{mode}' ) diff --git a/tests/integration/api_container_test.py b/tests/integration/api_container_test.py index 590c4fa0ce..ecda1d65cb 100644 --- a/tests/integration/api_container_test.py +++ b/tests/integration/api_container_test.py @@ -542,6 +542,24 @@ def test_create_with_binds_ro(self): inspect_data = self.client.inspect_container(container) self.check_container_data(inspect_data, False) + def test_create_with_binds_rw_rshared(self): + self.run_with_volume_propagation( + False, + 'rshared', + TEST_IMG, + ['touch', os.path.join(self.mount_dest, self.filename)], + ) + container = self.run_with_volume_propagation( + True, + 'rshared', + TEST_IMG, + ['ls', self.mount_dest], + ) + logs = self.client.logs(container).decode('utf-8') + assert self.filename in logs + inspect_data = self.client.inspect_container(container) + self.check_container_data(inspect_data, True, 'rshared') + @requires_api_version('1.30') def test_create_with_mounts(self): mount = docker.types.Mount( @@ -597,7 +615,7 @@ def test_create_with_volume_mount(self): assert mount['Source'] == mount_data['Name'] assert mount_data['RW'] is True - def check_container_data(self, inspect_data, rw): + def check_container_data(self, inspect_data, rw, propagation='rprivate'): assert 'Mounts' in inspect_data filtered = list(filter( lambda x: x['Destination'] == self.mount_dest, @@ -607,6 +625,7 @@ def check_container_data(self, inspect_data, rw): mount_data = filtered[0] assert mount_data['Source'] == self.mount_origin assert mount_data['RW'] == rw + assert mount_data['Propagation'] == propagation def run_with_volume(self, ro, *args, **kwargs): return self.run_container( @@ -624,6 +643,23 @@ def run_with_volume(self, ro, *args, **kwargs): **kwargs ) + def run_with_volume_propagation(self, ro, propagation, *args, **kwargs): + return self.run_container( + *args, + volumes={self.mount_dest: {}}, + host_config=self.client.create_host_config( + binds={ + self.mount_origin: { + 'bind': self.mount_dest, + 'ro': ro, + 'propagation': propagation + }, + }, + network_mode='none' + ), + **kwargs + ) + class ArchiveTest(BaseAPIIntegrationTest): def test_get_file_archive_from_container(self): From 378325363eb01edf60efb3a6d352b6d4047c985a Mon Sep 17 00:00:00 2001 From: Albin Kerouanton <557933+akerouanton@users.noreply.github.com> Date: Mon, 21 Aug 2023 15:30:21 +0200 Subject: [PATCH 31/45] integration: Fix bad subnet declaration (#3169) Some network integration tests are creating networks with subnet `2001:389::1/64`. This is an invalid subnet as the host fragment is non-zero (ie. it should be `2001:389::/64`). PR moby/moby#45759 is adding strict validation of network configuration. Docker API will now return an error whenever a bad subnet is passed. Signed-off-by: Albin Kerouanton --- tests/integration/api_network_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integration/api_network_test.py b/tests/integration/api_network_test.py index 78d54e282b..74dad60053 100644 --- a/tests/integration/api_network_test.py +++ b/tests/integration/api_network_test.py @@ -233,7 +233,7 @@ def test_create_with_ipv6_address(self): net_name, net_id = self.create_network( ipam=IPAMConfig( driver='default', - pool_configs=[IPAMPool(subnet="2001:389::1/64")], + pool_configs=[IPAMPool(subnet="2001:389::/64")], ), ) container = self.client.create_container( @@ -389,7 +389,7 @@ def test_connect_with_ipv6_address(self): driver='default', pool_configs=[ IPAMPool( - subnet="2001:389::1/64", iprange="2001:389::0/96", + subnet="2001:389::/64", iprange="2001:389::0/96", gateway="2001:389::ffff" ) ] @@ -455,7 +455,7 @@ def test_create_network_ipv6_enabled(self): driver='default', pool_configs=[ IPAMPool( - subnet="2001:389::1/64", iprange="2001:389::0/96", + subnet="2001:389::/64", iprange="2001:389::0/96", gateway="2001:389::ffff" ) ] From c38656dc7894363f32317affecc3e4279e1163f8 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton <557933+akerouanton@users.noreply.github.com> Date: Mon, 21 Aug 2023 15:31:57 +0200 Subject: [PATCH 32/45] integration: Remove test_create_check_duplicate (#3170) integration: check_duplicate is now the default behavior moby/moby#46251 marks CheckDuplicate as deprecated. Any NetworkCreate request with a conflicting network name will now return an error. Signed-off-by: Albin Kerouanton --- tests/integration/api_network_test.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/integration/api_network_test.py b/tests/integration/api_network_test.py index 74dad60053..6689044b68 100644 --- a/tests/integration/api_network_test.py +++ b/tests/integration/api_network_test.py @@ -327,8 +327,6 @@ def test_create_check_duplicate(self): net_name, net_id = self.create_network() with pytest.raises(docker.errors.APIError): self.client.create_network(net_name, check_duplicate=True) - net_id = self.client.create_network(net_name, check_duplicate=False) - self.tmp_networks.append(net_id['Id']) @requires_api_version('1.22') def test_connect_with_links(self): From 7752996f783bf56084902eb931836edd0b368a90 Mon Sep 17 00:00:00 2001 From: Mariano Scazzariello Date: Sat, 30 Sep 2023 00:20:44 +0200 Subject: [PATCH 33/45] Replace `network_config` with a dict of EndpointConfig - Renamed parameter from `network_config` to `networking_config` to be more semantically correct with the rest of the API. --- docker/models/containers.py | 74 +++--------- tests/integration/models_containers_test.py | 65 +++++++--- tests/unit/models_containers_test.py | 127 ++++++++++++++------ 3 files changed, 159 insertions(+), 107 deletions(-) diff --git a/docker/models/containers.py b/docker/models/containers.py index 3312b0e2d8..87e64ed48d 100644 --- a/docker/models/containers.py +++ b/docker/models/containers.py @@ -2,16 +2,16 @@ import ntpath from collections import namedtuple +from .images import Image +from .resource import Collection, Model from ..api import APIClient from ..constants import DEFAULT_DATA_CHUNK_SIZE from ..errors import ( ContainerError, DockerException, ImageNotFound, NotFound, create_unexpected_kwargs_error ) -from ..types import EndpointConfig, HostConfig, NetworkingConfig +from ..types import HostConfig, NetworkingConfig from ..utils import version_gte -from .images import Image -from .resource import Collection, Model class Container(Model): @@ -21,6 +21,7 @@ class Container(Model): query the Docker daemon for the current properties, causing :py:attr:`attrs` to be refreshed. """ + @property def name(self): """ @@ -680,33 +681,13 @@ def run(self, image, command=None, stdout=True, stderr=False, This mode is incompatible with ``ports``. Incompatible with ``network``. - network_config (dict): A dictionary containing options that are - passed to the network driver during the connection. + networking_config (Dict[str, EndpointConfig]): + Dictionary of EndpointConfig objects for each container network. + The key is the name of the network. Defaults to ``None``. - The dictionary contains the following keys: - - - ``aliases`` (:py:class:`list`): A list of aliases for - the network endpoint. - Names in that list can be used within the network to - reach this container. Defaults to ``None``. - - ``links`` (:py:class:`list`): A list of links for - the network endpoint endpoint. - Containers declared in this list will be linked to this - container. Defaults to ``None``. - - ``ipv4_address`` (str): The IP address to assign to - this container on the network, using the IPv4 protocol. - Defaults to ``None``. - - ``ipv6_address`` (str): The IP address to assign to - this container on the network, using the IPv6 protocol. - Defaults to ``None``. - - ``link_local_ips`` (:py:class:`list`): A list of link-local - (IPv4/IPv6) addresses. - - ``driver_opt`` (dict): A dictionary of options to provide to - the network driver. Defaults to ``None``. - - ``mac_address`` (str): MAC Address to assign to the network - interface. Defaults to ``None``. Requires API >= 1.25. Used in conjuction with ``network``. + Incompatible with ``network_mode``. oom_kill_disable (bool): Whether to disable OOM killer. oom_score_adj (int): An integer value containing the score given @@ -872,9 +853,9 @@ def run(self, image, command=None, stdout=True, stderr=False, 'together.' ) - if kwargs.get('network_config') and not kwargs.get('network'): + if kwargs.get('networking_config') and not kwargs.get('network'): raise RuntimeError( - 'The option "network_config" can not be used ' + 'The option "networking_config" can not be used ' 'without "network".' ) @@ -1030,6 +1011,7 @@ def list(self, all=False, before=None, filters=None, limit=-1, since=None, def prune(self, filters=None): return self.client.api.prune_containers(filters=filters) + prune.__doc__ = APIClient.prune_containers.__doc__ @@ -1124,17 +1106,6 @@ def prune(self, filters=None): ] -NETWORKING_CONFIG_ARGS = [ - 'aliases', - 'links', - 'ipv4_address', - 'ipv6_address', - 'link_local_ips', - 'driver_opt', - 'mac_address' -] - - def _create_container_args(kwargs): """ Convert arguments to create() to arguments to create_container(). @@ -1159,24 +1130,17 @@ def _create_container_args(kwargs): host_config_kwargs['binds'] = volumes network = kwargs.pop('network', None) - network_config = kwargs.pop('network_config', None) + networking_config = kwargs.pop('networking_config', None) if network: - endpoint_config = None - - if network_config: - clean_endpoint_args = {} - for arg_name in NETWORKING_CONFIG_ARGS: - if arg_name in network_config: - clean_endpoint_args[arg_name] = network_config[arg_name] - - if clean_endpoint_args: - endpoint_config = EndpointConfig( - host_config_kwargs['version'], **clean_endpoint_args - ) + if networking_config: + # Sanity check: check if the network is defined in the + # networking config dict, otherwise switch to None + if network not in networking_config: + networking_config = None create_kwargs['networking_config'] = NetworkingConfig( - {network: endpoint_config} - ) if endpoint_config else {network: None} + networking_config + ) if networking_config else {network: None} host_config_kwargs['network_mode'] = network # All kwargs should have been consumed by this point, so raise diff --git a/tests/integration/models_containers_test.py b/tests/integration/models_containers_test.py index 050efa01ca..330c658e1a 100644 --- a/tests/integration/models_containers_test.py +++ b/tests/integration/models_containers_test.py @@ -5,10 +5,10 @@ import pytest import docker -from ..helpers import random_name -from ..helpers import requires_api_version from .base import BaseIntegrationTest from .base import TEST_API_VERSION +from ..helpers import random_name +from ..helpers import requires_api_version class ContainerCollectionTest(BaseIntegrationTest): @@ -104,7 +104,7 @@ def test_run_with_network(self): assert 'Networks' in attrs['NetworkSettings'] assert list(attrs['NetworkSettings']['Networks'].keys()) == [net_name] - def test_run_with_network_config(self): + def test_run_with_networking_config(self): net_name = random_name() client = docker.from_env(version=TEST_API_VERSION) client.networks.create(net_name) @@ -113,10 +113,16 @@ def test_run_with_network_config(self): test_aliases = ['hello'] test_driver_opt = {'key1': 'a'} + networking_config = { + net_name: client.api.create_endpoint_config( + aliases=test_aliases, + driver_opt=test_driver_opt + ) + } + container = client.containers.run( 'alpine', 'echo hello world', network=net_name, - network_config={'aliases': test_aliases, - 'driver_opt': test_driver_opt}, + networking_config=networking_config, detach=True ) self.tmp_containers.append(container.id) @@ -131,7 +137,7 @@ def test_run_with_network_config(self): assert attrs['NetworkSettings']['Networks'][net_name]['DriverOpts'] \ == test_driver_opt - def test_run_with_network_config_undeclared_params(self): + def test_run_with_networking_config_with_undeclared_network(self): net_name = random_name() client = docker.from_env(version=TEST_API_VERSION) client.networks.create(net_name) @@ -140,11 +146,41 @@ def test_run_with_network_config_undeclared_params(self): test_aliases = ['hello'] test_driver_opt = {'key1': 'a'} + networking_config = { + net_name: client.api.create_endpoint_config( + aliases=test_aliases, + driver_opt=test_driver_opt + ), + 'bar': client.api.create_endpoint_config( + aliases=['test'], + driver_opt={'key2': 'b'} + ), + } + + with pytest.raises(docker.errors.APIError) as e: + container = client.containers.run( + 'alpine', 'echo hello world', network=net_name, + networking_config=networking_config, + detach=True + ) + self.tmp_containers.append(container.id) + + def test_run_with_networking_config_only_undeclared_network(self): + net_name = random_name() + client = docker.from_env(version=TEST_API_VERSION) + client.networks.create(net_name) + self.tmp_networks.append(net_name) + + networking_config = { + 'bar': client.api.create_endpoint_config( + aliases=['hello'], + driver_opt={'key1': 'a'} + ), + } + container = client.containers.run( 'alpine', 'echo hello world', network=net_name, - network_config={'aliases': test_aliases, - 'driver_opt': test_driver_opt, - 'undeclared_param': 'random_value'}, + networking_config=networking_config, detach=True ) self.tmp_containers.append(container.id) @@ -154,12 +190,9 @@ def test_run_with_network_config_undeclared_params(self): assert 'NetworkSettings' in attrs assert 'Networks' in attrs['NetworkSettings'] assert list(attrs['NetworkSettings']['Networks'].keys()) == [net_name] - assert attrs['NetworkSettings']['Networks'][net_name]['Aliases'] == \ - test_aliases - assert attrs['NetworkSettings']['Networks'][net_name]['DriverOpts'] \ - == test_driver_opt - assert 'undeclared_param' not in \ - attrs['NetworkSettings']['Networks'][net_name] + assert attrs['NetworkSettings']['Networks'][net_name]['Aliases'] is None + assert (attrs['NetworkSettings']['Networks'][net_name]['DriverOpts'] + is None) def test_run_with_none_driver(self): client = docker.from_env(version=TEST_API_VERSION) @@ -244,7 +277,7 @@ def test_get(self): container = client.containers.run("alpine", "sleep 300", detach=True) self.tmp_containers.append(container.id) assert client.containers.get(container.id).attrs[ - 'Config']['Image'] == "alpine" + 'Config']['Image'] == "alpine" def test_list(self): client = docker.from_env(version=TEST_API_VERSION) diff --git a/tests/unit/models_containers_test.py b/tests/unit/models_containers_test.py index f6dccaaba1..bd3092b678 100644 --- a/tests/unit/models_containers_test.py +++ b/tests/unit/models_containers_test.py @@ -1,11 +1,13 @@ -import pytest import unittest +import pytest + import docker from docker.constants import DEFAULT_DATA_CHUNK_SIZE, \ DEFAULT_DOCKER_API_VERSION from docker.models.containers import Container, _create_container_args from docker.models.images import Image +from docker.types import EndpointConfig from .fake_api import FAKE_CONTAINER_ID, FAKE_IMAGE_ID, FAKE_EXEC_ID from .fake_api_client import make_fake_client @@ -32,6 +34,13 @@ def test_run(self): ) def test_create_container_args(self): + networking_config = { + 'foo': EndpointConfig( + DEFAULT_DOCKER_API_VERSION, aliases=['test'], + driver_opt={'key1': 'a'} + ) + } + create_kwargs = _create_container_args(dict( image='alpine', command='echo hello world', @@ -75,7 +84,7 @@ def test_create_container_args(self): name='somename', network_disabled=False, network='foo', - network_config={'aliases': ['test'], 'driver_opt': {'key1': 'a'}}, + networking_config=networking_config, oom_kill_disable=True, oom_score_adj=5, pid_mode='host', @@ -349,35 +358,41 @@ def test_run_platform(self): host_config={'NetworkMode': 'default'}, ) - def test_run_network_config_without_network(self): + def test_run_networking_config_without_network(self): client = make_fake_client() with pytest.raises(RuntimeError): client.containers.run( image='alpine', - network_config={'aliases': ['test'], - 'driver_opt': {'key1': 'a'}} + networking_config={'aliases': ['test'], + 'driver_opt': {'key1': 'a'}} ) - def test_run_network_config_with_network_mode(self): + def test_run_networking_config_with_network_mode(self): client = make_fake_client() with pytest.raises(RuntimeError): client.containers.run( image='alpine', network_mode='none', - network_config={'aliases': ['test'], - 'driver_opt': {'key1': 'a'}} + networking_config={'aliases': ['test'], + 'driver_opt': {'key1': 'a'}} ) - def test_run_network_config(self): + def test_run_networking_config(self): client = make_fake_client() + networking_config = { + 'foo': EndpointConfig( + DEFAULT_DOCKER_API_VERSION, aliases=['test'], + driver_opt={'key1': 'a'} + ) + } + client.containers.run( image='alpine', network='foo', - network_config={'aliases': ['test'], - 'driver_opt': {'key1': 'a'}} + networking_config=networking_config ) client.api.create_container.assert_called_with( @@ -390,15 +405,24 @@ def test_run_network_config(self): host_config={'NetworkMode': 'foo'} ) - def test_run_network_config_undeclared_params(self): + def test_run_networking_config_with_undeclared_network(self): client = make_fake_client() + networking_config = { + 'foo': EndpointConfig( + DEFAULT_DOCKER_API_VERSION, aliases=['test_foo'], + driver_opt={'key2': 'b'} + ), + 'bar': EndpointConfig( + DEFAULT_DOCKER_API_VERSION, aliases=['test'], + driver_opt={'key1': 'a'} + ) + } + client.containers.run( image='alpine', network='foo', - network_config={'aliases': ['test'], - 'driver_opt': {'key1': 'a'}, - 'undeclared_param': 'random_value'} + networking_config=networking_config ) client.api.create_container.assert_called_with( @@ -406,18 +430,26 @@ def test_run_network_config_undeclared_params(self): image='alpine', command=None, networking_config={'EndpointsConfig': { - 'foo': {'Aliases': ['test'], 'DriverOpts': {'key1': 'a'}}} - }, + 'foo': {'Aliases': ['test_foo'], 'DriverOpts': {'key2': 'b'}}, + 'bar': {'Aliases': ['test'], 'DriverOpts': {'key1': 'a'}}, + }}, host_config={'NetworkMode': 'foo'} ) - def test_run_network_config_only_undeclared_params(self): + def test_run_networking_config_only_undeclared_network(self): client = make_fake_client() + networking_config = { + 'bar': EndpointConfig( + DEFAULT_DOCKER_API_VERSION, aliases=['test'], + driver_opt={'key1': 'a'} + ) + } + client.containers.run( image='alpine', network='foo', - network_config={'undeclared_param': 'random_value'} + networking_config=networking_config ) client.api.create_container.assert_called_with( @@ -455,13 +487,13 @@ def test_create_with_image_object(self): host_config={'NetworkMode': 'default'} ) - def test_create_network_config_without_network(self): + def test_create_networking_config_without_network(self): client = make_fake_client() client.containers.create( image='alpine', - network_config={'aliases': ['test'], - 'driver_opt': {'key1': 'a'}} + networking_config={'aliases': ['test'], + 'driver_opt': {'key1': 'a'}} ) client.api.create_container.assert_called_with( @@ -470,14 +502,14 @@ def test_create_network_config_without_network(self): host_config={'NetworkMode': 'default'} ) - def test_create_network_config_with_network_mode(self): + def test_create_networking_config_with_network_mode(self): client = make_fake_client() client.containers.create( image='alpine', network_mode='none', - network_config={'aliases': ['test'], - 'driver_opt': {'key1': 'a'}} + networking_config={'aliases': ['test'], + 'driver_opt': {'key1': 'a'}} ) client.api.create_container.assert_called_with( @@ -486,14 +518,20 @@ def test_create_network_config_with_network_mode(self): host_config={'NetworkMode': 'none'} ) - def test_create_network_config(self): + def test_create_networking_config(self): client = make_fake_client() + networking_config = { + 'foo': EndpointConfig( + DEFAULT_DOCKER_API_VERSION, aliases=['test'], + driver_opt={'key1': 'a'} + ) + } + client.containers.create( image='alpine', network='foo', - network_config={'aliases': ['test'], - 'driver_opt': {'key1': 'a'}} + networking_config=networking_config ) client.api.create_container.assert_called_with( @@ -505,33 +543,50 @@ def test_create_network_config(self): host_config={'NetworkMode': 'foo'} ) - def test_create_network_config_undeclared_params(self): + def test_create_networking_config_with_undeclared_network(self): client = make_fake_client() + networking_config = { + 'foo': EndpointConfig( + DEFAULT_DOCKER_API_VERSION, aliases=['test_foo'], + driver_opt={'key2': 'b'} + ), + 'bar': EndpointConfig( + DEFAULT_DOCKER_API_VERSION, aliases=['test'], + driver_opt={'key1': 'a'} + ) + } + client.containers.create( image='alpine', network='foo', - network_config={'aliases': ['test'], - 'driver_opt': {'key1': 'a'}, - 'undeclared_param': 'random_value'} + networking_config=networking_config ) client.api.create_container.assert_called_with( image='alpine', command=None, networking_config={'EndpointsConfig': { - 'foo': {'Aliases': ['test'], 'DriverOpts': {'key1': 'a'}}} - }, + 'foo': {'Aliases': ['test_foo'], 'DriverOpts': {'key2': 'b'}}, + 'bar': {'Aliases': ['test'], 'DriverOpts': {'key1': 'a'}}, + }}, host_config={'NetworkMode': 'foo'} ) - def test_create_network_config_only_undeclared_params(self): + def test_create_networking_config_only_undeclared_network(self): client = make_fake_client() + networking_config = { + 'bar': EndpointConfig( + DEFAULT_DOCKER_API_VERSION, aliases=['test'], + driver_opt={'key1': 'a'} + ) + } + client.containers.create( image='alpine', network='foo', - network_config={'undeclared_param': 'random_value'} + networking_config=networking_config ) client.api.create_container.assert_called_with( From c9e3efddb86d244e01303106faac72d9ec76a876 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Mon, 20 Nov 2023 22:55:28 +0200 Subject: [PATCH 34/45] feat: move websocket-client to extra dependency (#3123) Also bump minimum version to that prescribed by #3022 Signed-off-by: Aarni Koskela --- docker/api/client.py | 12 ++++++++++-- setup.py | 4 +++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/docker/api/client.py b/docker/api/client.py index a2cb459de0..20f8a2af7c 100644 --- a/docker/api/client.py +++ b/docker/api/client.py @@ -5,7 +5,6 @@ import requests import requests.exceptions -import websocket from .. import auth from ..constants import (DEFAULT_NUM_POOLS, DEFAULT_NUM_POOLS_SSH, @@ -309,7 +308,16 @@ def _attach_websocket(self, container, params=None): return self._create_websocket_connection(full_url) def _create_websocket_connection(self, url): - return websocket.create_connection(url) + try: + import websocket + return websocket.create_connection(url) + except ImportError as ie: + raise DockerException( + 'The `websocket-client` library is required ' + 'for using websocket connections. ' + 'You can install the `docker` library ' + 'with the [websocket] extra to install it.' + ) from ie def _get_raw_response_socket(self, response): self._raise_for_status(response) diff --git a/setup.py b/setup.py index 866aa23c8d..79bf3bdb68 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,6 @@ 'packaging >= 14.0', 'requests >= 2.26.0', 'urllib3 >= 1.26.0', - 'websocket-client >= 0.32.0', ] extras_require = { @@ -27,6 +26,9 @@ # Only required when connecting using the ssh:// protocol 'ssh': ['paramiko>=2.4.3'], + + # Only required when using websockets + 'websockets': ['websocket-client >= 1.3.0'], } with open('./test-requirements.txt') as test_reqs_txt: From 26e07251d42d3edc320ea967a68e855739a2a749 Mon Sep 17 00:00:00 2001 From: Milas Bowman Date: Mon, 20 Nov 2023 16:10:38 -0500 Subject: [PATCH 35/45] chore: fix lint issues ruff ruff ruff! Signed-off-by: Milas Bowman --- tests/integration/models_containers_test.py | 2 +- tests/unit/models_containers_test.py | 213 ++++++++++---------- 2 files changed, 111 insertions(+), 104 deletions(-) diff --git a/tests/integration/models_containers_test.py b/tests/integration/models_containers_test.py index 8fde851a63..219b9a4cb1 100644 --- a/tests/integration/models_containers_test.py +++ b/tests/integration/models_containers_test.py @@ -157,7 +157,7 @@ def test_run_with_networking_config_with_undeclared_network(self): ), } - with pytest.raises(docker.errors.APIError) as e: + with pytest.raises(docker.errors.APIError): container = client.containers.run( 'alpine', 'echo hello world', network=net_name, networking_config=networking_config, diff --git a/tests/unit/models_containers_test.py b/tests/unit/models_containers_test.py index bd3092b678..05005815f3 100644 --- a/tests/unit/models_containers_test.py +++ b/tests/unit/models_containers_test.py @@ -41,77 +41,73 @@ def test_create_container_args(self): ) } - create_kwargs = _create_container_args(dict( - image='alpine', - command='echo hello world', - blkio_weight_device=[{'Path': 'foo', 'Weight': 3}], - blkio_weight=2, - cap_add=['foo'], - cap_drop=['bar'], - cgroup_parent='foobar', - cgroupns='host', - cpu_period=1, - cpu_quota=2, - cpu_shares=5, - cpuset_cpus='0-3', - detach=False, - device_read_bps=[{'Path': 'foo', 'Rate': 3}], - device_read_iops=[{'Path': 'foo', 'Rate': 3}], - device_write_bps=[{'Path': 'foo', 'Rate': 3}], - device_write_iops=[{'Path': 'foo', 'Rate': 3}], - devices=['/dev/sda:/dev/xvda:rwm'], - dns=['8.8.8.8'], - domainname='example.com', - dns_opt=['foo'], - dns_search=['example.com'], - entrypoint='/bin/sh', - environment={'FOO': 'BAR'}, - extra_hosts={'foo': '1.2.3.4'}, - group_add=['blah'], - ipc_mode='foo', - kernel_memory=123, - labels={'key': 'value'}, - links={'foo': 'bar'}, - log_config={'Type': 'json-file', 'Config': {}}, - lxc_conf={'foo': 'bar'}, - healthcheck={'test': 'true'}, - hostname='somehost', - mac_address='abc123', - mem_limit=123, - mem_reservation=123, - mem_swappiness=2, - memswap_limit=456, - name='somename', - network_disabled=False, - network='foo', - networking_config=networking_config, - oom_kill_disable=True, - oom_score_adj=5, - pid_mode='host', - pids_limit=500, - platform='linux', - ports={ - 1111: 4567, - 2222: None - }, - privileged=True, - publish_all_ports=True, - read_only=True, - restart_policy={'Name': 'always'}, - security_opt=['blah'], - shm_size=123, - stdin_open=True, - stop_signal=9, - sysctls={'foo': 'bar'}, - tmpfs={'/blah': ''}, - tty=True, - ulimits=[{"Name": "nofile", "Soft": 1024, "Hard": 2048}], - user='bob', - userns_mode='host', - uts_mode='host', - version=DEFAULT_DOCKER_API_VERSION, - volume_driver='some_driver', - volumes=[ + create_kwargs = _create_container_args({ + 'image': 'alpine', + 'command': 'echo hello world', + 'blkio_weight_device': [{'Path': 'foo', 'Weight': 3}], + 'blkio_weight': 2, + 'cap_add': ['foo'], + 'cap_drop': ['bar'], + 'cgroup_parent': 'foobar', + 'cgroupns': 'host', + 'cpu_period': 1, + 'cpu_quota': 2, + 'cpu_shares': 5, + 'cpuset_cpus': '0-3', + 'detach': False, + 'device_read_bps': [{'Path': 'foo', 'Rate': 3}], + 'device_read_iops': [{'Path': 'foo', 'Rate': 3}], + 'device_write_bps': [{'Path': 'foo', 'Rate': 3}], + 'device_write_iops': [{'Path': 'foo', 'Rate': 3}], + 'devices': ['/dev/sda:/dev/xvda:rwm'], + 'dns': ['8.8.8.8'], + 'domainname': 'example.com', + 'dns_opt': ['foo'], + 'dns_search': ['example.com'], + 'entrypoint': '/bin/sh', + 'environment': {'FOO': 'BAR'}, + 'extra_hosts': {'foo': '1.2.3.4'}, + 'group_add': ['blah'], + 'ipc_mode': 'foo', + 'kernel_memory': 123, + 'labels': {'key': 'value'}, + 'links': {'foo': 'bar'}, + 'log_config': {'Type': 'json-file', 'Config': {}}, + 'lxc_conf': {'foo': 'bar'}, + 'healthcheck': {'test': 'true'}, + 'hostname': 'somehost', + 'mac_address': 'abc123', + 'mem_limit': 123, + 'mem_reservation': 123, + 'mem_swappiness': 2, + 'memswap_limit': 456, + 'name': 'somename', + 'network_disabled': False, + 'network': 'foo', + 'networking_config': networking_config, + 'oom_kill_disable': True, + 'oom_score_adj': 5, + 'pid_mode': 'host', + 'pids_limit': 500, + 'platform': 'linux', + 'ports': {1111: 4567, 2222: None}, + 'privileged': True, + 'publish_all_ports': True, + 'read_only': True, + 'restart_policy': {'Name': 'always'}, + 'security_opt': ['blah'], + 'shm_size': 123, + 'stdin_open': True, + 'stop_signal': 9, + 'sysctls': {'foo': 'bar'}, + 'tmpfs': {'/blah': ''}, + 'tty': True, + 'ulimits': [{"Name": "nofile", "Soft": 1024, "Hard": 2048}], + 'user': 'bob', + 'userns_mode': 'host', + 'uts_mode': 'host', + 'version': DEFAULT_DOCKER_API_VERSION, + 'volume_driver': 'some_driver', 'volumes': [ '/home/user1/:/mnt/vol2', '/var/www:/mnt/vol1:ro', 'volumename:/mnt/vol3r', @@ -119,18 +115,18 @@ def test_create_container_args(self): '/anothervolumewithnohostpath:ro', 'C:\\windows\\path:D:\\hello\\world:rw' ], - volumes_from=['container'], - working_dir='/code' - )) + 'volumes_from': ['container'], + 'working_dir': '/code', + }) - expected = dict( - image='alpine', - command='echo hello world', - domainname='example.com', - detach=False, - entrypoint='/bin/sh', - environment={'FOO': 'BAR'}, - host_config={ + expected = { + 'image': 'alpine', + 'command': 'echo hello world', + 'domainname': 'example.com', + 'detach': False, + 'entrypoint': '/bin/sh', + 'environment': {'FOO': 'BAR'}, + 'host_config': { 'Binds': [ '/home/user1/:/mnt/vol2', '/var/www:/mnt/vol1:ro', @@ -153,9 +149,13 @@ def test_create_container_args(self): 'CpuQuota': 2, 'CpuShares': 5, 'CpusetCpus': '0-3', - 'Devices': [{'PathOnHost': '/dev/sda', - 'CgroupPermissions': 'rwm', - 'PathInContainer': '/dev/xvda'}], + 'Devices': [ + { + 'PathOnHost': '/dev/sda', + 'CgroupPermissions': 'rwm', + 'PathInContainer': '/dev/xvda', + }, + ], 'Dns': ['8.8.8.8'], 'DnsOptions': ['foo'], 'DnsSearch': ['example.com'], @@ -187,28 +187,35 @@ def test_create_container_args(self): 'ShmSize': 123, 'Sysctls': {'foo': 'bar'}, 'Tmpfs': {'/blah': ''}, - 'Ulimits': [{"Name": "nofile", "Soft": 1024, "Hard": 2048}], + 'Ulimits': [ + {"Name": "nofile", "Soft": 1024, "Hard": 2048}, + ], 'UsernsMode': 'host', 'UTSMode': 'host', 'VolumeDriver': 'some_driver', 'VolumesFrom': ['container'], }, - healthcheck={'test': 'true'}, - hostname='somehost', - labels={'key': 'value'}, - mac_address='abc123', - name='somename', - network_disabled=False, - networking_config={'EndpointsConfig': { - 'foo': {'Aliases': ['test'], 'DriverOpts': {'key1': 'a'}}} + 'healthcheck': {'test': 'true'}, + 'hostname': 'somehost', + 'labels': {'key': 'value'}, + 'mac_address': 'abc123', + 'name': 'somename', + 'network_disabled': False, + 'networking_config': { + 'EndpointsConfig': { + 'foo': { + 'Aliases': ['test'], + 'DriverOpts': {'key1': 'a'}, + }, + } }, - platform='linux', - ports=[('1111', 'tcp'), ('2222', 'tcp')], - stdin_open=True, - stop_signal=9, - tty=True, - user='bob', - volumes=[ + 'platform': 'linux', + 'ports': [('1111', 'tcp'), ('2222', 'tcp')], + 'stdin_open': True, + 'stop_signal': 9, + 'tty': True, + 'user': 'bob', + 'volumes': [ '/mnt/vol2', '/mnt/vol1', '/mnt/vol3r', @@ -216,8 +223,8 @@ def test_create_container_args(self): '/anothervolumewithnohostpath', 'D:\\hello\\world' ], - working_dir='/code' - ) + 'working_dir': '/code', + } assert create_kwargs == expected From b2378db7f174fee78f67748308b3c98855454d68 Mon Sep 17 00:00:00 2001 From: Milas Bowman Date: Mon, 20 Nov 2023 16:18:08 -0500 Subject: [PATCH 36/45] chore: fix lint issue Signed-off-by: Milas Bowman --- docker/models/containers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docker/models/containers.py b/docker/models/containers.py index f8aeb39b13..4725d6f6fb 100644 --- a/docker/models/containers.py +++ b/docker/models/containers.py @@ -66,7 +66,9 @@ def status(self): @property def health(self): """ - The healthcheck status of the container. For example, ``healthy`, or ``unhealthy`. + The healthcheck status of the container. + + For example, ``healthy`, or ``unhealthy`. """ return self.attrs.get('State', {}).get('Health', {}).get('Status', 'unknown') From 976c84c481dce82d129980b87e88a102badb4109 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Nov 2023 16:56:50 -0500 Subject: [PATCH 37/45] build(deps): Bump urllib3 from 1.26.11 to 1.26.18 (#3183) Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.11 to 1.26.18. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/1.26.11...1.26.18) --- updated-dependencies: - dependency-name: urllib3 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 897cdbd5ef..6d932eb371 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,5 +2,5 @@ packaging==21.3 paramiko==2.11.0 pywin32==304; sys_platform == 'win32' requests==2.31.0 -urllib3==1.26.11 +urllib3==1.26.18 websocket-client==1.3.3 From db4878118b02124100c669b39249f0bdeed2aad0 Mon Sep 17 00:00:00 2001 From: Milas Bowman Date: Tue, 21 Nov 2023 10:42:53 -0500 Subject: [PATCH 38/45] breaking: Python 3.12 compatibility & remove custom SSL adapter (#3185) Add support for Python 3.12. `match_hostname` is gone in Python 3.12 and has been unused by Python since 3.7. The custom SSL adapter allows passing a specific SSL version; this was first introduced a looong time ago to handle some SSL issues at the time. Closes #3176. --------- Signed-off-by: Hugo van Kemenade Signed-off-by: Milas Bowman Co-authored-by: Hugo van Kemenade --- .github/workflows/ci.yml | 12 ++- .github/workflows/release.yml | 6 +- Dockerfile | 2 +- Dockerfile-docs | 2 +- Jenkinsfile | 147 ----------------------------- docker/api/client.py | 5 +- docker/client.py | 2 - docker/tls.py | 29 +----- docker/transport/__init__.py | 1 - docker/transport/ssladapter.py | 62 ------------ docker/utils/utils.py | 9 +- setup.py | 1 + test-requirements.txt | 6 +- tests/Dockerfile | 2 +- tests/Dockerfile-dind-certs | 2 +- tests/unit/models_networks_test.py | 18 ++-- tests/unit/ssladapter_test.py | 71 -------------- tests/unit/utils_test.py | 15 ++- tox.ini | 2 +- 19 files changed, 41 insertions(+), 353 deletions(-) delete mode 100644 Jenkinsfile delete mode 100644 docker/transport/ssladapter.py delete mode 100644 tests/unit/ssladapter_test.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dfbcc701eb..977199cebd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,15 +4,16 @@ on: [push, pull_request] env: DOCKER_BUILDKIT: '1' + FORCE_COLOR: 1 jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: - python-version: '3.11' + python-version: '3.x' - run: pip install -U ruff==0.0.284 - name: Run ruff run: ruff docker tests @@ -21,14 +22,15 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} + allow-prereleases: true - name: Install dependencies run: | python3 -m pip install --upgrade pip @@ -46,7 +48,7 @@ jobs: variant: [ "integration-dind", "integration-dind-ssl", "integration-dind-ssh" ] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: make ${{ matrix.variant }} run: | docker logout diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7c6358a225..b8b1f57d1f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,11 +12,15 @@ on: type: boolean default: true +env: + DOCKER_BUILDKIT: '1' + FORCE_COLOR: 1 + jobs: publish: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: diff --git a/Dockerfile b/Dockerfile index 3476c6d036..293888d725 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1 -ARG PYTHON_VERSION=3.10 +ARG PYTHON_VERSION=3.12 FROM python:${PYTHON_VERSION} diff --git a/Dockerfile-docs b/Dockerfile-docs index 11adbfe85d..266b2099e9 100644 --- a/Dockerfile-docs +++ b/Dockerfile-docs @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1 -ARG PYTHON_VERSION=3.10 +ARG PYTHON_VERSION=3.12 FROM python:${PYTHON_VERSION} diff --git a/Jenkinsfile b/Jenkinsfile deleted file mode 100644 index f9431eac06..0000000000 --- a/Jenkinsfile +++ /dev/null @@ -1,147 +0,0 @@ -#!groovy - -def imageNameBase = "dockerpinata/docker-py" -def imageNamePy3 -def imageDindSSH -def images = [:] - -def buildImage = { name, buildargs, pyTag -> - img = docker.image(name) - try { - img.pull() - } catch (Exception exc) { - img = docker.build(name, buildargs) - img.push() - } - if (pyTag?.trim()) images[pyTag] = img.id -} - -def buildImages = { -> - wrappedNode(label: "amd64 && ubuntu-2004 && overlay2", cleanWorkspace: true) { - stage("build image") { - checkout(scm) - - imageNamePy3 = "${imageNameBase}:py3-${gitCommit()}" - imageDindSSH = "${imageNameBase}:sshdind-${gitCommit()}" - withDockerRegistry(credentialsId:'dockerbuildbot-index.docker.io') { - buildImage(imageDindSSH, "-f tests/Dockerfile-ssh-dind .", "") - buildImage(imageNamePy3, "-f tests/Dockerfile --build-arg PYTHON_VERSION=3.10 .", "py3.10") - } - } - } -} - -def getDockerVersions = { -> - def dockerVersions = ["19.03.12"] - wrappedNode(label: "amd64 && ubuntu-2004 && overlay2") { - def result = sh(script: """docker run --rm \\ - --entrypoint=python \\ - ${imageNamePy3} \\ - /src/scripts/versions.py - """, returnStdout: true - ) - dockerVersions = dockerVersions + result.trim().tokenize(' ') - } - return dockerVersions -} - -def getAPIVersion = { engineVersion -> - def versionMap = [ - '18.09': '1.39', - '19.03': '1.40' - ] - def result = versionMap[engineVersion.substring(0, 5)] - if (!result) { - return '1.40' - } - return result -} - -def runTests = { Map settings -> - def dockerVersion = settings.get("dockerVersion", null) - def pythonVersion = settings.get("pythonVersion", null) - def testImage = settings.get("testImage", null) - def apiVersion = getAPIVersion(dockerVersion) - - if (!testImage) { - throw new Exception("Need test image object, e.g.: `runTests(testImage: img)`") - } - if (!dockerVersion) { - throw new Exception("Need Docker version to test, e.g.: `runTests(dockerVersion: '19.03.12')`") - } - if (!pythonVersion) { - throw new Exception("Need Python version being tested, e.g.: `runTests(pythonVersion: 'py3.x')`") - } - - { -> - wrappedNode(label: "amd64 && ubuntu-2004 && overlay2", cleanWorkspace: true) { - stage("test python=${pythonVersion} / docker=${dockerVersion}") { - checkout(scm) - def dindContainerName = "dpy-dind-\$BUILD_NUMBER-\$EXECUTOR_NUMBER-${pythonVersion}-${dockerVersion}" - def testContainerName = "dpy-tests-\$BUILD_NUMBER-\$EXECUTOR_NUMBER-${pythonVersion}-${dockerVersion}" - def testNetwork = "dpy-testnet-\$BUILD_NUMBER-\$EXECUTOR_NUMBER-${pythonVersion}-${dockerVersion}" - withDockerRegistry(credentialsId:'dockerbuildbot-index.docker.io') { - try { - // unit tests - sh """docker run --rm \\ - -e 'DOCKER_TEST_API_VERSION=${apiVersion}' \\ - ${testImage} \\ - py.test -v -rxs --cov=docker tests/unit - """ - // integration tests - sh """docker network create ${testNetwork}""" - sh """docker run --rm -d --name ${dindContainerName} -v /tmp --privileged --network ${testNetwork} \\ - ${imageDindSSH} dockerd -H tcp://0.0.0.0:2375 - """ - sh """docker run --rm \\ - --name ${testContainerName} \\ - -e "DOCKER_HOST=tcp://${dindContainerName}:2375" \\ - -e 'DOCKER_TEST_API_VERSION=${apiVersion}' \\ - --network ${testNetwork} \\ - --volumes-from ${dindContainerName} \\ - -v $DOCKER_CONFIG/config.json:/root/.docker/config.json \\ - ${testImage} \\ - py.test -v -rxs --cov=docker tests/integration - """ - sh """docker stop ${dindContainerName}""" - // start DIND container with SSH - sh """docker run --rm -d --name ${dindContainerName} -v /tmp --privileged --network ${testNetwork} \\ - ${imageDindSSH} dockerd --experimental""" - sh """docker exec ${dindContainerName} sh -c /usr/sbin/sshd """ - // run SSH tests only - sh """docker run --rm \\ - --name ${testContainerName} \\ - -e "DOCKER_HOST=ssh://${dindContainerName}:22" \\ - -e 'DOCKER_TEST_API_VERSION=${apiVersion}' \\ - --network ${testNetwork} \\ - --volumes-from ${dindContainerName} \\ - -v $DOCKER_CONFIG/config.json:/root/.docker/config.json \\ - ${testImage} \\ - py.test -v -rxs --cov=docker tests/ssh - """ - } finally { - sh """ - docker stop ${dindContainerName} - docker network rm ${testNetwork} - """ - } - } - } - } - } -} - - -buildImages() - -def dockerVersions = getDockerVersions() - -def testMatrix = [failFast: false] - -for (imgKey in new ArrayList(images.keySet())) { - for (version in dockerVersions) { - testMatrix["${imgKey}_${version}"] = runTests([testImage: images[imgKey], dockerVersion: version, pythonVersion: imgKey]) - } -} - -parallel(testMatrix) diff --git a/docker/api/client.py b/docker/api/client.py index 20f8a2af7c..499a7c785e 100644 --- a/docker/api/client.py +++ b/docker/api/client.py @@ -4,6 +4,7 @@ from functools import partial import requests +import requests.adapters import requests.exceptions from .. import auth @@ -14,7 +15,7 @@ from ..errors import (DockerException, InvalidVersion, TLSParameterError, create_api_error_from_http_exception) from ..tls import TLSConfig -from ..transport import SSLHTTPAdapter, UnixHTTPAdapter +from ..transport import UnixHTTPAdapter from ..utils import check_resource, config, update_headers, utils from ..utils.json_stream import json_stream from ..utils.proxy import ProxyConfig @@ -183,7 +184,7 @@ def __init__(self, base_url=None, version=None, if isinstance(tls, TLSConfig): tls.configure_client(self) elif tls: - self._custom_adapter = SSLHTTPAdapter( + self._custom_adapter = requests.adapters.HTTPAdapter( pool_connections=num_pools) self.mount('https://', self._custom_adapter) self.base_url = base_url diff --git a/docker/client.py b/docker/client.py index 4dbd846f1d..2910c12596 100644 --- a/docker/client.py +++ b/docker/client.py @@ -71,8 +71,6 @@ def from_env(cls, **kwargs): timeout (int): Default timeout for API calls, in seconds. max_pool_size (int): The maximum number of connections to save in the pool. - ssl_version (int): A valid `SSL version`_. - assert_hostname (bool): Verify the hostname of the server. environment (dict): The environment to read environment variables from. Default: the value of ``os.environ`` credstore_env (dict): Override environment variables when calling diff --git a/docker/tls.py b/docker/tls.py index a4dd002091..ad4966c903 100644 --- a/docker/tls.py +++ b/docker/tls.py @@ -1,8 +1,6 @@ import os -import ssl from . import errors -from .transport import SSLHTTPAdapter class TLSConfig: @@ -15,35 +13,18 @@ class TLSConfig: verify (bool or str): This can be a bool or a path to a CA cert file to verify against. If ``True``, verify using ca_cert; if ``False`` or not specified, do not verify. - ssl_version (int): A valid `SSL version`_. - assert_hostname (bool): Verify the hostname of the server. - - .. _`SSL version`: - https://docs.python.org/3.5/library/ssl.html#ssl.PROTOCOL_TLSv1 """ cert = None ca_cert = None verify = None - ssl_version = None - def __init__(self, client_cert=None, ca_cert=None, verify=None, - ssl_version=None, assert_hostname=None, - assert_fingerprint=None): + def __init__(self, client_cert=None, ca_cert=None, verify=None): # Argument compatibility/mapping with # https://docs.docker.com/engine/articles/https/ # This diverges from the Docker CLI in that users can specify 'tls' # here, but also disable any public/default CA pool verification by # leaving verify=False - self.assert_hostname = assert_hostname - self.assert_fingerprint = assert_fingerprint - - # If the user provides an SSL version, we should use their preference - if ssl_version: - self.ssl_version = ssl_version - else: - self.ssl_version = ssl.PROTOCOL_TLS_CLIENT - # "client_cert" must have both or neither cert/key files. In # either case, Alert the user when both are expected, but any are # missing. @@ -77,8 +58,6 @@ def configure_client(self, client): """ Configure a client with these TLS options. """ - client.ssl_version = self.ssl_version - if self.verify and self.ca_cert: client.verify = self.ca_cert else: @@ -86,9 +65,3 @@ def configure_client(self, client): if self.cert: client.cert = self.cert - - client.mount('https://', SSLHTTPAdapter( - ssl_version=self.ssl_version, - assert_hostname=self.assert_hostname, - assert_fingerprint=self.assert_fingerprint, - )) diff --git a/docker/transport/__init__.py b/docker/transport/__init__.py index 54492c11ac..07bc7fd582 100644 --- a/docker/transport/__init__.py +++ b/docker/transport/__init__.py @@ -1,5 +1,4 @@ from .unixconn import UnixHTTPAdapter -from .ssladapter import SSLHTTPAdapter try: from .npipeconn import NpipeHTTPAdapter from .npipesocket import NpipeSocket diff --git a/docker/transport/ssladapter.py b/docker/transport/ssladapter.py deleted file mode 100644 index 69274bd1dd..0000000000 --- a/docker/transport/ssladapter.py +++ /dev/null @@ -1,62 +0,0 @@ -""" Resolves OpenSSL issues in some servers: - https://lukasa.co.uk/2013/01/Choosing_SSL_Version_In_Requests/ - https://github.com/kennethreitz/requests/pull/799 -""" -from packaging.version import Version -from requests.adapters import HTTPAdapter - -from docker.transport.basehttpadapter import BaseHTTPAdapter - -import urllib3 - - -PoolManager = urllib3.poolmanager.PoolManager - - -class SSLHTTPAdapter(BaseHTTPAdapter): - '''An HTTPS Transport Adapter that uses an arbitrary SSL version.''' - - __attrs__ = HTTPAdapter.__attrs__ + ['assert_fingerprint', - 'assert_hostname', - 'ssl_version'] - - def __init__(self, ssl_version=None, assert_hostname=None, - assert_fingerprint=None, **kwargs): - self.ssl_version = ssl_version - self.assert_hostname = assert_hostname - self.assert_fingerprint = assert_fingerprint - super().__init__(**kwargs) - - def init_poolmanager(self, connections, maxsize, block=False): - kwargs = { - 'num_pools': connections, - 'maxsize': maxsize, - 'block': block, - 'assert_hostname': self.assert_hostname, - 'assert_fingerprint': self.assert_fingerprint, - } - if self.ssl_version and self.can_override_ssl_version(): - kwargs['ssl_version'] = self.ssl_version - - self.poolmanager = PoolManager(**kwargs) - - def get_connection(self, *args, **kwargs): - """ - Ensure assert_hostname is set correctly on our pool - - We already take care of a normal poolmanager via init_poolmanager - - But we still need to take care of when there is a proxy poolmanager - """ - conn = super().get_connection(*args, **kwargs) - if conn.assert_hostname != self.assert_hostname: - conn.assert_hostname = self.assert_hostname - return conn - - def can_override_ssl_version(self): - urllib_ver = urllib3.__version__.split('-')[0] - if urllib_ver is None: - return False - if urllib_ver == 'dev': - return True - return Version(urllib_ver) > Version('1.5') diff --git a/docker/utils/utils.py b/docker/utils/utils.py index 0f28afb116..759ddd2f1a 100644 --- a/docker/utils/utils.py +++ b/docker/utils/utils.py @@ -341,7 +341,7 @@ def parse_devices(devices): return device_list -def kwargs_from_env(ssl_version=None, assert_hostname=None, environment=None): +def kwargs_from_env(environment=None): if not environment: environment = os.environ host = environment.get('DOCKER_HOST') @@ -369,18 +369,11 @@ def kwargs_from_env(ssl_version=None, assert_hostname=None, environment=None): if not cert_path: cert_path = os.path.join(os.path.expanduser('~'), '.docker') - if not tls_verify and assert_hostname is None: - # assert_hostname is a subset of TLS verification, - # so if it's not set already then set it to false. - assert_hostname = False - params['tls'] = TLSConfig( client_cert=(os.path.join(cert_path, 'cert.pem'), os.path.join(cert_path, 'key.pem')), ca_cert=os.path.join(cert_path, 'ca.pem'), verify=tls_verify, - ssl_version=ssl_version, - assert_hostname=assert_hostname, ) return params diff --git a/setup.py b/setup.py index 79bf3bdb68..d63cbe0a1c 100644 --- a/setup.py +++ b/setup.py @@ -74,6 +74,7 @@ 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', 'Topic :: Software Development', 'Topic :: Utilities', 'License :: OSI Approved :: Apache Software License', diff --git a/test-requirements.txt b/test-requirements.txt index 951b3be9fc..031d0acf0a 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,6 +1,6 @@ setuptools==65.5.1 -coverage==6.4.2 +coverage==7.2.7 ruff==0.0.284 -pytest==7.1.2 -pytest-cov==3.0.0 +pytest==7.4.2 +pytest-cov==4.1.0 pytest-timeout==2.1.0 diff --git a/tests/Dockerfile b/tests/Dockerfile index 366abe23bb..d7c14b6cca 100644 --- a/tests/Dockerfile +++ b/tests/Dockerfile @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1 -ARG PYTHON_VERSION=3.10 +ARG PYTHON_VERSION=3.12 FROM python:${PYTHON_VERSION} diff --git a/tests/Dockerfile-dind-certs b/tests/Dockerfile-dind-certs index 288a340ab1..7b819eb154 100644 --- a/tests/Dockerfile-dind-certs +++ b/tests/Dockerfile-dind-certs @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1 -ARG PYTHON_VERSION=3.10 +ARG PYTHON_VERSION=3.12 FROM python:${PYTHON_VERSION} RUN mkdir /tmp/certs diff --git a/tests/unit/models_networks_test.py b/tests/unit/models_networks_test.py index 58c9fce669..f10e1e3e33 100644 --- a/tests/unit/models_networks_test.py +++ b/tests/unit/models_networks_test.py @@ -10,8 +10,8 @@ def test_create(self): client = make_fake_client() network = client.networks.create("foobar", labels={'foo': 'bar'}) assert network.id == FAKE_NETWORK_ID - assert client.api.inspect_network.called_once_with(FAKE_NETWORK_ID) - assert client.api.create_network.called_once_with( + client.api.inspect_network.assert_called_once_with(FAKE_NETWORK_ID) + client.api.create_network.assert_called_once_with( "foobar", labels={'foo': 'bar'} ) @@ -20,21 +20,21 @@ def test_get(self): client = make_fake_client() network = client.networks.get(FAKE_NETWORK_ID) assert network.id == FAKE_NETWORK_ID - assert client.api.inspect_network.called_once_with(FAKE_NETWORK_ID) + client.api.inspect_network.assert_called_once_with(FAKE_NETWORK_ID) def test_list(self): client = make_fake_client() networks = client.networks.list() assert networks[0].id == FAKE_NETWORK_ID - assert client.api.networks.called_once_with() + client.api.networks.assert_called_once_with() client = make_fake_client() client.networks.list(ids=["abc"]) - assert client.api.networks.called_once_with(ids=["abc"]) + client.api.networks.assert_called_once_with(ids=["abc"]) client = make_fake_client() client.networks.list(names=["foobar"]) - assert client.api.networks.called_once_with(names=["foobar"]) + client.api.networks.assert_called_once_with(names=["foobar"]) class NetworkTest(unittest.TestCase): @@ -43,7 +43,7 @@ def test_connect(self): client = make_fake_client() network = client.networks.get(FAKE_NETWORK_ID) network.connect(FAKE_CONTAINER_ID) - assert client.api.connect_container_to_network.called_once_with( + client.api.connect_container_to_network.assert_called_once_with( FAKE_CONTAINER_ID, FAKE_NETWORK_ID ) @@ -52,7 +52,7 @@ def test_disconnect(self): client = make_fake_client() network = client.networks.get(FAKE_NETWORK_ID) network.disconnect(FAKE_CONTAINER_ID) - assert client.api.disconnect_container_from_network.called_once_with( + client.api.disconnect_container_from_network.assert_called_once_with( FAKE_CONTAINER_ID, FAKE_NETWORK_ID ) @@ -61,4 +61,4 @@ def test_remove(self): client = make_fake_client() network = client.networks.get(FAKE_NETWORK_ID) network.remove() - assert client.api.remove_network.called_once_with(FAKE_NETWORK_ID) + client.api.remove_network.assert_called_once_with(FAKE_NETWORK_ID) diff --git a/tests/unit/ssladapter_test.py b/tests/unit/ssladapter_test.py deleted file mode 100644 index d3f2407c39..0000000000 --- a/tests/unit/ssladapter_test.py +++ /dev/null @@ -1,71 +0,0 @@ -import unittest -from ssl import match_hostname, CertificateError - -import pytest -from docker.transport import ssladapter - -try: - from ssl import OP_NO_SSLv3, OP_NO_SSLv2, OP_NO_TLSv1 -except ImportError: - OP_NO_SSLv2 = 0x1000000 - OP_NO_SSLv3 = 0x2000000 - OP_NO_TLSv1 = 0x4000000 - - -class SSLAdapterTest(unittest.TestCase): - def test_only_uses_tls(self): - ssl_context = ssladapter.urllib3.util.ssl_.create_urllib3_context() - - assert ssl_context.options & OP_NO_SSLv3 - # if OpenSSL is compiled without SSL2 support, OP_NO_SSLv2 will be 0 - assert not bool(OP_NO_SSLv2) or ssl_context.options & OP_NO_SSLv2 - assert not ssl_context.options & OP_NO_TLSv1 - - -class MatchHostnameTest(unittest.TestCase): - cert = { - 'issuer': ( - (('countryName', 'US'),), - (('stateOrProvinceName', 'California'),), - (('localityName', 'San Francisco'),), - (('organizationName', 'Docker Inc'),), - (('organizationalUnitName', 'Docker-Python'),), - (('commonName', 'localhost'),), - (('emailAddress', 'info@docker.com'),) - ), - 'notAfter': 'Mar 25 23:08:23 2030 GMT', - 'notBefore': 'Mar 25 23:08:23 2016 GMT', - 'serialNumber': 'BD5F894C839C548F', - 'subject': ( - (('countryName', 'US'),), - (('stateOrProvinceName', 'California'),), - (('localityName', 'San Francisco'),), - (('organizationName', 'Docker Inc'),), - (('organizationalUnitName', 'Docker-Python'),), - (('commonName', 'localhost'),), - (('emailAddress', 'info@docker.com'),) - ), - 'subjectAltName': ( - ('DNS', 'localhost'), - ('DNS', '*.gensokyo.jp'), - ('IP Address', '127.0.0.1'), - ), - 'version': 3 - } - - def test_match_ip_address_success(self): - assert match_hostname(self.cert, '127.0.0.1') is None - - def test_match_localhost_success(self): - assert match_hostname(self.cert, 'localhost') is None - - def test_match_dns_success(self): - assert match_hostname(self.cert, 'touhou.gensokyo.jp') is None - - def test_match_ip_address_failure(self): - with pytest.raises(CertificateError): - match_hostname(self.cert, '192.168.0.25') - - def test_match_dns_failure(self): - with pytest.raises(CertificateError): - match_hostname(self.cert, 'foobar.co.uk') diff --git a/tests/unit/utils_test.py b/tests/unit/utils_test.py index b47cb0c62f..de79e3037d 100644 --- a/tests/unit/utils_test.py +++ b/tests/unit/utils_test.py @@ -75,13 +75,12 @@ def test_kwargs_from_env_tls(self): os.environ.update(DOCKER_HOST='tcp://192.168.59.103:2376', DOCKER_CERT_PATH=TEST_CERT_DIR, DOCKER_TLS_VERIFY='1') - kwargs = kwargs_from_env(assert_hostname=False) + kwargs = kwargs_from_env() assert 'tcp://192.168.59.103:2376' == kwargs['base_url'] assert 'ca.pem' in kwargs['tls'].ca_cert assert 'cert.pem' in kwargs['tls'].cert[0] assert 'key.pem' in kwargs['tls'].cert[1] - assert kwargs['tls'].assert_hostname is False - assert kwargs['tls'].verify + assert kwargs['tls'].verify is True parsed_host = parse_host(kwargs['base_url'], IS_WINDOWS_PLATFORM, True) kwargs['version'] = DEFAULT_DOCKER_API_VERSION @@ -97,12 +96,11 @@ def test_kwargs_from_env_tls_verify_false(self): os.environ.update(DOCKER_HOST='tcp://192.168.59.103:2376', DOCKER_CERT_PATH=TEST_CERT_DIR, DOCKER_TLS_VERIFY='') - kwargs = kwargs_from_env(assert_hostname=True) + kwargs = kwargs_from_env() assert 'tcp://192.168.59.103:2376' == kwargs['base_url'] assert 'ca.pem' in kwargs['tls'].ca_cert assert 'cert.pem' in kwargs['tls'].cert[0] assert 'key.pem' in kwargs['tls'].cert[1] - assert kwargs['tls'].assert_hostname is True assert kwargs['tls'].verify is False parsed_host = parse_host(kwargs['base_url'], IS_WINDOWS_PLATFORM, True) kwargs['version'] = DEFAULT_DOCKER_API_VERSION @@ -123,12 +121,12 @@ def test_kwargs_from_env_tls_verify_false_no_cert(self): HOME=temp_dir, DOCKER_TLS_VERIFY='') os.environ.pop('DOCKER_CERT_PATH', None) - kwargs = kwargs_from_env(assert_hostname=True) + kwargs = kwargs_from_env() assert 'tcp://192.168.59.103:2376' == kwargs['base_url'] def test_kwargs_from_env_no_cert_path(self): + temp_dir = tempfile.mkdtemp() try: - temp_dir = tempfile.mkdtemp() cert_dir = os.path.join(temp_dir, '.docker') shutil.copytree(TEST_CERT_DIR, cert_dir) @@ -142,8 +140,7 @@ def test_kwargs_from_env_no_cert_path(self): assert cert_dir in kwargs['tls'].cert[0] assert cert_dir in kwargs['tls'].cert[1] finally: - if temp_dir: - shutil.rmtree(temp_dir) + shutil.rmtree(temp_dir) def test_kwargs_from_env_alternate_env(self): # Values in os.environ are entirely ignored if an alternate is diff --git a/tox.ini b/tox.ini index 2028dd3957..03467aea26 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py{37,38,39,310,311}, ruff +envlist = py{37,38,39,310,311,312}, ruff skipsdist=True [testenv] From fd2f5029f0b9c1b67addfa8956900db018efdf25 Mon Sep 17 00:00:00 2001 From: Milas Bowman Date: Tue, 21 Nov 2023 12:08:25 -0500 Subject: [PATCH 39/45] chore: add changelog for 7.0.0 (#3186) Signed-off-by: Milas Bowman --- docs/change-log.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/docs/change-log.md b/docs/change-log.md index 0d60f882d6..7719bbfb64 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -1,6 +1,40 @@ Changelog ========== +7.0.0 +----- +### Upgrade Notes +- Removed SSL version (`ssl_version`) and explicit hostname check (`assert_hostname`) options + - `assert_hostname` has not been used since Python 3.6 and was removed in 3.12 + - Python 3.7+ supports TLSv1.3 by default +- Websocket support is no longer included by default + - Use `pip install docker[websockets]` to include `websocket-client` dependency + - By default, `docker-py` hijacks the TCP connection and does not use Websockets + - Websocket client is only required to use `attach_socket(container, ws=True)` +- Python 3.7 no longer officially supported (reached end-of-life June 2023) + +### Features +- Python 3.12 support +- Full `networking_config` support for `containers.create()` + - Replaces `network_driver_opt` (added in 6.1.0) +- Add `health()` property to container that returns status (e.g. `unhealthy`) +- Add `pause` option to `container.commit()` +- Add support for bind mount propagation (e.g. `rshared`, `private`) + +### Bugfixes +- Consistently return `docker.errors.NotFound` on 404 responses + +### Miscellaneous +- Upgraded urllib3 version in `requirements.txt` (used for development/tests) +- Documentation typo fixes & formatting improvements +- Fixed integration test compatibility for newer Moby engine versions +- Switch to [ruff](https://github.com/astral-sh/ruff) for linting + +6.1.3 +----- +#### Bugfixes +- Fix compatibility with [`eventlet/eventlet`](https://github.com/eventlet/eventlet) + 6.1.2 ----- From 586988ce2d942a7cc093a9c162fa0401ceefc225 Mon Sep 17 00:00:00 2001 From: Milas Bowman Date: Tue, 21 Nov 2023 12:14:23 -0500 Subject: [PATCH 40/45] chore: remove support for Python 3.7 (#3187) Python 3.7 reached EOL in June 2023: https://endoflife.date/python Signed-off-by: Milas Bowman --- .github/workflows/ci.yml | 2 +- docker/version.py | 12 +++--------- setup.py | 3 +-- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 977199cebd..127d5b6822 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 diff --git a/docker/version.py b/docker/version.py index 44eac8c5dc..dca45bf047 100644 --- a/docker/version.py +++ b/docker/version.py @@ -1,14 +1,8 @@ try: from ._version import __version__ except ImportError: + from importlib.metadata import version, PackageNotFoundError try: - # importlib.metadata available in Python 3.8+, the fallback (0.0.0) - # is fine because release builds use _version (above) rather than - # this code path, so it only impacts developing w/ 3.7 - from importlib.metadata import version, PackageNotFoundError - try: - __version__ = version('docker') - except PackageNotFoundError: - __version__ = '0.0.0' - except ImportError: + __version__ = version('docker') + except PackageNotFoundError: __version__ = '0.0.0' diff --git a/setup.py b/setup.py index d63cbe0a1c..98736247d6 100644 --- a/setup.py +++ b/setup.py @@ -59,7 +59,7 @@ install_requires=requirements, tests_require=test_requirements, extras_require=extras_require, - python_requires='>=3.7', + python_requires='>=3.8', zip_safe=False, test_suite='tests', classifiers=[ @@ -69,7 +69,6 @@ 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', From 714096923918183f3ab4e11973156551dc5559f7 Mon Sep 17 00:00:00 2001 From: Milas Bowman Date: Tue, 21 Nov 2023 12:17:12 -0500 Subject: [PATCH 41/45] chore: update MAINTAINERS and remove CODEOWNERS (#3188) Update `MAINTAINERS` with the current folks, adn remove the `CODEOWNERS` file entirely -- it's not really helpful here, as this project isn't big enough to have multiple subsections with different maintainers/owners. Signed-off-by: Milas Bowman --- .github/CODEOWNERS | 6 ------ MAINTAINERS | 16 ++++++++++++++-- 2 files changed, 14 insertions(+), 8 deletions(-) delete mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS deleted file mode 100644 index 5df3014937..0000000000 --- a/.github/CODEOWNERS +++ /dev/null @@ -1,6 +0,0 @@ -# GitHub code owners -# See https://help.github.com/articles/about-codeowners/ -# -# KEEP THIS FILE SORTED. Order is important. Last match takes precedence. - -* @aiordache @ulyssessouza diff --git a/MAINTAINERS b/MAINTAINERS index b74cb28fd3..96ba4752e8 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -11,17 +11,19 @@ [Org] [Org."Core maintainers"] people = [ - "aiordache", - "ulyssessouza", + "glours", + "milas", ] [Org.Alumni] people = [ + "aiordache", "aanand", "bfirsh", "dnephin", "mnowster", "mpetazzoni", "shin-", + "ulyssessouza", ] [people] @@ -52,6 +54,16 @@ Email = "dnephin@gmail.com" GitHub = "dnephin" + [people.glours] + Name = "Guillaume Lours" + Email = "705411+glours@users.noreply.github.com" + GitHub = "glours" + + [people.milas] + Name = "Milas Bowman" + Email = "devnull@milas.dev" + GitHub = "milas" + [people.mnowster] Name = "Mazz Mosley" Email = "mazz@houseofmnowster.com" From cb8f2c6630584d6d1b2d9296a0c780af0f5e5549 Mon Sep 17 00:00:00 2001 From: Milas Bowman Date: Mon, 27 Nov 2023 09:17:47 -0500 Subject: [PATCH 42/45] chore: fix missing setuptools in CI (#3189) Install `setuptools` in addition to `wheel` before trying to run `python setup.py` manually. Note that `setuptools` is already correctly listed in the `pyproject.toml` file for consumers installing via `pip` etc, but in CI the file is run directly to generate `sdist` and `bdist_wheel` artifacts for PyPI. Signed-off-by: Milas Bowman --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b8b1f57d1f..721020ac33 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: - name: Generate Pacakge run: | - pip3 install wheel + pip3 install setuptools wheel python setup.py sdist bdist_wheel env: SETUPTOOLS_SCM_PRETEND_VERSION_FOR_DOCKER: ${{ inputs.tag }} From a9b5494fd0574e520c735e1d0c4a303528d48063 Mon Sep 17 00:00:00 2001 From: Daniel Lombardi Date: Tue, 5 Dec 2023 02:03:13 -0300 Subject: [PATCH 43/45] fix: validate tag before build using OCI regex (#3191) Sources: * https://github.com/opencontainers/distribution-spec * https://docs.docker.com/engine/reference/commandline/tag/ Closes #3153. --------- Signed-off-by: Daniel Lombardi --- docker/api/build.py | 9 +- docker/utils/__init__.py | 2 +- docker/utils/build.py | 8 ++ tests/unit/api_build_test.py | 257 +++++++++++++++++++---------------- 4 files changed, 157 insertions(+), 119 deletions(-) diff --git a/docker/api/build.py b/docker/api/build.py index 439f4dc351..9c8b4e6ae9 100644 --- a/docker/api/build.py +++ b/docker/api/build.py @@ -129,13 +129,16 @@ def build(self, path=None, tag=None, quiet=False, fileobj=None, raise errors.DockerException( 'Can not use custom encoding if gzip is enabled' ) - + if tag is not None: + if not utils.match_tag(tag): + raise errors.DockerException( + f"invalid tag '{tag}': invalid reference format" + ) for key in container_limits.keys(): if key not in constants.CONTAINER_LIMITS_KEYS: raise errors.DockerException( - f'Invalid container_limits key {key}' + f"invalid tag '{tag}': invalid reference format" ) - if custom_context: if not fileobj: raise TypeError("You must specify fileobj with custom_context") diff --git a/docker/utils/__init__.py b/docker/utils/__init__.py index 944c6e65e0..b4bef7d47c 100644 --- a/docker/utils/__init__.py +++ b/docker/utils/__init__.py @@ -1,5 +1,5 @@ -from .build import create_archive, exclude_paths, mkbuildcontext, tar +from .build import match_tag, create_archive, exclude_paths, mkbuildcontext, tar from .decorators import check_resource, minimum_version, update_headers from .utils import ( compare_version, convert_port_bindings, convert_volume_binds, diff --git a/docker/utils/build.py b/docker/utils/build.py index 8d18c2be71..a5c4b0c2d9 100644 --- a/docker/utils/build.py +++ b/docker/utils/build.py @@ -9,6 +9,14 @@ _SEP = re.compile('/|\\\\') if IS_WINDOWS_PLATFORM else re.compile('/') +_TAG = re.compile( + r"^[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*(\/[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*)*" \ + + "(:[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127})?$" +) + + +def match_tag(tag: str) -> bool: + return bool(_TAG.match(tag)) def tar(path, exclude=None, dockerfile=None, fileobj=None, gzip=False): diff --git a/tests/unit/api_build_test.py b/tests/unit/api_build_test.py index cbecd1e544..01958c3e1f 100644 --- a/tests/unit/api_build_test.py +++ b/tests/unit/api_build_test.py @@ -2,181 +2,206 @@ import io import shutil +import pytest + import docker -from docker import auth +from docker import auth, errors from docker.api.build import process_dockerfile -import pytest - from ..helpers import make_tree from .api_test import BaseAPIClientTest, fake_request, url_prefix class BuildTest(BaseAPIClientTest): def test_build_container(self): - script = io.BytesIO('\n'.join([ - 'FROM busybox', - 'RUN mkdir -p /tmp/test', - 'EXPOSE 8080', - 'ADD https://dl.dropboxusercontent.com/u/20637798/silence.tar.gz' - ' /tmp/silence.tar.gz' - ]).encode('ascii')) + script = io.BytesIO( + "\n".join( + [ + "FROM busybox", + "RUN mkdir -p /tmp/test", + "EXPOSE 8080", + "ADD https://dl.dropboxusercontent.com/u/20637798/silence.tar.gz" + " /tmp/silence.tar.gz", + ] + ).encode("ascii") + ) self.client.build(fileobj=script) def test_build_container_pull(self): - script = io.BytesIO('\n'.join([ - 'FROM busybox', - 'RUN mkdir -p /tmp/test', - 'EXPOSE 8080', - 'ADD https://dl.dropboxusercontent.com/u/20637798/silence.tar.gz' - ' /tmp/silence.tar.gz' - ]).encode('ascii')) + script = io.BytesIO( + "\n".join( + [ + "FROM busybox", + "RUN mkdir -p /tmp/test", + "EXPOSE 8080", + "ADD https://dl.dropboxusercontent.com/u/20637798/silence.tar.gz" + " /tmp/silence.tar.gz", + ] + ).encode("ascii") + ) self.client.build(fileobj=script, pull=True) def test_build_container_custom_context(self): - script = io.BytesIO('\n'.join([ - 'FROM busybox', - 'RUN mkdir -p /tmp/test', - 'EXPOSE 8080', - 'ADD https://dl.dropboxusercontent.com/u/20637798/silence.tar.gz' - ' /tmp/silence.tar.gz' - ]).encode('ascii')) + script = io.BytesIO( + "\n".join( + [ + "FROM busybox", + "RUN mkdir -p /tmp/test", + "EXPOSE 8080", + "ADD https://dl.dropboxusercontent.com/u/20637798/silence.tar.gz" + " /tmp/silence.tar.gz", + ] + ).encode("ascii") + ) context = docker.utils.mkbuildcontext(script) self.client.build(fileobj=context, custom_context=True) def test_build_container_custom_context_gzip(self): - script = io.BytesIO('\n'.join([ - 'FROM busybox', - 'RUN mkdir -p /tmp/test', - 'EXPOSE 8080', - 'ADD https://dl.dropboxusercontent.com/u/20637798/silence.tar.gz' - ' /tmp/silence.tar.gz' - ]).encode('ascii')) + script = io.BytesIO( + "\n".join( + [ + "FROM busybox", + "RUN mkdir -p /tmp/test", + "EXPOSE 8080", + "ADD https://dl.dropboxusercontent.com/u/20637798/silence.tar.gz" + " /tmp/silence.tar.gz", + ] + ).encode("ascii") + ) context = docker.utils.mkbuildcontext(script) gz_context = gzip.GzipFile(fileobj=context) - self.client.build( - fileobj=gz_context, - custom_context=True, - encoding="gzip" - ) + self.client.build(fileobj=gz_context, custom_context=True, encoding="gzip") def test_build_remote_with_registry_auth(self): - self.client._auth_configs = auth.AuthConfig({ - 'auths': { - 'https://example.com': { - 'user': 'example', - 'password': 'example', - 'email': 'example@example.com' + self.client._auth_configs = auth.AuthConfig( + { + "auths": { + "https://example.com": { + "user": "example", + "password": "example", + "email": "example@example.com", + } } } - }) + ) - expected_params = {'t': None, 'q': False, 'dockerfile': None, - 'rm': False, 'nocache': False, 'pull': False, - 'forcerm': False, - 'remote': 'https://github.com/docker-library/mongo'} + expected_params = { + "t": None, + "q": False, + "dockerfile": None, + "rm": False, + "nocache": False, + "pull": False, + "forcerm": False, + "remote": "https://github.com/docker-library/mongo", + } expected_headers = { - 'X-Registry-Config': auth.encode_header( - self.client._auth_configs.auths - ) + "X-Registry-Config": auth.encode_header(self.client._auth_configs.auths) } - self.client.build(path='https://github.com/docker-library/mongo') + self.client.build(path="https://github.com/docker-library/mongo") fake_request.assert_called_with( - 'POST', + "POST", f"{url_prefix}build", stream=True, data=None, headers=expected_headers, params=expected_params, - timeout=None + timeout=None, ) def test_build_container_with_named_dockerfile(self): - self.client.build('.', dockerfile='nameddockerfile') + self.client.build(".", dockerfile="nameddockerfile") + + def test_build_with_invalid_tag(self): + with pytest.raises(errors.DockerException): + self.client.build(".", tag="https://example.com") def test_build_container_with_container_limits(self): - self.client.build('.', container_limits={ - 'memory': 1024 * 1024, - 'cpusetcpus': 1, - 'cpushares': 1000, - 'memswap': 1024 * 1024 * 8 - }) + self.client.build( + ".", + container_limits={ + "memory": 1024 * 1024, + "cpusetcpus": 1, + "cpushares": 1000, + "memswap": 1024 * 1024 * 8, + }, + ) def test_build_container_invalid_container_limits(self): with pytest.raises(docker.errors.DockerException): - self.client.build('.', container_limits={ - 'foo': 'bar' - }) + self.client.build(".", container_limits={"foo": "bar"}) def test_set_auth_headers_with_empty_dict_and_auth_configs(self): - self.client._auth_configs = auth.AuthConfig({ - 'auths': { - 'https://example.com': { - 'user': 'example', - 'password': 'example', - 'email': 'example@example.com' + self.client._auth_configs = auth.AuthConfig( + { + "auths": { + "https://example.com": { + "user": "example", + "password": "example", + "email": "example@example.com", + } } } - }) + ) headers = {} expected_headers = { - 'X-Registry-Config': auth.encode_header( - self.client._auth_configs.auths - ) + "X-Registry-Config": auth.encode_header(self.client._auth_configs.auths) } self.client._set_auth_headers(headers) assert headers == expected_headers def test_set_auth_headers_with_dict_and_auth_configs(self): - self.client._auth_configs = auth.AuthConfig({ - 'auths': { - 'https://example.com': { - 'user': 'example', - 'password': 'example', - 'email': 'example@example.com' + self.client._auth_configs = auth.AuthConfig( + { + "auths": { + "https://example.com": { + "user": "example", + "password": "example", + "email": "example@example.com", + } } } - }) + ) - headers = {'foo': 'bar'} + headers = {"foo": "bar"} expected_headers = { - 'X-Registry-Config': auth.encode_header( - self.client._auth_configs.auths - ), - 'foo': 'bar' + "X-Registry-Config": auth.encode_header(self.client._auth_configs.auths), + "foo": "bar", } self.client._set_auth_headers(headers) assert headers == expected_headers def test_set_auth_headers_with_dict_and_no_auth_configs(self): - headers = {'foo': 'bar'} - expected_headers = { - 'foo': 'bar' - } + headers = {"foo": "bar"} + expected_headers = {"foo": "bar"} self.client._set_auth_headers(headers) assert headers == expected_headers @pytest.mark.skipif( - not docker.constants.IS_WINDOWS_PLATFORM, - reason='Windows-specific syntax') + not docker.constants.IS_WINDOWS_PLATFORM, reason="Windows-specific syntax" + ) def test_process_dockerfile_win_longpath_prefix(self): dirs = [ - 'foo', 'foo/bar', 'baz', + "foo", + "foo/bar", + "baz", ] files = [ - 'Dockerfile', 'foo/Dockerfile.foo', 'foo/bar/Dockerfile.bar', - 'baz/Dockerfile.baz', + "Dockerfile", + "foo/Dockerfile.foo", + "foo/bar/Dockerfile.bar", + "baz/Dockerfile.baz", ] base = make_tree(dirs, files) @@ -186,40 +211,42 @@ def pre(path): return docker.constants.WINDOWS_LONGPATH_PREFIX + path assert process_dockerfile(None, pre(base)) == (None, None) - assert process_dockerfile('Dockerfile', pre(base)) == ( - 'Dockerfile', None + assert process_dockerfile("Dockerfile", pre(base)) == ("Dockerfile", None) + assert process_dockerfile("foo/Dockerfile.foo", pre(base)) == ( + "foo/Dockerfile.foo", + None, ) - assert process_dockerfile('foo/Dockerfile.foo', pre(base)) == ( - 'foo/Dockerfile.foo', None + assert process_dockerfile("../Dockerfile", pre(f"{base}\\foo"))[1] is not None + assert process_dockerfile("../baz/Dockerfile.baz", pre(f"{base}/baz")) == ( + "../baz/Dockerfile.baz", + None, ) - assert process_dockerfile( - '../Dockerfile', pre(f"{base}\\foo") - )[1] is not None - assert process_dockerfile( - '../baz/Dockerfile.baz', pre(f"{base}/baz") - ) == ('../baz/Dockerfile.baz', None) def test_process_dockerfile(self): dirs = [ - 'foo', 'foo/bar', 'baz', + "foo", + "foo/bar", + "baz", ] files = [ - 'Dockerfile', 'foo/Dockerfile.foo', 'foo/bar/Dockerfile.bar', - 'baz/Dockerfile.baz', + "Dockerfile", + "foo/Dockerfile.foo", + "foo/bar/Dockerfile.bar", + "baz/Dockerfile.baz", ] base = make_tree(dirs, files) self.addCleanup(shutil.rmtree, base) assert process_dockerfile(None, base) == (None, None) - assert process_dockerfile('Dockerfile', base) == ('Dockerfile', None) - assert process_dockerfile('foo/Dockerfile.foo', base) == ( - 'foo/Dockerfile.foo', None + assert process_dockerfile("Dockerfile", base) == ("Dockerfile", None) + assert process_dockerfile("foo/Dockerfile.foo", base) == ( + "foo/Dockerfile.foo", + None, ) - assert process_dockerfile( - '../Dockerfile', f"{base}/foo" - )[1] is not None - assert process_dockerfile('../baz/Dockerfile.baz', f"{base}/baz") == ( - '../baz/Dockerfile.baz', None + assert process_dockerfile("../Dockerfile", f"{base}/foo")[1] is not None + assert process_dockerfile("../baz/Dockerfile.baz", f"{base}/baz") == ( + "../baz/Dockerfile.baz", + None, ) From 3d0a3f1d77878a7197b5b65ba0abefd3b72c6f72 Mon Sep 17 00:00:00 2001 From: Emran Batmanghelich Date: Tue, 5 Dec 2023 18:35:44 +0330 Subject: [PATCH 44/45] feat: accept all / filters / keep_storage in prune_builds (#3192) Added in API v1.39. --------- Signed-off-by: Emran Batmanghelich --- docker/api/build.py | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/docker/api/build.py b/docker/api/build.py index 9c8b4e6ae9..abd5ab52a8 100644 --- a/docker/api/build.py +++ b/docker/api/build.py @@ -279,10 +279,24 @@ def build(self, path=None, tag=None, quiet=False, fileobj=None, return self._stream_helper(response, decode=decode) @utils.minimum_version('1.31') - def prune_builds(self): + def prune_builds(self, filters=None, keep_storage=None, all=None): """ Delete the builder cache + Args: + filters (dict): Filters to process on the prune list. + Needs Docker API v1.39+ + Available filters: + - dangling (bool): When set to true (or 1), prune only + unused and untagged images. + - until (str): Can be Unix timestamps, date formatted + timestamps, or Go duration strings (e.g. 10m, 1h30m) computed + relative to the daemon's local time. + keep_storage (int): Amount of disk space in bytes to keep for cache. + Needs Docker API v1.39+ + all (bool): Remove all types of build cache. + Needs Docker API v1.39+ + Returns: (dict): A dictionary containing information about the operation's result. The ``SpaceReclaimed`` key indicates the amount of @@ -293,7 +307,20 @@ def prune_builds(self): If the server returns an error. """ url = self._url("/build/prune") - return self._result(self._post(url), True) + if (filters, keep_storage, all) != (None, None, None) \ + and utils.version_lt(self._version, '1.39'): + raise errors.InvalidVersion( + '`filters`, `keep_storage`, and `all` args are only available ' + 'for API version > 1.38' + ) + params = {} + if filters is not None: + params['filters'] = utils.convert_filters(filters) + if keep_storage is not None: + params['keep-storage'] = keep_storage + if all is not None: + params['all'] = all + return self._result(self._post(url, params=params), True) def _set_auth_headers(self, headers): log.debug('Looking for auth config') From 5388413dde6894c41b90945507cd81c343b9aeee Mon Sep 17 00:00:00 2001 From: Milas Bowman Date: Thu, 7 Dec 2023 15:41:29 -0500 Subject: [PATCH 45/45] chore: update changelog and maintainer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Preparing for the 7.0.0 final release 🎉 Added a couple more changelog items that came in as part of `7.0.0b2` and updated the maintainer to be generically Docker, Inc. instead of an individual. Signed-off-by: Milas Bowman --- docs/change-log.md | 2 ++ setup.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/change-log.md b/docs/change-log.md index 7719bbfb64..faf868ff8c 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -20,9 +20,11 @@ Changelog - Add `health()` property to container that returns status (e.g. `unhealthy`) - Add `pause` option to `container.commit()` - Add support for bind mount propagation (e.g. `rshared`, `private`) +- Add `filters`, `keep_storage`, and `all` parameters to `prune_builds()` (requires API v1.39+) ### Bugfixes - Consistently return `docker.errors.NotFound` on 404 responses +- Validate tag format before image push ### Miscellaneous - Upgraded urllib3 version in `requirements.txt` (used for development/tests) diff --git a/setup.py b/setup.py index 98736247d6..b6a024f81a 100644 --- a/setup.py +++ b/setup.py @@ -78,6 +78,6 @@ 'Topic :: Utilities', 'License :: OSI Approved :: Apache Software License', ], - maintainer='Ulysses Souza', - maintainer_email='ulysses.souza@docker.com', + maintainer='Docker, Inc.', + maintainer_email='no-reply@docker.com', )