From 78439ebbe1aae77ff0fd9b666894d80807182e28 Mon Sep 17 00:00:00 2001 From: Mathieu Virbel Date: Thu, 1 Jun 2023 16:19:01 +0200 Subject: [PATCH 01/26] fix: eventlet compatibility (#3132) Check if poll attribute exists on select module instead of win32 platform check The implementation done in #2865 is breaking usage of docker-py library within eventlet. As per the Python `select.poll` documentation (https://docs.python.org/3/library/select.html#select.poll) and eventlet select removal advice (https://github.com/eventlet/eventlet/issues/608#issuecomment-612359458), it is preferable to use an implementation based on the availability of the `poll()` method that trying to check if the platform is `win32`. Fixes #3131 Signed-off-by: Mathieu Virbel --- docker/utils/socket.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docker/utils/socket.py b/docker/utils/socket.py index efb7458ee..cdc485ea3 100644 --- a/docker/utils/socket.py +++ b/docker/utils/socket.py @@ -3,7 +3,6 @@ import select import socket as pysocket import struct -import sys try: from ..transport import NpipeSocket @@ -32,7 +31,7 @@ def read(socket, n=4096): recoverable_errors = (errno.EINTR, errno.EDEADLK, errno.EWOULDBLOCK) if not isinstance(socket, NpipeSocket): - if sys.platform == 'win32': + if not hasattr(select, "poll"): # Limited to 1024 select.select([socket], [], []) else: 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 02/26] 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 78d7e3728..7add69b12 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 fb974de27a2ad4295807ab61e47a781c5518d384 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Mon, 14 Aug 2023 10:26:36 +0200 Subject: [PATCH 03/26] 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 0cb8fec68..0782b12cc 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 6a6686e37..cb3d66711 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 04/26] 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 bf95cd6a3..4705acca5 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 05/26] 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 0782b12cc..b510979de 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 7bf156afb..e2fce48b0 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 06/26] 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 2db678dcc..921ffbcb8 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 07/26] 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 8cf8670ba..75e30a8c9 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 08/26] 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 40607e79a..b8d5957b6 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 2eeefda1e..64838397a 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 e28593294..b3428aa16 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 09/26] 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 36660b660..897cdbd5e 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 10/26] 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 4705acca5..366abe23b 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 11/26] 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 3a1a3d964..439f4dc35 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 65b9d9d19..8633025f3 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 b8d5957b6..ec28fd581 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 652b7c245..3aed06517 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 cb3885548..4bce78870 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 380e8c4c4..e340fb6dd 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 dbaa01cb5..b607b7714 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 42a1bc1a5..d059fd9fb 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 b7ab53fba..37c703e78 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 75e30a8c9..d03e10f69 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 e3ec39d28..abb4b12b5 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 89030e592..d3a35e84b 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 fae10f266..09d373dd6 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 84df0f7e6..6d54aa65c 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 a3383ef75..0b07c350e 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 59564c4cd..6b38eacdb 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 cf1baf496..5aab98cd4 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 90e9f60f5..be745381e 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 e81393660..9fd6e8f6b 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 49e98ed91..e7164b6ce 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 7b2bbf4ba..15e386900 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 dc3b37cc8..e9971e0d2 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 bdb07f96b..e0785774b 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 d1622fa88..d7a22a04a 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 b510979de..590c4fa0c 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 031079c91..e4073757e 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 16f4d60ab..82ea84741 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 eac4c9790..5b0470b93 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 4b91add4b..d6ff130a1 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 7e07a2695..cbecd1e54 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 450425084..1760239fd 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 b3428aa16..aea3a0e13 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 8afab7379..d3daa44c4 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 4b6099c90..78c0bab12 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 a8d9193f7..0a97ca515 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 dd5b5f8b5..26254fadd 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 e7c7eec82..1148d7ac1 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 6acfb64b8..133a99f80 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 aee1b9e80..3fc7c68cd 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 12cb7bd65..9c8a55bd5 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 12/26] 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 f23873f0e..dfbcc701e 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 861731188..acf22ef7c 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 ae6ae34ef..79486e3ec 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 46beb532a..c1c518c56 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 ff5184414..7260e9537 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 0a6707f99..dbf172fda 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 31ad28e34..db3e1fbfd 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 e37fc3ba2..54492c11a 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 b425746e7..89f223893 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 81c8186c8..944c6e65e 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 9554358e5..eb87cefbb 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 b7457fa77..951b3be9f 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 9edc15c54..2028dd395 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 13/26] 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 db3e1fbfd..a1247700d 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 14/26] 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 6d54aa65c..a28061383 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 e9971e0d2..a529f8be8 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 ff6da7141..d0145d235 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 d7a22a04a..ae71a57bf 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 133a99f80..87d892757 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 15/26] 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 8633025f3..ce1b3a307 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 15e386900..234be3207 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 eb87cefbb..82d486900 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 d0145d235..866aa23c8 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 606c3b7e1..e5e7904d8 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 c54583b0b..9ecdcd86a 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 cb3d66711..7081b53b8 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 3ecb02834..a35c30d3e 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 a2a12a5cb..1a13f2817 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 5b0470b93..3cf74cbc5 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 94aa20100..d335da4a7 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 08d7ad295..f4052e4ba 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 ef48e12ed..160d53f1e 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 0592af5e0..2eabd2685 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 16/26] 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 16f5245e9..85d768c93 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 cdc485ea3..2306ed073 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 82d486900..0a6727966 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 e5e7904d8..2add2d87a 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 a35c30d3e..3f1633900 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 10313a637..7d2b228cc 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 160d53f1e..d060f465f 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 78c0bab12..99aa23bc2 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 17/26] 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 ce1b3a307..a2cb459de 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 4bce78870..7a301ba40 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 e340fb6dd..493f470e5 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 b607b7714..4faf8e701 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 37c703e78..5edeaa7f6 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 64838397a..44bb92a0f 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 f4dffb2e2..a4dd00209 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 45988b2df..d335d8718 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 a92beb621..6e1d0ee72 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 096b2cc16..04e6ccb2d 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 6b38eacdb..8d18c2be7 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 f384175f7..266193e56 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 234be3207..4affeb339 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 26254fadd..0ed890fdf 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 18/26] 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 d761aef13..8c3fe2500 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 6d6d6726b..25f0d8c6b 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 19/26] 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 5edeaa7f6..4e63a5ba6 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 abb4b12b5..b4777d8da 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 20/26] 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 cffe12fc2..d6aab9665 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 3cf74cbc5..4d33e622e 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 99aa23bc2..7bc2ea8cd 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 21/26] 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 acf018d2f..464403979 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 1148d7ac1..7012b2123 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 9c8a55bd5..b47cb0c62 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 22/26] 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 aea3a0e13..22b27fe0d 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 23/26] 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 87d892757..0524becdc 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 24/26] 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 ec28fd581..5a267d13f 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 4affeb339..0f28afb11 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 590c4fa0c..ecda1d65c 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 25/26] 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 78d54e282..74dad6005 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 26/26] 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 74dad6005..6689044b6 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):