Repository: docker/docker-py

Issue:
logs() separate param for stream and follow
From: https://github.com/docker/compose/pull/2720/files#r52222296

Current the `follow` param is set based on `stream`

I think `follow=True` does imply `stream=True`, but `stream=True` doesn't imply `follow=True`, you may still want to stream without following.

logs() separate param for stream and follow
From: https://github.com/docker/compose/pull/2720/files#r52222296

Current the `follow` param is set based on `stream`

I think `follow=True` does imply `stream=True`, but `stream=True` doesn't imply `follow=True`, you may still want to stream without following.



Discussion:
This is about performance, right? (and I guess integrity, one can only have a single piece of code to handle streamed response) 

I don't think it's just about performance. It's to support `follow=False, stream=True`. 

Stream is a client-side option, follow is a server side option. We want to be able to stream (read the http response line by line and return an iterator instead of buffering the entire response), and not follow the logs.

Otherwise running `logs(stream=False)` on a container with lots of logs would cause the process to use lots of memory (which I guess in a sense is performance related).

This is about performance, right? (and I guess integrity, one can only have a single piece of code to handle streamed response) 

I don't think it's just about performance. It's to support `follow=False, stream=True`. 

Stream is a client-side option, follow is a server side option. We want to be able to stream (read the http response line by line and return an iterator instead of buffering the entire response), and not follow the logs.

Otherwise running `logs(stream=False)` on a container with lots of logs would cause the process to use lots of memory (which I guess in a sense is performance related).


Resolving patch:
diff --git a/docker/api/container.py b/docker/api/container.py
--- a/docker/api/container.py
+++ b/docker/api/container.py
@@ -193,12 +193,14 @@ def kill(self, container, signal=None):
 
     @utils.check_resource
     def logs(self, container, stdout=True, stderr=True, stream=False,
-             timestamps=False, tail='all', since=None):
+             timestamps=False, tail='all', since=None, follow=None):
         if utils.compare_version('1.11', self._version) >= 0:
+            if follow is None:
+                follow = stream
             params = {'stderr': stderr and 1 or 0,
                       'stdout': stdout and 1 or 0,
                       'timestamps': timestamps and 1 or 0,
-                      'follow': stream and 1 or 0,
+                      'follow': follow and 1 or 0,
                       }
             if utils.compare_version('1.13', self._version) >= 0:
                 if tail != 'all' and (not isinstance(tail, int) or tail < 0):


Test patch:
diff --git a/tests/integration/container_test.py b/tests/integration/container_test.py
--- a/tests/integration/container_test.py
+++ b/tests/integration/container_test.py
@@ -666,7 +666,7 @@ def test_logs_tail_option(self):
         logs = self.client.logs(id, tail=1)
         self.assertEqual(logs, 'Line2\n'.encode(encoding='ascii'))
 
-    def test_logs_streaming(self):
+    def test_logs_streaming_and_follow(self):
         snippet = 'Flowering Nights (Sakuya Iyazoi)'
         container = self.client.create_container(
             BUSYBOX, 'echo {0}'.format(snippet)
@@ -675,7 +675,7 @@ def test_logs_streaming(self):
         self.tmp_containers.append(id)
         self.client.start(id)
         logs = six.binary_type()
-        for chunk in self.client.logs(id, stream=True):
+        for chunk in self.client.logs(id, stream=True, follow=True):
             logs += chunk
 
         exitcode = self.client.wait(id)
diff --git a/tests/unit/container_test.py b/tests/unit/container_test.py
--- a/tests/unit/container_test.py
+++ b/tests/unit/container_test.py
@@ -1119,6 +1119,36 @@ def test_logs_with_dict_instead_of_id(self):
         )
 
     def test_log_streaming(self):
+        with mock.patch('docker.Client.inspect_container',
+                        fake_inspect_container):
+            self.client.logs(fake_api.FAKE_CONTAINER_ID, stream=True,
+                             follow=False)
+
+        fake_request.assert_called_with(
+            'GET',
+            url_prefix + 'containers/3cc2351ab11b/logs',
+            params={'timestamps': 0, 'follow': 0, 'stderr': 1, 'stdout': 1,
+                    'tail': 'all'},
+            timeout=DEFAULT_TIMEOUT_SECONDS,
+            stream=True
+        )
+
+    def test_log_following(self):
+        with mock.patch('docker.Client.inspect_container',
+                        fake_inspect_container):
+            self.client.logs(fake_api.FAKE_CONTAINER_ID, stream=False,
+                             follow=True)
+
+        fake_request.assert_called_with(
+            'GET',
+            url_prefix + 'containers/3cc2351ab11b/logs',
+            params={'timestamps': 0, 'follow': 1, 'stderr': 1, 'stdout': 1,
+                    'tail': 'all'},
+            timeout=DEFAULT_TIMEOUT_SECONDS,
+            stream=False
+        )
+
+    def test_log_following_backwards(self):
         with mock.patch('docker.Client.inspect_container',
                         fake_inspect_container):
             self.client.logs(fake_api.FAKE_CONTAINER_ID, stream=True)
@@ -1132,12 +1162,27 @@ def test_log_streaming(self):
             stream=True
         )
 
+    def test_log_streaming_and_following(self):
+        with mock.patch('docker.Client.inspect_container',
+                        fake_inspect_container):
+            self.client.logs(fake_api.FAKE_CONTAINER_ID, stream=True,
+                             follow=True)
+
+        fake_request.assert_called_with(
+            'GET',
+            url_prefix + 'containers/3cc2351ab11b/logs',
+            params={'timestamps': 0, 'follow': 1, 'stderr': 1, 'stdout': 1,
+                    'tail': 'all'},
+            timeout=DEFAULT_TIMEOUT_SECONDS,
+            stream=True
+        )
+
     def test_log_tail(self):
 
         with mock.patch('docker.Client.inspect_container',
                         fake_inspect_container):
             self.client.logs(fake_api.FAKE_CONTAINER_ID, stream=False,
-                             tail=10)
+                             follow=False, tail=10)
 
         fake_request.assert_called_with(
             'GET',
@@ -1153,7 +1198,7 @@ def test_log_since(self):
         with mock.patch('docker.Client.inspect_container',
                         fake_inspect_container):
             self.client.logs(fake_api.FAKE_CONTAINER_ID, stream=False,
-                             since=ts)
+                             follow=False, since=ts)
 
         fake_request.assert_called_with(
             'GET',
@@ -1170,7 +1215,7 @@ def test_log_since_with_datetime(self):
         with mock.patch('docker.Client.inspect_container',
                         fake_inspect_container):
             self.client.logs(fake_api.FAKE_CONTAINER_ID, stream=False,
-                             since=time)
+                             follow=False, since=time)
 
         fake_request.assert_called_with(
             'GET',
@@ -1188,7 +1233,7 @@ def test_log_tty(self):
             with mock.patch('docker.Client._stream_raw_result',
                             m):
                 self.client.logs(fake_api.FAKE_CONTAINER_ID,
-                                 stream=True)
+                                 follow=True, stream=True)
 
         self.assertTrue(m.called)
         fake_request.assert_called_with(