diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 127d5b682..9b43a27bc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,13 +11,26 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: '3.x' - - run: pip install -U ruff==0.0.284 + - run: pip install -U ruff==0.1.8 - name: Run ruff run: ruff docker tests + build: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.x' + - run: pip3 install build && python -m build . + - uses: actions/upload-artifact@v4 + with: + name: dist + path: dist + unit-tests: runs-on: ubuntu-latest strategy: @@ -27,14 +40,14 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} allow-prereleases: true - name: Install dependencies run: | python3 -m pip install --upgrade pip - pip3 install -r test-requirements.txt -r requirements.txt + pip3 install '.[ssh,dev]' - name: Run unit tests run: | docker logout @@ -49,6 +62,9 @@ jobs: steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true - name: make ${{ matrix.variant }} run: | docker logout diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 721020ac3..17be00163 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,16 +22,18 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: '3.x' - - name: Generate Pacakge + - name: Generate Package run: | - pip3 install setuptools wheel - python setup.py sdist bdist_wheel + pip3 install build + python -m build . env: - SETUPTOOLS_SCM_PRETEND_VERSION_FOR_DOCKER: ${{ inputs.tag }} + # This is also supported by Hatch; see + # https://github.com/ofek/hatch-vcs#version-source-environment-variables + SETUPTOOLS_SCM_PRETEND_VERSION: ${{ inputs.tag }} - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.readthedocs.yml b/.readthedocs.yml index 80000ee7f..907454ea9 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -4,14 +4,14 @@ sphinx: configuration: docs/conf.py build: - os: ubuntu-20.04 + os: ubuntu-22.04 tools: - python: '3.10' + python: '3.12' python: install: - - requirements: docs-requirements.txt - method: pip path: . extra_requirements: - ssh + - docs diff --git a/Dockerfile b/Dockerfile index 293888d72..0189f46c8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,17 +1,13 @@ # syntax=docker/dockerfile:1 ARG PYTHON_VERSION=3.12 - FROM python:${PYTHON_VERSION} WORKDIR /src - -COPY requirements.txt /src/requirements.txt -RUN pip install --no-cache-dir -r requirements.txt - -COPY test-requirements.txt /src/test-requirements.txt -RUN pip install --no-cache-dir -r test-requirements.txt - COPY . . -ARG SETUPTOOLS_SCM_PRETEND_VERSION_DOCKER -RUN pip install --no-cache-dir . + +ARG VERSION +RUN --mount=type=cache,target=/cache/pip \ + PIP_CACHE_DIR=/cache/pip \ + SETUPTOOLS_SCM_PRETEND_VERSION=${VERSION} \ + pip install .[ssh] diff --git a/Dockerfile-docs b/Dockerfile-docs index 266b2099e..14d615c43 100644 --- a/Dockerfile-docs +++ b/Dockerfile-docs @@ -11,7 +11,12 @@ RUN addgroup --gid $gid sphinx \ && useradd --uid $uid --gid $gid -M sphinx WORKDIR /src -COPY requirements.txt docs-requirements.txt ./ -RUN pip install --no-cache-dir -r requirements.txt -r docs-requirements.txt +COPY . . + +ARG VERSION +RUN --mount=type=cache,target=/cache/pip \ + PIP_CACHE_DIR=/cache/pip \ + SETUPTOOLS_SCM_PRETEND_VERSION=${VERSION} \ + pip install .[ssh,docs] USER sphinx diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 2ba6e0274..000000000 --- a/MANIFEST.in +++ /dev/null @@ -1,9 +0,0 @@ -include test-requirements.txt -include requirements.txt -include README.md -include README.rst -include LICENSE -recursive-include tests *.py -recursive-include tests/unit/testdata * -recursive-include tests/integration/testdata * -recursive-include tests/gpg-keys * diff --git a/Makefile b/Makefile index 79486e3ec..13a00f5e2 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ -TEST_API_VERSION ?= 1.41 -TEST_ENGINE_VERSION ?= 20.10 +TEST_API_VERSION ?= 1.44 +TEST_ENGINE_VERSION ?= 25.0 ifeq ($(OS),Windows_NT) PLATFORM := Windows @@ -11,12 +11,17 @@ ifeq ($(PLATFORM),Linux) uid_args := "--build-arg uid=$(shell id -u) --build-arg gid=$(shell id -g)" endif +SETUPTOOLS_SCM_PRETEND_VERSION_DOCKER ?= $(shell git describe --match '[0-9]*' --dirty='.m' --always --tags 2>/dev/null | sed -r 's/-([0-9]+)/.dev\1/' | sed 's/-/+/') +ifeq ($(SETUPTOOLS_SCM_PRETEND_VERSION_DOCKER),) + SETUPTOOLS_SCM_PRETEND_VERSION_DOCKER = "dev" +endif + .PHONY: all all: test .PHONY: clean clean: - -docker rm -f dpy-dind-py3 dpy-dind-certs dpy-dind-ssl + -docker rm -f dpy-dind dpy-dind-certs dpy-dind-ssl find -name "__pycache__" | xargs rm -rf .PHONY: build-dind-ssh @@ -25,35 +30,46 @@ build-dind-ssh: --pull \ -t docker-dind-ssh \ -f tests/Dockerfile-ssh-dind \ + --build-arg VERSION=${SETUPTOOLS_SCM_PRETEND_VERSION_DOCKER} \ --build-arg ENGINE_VERSION=${TEST_ENGINE_VERSION} \ --build-arg API_VERSION=${TEST_API_VERSION} \ --build-arg APT_MIRROR . -.PHONY: build-py3 -build-py3: +.PHONY: build +build: docker build \ --pull \ -t docker-sdk-python3 \ -f tests/Dockerfile \ + --build-arg VERSION=${SETUPTOOLS_SCM_PRETEND_VERSION_DOCKER} \ --build-arg APT_MIRROR . .PHONY: build-docs build-docs: - docker build -t docker-sdk-python-docs -f Dockerfile-docs $(uid_args) . + docker build \ + -t docker-sdk-python-docs \ + -f Dockerfile-docs \ + --build-arg VERSION=${SETUPTOOLS_SCM_PRETEND_VERSION_DOCKER} \ + $(uid_args) \ + . .PHONY: build-dind-certs build-dind-certs: - docker build -t dpy-dind-certs -f tests/Dockerfile-dind-certs . + docker build \ + -t dpy-dind-certs \ + -f tests/Dockerfile-dind-certs \ + --build-arg VERSION=${SETUPTOOLS_SCM_PRETEND_VERSION_DOCKER} \ + . .PHONY: test -test: ruff unit-test-py3 integration-dind integration-dind-ssl +test: ruff unit-test integration-dind integration-dind-ssl -.PHONY: unit-test-py3 -unit-test-py3: build-py3 +.PHONY: unit-test +unit-test: build docker run -t --rm docker-sdk-python3 py.test tests/unit -.PHONY: integration-test-py3 -integration-test-py3: build-py3 +.PHONY: integration-test +integration-test: build docker run -t --rm -v /var/run/docker.sock:/var/run/docker.sock docker-sdk-python3 py.test -v tests/integration/${file} .PHONY: setup-network @@ -61,15 +77,15 @@ setup-network: docker network inspect dpy-tests || docker network create dpy-tests .PHONY: integration-dind -integration-dind: integration-dind-py3 +integration-dind: integration-dind -.PHONY: integration-dind-py3 -integration-dind-py3: build-py3 setup-network - docker rm -vf dpy-dind-py3 || : +.PHONY: integration-dind +integration-dind: build setup-network + docker rm -vf dpy-dind || : docker run \ --detach \ - --name dpy-dind-py3 \ + --name dpy-dind \ --network dpy-tests \ --pull=always \ --privileged \ @@ -82,10 +98,10 @@ integration-dind-py3: build-py3 setup-network --rm \ --tty \ busybox \ - sh -c 'while ! nc -z dpy-dind-py3 2375; do sleep 1; done' + sh -c 'while ! nc -z dpy-dind 2375; do sleep 1; done' docker run \ - --env="DOCKER_HOST=tcp://dpy-dind-py3:2375" \ + --env="DOCKER_HOST=tcp://dpy-dind:2375" \ --env="DOCKER_TEST_API_VERSION=${TEST_API_VERSION}" \ --network dpy-tests \ --rm \ @@ -93,11 +109,11 @@ integration-dind-py3: build-py3 setup-network docker-sdk-python3 \ py.test tests/integration/${file} - docker rm -vf dpy-dind-py3 + docker rm -vf dpy-dind .PHONY: integration-dind-ssh -integration-dind-ssh: build-dind-ssh build-py3 setup-network +integration-dind-ssh: build-dind-ssh build setup-network docker rm -vf dpy-dind-ssh || : docker run -d --network dpy-tests --name dpy-dind-ssh --privileged \ docker-dind-ssh dockerd --experimental @@ -116,7 +132,7 @@ integration-dind-ssh: build-dind-ssh build-py3 setup-network .PHONY: integration-dind-ssl -integration-dind-ssl: build-dind-certs build-py3 setup-network +integration-dind-ssl: build-dind-certs build setup-network docker rm -vf dpy-dind-certs dpy-dind-ssl || : docker run -d --name dpy-dind-certs dpy-dind-certs @@ -164,7 +180,7 @@ integration-dind-ssl: build-dind-certs build-py3 setup-network docker rm -vf dpy-dind-ssl dpy-dind-certs .PHONY: ruff -ruff: build-py3 +ruff: build docker run -t --rm docker-sdk-python3 ruff docker tests .PHONY: docs @@ -172,5 +188,5 @@ docs: build-docs docker run --rm -t -v `pwd`:/src docker-sdk-python-docs sphinx-build docs docs/_build .PHONY: shell -shell: build-py3 +shell: build docker run -it -v /var/run/docker.sock:/var/run/docker.sock docker-sdk-python3 python diff --git a/README.md b/README.md index 921ffbcb8..a6e06a229 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ A Python library for the Docker Engine API. It lets you do anything the `docker` ## Installation -The latest stable version [is available on PyPI](https://pypi.python.org/pypi/docker/). Either add `docker` to your `requirements.txt` file or install with pip: +The latest stable version [is available on PyPI](https://pypi.python.org/pypi/docker/). Install with pip: pip install docker diff --git a/docker/__init__.py b/docker/__init__.py index c1c518c56..fb7a5e921 100644 --- a/docker/__init__.py +++ b/docker/__init__.py @@ -1,7 +1,6 @@ from .api import APIClient from .client import DockerClient, from_env -from .context import Context -from .context import ContextAPI +from .context import Context, ContextAPI from .tls import TLSConfig from .version import __version__ diff --git a/docker/api/build.py b/docker/api/build.py index abd5ab52a..47216a58f 100644 --- a/docker/api/build.py +++ b/docker/api/build.py @@ -3,11 +3,7 @@ import os import random -from .. import auth -from .. import constants -from .. import errors -from .. import utils - +from .. import auth, constants, errors, utils log = logging.getLogger(__name__) diff --git a/docker/api/client.py b/docker/api/client.py index 499a7c785..394ceb1f5 100644 --- a/docker/api/client.py +++ b/docker/api/client.py @@ -8,12 +8,22 @@ import requests.exceptions from .. import auth -from ..constants import (DEFAULT_NUM_POOLS, DEFAULT_NUM_POOLS_SSH, - DEFAULT_MAX_POOL_SIZE, DEFAULT_TIMEOUT_SECONDS, - DEFAULT_USER_AGENT, IS_WINDOWS_PLATFORM, - MINIMUM_DOCKER_API_VERSION, STREAM_HEADER_SIZE_BYTES) -from ..errors import (DockerException, InvalidVersion, TLSParameterError, - create_api_error_from_http_exception) +from ..constants import ( + DEFAULT_MAX_POOL_SIZE, + DEFAULT_NUM_POOLS, + DEFAULT_NUM_POOLS_SSH, + DEFAULT_TIMEOUT_SECONDS, + DEFAULT_USER_AGENT, + IS_WINDOWS_PLATFORM, + MINIMUM_DOCKER_API_VERSION, + STREAM_HEADER_SIZE_BYTES, +) +from ..errors import ( + DockerException, + InvalidVersion, + TLSParameterError, + create_api_error_from_http_exception, +) from ..tls import TLSConfig from ..transport import UnixHTTPAdapter from ..utils import check_resource, config, update_headers, utils diff --git a/docker/api/container.py b/docker/api/container.py index 5a267d13f..d1b870f9c 100644 --- a/docker/api/container.py +++ b/docker/api/container.py @@ -1,13 +1,14 @@ from datetime import datetime -from .. import errors -from .. import utils +from .. import errors, utils from ..constants import DEFAULT_DATA_CHUNK_SIZE -from ..types import CancellableStream -from ..types import ContainerConfig -from ..types import EndpointConfig -from ..types import HostConfig -from ..types import NetworkingConfig +from ..types import ( + CancellableStream, + ContainerConfig, + EndpointConfig, + HostConfig, + NetworkingConfig, +) class ContainerApiMixin: @@ -843,7 +844,7 @@ def logs(self, container, stdout=True, stderr=True, stream=False, float (in fractional seconds) Returns: - (generator or str) + (generator of bytes or bytes) Raises: :py:class:`docker.errors.APIError` diff --git a/docker/api/exec_api.py b/docker/api/exec_api.py index 63df9e6c6..d8fc50dd3 100644 --- a/docker/api/exec_api.py +++ b/docker/api/exec_api.py @@ -1,5 +1,4 @@ -from .. import errors -from .. import utils +from .. import errors, utils from ..types import CancellableStream diff --git a/docker/api/image.py b/docker/api/image.py index 5e1466ec3..85109473b 100644 --- a/docker/api/image.py +++ b/docker/api/image.py @@ -47,7 +47,7 @@ def history(self, image): image (str): The image to show history for Returns: - (str): The history of the image + (list): The history of the image Raises: :py:class:`docker.errors.APIError` diff --git a/docker/api/network.py b/docker/api/network.py index dd4e3761a..2b1925710 100644 --- a/docker/api/network.py +++ b/docker/api/network.py @@ -1,7 +1,6 @@ -from ..errors import InvalidVersion -from ..utils import check_resource, minimum_version -from ..utils import version_lt from .. import utils +from ..errors import InvalidVersion +from ..utils import check_resource, minimum_version, version_lt class NetworkApiMixin: diff --git a/docker/api/secret.py b/docker/api/secret.py index cd440b95f..db1701bdc 100644 --- a/docker/api/secret.py +++ b/docker/api/secret.py @@ -1,7 +1,6 @@ import base64 -from .. import errors -from .. import utils +from .. import errors, utils class SecretApiMixin: diff --git a/docker/api/swarm.py b/docker/api/swarm.py index d09dd087b..d60d18b61 100644 --- a/docker/api/swarm.py +++ b/docker/api/swarm.py @@ -1,9 +1,8 @@ -import logging import http.client as http_client +import logging + +from .. import errors, types, utils from ..constants import DEFAULT_SWARM_ADDR_POOL, DEFAULT_SWARM_SUBNET_SIZE -from .. import errors -from .. import types -from .. import utils log = logging.getLogger(__name__) diff --git a/docker/api/volume.py b/docker/api/volume.py index 98b42a124..c6c036fad 100644 --- a/docker/api/volume.py +++ b/docker/api/volume.py @@ -1,5 +1,4 @@ -from .. import errors -from .. import utils +from .. import errors, utils class VolumeApiMixin: diff --git a/docker/auth.py b/docker/auth.py index 7a301ba40..96a6e3a65 100644 --- a/docker/auth.py +++ b/docker/auth.py @@ -2,8 +2,7 @@ import json import logging -from . import credentials -from . import errors +from . import credentials, errors from .utils import config INDEX_NAME = 'docker.io' diff --git a/docker/client.py b/docker/client.py index 2910c1259..9012d24c9 100644 --- a/docker/client.py +++ b/docker/client.py @@ -1,5 +1,5 @@ from .api.client import APIClient -from .constants import (DEFAULT_TIMEOUT_SECONDS, DEFAULT_MAX_POOL_SIZE) +from .constants import DEFAULT_MAX_POOL_SIZE, DEFAULT_TIMEOUT_SECONDS from .models.configs import ConfigCollection from .models.containers import ContainerCollection from .models.images import ImageCollection diff --git a/docker/constants.py b/docker/constants.py index ed341a902..3c527b47e 100644 --- a/docker/constants.py +++ b/docker/constants.py @@ -1,8 +1,9 @@ import sys + from .version import __version__ -DEFAULT_DOCKER_API_VERSION = '1.41' -MINIMUM_DOCKER_API_VERSION = '1.21' +DEFAULT_DOCKER_API_VERSION = '1.44' +MINIMUM_DOCKER_API_VERSION = '1.24' DEFAULT_TIMEOUT_SECONDS = 60 STREAM_HEADER_SIZE_BYTES = 8 CONTAINER_LIMITS_KEYS = [ diff --git a/docker/context/__init__.py b/docker/context/__init__.py index dbf172fda..46d462b0c 100644 --- a/docker/context/__init__.py +++ b/docker/context/__init__.py @@ -1,2 +1,2 @@ -from .context import Context from .api import ContextAPI +from .context import Context diff --git a/docker/context/api.py b/docker/context/api.py index 493f470e5..9ac4ff470 100644 --- a/docker/context/api.py +++ b/docker/context/api.py @@ -2,11 +2,14 @@ import os from docker import errors -from docker.context.config import get_meta_dir -from docker.context.config import METAFILE -from docker.context.config import get_current_context_name -from docker.context.config import write_context_name_to_docker_config -from docker.context import Context + +from .config import ( + METAFILE, + get_current_context_name, + get_meta_dir, + write_context_name_to_docker_config, +) +from .context import Context class ContextAPI: diff --git a/docker/context/config.py b/docker/context/config.py index 8c3fe2500..5a6373aa4 100644 --- a/docker/context/config.py +++ b/docker/context/config.py @@ -1,10 +1,9 @@ -import os -import json import hashlib +import json +import os from docker import utils -from docker.constants import IS_WINDOWS_PLATFORM -from docker.constants import DEFAULT_UNIX_SOCKET +from docker.constants import DEFAULT_UNIX_SOCKET, IS_WINDOWS_PLATFORM from docker.utils.config import find_config_file METAFILE = "meta.json" diff --git a/docker/context/context.py b/docker/context/context.py index 4faf8e701..da17d9478 100644 --- a/docker/context/context.py +++ b/docker/context/context.py @@ -1,12 +1,16 @@ -import os import json +import os from shutil import copyfile, rmtree -from docker.tls import TLSConfig + from docker.errors import ContextException -from docker.context.config import get_meta_dir -from docker.context.config import get_meta_file -from docker.context.config import get_tls_dir -from docker.context.config import get_context_host +from docker.tls import TLSConfig + +from .config import ( + get_context_host, + get_meta_dir, + get_meta_file, + get_tls_dir, +) class Context: diff --git a/docker/credentials/__init__.py b/docker/credentials/__init__.py index a1247700d..80d19e798 100644 --- a/docker/credentials/__init__.py +++ b/docker/credentials/__init__.py @@ -1,8 +1,8 @@ -from .store import Store -from .errors import StoreError, CredentialsNotFound from .constants import ( DEFAULT_LINUX_STORE, DEFAULT_OSX_STORE, DEFAULT_WIN32_STORE, PROGRAM_PREFIX, ) +from .errors import CredentialsNotFound, StoreError +from .store import Store diff --git a/docker/credentials/store.py b/docker/credentials/store.py index 4e63a5ba6..00d693a4b 100644 --- a/docker/credentials/store.py +++ b/docker/credentials/store.py @@ -4,8 +4,7 @@ import subprocess import warnings -from . import constants -from . import errors +from . import constants, errors from .utils import create_environment_dict diff --git a/docker/models/configs.py b/docker/models/configs.py index 3588c8b5d..4eba87f4e 100644 --- a/docker/models/configs.py +++ b/docker/models/configs.py @@ -1,5 +1,5 @@ from ..api import APIClient -from .resource import Model, Collection +from .resource import Collection, Model class Config(Model): @@ -30,6 +30,7 @@ class ConfigCollection(Collection): def create(self, **kwargs): obj = self.client.api.create_config(**kwargs) + obj.setdefault("Spec", {})["Name"] = kwargs.get("name") return self.prepare_model(obj) create.__doc__ = APIClient.create_config.__doc__ diff --git a/docker/models/containers.py b/docker/models/containers.py index 4725d6f6f..4795523a1 100644 --- a/docker/models/containers.py +++ b/docker/models/containers.py @@ -2,16 +2,19 @@ import ntpath from collections import namedtuple -from .images import Image -from .resource import Collection, Model from ..api import APIClient from ..constants import DEFAULT_DATA_CHUNK_SIZE from ..errors import ( - ContainerError, DockerException, ImageNotFound, - NotFound, create_unexpected_kwargs_error + ContainerError, + DockerException, + ImageNotFound, + NotFound, + create_unexpected_kwargs_error, ) from ..types import HostConfig, NetworkingConfig from ..utils import version_gte +from .images import Image +from .resource import Collection, Model class Container(Model): @@ -310,7 +313,7 @@ def logs(self, **kwargs): float (in nanoseconds) Returns: - (generator or str): Logs from the container. + (generator of bytes or bytes): Logs from the container. Raises: :py:class:`docker.errors.APIError` @@ -903,9 +906,9 @@ def run(self, image, command=None, stdout=True, stderr=False, container, exit_status, command, image, out ) - return out if stream or out is None else b''.join( - [line for line in out] - ) + if stream or out is None: + return out + return b''.join(out) def create(self, image, command=None, **kwargs): """ diff --git a/docker/models/images.py b/docker/models/images.py index b4777d8da..4f058d24d 100644 --- a/docker/models/images.py +++ b/docker/models/images.py @@ -51,7 +51,7 @@ def history(self): Show the history of an image. Returns: - (str): The history of the image. + (list): The history of the image. Raises: :py:class:`docker.errors.APIError` diff --git a/docker/models/networks.py b/docker/models/networks.py index f50287907..9b3ed7829 100644 --- a/docker/models/networks.py +++ b/docker/models/networks.py @@ -1,7 +1,7 @@ from ..api import APIClient from ..utils import version_gte from .containers import Container -from .resource import Model, Collection +from .resource import Collection, Model class Network(Model): diff --git a/docker/models/nodes.py b/docker/models/nodes.py index 8dd9350c0..2fa480c54 100644 --- a/docker/models/nodes.py +++ b/docker/models/nodes.py @@ -1,4 +1,4 @@ -from .resource import Model, Collection +from .resource import Collection, Model class Node(Model): diff --git a/docker/models/secrets.py b/docker/models/secrets.py index da01d44c8..38c48dc7e 100644 --- a/docker/models/secrets.py +++ b/docker/models/secrets.py @@ -1,5 +1,5 @@ from ..api import APIClient -from .resource import Model, Collection +from .resource import Collection, Model class Secret(Model): diff --git a/docker/models/services.py b/docker/models/services.py index 70037041a..09502633e 100644 --- a/docker/models/services.py +++ b/docker/models/services.py @@ -1,7 +1,9 @@ import copy -from docker.errors import create_unexpected_kwargs_error, InvalidArgument -from docker.types import TaskTemplate, ContainerSpec, Placement, ServiceMode -from .resource import Model, Collection + +from docker.errors import InvalidArgument, create_unexpected_kwargs_error +from docker.types import ContainerSpec, Placement, ServiceMode, TaskTemplate + +from .resource import Collection, Model class Service(Model): diff --git a/docker/models/swarm.py b/docker/models/swarm.py index 1e39f3fd2..271cc5dcb 100644 --- a/docker/models/swarm.py +++ b/docker/models/swarm.py @@ -1,5 +1,6 @@ from docker.api import APIClient from docker.errors import APIError + from .resource import Model diff --git a/docker/models/volumes.py b/docker/models/volumes.py index 3c2e83780..12c9f14b2 100644 --- a/docker/models/volumes.py +++ b/docker/models/volumes.py @@ -1,5 +1,5 @@ from ..api import APIClient -from .resource import Model, Collection +from .resource import Collection, Model class Volume(Model): diff --git a/docker/transport/__init__.py b/docker/transport/__init__.py index 07bc7fd58..8c68b1f6e 100644 --- a/docker/transport/__init__.py +++ b/docker/transport/__init__.py @@ -1,4 +1,5 @@ from .unixconn import UnixHTTPAdapter + try: from .npipeconn import NpipeHTTPAdapter from .npipesocket import NpipeSocket diff --git a/docker/transport/basehttpadapter.py b/docker/transport/basehttpadapter.py index dfbb193b9..2301b6b07 100644 --- a/docker/transport/basehttpadapter.py +++ b/docker/transport/basehttpadapter.py @@ -6,3 +6,8 @@ def close(self): super().close() if hasattr(self, 'pools'): self.pools.clear() + + # Fix for requests 2.32.2+: + # https://github.com/psf/requests/commit/c98e4d133ef29c46a9b68cd783087218a8075e05 + def get_connection_with_tls_context(self, request, verify, proxies=None, cert=None): + return self.get_connection(request.url, proxies) diff --git a/docker/transport/npipeconn.py b/docker/transport/npipeconn.py index d335d8718..44d6921c2 100644 --- a/docker/transport/npipeconn.py +++ b/docker/transport/npipeconn.py @@ -1,13 +1,13 @@ import queue + import requests.adapters +import urllib3 +import urllib3.connection -from docker.transport.basehttpadapter import BaseHTTPAdapter from .. import constants +from .basehttpadapter import BaseHTTPAdapter from .npipesocket import NpipeSocket -import urllib3 -import urllib3.connection - RecentlyUsedContainer = urllib3._collections.RecentlyUsedContainer diff --git a/docker/transport/npipesocket.py b/docker/transport/npipesocket.py index 9cbe40cc7..d91938e76 100644 --- a/docker/transport/npipesocket.py +++ b/docker/transport/npipesocket.py @@ -1,12 +1,12 @@ import functools -import time import io +import time -import win32file -import win32pipe import pywintypes -import win32event import win32api +import win32event +import win32file +import win32pipe cERROR_PIPE_BUSY = 0xe7 cSECURITY_SQOS_PRESENT = 0x100000 diff --git a/docker/transport/sshconn.py b/docker/transport/sshconn.py index 6e1d0ee72..187066801 100644 --- a/docker/transport/sshconn.py +++ b/docker/transport/sshconn.py @@ -1,19 +1,19 @@ -import paramiko -import queue -import urllib.parse -import requests.adapters import logging import os +import queue import signal import socket import subprocess +import urllib.parse -from docker.transport.basehttpadapter import BaseHTTPAdapter -from .. import constants - +import paramiko +import requests.adapters import urllib3 import urllib3.connection +from .. import constants +from .basehttpadapter import BaseHTTPAdapter + RecentlyUsedContainer = urllib3._collections.RecentlyUsedContainer diff --git a/docker/transport/unixconn.py b/docker/transport/unixconn.py index 09d373dd6..d571833f0 100644 --- a/docker/transport/unixconn.py +++ b/docker/transport/unixconn.py @@ -1,12 +1,11 @@ -import requests.adapters import socket -from docker.transport.basehttpadapter import BaseHTTPAdapter -from .. import constants - +import requests.adapters import urllib3 import urllib3.connection +from .. import constants +from .basehttpadapter import BaseHTTPAdapter RecentlyUsedContainer = urllib3._collections.RecentlyUsedContainer diff --git a/docker/types/__init__.py b/docker/types/__init__.py index 89f223893..fbe247210 100644 --- a/docker/types/__init__.py +++ b/docker/types/__init__.py @@ -1,13 +1,24 @@ -from .containers import ( - ContainerConfig, HostConfig, LogConfig, Ulimit, DeviceRequest -) +from .containers import ContainerConfig, DeviceRequest, HostConfig, LogConfig, Ulimit from .daemon import CancellableStream from .healthcheck import Healthcheck from .networks import EndpointConfig, IPAMConfig, IPAMPool, NetworkingConfig from .services import ( - ConfigReference, ContainerSpec, DNSConfig, DriverConfig, EndpointSpec, - Mount, Placement, PlacementPreference, Privileges, Resources, - RestartPolicy, RollbackConfig, SecretReference, ServiceMode, TaskTemplate, - UpdateConfig, NetworkAttachmentConfig + ConfigReference, + ContainerSpec, + DNSConfig, + DriverConfig, + EndpointSpec, + Mount, + NetworkAttachmentConfig, + Placement, + PlacementPreference, + Privileges, + Resources, + RestartPolicy, + RollbackConfig, + SecretReference, + ServiceMode, + TaskTemplate, + UpdateConfig, ) -from .swarm import SwarmSpec, SwarmExternalCA +from .swarm import SwarmExternalCA, SwarmSpec diff --git a/docker/types/containers.py b/docker/types/containers.py index a28061383..598188a25 100644 --- a/docker/types/containers.py +++ b/docker/types/containers.py @@ -1,8 +1,16 @@ from .. import errors from ..utils.utils import ( - convert_port_bindings, convert_tmpfs_mounts, convert_volume_binds, - format_environment, format_extra_hosts, normalize_links, parse_bytes, - parse_devices, split_command, version_gte, version_lt, + convert_port_bindings, + convert_tmpfs_mounts, + convert_volume_binds, + format_environment, + format_extra_hosts, + normalize_links, + parse_bytes, + parse_devices, + split_command, + version_gte, + version_lt, ) from .base import DictType from .healthcheck import Healthcheck diff --git a/docker/types/services.py b/docker/types/services.py index 0b07c350e..821115411 100644 --- a/docker/types/services.py +++ b/docker/types/services.py @@ -1,8 +1,12 @@ from .. import errors from ..constants import IS_WINDOWS_PLATFORM from ..utils import ( - check_resource, format_environment, format_extra_hosts, parse_bytes, - split_command, convert_service_networks, + check_resource, + convert_service_networks, + format_environment, + format_extra_hosts, + parse_bytes, + split_command, ) diff --git a/docker/utils/__init__.py b/docker/utils/__init__.py index b4bef7d47..c086a9f07 100644 --- a/docker/utils/__init__.py +++ b/docker/utils/__init__.py @@ -1,13 +1,28 @@ -from .build import match_tag, create_archive, exclude_paths, mkbuildcontext, tar +from .build import create_archive, exclude_paths, match_tag, mkbuildcontext, tar from .decorators import check_resource, minimum_version, update_headers from .utils import ( - compare_version, convert_port_bindings, convert_volume_binds, - parse_repository_tag, parse_host, - kwargs_from_env, convert_filters, datetime_to_timestamp, - create_host_config, parse_bytes, parse_env_file, version_lt, - version_gte, decode_json_header, split_command, create_ipam_config, - create_ipam_pool, parse_devices, normalize_links, convert_service_networks, - format_environment, format_extra_hosts + compare_version, + convert_filters, + convert_port_bindings, + convert_service_networks, + convert_volume_binds, + create_host_config, + create_ipam_config, + create_ipam_pool, + datetime_to_timestamp, + decode_json_header, + format_environment, + format_extra_hosts, + kwargs_from_env, + normalize_links, + parse_bytes, + parse_devices, + parse_env_file, + parse_host, + parse_repository_tag, + split_command, + version_gte, + version_lt, ) diff --git a/docker/utils/build.py b/docker/utils/build.py index a5c4b0c2d..b84139104 100644 --- a/docker/utils/build.py +++ b/docker/utils/build.py @@ -4,14 +4,14 @@ import tarfile import tempfile -from .fnmatch import fnmatch from ..constants import IS_WINDOWS_PLATFORM - +from .fnmatch import fnmatch _SEP = re.compile('/|\\\\') if IS_WINDOWS_PLATFORM else re.compile('/') _TAG = re.compile( - r"^[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*(\/[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*)*" \ - + "(:[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127})?$" + r"^[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*" + r"(?::[0-9]+)?(/[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*)*" + r"(:[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127})?$" ) diff --git a/docker/utils/json_stream.py b/docker/utils/json_stream.py index 266193e56..41d25920c 100644 --- a/docker/utils/json_stream.py +++ b/docker/utils/json_stream.py @@ -3,7 +3,6 @@ from ..errors import StreamParseError - json_decoder = json.JSONDecoder() diff --git a/docker/utils/socket.py b/docker/utils/socket.py index 2306ed073..c7cb584d4 100644 --- a/docker/utils/socket.py +++ b/docker/utils/socket.py @@ -64,7 +64,7 @@ def read_exactly(socket, n): Reads exactly n bytes from socket Raises SocketError if there isn't enough data """ - data = bytes() + data = b"" while len(data) < n: next_data = read(socket, n - len(data)) if not next_data: @@ -152,7 +152,7 @@ def consume_socket_output(frames, demux=False): if demux is False: # If the streams are multiplexed, the generator returns strings, that # we just need to concatenate. - return bytes().join(frames) + return b"".join(frames) # If the streams are demultiplexed, the generator yields tuples # (stdout, stderr) diff --git a/docker/utils/utils.py b/docker/utils/utils.py index 759ddd2f1..f36a3afb8 100644 --- a/docker/utils/utils.py +++ b/docker/utils/utils.py @@ -5,18 +5,20 @@ import os.path import shlex import string -from datetime import datetime -from packaging.version import Version +from datetime import datetime, timezone +from functools import lru_cache +from itertools import zip_longest +from urllib.parse import urlparse, urlunparse from .. import errors -from ..constants import DEFAULT_HTTP_HOST -from ..constants import DEFAULT_UNIX_SOCKET -from ..constants import DEFAULT_NPIPE -from ..constants import BYTE_UNITS +from ..constants import ( + BYTE_UNITS, + DEFAULT_HTTP_HOST, + DEFAULT_NPIPE, + DEFAULT_UNIX_SOCKET, +) from ..tls import TLSConfig -from urllib.parse import urlparse, urlunparse - URLComponents = collections.namedtuple( 'URLComponents', 'scheme netloc url params query fragment', @@ -43,6 +45,7 @@ def decode_json_header(header): return json.loads(data) +@lru_cache(maxsize=None) def compare_version(v1, v2): """Compare docker versions @@ -55,14 +58,20 @@ def compare_version(v1, v2): >>> compare_version(v2, v2) 0 """ - s1 = Version(v1) - s2 = Version(v2) - if s1 == s2: + if v1 == v2: return 0 - elif s1 > s2: - return -1 - else: - return 1 + # Split into `sys.version_info` like tuples. + s1 = tuple(int(p) for p in v1.split('.')) + s2 = tuple(int(p) for p in v2.split('.')) + # Compare each component, padding with 0 if necessary. + for c1, c2 in zip_longest(s1, s2, fillvalue=0): + if c1 == c2: + continue + elif c1 > c2: + return -1 + else: + return 1 + return 0 def version_lt(v1, v2): @@ -152,7 +161,7 @@ def convert_volume_binds(binds): ] if 'propagation' in v and v['propagation'] in propagation_modes: if mode: - mode = ','.join([mode, v['propagation']]) + mode = f"{mode},{v['propagation']}" else: mode = v['propagation'] @@ -394,8 +403,8 @@ def convert_filters(filters): def datetime_to_timestamp(dt): - """Convert a UTC datetime to a Unix timestamp""" - delta = dt - datetime.utcfromtimestamp(0) + """Convert a datetime to a Unix timestamp""" + delta = dt.astimezone(timezone.utc) - datetime(1970, 1, 1, tzinfo=timezone.utc) return delta.seconds + delta.days * 24 * 3600 diff --git a/docker/version.py b/docker/version.py index dca45bf04..72b12b84d 100644 --- a/docker/version.py +++ b/docker/version.py @@ -1,7 +1,7 @@ try: from ._version import __version__ except ImportError: - from importlib.metadata import version, PackageNotFoundError + from importlib.metadata import PackageNotFoundError, version try: __version__ = version('docker') except PackageNotFoundError: diff --git a/docs-requirements.txt b/docs-requirements.txt deleted file mode 100644 index 04d1aff26..000000000 --- a/docs-requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -myst-parser==0.18.0 -Sphinx==5.1.1 diff --git a/docs/change-log.md b/docs/change-log.md index faf868ff8..ebbdb7130 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -1,6 +1,27 @@ Changelog ========== +7.1.0 +----- +### Upgrade Notes +- Bumped minimum engine API version to 1.24 +- Bumped default engine API version to 1.44 (Moby 25.0) + +### Bugfixes +- Fixed issue with tag parsing when the registry address includes ports that resulted in `invalid tag format` errors +- Fixed issue preventing creating new configs (`ConfigCollection`), which failed with a `KeyError` due to the `name` field +- Fixed an issue due to an update in the [requests](https://github.com/psf/requests) package breaking `docker-py` by applying the [suggested fix](https://github.com/psf/requests/pull/6710) + +### Miscellaneous +- Documentation improvements +- Updated Ruff (linter) and fixed minor linting issues +- Packaging/CI updates + - Started using hatch for packaging (https://github.com/pypa/hatch) + - Updated `setup-python` github action +- Updated tests + - Stopped checking for deprecated container and image related fields (`Container` and `ContainerConfig`) + - Updated tests that check `NetworkSettings.Networks..Aliases` due to engine changes + 7.0.0 ----- ### Upgrade Notes diff --git a/docs/conf.py b/docs/conf.py index a529f8be8..02694d3cd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -19,6 +19,7 @@ import os import sys from importlib.metadata import version + sys.path.insert(0, os.path.abspath('..')) diff --git a/pyproject.toml b/pyproject.toml index 0a6727966..525a9b81a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,18 +1,100 @@ [build-system] -requires = ["setuptools>=45", "setuptools_scm[toml]>=6.2"] +requires = ["hatchling", "hatch-vcs"] +build-backend = "hatchling.build" -[tool.setuptools_scm] -write_to = 'docker/_version.py' +[project] +name = "docker" +dynamic = ["version"] +description = "A Python library for the Docker Engine API." +readme = "README.md" +license = "Apache-2.0" +requires-python = ">=3.8" +maintainers = [ + { name = "Docker Inc.", email = "no-reply@docker.com" }, +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Other Environment", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Software Development", + "Topic :: Utilities", +] + +dependencies = [ + "requests >= 2.26.0", + "urllib3 >= 1.26.0", + "pywin32>=304; sys_platform == \"win32\"", +] + +[project.optional-dependencies] +# ssh feature allows DOCKER_HOST=ssh://... style connections +ssh = [ + "paramiko>=2.4.3", +] +# tls is always supported, the feature is a no-op for backwards compatibility +tls = [] +# websockets can be used as an alternate container attach mechanism but +# by default docker-py hijacks the TCP connection and does not use Websockets +# unless attach_socket(container, ws=True) is called +websockets = [ + "websocket-client >= 1.3.0", +] +# docs are dependencies required to build the ReadTheDocs site +# this is only needed for CI / working on the docs! +docs = [ + "myst-parser==0.18.0", + "Sphinx==5.1.1", + +] +# dev are dependencies required to test & lint this project +# this is only needed if you are making code changes to docker-py! +dev = [ + "coverage==7.2.7", + "pytest==7.4.2", + "pytest-cov==4.1.0", + "pytest-timeout==2.1.0", + "ruff==0.1.8", +] + +[project.urls] +Changelog = "https://docker-py.readthedocs.io/en/stable/change-log.html" +Documentation = "https://docker-py.readthedocs.io" +Homepage = "https://github.com/docker/docker-py" +Source = "https://github.com/docker/docker-py" +Tracker = "https://github.com/docker/docker-py/issues" + +[tool.hatch.version] +source = "vcs" + +[tool.hatch.build.hooks.vcs] +version-file = "docker/_version.py" + +[tool.hatch.build.targets.sdist] +include = [ + "/docker", +] [tool.ruff] -target-version = "py37" +target-version = "py38" extend-select = [ "B", "C", "F", + "I", + "UP", "W", ] ignore = [ + "UP012", # unnecessary `UTF-8` argument (we want to be explicit) "C901", # too complex (there's a whole bunch of these) ] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 6d932eb37..000000000 --- a/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -packaging==21.3 -paramiko==2.11.0 -pywin32==304; sys_platform == 'win32' -requests==2.31.0 -urllib3==1.26.18 -websocket-client==1.3.3 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index a37e5521d..000000000 --- a/setup.cfg +++ /dev/null @@ -1,3 +0,0 @@ -[metadata] -description_file = README.rst -license = Apache License 2.0 diff --git a/setup.py b/setup.py deleted file mode 100644 index b6a024f81..000000000 --- a/setup.py +++ /dev/null @@ -1,83 +0,0 @@ -#!/usr/bin/env python - -import codecs -import os - -from setuptools import find_packages -from setuptools import setup - -ROOT_DIR = os.path.dirname(__file__) -SOURCE_DIR = os.path.join(ROOT_DIR) - -requirements = [ - 'packaging >= 14.0', - 'requests >= 2.26.0', - 'urllib3 >= 1.26.0', -] - -extras_require = { - # win32 APIs if on Windows (required for npipe support) - ':sys_platform == "win32"': 'pywin32>=304', - - # This is now a no-op, as similarly the requests[security] extra is - # a no-op as of requests 2.26.0, this is always available/by default now - # see https://github.com/psf/requests/pull/5867 - 'tls': [], - - # Only required when connecting using the ssh:// protocol - 'ssh': ['paramiko>=2.4.3'], - - # Only required when using websockets - 'websockets': ['websocket-client >= 1.3.0'], -} - -with open('./test-requirements.txt') as test_reqs_txt: - test_requirements = list(test_reqs_txt) - - -long_description = '' -with codecs.open('./README.md', encoding='utf-8') as readme_md: - long_description = readme_md.read() - -setup( - name="docker", - use_scm_version={ - 'write_to': 'docker/_version.py' - }, - description="A Python library for the Docker Engine API.", - long_description=long_description, - long_description_content_type='text/markdown', - 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', - 'Source': 'https://github.com/docker/docker-py', - 'Tracker': 'https://github.com/docker/docker-py/issues', - }, - packages=find_packages(exclude=["tests.*", "tests"]), - setup_requires=['setuptools_scm'], - install_requires=requirements, - tests_require=test_requirements, - extras_require=extras_require, - python_requires='>=3.8', - zip_safe=False, - test_suite='tests', - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Environment :: Other Environment', - 'Intended Audience :: Developers', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12', - 'Topic :: Software Development', - 'Topic :: Utilities', - 'License :: OSI Approved :: Apache Software License', - ], - maintainer='Docker, Inc.', - maintainer_email='no-reply@docker.com', -) diff --git a/test-requirements.txt b/test-requirements.txt deleted file mode 100644 index 031d0acf0..000000000 --- a/test-requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -setuptools==65.5.1 -coverage==7.2.7 -ruff==0.0.284 -pytest==7.4.2 -pytest-cov==4.1.0 -pytest-timeout==2.1.0 diff --git a/tests/Dockerfile b/tests/Dockerfile index d7c14b6cc..c0af8e85f 100644 --- a/tests/Dockerfile +++ b/tests/Dockerfile @@ -1,7 +1,6 @@ # syntax=docker/dockerfile:1 ARG PYTHON_VERSION=3.12 - FROM python:${PYTHON_VERSION} RUN apt-get update && apt-get -y install --no-install-recommends \ @@ -27,16 +26,10 @@ RUN curl -sSL -o /opt/docker-credential-pass.tar.gz \ chmod +x /usr/local/bin/docker-credential-pass WORKDIR /src +COPY . . -COPY requirements.txt /src/requirements.txt -RUN --mount=type=cache,target=/root/.cache/pip \ - pip install -r requirements.txt - -COPY test-requirements.txt /src/test-requirements.txt -RUN --mount=type=cache,target=/root/.cache/pip \ - pip install -r test-requirements.txt - -COPY . /src -ARG SETUPTOOLS_SCM_PRETEND_VERSION=99.0.0+docker -RUN --mount=type=cache,target=/root/.cache/pip \ - pip install -e . +ARG VERSION +RUN --mount=type=cache,target=/cache/pip \ + PIP_CACHE_DIR=/cache/pip \ + SETUPTOOLS_SCM_PRETEND_VERSION=${VERSION} \ + pip install .[dev,ssh,websockets] diff --git a/tests/Dockerfile-ssh-dind b/tests/Dockerfile-ssh-dind index 0da15aa40..250c20f2c 100644 --- a/tests/Dockerfile-ssh-dind +++ b/tests/Dockerfile-ssh-dind @@ -1,7 +1,7 @@ # syntax=docker/dockerfile:1 -ARG API_VERSION=1.41 -ARG ENGINE_VERSION=20.10 +ARG API_VERSION=1.44 +ARG ENGINE_VERSION=25.0 FROM docker:${ENGINE_VERSION}-dind diff --git a/tests/helpers.py b/tests/helpers.py index 748ee70a7..3d60a3faf 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -8,10 +8,11 @@ import tempfile import time -import docker import paramiko import pytest +import docker + def make_tree(dirs, files): base = tempfile.mkdtemp() diff --git a/tests/integration/api_build_test.py b/tests/integration/api_build_test.py index 2add2d87a..62e93a738 100644 --- a/tests/integration/api_build_test.py +++ b/tests/integration/api_build_test.py @@ -3,13 +3,13 @@ import shutil import tempfile +import pytest + from docker import errors from docker.utils.proxy import ProxyConfig -import pytest - -from .base import BaseAPIIntegrationTest, TEST_IMG from ..helpers import random_name, requires_api_version, requires_experimental +from .base import TEST_IMG, BaseAPIIntegrationTest class BuildTest(BaseAPIIntegrationTest): @@ -389,9 +389,7 @@ def test_build_stderr_data(self): lines = [] for chunk in stream: lines.append(chunk.get('stream')) - expected = '{0}{2}\n{1}'.format( - control_chars[0], control_chars[1], snippet - ) + expected = f'{control_chars[0]}{snippet}\n{control_chars[1]}' assert any(line == expected for line in lines) def test_build_gzip_encoding(self): diff --git a/tests/integration/api_config_test.py b/tests/integration/api_config_test.py index 982ec468a..4261599d8 100644 --- a/tests/integration/api_config_test.py +++ b/tests/integration/api_config_test.py @@ -1,6 +1,7 @@ -import docker import pytest +import docker + from ..helpers import force_leave_swarm, requires_api_version from .base import BaseAPIIntegrationTest diff --git a/tests/integration/api_container_test.py b/tests/integration/api_container_test.py index aa27fbfd7..0215e14c2 100644 --- a/tests/integration/api_container_test.py +++ b/tests/integration/api_container_test.py @@ -9,15 +9,17 @@ import requests import docker -from .. import helpers -from ..helpers import assert_cat_socket_detached_with_keys -from ..helpers import ctrl_with -from ..helpers import requires_api_version, skip_if_desktop -from .base import BaseAPIIntegrationTest -from .base import TEST_IMG from docker.constants import IS_WINDOWS_PLATFORM -from docker.utils.socket import next_frame_header -from docker.utils.socket import read_exactly +from docker.utils.socket import next_frame_header, read_exactly + +from .. import helpers +from ..helpers import ( + assert_cat_socket_detached_with_keys, + ctrl_with, + requires_api_version, + skip_if_desktop, +) +from .base import TEST_IMG, BaseAPIIntegrationTest class ListContainersTest(BaseAPIIntegrationTest): diff --git a/tests/integration/api_exec_test.py b/tests/integration/api_exec_test.py index 4d7748f5e..5b829e287 100644 --- a/tests/integration/api_exec_test.py +++ b/tests/integration/api_exec_test.py @@ -1,11 +1,12 @@ -from ..helpers import assert_cat_socket_detached_with_keys -from ..helpers import ctrl_with -from ..helpers import requires_api_version -from .base import BaseAPIIntegrationTest -from .base import TEST_IMG from docker.utils.proxy import ProxyConfig -from docker.utils.socket import next_frame_header -from docker.utils.socket import read_exactly +from docker.utils.socket import next_frame_header, read_exactly + +from ..helpers import ( + assert_cat_socket_detached_with_keys, + ctrl_with, + requires_api_version, +) +from .base import TEST_IMG, BaseAPIIntegrationTest class ExecTest(BaseAPIIntegrationTest): diff --git a/tests/integration/api_healthcheck_test.py b/tests/integration/api_healthcheck_test.py index 9ecdcd86a..f00d804b4 100644 --- a/tests/integration/api_healthcheck_test.py +++ b/tests/integration/api_healthcheck_test.py @@ -1,5 +1,5 @@ -from .base import BaseAPIIntegrationTest, TEST_IMG from .. import helpers +from .base import TEST_IMG, BaseAPIIntegrationTest SECOND = 1000000000 diff --git a/tests/integration/api_image_test.py b/tests/integration/api_image_test.py index 7081b53b8..d3915c9b5 100644 --- a/tests/integration/api_image_test.py +++ b/tests/integration/api_image_test.py @@ -2,19 +2,18 @@ import json import shutil import socket +import socketserver import tarfile import tempfile import threading - -import pytest from http.server import SimpleHTTPRequestHandler -import socketserver +import pytest import docker from ..helpers import requires_api_version, requires_experimental -from .base import BaseAPIIntegrationTest, TEST_IMG +from .base import TEST_IMG, BaseAPIIntegrationTest class ListImagesTest(BaseAPIIntegrationTest): @@ -85,13 +84,8 @@ def test_commit(self): img_id = res['Id'] self.tmp_imgs.append(img_id) img = self.client.inspect_image(img_id) - assert 'Container' in img - assert img['Container'].startswith(id) - assert 'ContainerConfig' in img - assert 'Image' in img['ContainerConfig'] - assert TEST_IMG == img['ContainerConfig']['Image'] - busybox_id = self.client.inspect_image(TEST_IMG)['Id'] assert 'Parent' in img + busybox_id = self.client.inspect_image(TEST_IMG)['Id'] assert img['Parent'] == busybox_id def test_commit_with_changes(self): @@ -103,8 +97,6 @@ def test_commit_with_changes(self): ) self.tmp_imgs.append(img_id) img = self.client.inspect_image(img_id) - assert 'Container' in img - assert img['Container'].startswith(cid['Id']) assert '8000/tcp' in img['Config']['ExposedPorts'] assert img['Config']['Cmd'] == ['bash'] diff --git a/tests/integration/api_network_test.py b/tests/integration/api_network_test.py index 6689044b6..ce2e8ea4c 100644 --- a/tests/integration/api_network_test.py +++ b/tests/integration/api_network_test.py @@ -1,9 +1,10 @@ +import pytest + import docker from docker.types import IPAMConfig, IPAMPool -import pytest from ..helpers import random_name, requires_api_version -from .base import BaseAPIIntegrationTest, TEST_IMG +from .base import TEST_IMG, BaseAPIIntegrationTest class TestNetworks(BaseAPIIntegrationTest): diff --git a/tests/integration/api_plugin_test.py b/tests/integration/api_plugin_test.py index 3f1633900..168c81b23 100644 --- a/tests/integration/api_plugin_test.py +++ b/tests/integration/api_plugin_test.py @@ -1,10 +1,11 @@ import os -import docker import pytest -from .base import BaseAPIIntegrationTest +import docker + from ..helpers import requires_api_version +from .base import BaseAPIIntegrationTest SSHFS = 'vieux/sshfs:latest' diff --git a/tests/integration/api_secret_test.py b/tests/integration/api_secret_test.py index fd9854341..588aaeb99 100644 --- a/tests/integration/api_secret_test.py +++ b/tests/integration/api_secret_test.py @@ -1,6 +1,7 @@ -import docker import pytest +import docker + from ..helpers import force_leave_swarm, requires_api_version from .base import BaseAPIIntegrationTest diff --git a/tests/integration/api_service_test.py b/tests/integration/api_service_test.py index dec3fa007..7a7ae4ea4 100644 --- a/tests/integration/api_service_test.py +++ b/tests/integration/api_service_test.py @@ -1,13 +1,12 @@ import random import time -import docker import pytest -from ..helpers import ( - force_leave_swarm, requires_api_version, requires_experimental -) -from .base import BaseAPIIntegrationTest, TEST_IMG +import docker + +from ..helpers import force_leave_swarm, requires_api_version, requires_experimental +from .base import TEST_IMG, BaseAPIIntegrationTest class ServiceTest(BaseAPIIntegrationTest): diff --git a/tests/integration/api_swarm_test.py b/tests/integration/api_swarm_test.py index b4125d24d..00477e103 100644 --- a/tests/integration/api_swarm_test.py +++ b/tests/integration/api_swarm_test.py @@ -1,7 +1,9 @@ import copy -import docker + import pytest +import docker + from ..helpers import force_leave_swarm, requires_api_version from .base import BaseAPIIntegrationTest diff --git a/tests/integration/api_volume_test.py b/tests/integration/api_volume_test.py index 2085e8311..ecd19da2d 100644 --- a/tests/integration/api_volume_test.py +++ b/tests/integration/api_volume_test.py @@ -1,6 +1,7 @@ -import docker import pytest +import docker + from ..helpers import requires_api_version from .base import BaseAPIIntegrationTest diff --git a/tests/integration/base.py b/tests/integration/base.py index e4073757e..51ee05daa 100644 --- a/tests/integration/base.py +++ b/tests/integration/base.py @@ -3,9 +3,10 @@ import unittest import docker -from .. import helpers from docker.utils import kwargs_from_env +from .. import helpers + TEST_IMG = 'alpine:3.10' TEST_API_VERSION = os.environ.get('DOCKER_TEST_API_VERSION') diff --git a/tests/integration/client_test.py b/tests/integration/client_test.py index 7df172c88..1d1be077e 100644 --- a/tests/integration/client_test.py +++ b/tests/integration/client_test.py @@ -1,10 +1,9 @@ import threading import unittest +from datetime import datetime, timedelta import docker -from datetime import datetime, timedelta - from ..helpers import requires_api_version from .base import TEST_API_VERSION diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index ae9459558..443c5b795 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -1,9 +1,10 @@ import sys import warnings +import pytest + import docker.errors from docker.utils import kwargs_from_env -import pytest from .base import TEST_IMG diff --git a/tests/integration/context_api_test.py b/tests/integration/context_api_test.py index 1a13f2817..2131ebe74 100644 --- a/tests/integration/context_api_test.py +++ b/tests/integration/context_api_test.py @@ -1,9 +1,12 @@ import os import tempfile + import pytest + from docker import errors from docker.context import ContextAPI from docker.tls import TLSConfig + from .base import BaseAPIIntegrationTest diff --git a/tests/integration/credentials/store_test.py b/tests/integration/credentials/store_test.py index 82ea84741..e1eba33a1 100644 --- a/tests/integration/credentials/store_test.py +++ b/tests/integration/credentials/store_test.py @@ -6,8 +6,11 @@ import pytest from docker.credentials import ( - CredentialsNotFound, Store, StoreError, DEFAULT_LINUX_STORE, - DEFAULT_OSX_STORE + DEFAULT_LINUX_STORE, + DEFAULT_OSX_STORE, + CredentialsNotFound, + Store, + StoreError, ) diff --git a/tests/integration/credentials/utils_test.py b/tests/integration/credentials/utils_test.py index 464403979..75bdea100 100644 --- a/tests/integration/credentials/utils_test.py +++ b/tests/integration/credentials/utils_test.py @@ -1,7 +1,7 @@ import os +from unittest import mock from docker.credentials.utils import create_environment_dict -from unittest import mock @mock.patch.dict(os.environ) diff --git a/tests/integration/errors_test.py b/tests/integration/errors_test.py index e2fce48b0..438caacbc 100644 --- a/tests/integration/errors_test.py +++ b/tests/integration/errors_test.py @@ -1,7 +1,9 @@ -from docker.errors import APIError -from .base import BaseAPIIntegrationTest, TEST_IMG import pytest +from docker.errors import APIError + +from .base import TEST_IMG, BaseAPIIntegrationTest + class ErrorsTest(BaseAPIIntegrationTest): def test_api_error_parses_json(self): diff --git a/tests/integration/models_containers_test.py b/tests/integration/models_containers_test.py index 219b9a4cb..476263ae2 100644 --- a/tests/integration/models_containers_test.py +++ b/tests/integration/models_containers_test.py @@ -5,10 +5,9 @@ import pytest import docker -from .base import BaseIntegrationTest -from .base import TEST_API_VERSION -from ..helpers import random_name -from ..helpers import requires_api_version + +from ..helpers import random_name, requires_api_version +from .base import TEST_API_VERSION, BaseIntegrationTest class ContainerCollectionTest(BaseIntegrationTest): @@ -110,12 +109,12 @@ def test_run_with_networking_config(self): client.networks.create(net_name) self.tmp_networks.append(net_name) - test_aliases = ['hello'] + test_alias = 'hello' test_driver_opt = {'key1': 'a'} networking_config = { net_name: client.api.create_endpoint_config( - aliases=test_aliases, + aliases=[test_alias], driver_opt=test_driver_opt ) } @@ -132,8 +131,10 @@ def test_run_with_networking_config(self): assert 'NetworkSettings' in attrs assert 'Networks' in attrs['NetworkSettings'] assert list(attrs['NetworkSettings']['Networks'].keys()) == [net_name] - assert attrs['NetworkSettings']['Networks'][net_name]['Aliases'] == \ - test_aliases + # Expect Aliases to list 'test_alias' and the container's short-id. + # In API version 1.45, the short-id will be removed. + assert attrs['NetworkSettings']['Networks'][net_name]['Aliases'] \ + == [test_alias, attrs['Id'][:12]] assert attrs['NetworkSettings']['Networks'][net_name]['DriverOpts'] \ == test_driver_opt @@ -190,7 +191,9 @@ def test_run_with_networking_config_only_undeclared_network(self): assert 'NetworkSettings' in attrs assert 'Networks' in attrs['NetworkSettings'] assert list(attrs['NetworkSettings']['Networks'].keys()) == [net_name] - assert attrs['NetworkSettings']['Networks'][net_name]['Aliases'] is None + # Aliases should include the container's short-id (but it will be removed + # in API v1.45). + assert attrs['NetworkSettings']['Networks'][net_name]['Aliases'] == [attrs["Id"][:12]] assert (attrs['NetworkSettings']['Networks'][net_name]['DriverOpts'] is None) diff --git a/tests/integration/models_images_test.py b/tests/integration/models_images_test.py index d335da4a7..9d42cc48f 100644 --- a/tests/integration/models_images_test.py +++ b/tests/integration/models_images_test.py @@ -1,11 +1,12 @@ import io import tempfile -import docker import pytest -from .base import BaseIntegrationTest, TEST_IMG, TEST_API_VERSION +import docker + from ..helpers import random_name +from .base import TEST_API_VERSION, TEST_IMG, BaseIntegrationTest class ImageCollectionTest(BaseIntegrationTest): diff --git a/tests/integration/models_networks_test.py b/tests/integration/models_networks_test.py index f4052e4ba..f5e6fcf57 100644 --- a/tests/integration/models_networks_test.py +++ b/tests/integration/models_networks_test.py @@ -1,6 +1,7 @@ import docker + from .. import helpers -from .base import BaseIntegrationTest, TEST_API_VERSION +from .base import TEST_API_VERSION, BaseIntegrationTest class NetworkCollectionTest(BaseIntegrationTest): diff --git a/tests/integration/models_resources_test.py b/tests/integration/models_resources_test.py index 4aafe0cc7..7d9762702 100644 --- a/tests/integration/models_resources_test.py +++ b/tests/integration/models_resources_test.py @@ -1,5 +1,6 @@ import docker -from .base import BaseIntegrationTest, TEST_API_VERSION + +from .base import TEST_API_VERSION, BaseIntegrationTest class ModelTest(BaseIntegrationTest): diff --git a/tests/integration/models_services_test.py b/tests/integration/models_services_test.py index f1439a418..947ba46d2 100644 --- a/tests/integration/models_services_test.py +++ b/tests/integration/models_services_test.py @@ -1,13 +1,14 @@ import unittest -import docker import pytest -from .. import helpers -from .base import TEST_API_VERSION +import docker from docker.errors import InvalidArgument from docker.types.services import ServiceMode +from .. import helpers +from .base import TEST_API_VERSION + class ServiceTest(unittest.TestCase): @classmethod diff --git a/tests/integration/models_swarm_test.py b/tests/integration/models_swarm_test.py index 6c1836dc6..f43824c75 100644 --- a/tests/integration/models_swarm_test.py +++ b/tests/integration/models_swarm_test.py @@ -1,10 +1,11 @@ import unittest +import pytest + import docker from .. import helpers from .base import TEST_API_VERSION -import pytest class SwarmTest(unittest.TestCase): diff --git a/tests/integration/models_volumes_test.py b/tests/integration/models_volumes_test.py index 47b4a4550..7d3ffda99 100644 --- a/tests/integration/models_volumes_test.py +++ b/tests/integration/models_volumes_test.py @@ -1,5 +1,6 @@ import docker -from .base import BaseIntegrationTest, TEST_API_VERSION + +from .base import TEST_API_VERSION, BaseIntegrationTest class VolumesTest(BaseIntegrationTest): diff --git a/tests/integration/regression_test.py b/tests/integration/regression_test.py index 7d2b228cc..5df9d3121 100644 --- a/tests/integration/regression_test.py +++ b/tests/integration/regression_test.py @@ -1,10 +1,11 @@ import io import random +import pytest + import docker -from .base import BaseAPIIntegrationTest, TEST_IMG -import pytest +from .base import TEST_IMG, BaseAPIIntegrationTest class TestRegressions(BaseAPIIntegrationTest): diff --git a/tests/ssh/api_build_test.py b/tests/ssh/api_build_test.py index d060f465f..20476fc74 100644 --- a/tests/ssh/api_build_test.py +++ b/tests/ssh/api_build_test.py @@ -3,13 +3,13 @@ import shutil import tempfile +import pytest + from docker import errors from docker.utils.proxy import ProxyConfig -import pytest - -from .base import BaseAPIIntegrationTest, TEST_IMG from ..helpers import random_name, requires_api_version, requires_experimental +from .base import TEST_IMG, BaseAPIIntegrationTest class BuildTest(BaseAPIIntegrationTest): @@ -380,9 +380,7 @@ def test_build_stderr_data(self): lines = [] for chunk in stream: lines.append(chunk.get('stream')) - expected = '{0}{2}\n{1}'.format( - control_chars[0], control_chars[1], snippet - ) + expected = f'{control_chars[0]}{snippet}\n{control_chars[1]}' assert any(line == expected for line in lines) def test_build_gzip_encoding(self): diff --git a/tests/ssh/base.py b/tests/ssh/base.py index d6ff130a1..bf3c11d7a 100644 --- a/tests/ssh/base.py +++ b/tests/ssh/base.py @@ -5,9 +5,10 @@ import pytest import docker -from .. import helpers from docker.utils import kwargs_from_env +from .. import helpers + TEST_IMG = 'alpine:3.10' TEST_API_VERSION = os.environ.get('DOCKER_TEST_API_VERSION') diff --git a/tests/ssh/connect_test.py b/tests/ssh/connect_test.py index 3d33a96db..8780e3b8b 100644 --- a/tests/ssh/connect_test.py +++ b/tests/ssh/connect_test.py @@ -1,9 +1,11 @@ import os import unittest -import docker import paramiko.ssh_exception import pytest + +import docker + from .base import TEST_API_VERSION diff --git a/tests/unit/api_container_test.py b/tests/unit/api_container_test.py index c4e2250be..b2e5237a2 100644 --- a/tests/unit/api_container_test.py +++ b/tests/unit/api_container_test.py @@ -1,17 +1,22 @@ import datetime import json import signal +from unittest import mock + +import pytest import docker from docker.api import APIClient -from unittest import mock -import pytest -from . import fake_api from ..helpers import requires_api_version +from . import fake_api from .api_test import ( - BaseAPIClientTest, url_prefix, fake_request, DEFAULT_TIMEOUT_SECONDS, - fake_inspect_container, url_base + DEFAULT_TIMEOUT_SECONDS, + BaseAPIClientTest, + fake_inspect_container, + fake_request, + url_base, + url_prefix, ) diff --git a/tests/unit/api_exec_test.py b/tests/unit/api_exec_test.py index 1760239fd..9d789723a 100644 --- a/tests/unit/api_exec_test.py +++ b/tests/unit/api_exec_test.py @@ -2,7 +2,10 @@ from . import fake_api from .api_test import ( - BaseAPIClientTest, url_prefix, fake_request, DEFAULT_TIMEOUT_SECONDS, + DEFAULT_TIMEOUT_SECONDS, + BaseAPIClientTest, + fake_request, + url_prefix, ) diff --git a/tests/unit/api_image_test.py b/tests/unit/api_image_test.py index 22b27fe0d..148109d37 100644 --- a/tests/unit/api_image_test.py +++ b/tests/unit/api_image_test.py @@ -1,12 +1,17 @@ -import docker +from unittest import mock + import pytest -from . import fake_api +import docker from docker import auth -from unittest import mock + +from . import fake_api from .api_test import ( - BaseAPIClientTest, fake_request, DEFAULT_TIMEOUT_SECONDS, url_prefix, - fake_resolve_authconfig + DEFAULT_TIMEOUT_SECONDS, + BaseAPIClientTest, + fake_request, + fake_resolve_authconfig, + url_prefix, ) diff --git a/tests/unit/api_network_test.py b/tests/unit/api_network_test.py index d3daa44c4..1f9e59665 100644 --- a/tests/unit/api_network_test.py +++ b/tests/unit/api_network_test.py @@ -1,8 +1,9 @@ import json +from unittest import mock -from .api_test import BaseAPIClientTest, url_prefix, response from docker.types import IPAMConfig, IPAMPool -from unittest import mock + +from .api_test import BaseAPIClientTest, response, url_prefix class NetworkTest(BaseAPIClientTest): diff --git a/tests/unit/api_test.py b/tests/unit/api_test.py index 7bc2ea8cd..3ce127b34 100644 --- a/tests/unit/api_test.py +++ b/tests/unit/api_test.py @@ -1,29 +1,29 @@ import datetime +import http.server import io import json import os import re import shutil import socket +import socketserver import struct import tempfile import threading import time import unittest -import socketserver -import http.server +from unittest import mock -import docker import pytest import requests import urllib3 + +import docker from docker.api import APIClient from docker.constants import DEFAULT_DOCKER_API_VERSION -from unittest import mock from . import fake_api - DEFAULT_TIMEOUT_SECONDS = docker.constants.DEFAULT_TIMEOUT_SECONDS @@ -82,7 +82,7 @@ def fake_delete(self, url, *args, **kwargs): def fake_read_from_socket(self, response, stream, tty=False, demux=False): - return bytes() + return b'' url_base = f'{fake_api.prefix}/' diff --git a/tests/unit/api_volume_test.py b/tests/unit/api_volume_test.py index 0a97ca515..fd3206396 100644 --- a/tests/unit/api_volume_test.py +++ b/tests/unit/api_volume_test.py @@ -3,7 +3,7 @@ import pytest from ..helpers import requires_api_version -from .api_test import BaseAPIClientTest, url_prefix, fake_request +from .api_test import BaseAPIClientTest, fake_request, url_prefix class VolumeTest(BaseAPIClientTest): diff --git a/tests/unit/auth_test.py b/tests/unit/auth_test.py index 0ed890fdf..b2fedb32e 100644 --- a/tests/unit/auth_test.py +++ b/tests/unit/auth_test.py @@ -6,11 +6,12 @@ import shutil import tempfile import unittest - -from docker import auth, credentials, errors from unittest import mock + import pytest +from docker import auth, credentials, errors + class RegressionTest(unittest.TestCase): def test_803_urlsafe_encode(self): diff --git a/tests/unit/client_test.py b/tests/unit/client_test.py index 7012b2123..60a6d5c0f 100644 --- a/tests/unit/client_test.py +++ b/tests/unit/client_test.py @@ -1,15 +1,18 @@ import datetime import os import unittest +from unittest import mock -import docker import pytest + +import docker from docker.constants import ( - DEFAULT_DOCKER_API_VERSION, DEFAULT_TIMEOUT_SECONDS, - DEFAULT_MAX_POOL_SIZE, IS_WINDOWS_PLATFORM + DEFAULT_DOCKER_API_VERSION, + DEFAULT_MAX_POOL_SIZE, + DEFAULT_TIMEOUT_SECONDS, + IS_WINDOWS_PLATFORM, ) from docker.utils import kwargs_from_env -from unittest import mock from . import fake_api diff --git a/tests/unit/context_test.py b/tests/unit/context_test.py index 25f0d8c6b..9e9fc9ba1 100644 --- a/tests/unit/context_test.py +++ b/tests/unit/context_test.py @@ -1,10 +1,10 @@ import unittest -import docker + import pytest -from docker.constants import DEFAULT_UNIX_SOCKET -from docker.constants import DEFAULT_NPIPE -from docker.constants import IS_WINDOWS_PLATFORM -from docker.context import ContextAPI, Context + +import docker +from docker.constants import DEFAULT_NPIPE, DEFAULT_UNIX_SOCKET, IS_WINDOWS_PLATFORM +from docker.context import Context, ContextAPI class BaseContextTest(unittest.TestCase): diff --git a/tests/unit/dockertypes_test.py b/tests/unit/dockertypes_test.py index f3d562e10..03e7d2eda 100644 --- a/tests/unit/dockertypes_test.py +++ b/tests/unit/dockertypes_test.py @@ -1,15 +1,22 @@ import unittest +from unittest import mock import pytest from docker.constants import DEFAULT_DOCKER_API_VERSION from docker.errors import InvalidArgument, InvalidVersion from docker.types import ( - ContainerSpec, EndpointConfig, HostConfig, IPAMConfig, - IPAMPool, LogConfig, Mount, ServiceMode, Ulimit, + ContainerSpec, + EndpointConfig, + HostConfig, + IPAMConfig, + IPAMPool, + LogConfig, + Mount, + ServiceMode, + Ulimit, ) from docker.types.services import convert_service_ports -from unittest import mock def create_host_config(*args, **kwargs): diff --git a/tests/unit/errors_test.py b/tests/unit/errors_test.py index f8c3a6663..5ccc40474 100644 --- a/tests/unit/errors_test.py +++ b/tests/unit/errors_test.py @@ -2,9 +2,14 @@ import requests -from docker.errors import (APIError, ContainerError, DockerException, - create_unexpected_kwargs_error, - create_api_error_from_http_exception) +from docker.errors import ( + APIError, + ContainerError, + DockerException, + create_api_error_from_http_exception, + create_unexpected_kwargs_error, +) + from .fake_api import FAKE_CONTAINER_ID, FAKE_IMAGE_ID from .fake_api_client import make_fake_client diff --git a/tests/unit/fake_api.py b/tests/unit/fake_api.py index 0524becdc..03e53cc64 100644 --- a/tests/unit/fake_api.py +++ b/tests/unit/fake_api.py @@ -19,6 +19,8 @@ FAKE_NODE_ID = '24ifsmvkjbyhk' FAKE_SECRET_ID = 'epdyrw4tsi03xy3deu8g8ly6o' FAKE_SECRET_NAME = 'super_secret' +FAKE_CONFIG_ID = 'sekvs771242jfdjnvfuds8232' +FAKE_CONFIG_NAME = 'super_config' # Each method is prefixed with HTTP method (get, post...) # for clarity and readability @@ -512,6 +514,11 @@ def post_fake_secret(): response = {'ID': FAKE_SECRET_ID} return status_code, response +def post_fake_config(): + status_code = 200 + response = {'ID': FAKE_CONFIG_ID} + return status_code, response + # Maps real api url to fake response callback prefix = 'http+docker://localhost' @@ -630,4 +637,6 @@ def post_fake_secret(): post_fake_network_disconnect, f'{prefix}/{CURRENT_VERSION}/secrets/create': post_fake_secret, + f'{prefix}/{CURRENT_VERSION}/configs/create': + post_fake_config, } diff --git a/tests/unit/fake_api_client.py b/tests/unit/fake_api_client.py index 95cf63b49..017e99d0c 100644 --- a/tests/unit/fake_api_client.py +++ b/tests/unit/fake_api_client.py @@ -1,8 +1,9 @@ import copy +from unittest import mock import docker from docker.constants import DEFAULT_DOCKER_API_VERSION -from unittest import mock + from . import fake_api @@ -37,6 +38,7 @@ def make_fake_api_client(overrides=None): 'create_host_config.side_effect': api_client.create_host_config, 'create_network.return_value': fake_api.post_fake_network()[1], 'create_secret.return_value': fake_api.post_fake_secret()[1], + 'create_config.return_value': fake_api.post_fake_config()[1], 'exec_create.return_value': fake_api.post_fake_exec_create()[1], 'exec_start.return_value': fake_api.post_fake_exec_start()[1], 'images.return_value': fake_api.get_fake_images()[1], diff --git a/tests/unit/models_configs_test.py b/tests/unit/models_configs_test.py new file mode 100644 index 000000000..9f1083068 --- /dev/null +++ b/tests/unit/models_configs_test.py @@ -0,0 +1,11 @@ +import unittest + +from .fake_api import FAKE_CONFIG_NAME +from .fake_api_client import make_fake_client + + +class CreateConfigsTest(unittest.TestCase): + def test_create_config(self): + client = make_fake_client() + config = client.configs.create(name="super_config", data="config") + assert config.__repr__() == f"" diff --git a/tests/unit/models_containers_test.py b/tests/unit/models_containers_test.py index 05005815f..0e2ae341a 100644 --- a/tests/unit/models_containers_test.py +++ b/tests/unit/models_containers_test.py @@ -3,12 +3,12 @@ import pytest import docker -from docker.constants import DEFAULT_DATA_CHUNK_SIZE, \ - DEFAULT_DOCKER_API_VERSION +from docker.constants import DEFAULT_DATA_CHUNK_SIZE, DEFAULT_DOCKER_API_VERSION from docker.models.containers import Container, _create_container_args from docker.models.images import Image from docker.types import EndpointConfig -from .fake_api import FAKE_CONTAINER_ID, FAKE_IMAGE_ID, FAKE_EXEC_ID + +from .fake_api import FAKE_CONTAINER_ID, FAKE_EXEC_ID, FAKE_IMAGE_ID from .fake_api_client import make_fake_client diff --git a/tests/unit/models_networks_test.py b/tests/unit/models_networks_test.py index f10e1e3e3..099fb2193 100644 --- a/tests/unit/models_networks_test.py +++ b/tests/unit/models_networks_test.py @@ -1,6 +1,6 @@ import unittest -from .fake_api import FAKE_NETWORK_ID, FAKE_CONTAINER_ID +from .fake_api import FAKE_CONTAINER_ID, FAKE_NETWORK_ID from .fake_api_client import make_fake_client diff --git a/tests/unit/models_secrets_test.py b/tests/unit/models_secrets_test.py index 1c261a871..1f5aaace2 100644 --- a/tests/unit/models_secrets_test.py +++ b/tests/unit/models_secrets_test.py @@ -1,7 +1,7 @@ import unittest -from .fake_api_client import make_fake_client from .fake_api import FAKE_SECRET_NAME +from .fake_api_client import make_fake_client class CreateServiceTest(unittest.TestCase): diff --git a/tests/unit/models_services_test.py b/tests/unit/models_services_test.py index 45c63ac9e..027756343 100644 --- a/tests/unit/models_services_test.py +++ b/tests/unit/models_services_test.py @@ -1,4 +1,5 @@ import unittest + from docker.models.services import _get_create_service_kwargs diff --git a/tests/unit/sshadapter_test.py b/tests/unit/sshadapter_test.py index 874239ac8..873666210 100644 --- a/tests/unit/sshadapter_test.py +++ b/tests/unit/sshadapter_test.py @@ -1,4 +1,5 @@ import unittest + import docker from docker.transport.sshconn import SSHSocket diff --git a/tests/unit/swarm_test.py b/tests/unit/swarm_test.py index 3fc7c68cd..4c0f2fd00 100644 --- a/tests/unit/swarm_test.py +++ b/tests/unit/swarm_test.py @@ -1,8 +1,8 @@ import json -from . import fake_api from ..helpers import requires_api_version -from .api_test import BaseAPIClientTest, url_prefix, fake_request +from . import fake_api +from .api_test import BaseAPIClientTest, fake_request, url_prefix class SwarmTest(BaseAPIClientTest): diff --git a/tests/unit/utils_build_test.py b/tests/unit/utils_build_test.py index fa7d833de..2089afb49 100644 --- a/tests/unit/utils_build_test.py +++ b/tests/unit/utils_build_test.py @@ -6,11 +6,10 @@ import tempfile import unittest +import pytest from docker.constants import IS_WINDOWS_PLATFORM -from docker.utils import exclude_paths, tar - -import pytest +from docker.utils import exclude_paths, match_tag, tar from ..helpers import make_tree @@ -489,3 +488,51 @@ def test_tar_directory_link(self): assert member in names assert 'a/c/b' in names assert 'a/c/b/utils.py' not in names + + +# selected test cases from https://github.com/distribution/reference/blob/8507c7fcf0da9f570540c958ea7b972c30eeaeca/reference_test.go#L13-L328 +@pytest.mark.parametrize("tag,expected", [ + ("test_com", True), + ("test.com:tag", True), + # N.B. this implicitly means "docker.io/library/test.com:5000" + # i.e. the `5000` is a tag, not a port here! + ("test.com:5000", True), + ("test.com/repo:tag", True), + ("test:5000/repo", True), + ("test:5000/repo:tag", True), + ("test:5000/repo", True), + ("", False), + (":justtag", False), + ("Uppercase:tag", False), + ("test:5000/Uppercase/lowercase:tag", False), + ("lowercase:Uppercase", True), + # length limits not enforced + pytest.param("a/"*128 + "a:tag", False, marks=pytest.mark.xfail), + ("a/"*127 + "a:tag-puts-this-over-max", True), + ("aa/asdf$$^/aa", False), + ("sub-dom1.foo.com/bar/baz/quux", True), + ("sub-dom1.foo.com/bar/baz/quux:some-long-tag", True), + ("b.gcr.io/test.example.com/my-app:test.example.com", True), + ("xn--n3h.com/myimage:xn--n3h.com", True), + ("foo_bar.com:8080", True), + ("foo/foo_bar.com:8080", True), + ("192.168.1.1", True), + ("192.168.1.1:tag", True), + ("192.168.1.1:5000", True), + ("192.168.1.1/repo", True), + ("192.168.1.1:5000/repo", True), + ("192.168.1.1:5000/repo:5050", True), + # regex does not properly handle ipv6 + pytest.param("[2001:db8::1]", False, marks=pytest.mark.xfail), + ("[2001:db8::1]:5000", False), + pytest.param("[2001:db8::1]/repo", True, marks=pytest.mark.xfail), + pytest.param("[2001:db8:1:2:3:4:5:6]/repo:tag", True, marks=pytest.mark.xfail), + pytest.param("[2001:db8::1]:5000/repo", True, marks=pytest.mark.xfail), + pytest.param("[2001:db8::1]:5000/repo:tag", True, marks=pytest.mark.xfail), + pytest.param("[2001:db8::]:5000/repo", True, marks=pytest.mark.xfail), + pytest.param("[::1]:5000/repo", True, marks=pytest.mark.xfail), + ("[fe80::1%eth0]:5000/repo", False), + ("[fe80::1%@invalidzone]:5000/repo", False), +]) +def test_match_tag(tag: str, expected: bool): + assert match_tag(tag) == expected diff --git a/tests/unit/utils_config_test.py b/tests/unit/utils_config_test.py index 27d5a7cd4..c87231a99 100644 --- a/tests/unit/utils_config_test.py +++ b/tests/unit/utils_config_test.py @@ -1,12 +1,12 @@ +import json import os -import unittest import shutil import tempfile -import json - -from pytest import mark, fixture +import unittest from unittest import mock +from pytest import fixture, mark + from docker.utils import config diff --git a/tests/unit/utils_json_stream_test.py b/tests/unit/utils_json_stream_test.py index 821ebe42d..5a8310cb5 100644 --- a/tests/unit/utils_json_stream_test.py +++ b/tests/unit/utils_json_stream_test.py @@ -1,4 +1,4 @@ -from docker.utils.json_stream import json_splitter, stream_as_text, json_stream +from docker.utils.json_stream import json_splitter, json_stream, stream_as_text class TestJsonSplitter: diff --git a/tests/unit/utils_test.py b/tests/unit/utils_test.py index de79e3037..21da0b58e 100644 --- a/tests/unit/utils_test.py +++ b/tests/unit/utils_test.py @@ -7,15 +7,28 @@ import unittest import pytest + from docker.api.client import APIClient -from docker.constants import IS_WINDOWS_PLATFORM, DEFAULT_DOCKER_API_VERSION +from docker.constants import DEFAULT_DOCKER_API_VERSION, IS_WINDOWS_PLATFORM from docker.errors import DockerException -from docker.utils import (convert_filters, convert_volume_binds, - decode_json_header, kwargs_from_env, parse_bytes, - parse_devices, parse_env_file, parse_host, - parse_repository_tag, split_command, update_headers) +from docker.utils import ( + compare_version, + convert_filters, + convert_volume_binds, + decode_json_header, + format_environment, + kwargs_from_env, + parse_bytes, + parse_devices, + parse_env_file, + parse_host, + parse_repository_tag, + split_command, + update_headers, + version_gte, + version_lt, +) from docker.utils.ports import build_port_bindings, split_port -from docker.utils.utils import format_environment TEST_CERT_DIR = os.path.join( os.path.dirname(__file__), @@ -629,3 +642,19 @@ def test_format_env_no_value(self): 'BAR': '', } assert sorted(format_environment(env_dict)) == ['BAR=', 'FOO'] + + +def test_compare_versions(): + assert compare_version('1.0', '1.1') == 1 + assert compare_version('1.10', '1.1') == -1 + assert compare_version('1.10', '1.10') == 0 + assert compare_version('1.10.0', '1.10.1') == 1 + assert compare_version('1.9', '1.10') == 1 + assert compare_version('1.9.1', '1.10') == 1 + # Test comparison helpers + assert version_lt('1.0', '1.27') + assert version_gte('1.27', '1.20') + # Test zero-padding + assert compare_version('1', '1.0') == 0 + assert compare_version('1.10', '1.10.1') == 1 + assert compare_version('1.10.0', '1.10') == 0 diff --git a/tox.ini b/tox.ini index 03467aea2..19689b964 100644 --- a/tox.ini +++ b/tox.ini @@ -6,11 +6,8 @@ skipsdist=True usedevelop=True commands = py.test -v --cov=docker {posargs:tests/unit} -deps = - -r{toxinidir}/test-requirements.txt - -r{toxinidir}/requirements.txt +extras = dev [testenv:ruff] commands = ruff docker tests setup.py -deps = - -r{toxinidir}/test-requirements.txt +extras = dev