diff --git a/README.md b/README.md index 2db678dc..921ffbcb 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. diff --git a/docker/api/container.py b/docker/api/container.py index f389154e..ec28fd58 100644 --- a/docker/api/container.py +++ b/docker/api/container.py @@ -112,7 +112,7 @@ class ContainerApiMixin: @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 @@ class ContainerApiMixin: 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 @@ class ContainerApiMixin: 'tag': tag, 'comment': message, 'author': author, + 'pause': pause, 'changes': changes } u = self._url("/commit") diff --git a/docker/errors.py b/docker/errors.py index 6dd35fac..d03e10f6 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() diff --git a/docker/models/containers.py b/docker/models/containers.py index ebd43224..44bb92a0 100644 --- a/docker/models/containers.py +++ b/docker/models/containers.py @@ -121,6 +121,7 @@ class Container(Model): 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/docs/user_guides/multiplex.rst b/docs/user_guides/multiplex.rst index 78d7e372..7add69b1 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 diff --git a/requirements.txt b/requirements.txt index 36660b66..897cdbd5 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 diff --git a/tests/Dockerfile b/tests/Dockerfile index bf95cd6a..366abe23 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 \ - && sed -ri "s/(security).debian.org/${APT_MIRROR:-security.debian.org}/g" /etc/apt/sources.list - RUN apt-get update && apt-get -y install --no-install-recommends \ gnupg2 \ pass diff --git a/tests/integration/api_container_test.py b/tests/integration/api_container_test.py index 6cdbb94c..590c4fa0 100644 --- a/tests/integration/api_container_test.py +++ b/tests/integration/api_container_test.py @@ -122,8 +122,8 @@ class CreateContainerTest(BaseAPIIntegrationTest): 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): @@ -1390,7 +1390,7 @@ class GetContainerStatsTest(BaseAPIIntegrationTest): 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 @@ -1403,7 +1403,7 @@ class GetContainerStatsTest(BaseAPIIntegrationTest): 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 1966a05a..7081b53b 100644 --- a/tests/integration/api_image_test.py +++ b/tests/integration/api_image_test.py @@ -32,7 +32,7 @@ class ListImagesTest(BaseAPIIntegrationTest): 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 @@ class PullImageTest(BaseAPIIntegrationTest): 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 diff --git a/tests/integration/errors_test.py b/tests/integration/errors_test.py index 7bf156af..e2fce48b 100644 --- a/tests/integration/errors_test.py +++ b/tests/integration/errors_test.py @@ -9,7 +9,7 @@ class ErrorsTest(BaseAPIIntegrationTest): 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) diff --git a/tests/unit/api_image_test.py b/tests/unit/api_image_test.py index b10061fd..22b27fe0 100644 --- a/tests/unit/api_image_test.py +++ b/tests/unit/api_image_test.py @@ -102,6 +102,7 @@ class ImageTest(BaseAPIClientTest): 'tag': None, 'container': fake_api.FAKE_CONTAINER_ID, 'author': None, + 'pause': True, 'changes': None }, timeout=DEFAULT_TIMEOUT_SECONDS