Repository: docker/docker-py

Issue:
Using docker.client.DockerClient.containers.run with auto_remove=True causes a crash
What happens is straightforward: setting auto_remove=True in the arguments to run() causes the container to be removed [before the Python client attempts to read the logs](https://github.com/docker/docker-py/blob/master/docker/models/containers.py#L734).  This then raises an error like:

```
docker.errors.NotFound: 404 Client Error: Not Found ("No such container: 0e9a9dd8072a1b80ead74bc47466493ffea92eca7acf1c5f3b29ce6faf861161")
```

This is not how executing `docker run -rm` works, where the output still goes to stdout, and it means that calling run() with auto_remove=True isn't useful.


Discussion:
+1
Minimal steps to reproduce using [example from manual](http://docker-py.readthedocs.io/en/stable/containers.html#docker.models.containers.ContainerCollection.run):

```
In [1]: import docker
In [2]: client = docker.from_env()
In [3]: client.containers.run('alpine:2.7', 'echo hello world')
Out[3]: b'hello world\n'
In [4]: client.containers.run('alpine:2.7', 'echo hello world', auto_remove=True)
---------------------------------------------------------------------------
HTTPError                                 Traceback (most recent call last)
(...)
~/.pyenv/versions/3.6.3/lib/python3.6/site-packages/docker/errors.py in create_api_error_from_http_exception(e)
     29         else:
     30             cls = NotFound
---> 31     raise cls(e, response=response, explanation=explanation)
     32
     33

NotFound: 404 Client Error: Not Found ("No such container: 6151f0219d2dbb90df7288f30f1221e817fd9d3c63afdbadf2ad2a87ba6e7e45")
```



Resolving patch:
diff --git a/docker/models/containers.py b/docker/models/containers.py
--- a/docker/models/containers.py
+++ b/docker/models/containers.py
@@ -629,6 +629,9 @@ def run(self, image, command=None, stdout=True, stderr=False,
                 (e.g. ``SIGINT``).
             storage_opt (dict): Storage driver options per container as a
                 key-value mapping.
+            stream (bool): If true and ``detach`` is false, return a log
+                generator instead of a string. Ignored if ``detach`` is true.
+                Default: ``False``.
             sysctls (dict): Kernel parameters to set in the container.
             tmpfs (dict): Temporary filesystems to mount, as a dictionary
                 mapping a path inside the container to options for that path.
@@ -696,6 +699,7 @@ def run(self, image, command=None, stdout=True, stderr=False,
         """
         if isinstance(image, Image):
             image = image.id
+        stream = kwargs.pop('stream', False)
         detach = kwargs.pop("detach", False)
         if detach and remove:
             if version_gte(self.client.api._version, '1.25'):
@@ -723,23 +727,28 @@ def run(self, image, command=None, stdout=True, stderr=False,
         if detach:
             return container
 
-        exit_status = container.wait()
-        if exit_status != 0:
-            stdout = False
-            stderr = True
-
         logging_driver = container.attrs['HostConfig']['LogConfig']['Type']
 
+        out = None
         if logging_driver == 'json-file' or logging_driver == 'journald':
-            out = container.logs(stdout=stdout, stderr=stderr)
-        else:
-            out = None
+            out = container.logs(
+                stdout=stdout, stderr=stderr, stream=True, follow=True
+            )
+
+        exit_status = container.wait()
+        if exit_status != 0:
+            out = container.logs(stdout=False, stderr=True)
 
         if remove:
             container.remove()
         if exit_status != 0:
-            raise ContainerError(container, exit_status, command, image, out)
-        return out
+            raise ContainerError(
+                container, exit_status, command, image, out
+            )
+
+        return out if stream or out is None else b''.join(
+            [line for line in out]
+        )
 
     def create(self, image, command=None, **kwargs):
         """


Test patch:
diff --git a/tests/integration/models_containers_test.py b/tests/integration/models_containers_test.py
--- a/tests/integration/models_containers_test.py
+++ b/tests/integration/models_containers_test.py
@@ -1,7 +1,7 @@
 import docker
 import tempfile
 from .base import BaseIntegrationTest, TEST_API_VERSION
-from ..helpers import random_name
+from ..helpers import random_name, requires_api_version
 
 
 class ContainerCollectionTest(BaseIntegrationTest):
@@ -95,7 +95,7 @@ def test_run_with_none_driver(self):
             "alpine", "echo hello",
             log_config=dict(type='none')
         )
-        self.assertEqual(out, None)
+        assert out is None
 
     def test_run_with_json_file_driver(self):
         client = docker.from_env(version=TEST_API_VERSION)
@@ -104,7 +104,24 @@ def test_run_with_json_file_driver(self):
             "alpine", "echo hello",
             log_config=dict(type='json-file')
         )
-        self.assertEqual(out, b'hello\n')
+        assert out == b'hello\n'
+
+    @requires_api_version('1.25')
+    def test_run_with_auto_remove(self):
+        client = docker.from_env(version=TEST_API_VERSION)
+        out = client.containers.run(
+            'alpine', 'echo hello', auto_remove=True
+        )
+        assert out == b'hello\n'
+
+    def test_run_with_streamed_logs(self):
+        client = docker.from_env(version=TEST_API_VERSION)
+        out = client.containers.run(
+            'alpine', 'sh -c "echo hello && echo world"', stream=True
+        )
+        logs = [line for line in out]
+        assert logs[0] == b'hello\n'
+        assert logs[1] == b'world\n'
 
     def test_get(self):
         client = docker.from_env(version=TEST_API_VERSION)
diff --git a/tests/unit/fake_api_client.py b/tests/unit/fake_api_client.py
--- a/tests/unit/fake_api_client.py
+++ b/tests/unit/fake_api_client.py
@@ -43,7 +43,7 @@ def make_fake_api_client():
             fake_api.get_fake_inspect_container()[1],
         'inspect_image.return_value': fake_api.get_fake_inspect_image()[1],
         'inspect_network.return_value': fake_api.get_fake_network()[1],
-        'logs.return_value': 'hello world\n',
+        'logs.return_value': [b'hello world\n'],
         'networks.return_value': fake_api.get_fake_network_list()[1],
         'start.return_value': None,
         'wait.return_value': 0,
diff --git a/tests/unit/models_containers_test.py b/tests/unit/models_containers_test.py
--- a/tests/unit/models_containers_test.py
+++ b/tests/unit/models_containers_test.py
@@ -12,7 +12,7 @@ def test_run(self):
         client = make_fake_client()
         out = client.containers.run("alpine", "echo hello world")
 
-        assert out == 'hello world\n'
+        assert out == b'hello world\n'
 
         client.api.create_container.assert_called_with(
             image="alpine",
@@ -24,9 +24,8 @@ def test_run(self):
         client.api.start.assert_called_with(FAKE_CONTAINER_ID)
         client.api.wait.assert_called_with(FAKE_CONTAINER_ID)
         client.api.logs.assert_called_with(
-            FAKE_CONTAINER_ID,
-            stderr=False,
-            stdout=True
+            FAKE_CONTAINER_ID, stderr=False, stdout=True, stream=True,
+            follow=True
         )
 
     def test_create_container_args(self):