Repository: docker/docker-py

Issue:
attach is causing an "Invalid Argument" exception from os.read
``` python
stream = client.attach(container, stream=True, stdout=True, stderr=True)
for chunk in stream:
    pass
```

Results in:

```
  File "/Users/michael/work/oss/marina/marina/build.py", line 695, in watcher
    for chunk in stream:
  File ".venv/lib/python3.5/site-packages/docker/utils/socket.py", line 67, in frames_iter
    yield read(socket, n)
  File ".venv/lib/python3.5/site-packages/docker/utils/socket.py", line 25, in read
    return os.read(socket.fileno(), n)
OSError: [Errno 22] Invalid argument
```

Using docker-py 1.10.2 on OS X 10.11.6 with docker for mac 1.12.0-rc3. Reverting to 1.9.0 fixes the issue.


Comments:

Patch:
diff --git a/Dockerfile b/Dockerfile
index 012a1259..993ac012 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,4 @@
 FROM python:2.7
-MAINTAINER Joffrey F <joffrey@docker.com>
 
 RUN mkdir /home/docker-py
 WORKDIR /home/docker-py
diff --git a/Dockerfile-py3 b/Dockerfile-py3
index 21e713bb..c7466517 100644
--- a/Dockerfile-py3
+++ b/Dockerfile-py3
@@ -1,5 +1,4 @@
 FROM python:3.5
-MAINTAINER Joffrey F <joffrey@docker.com>
 
 RUN mkdir /home/docker-py
 WORKDIR /home/docker-py
diff --git a/docker/api/image.py b/docker/api/image.py
index 2c8cbb23..c1ebc69c 100644
--- a/docker/api/image.py
+++ b/docker/api/image.py
@@ -469,6 +469,11 @@ class ImageApiMixin(object):
         Raises:
             :py:class:`docker.errors.APIError`
                 If the server returns an error.
+
+        Example:
+
+            >>> client.tag('ubuntu', 'localhost:5000/ubuntu', 'latest',
+                           force=True)
         """
         params = {
             'tag': tag,
diff --git a/docker/utils/socket.py b/docker/utils/socket.py
index 164b845a..4080f253 100644
--- a/docker/utils/socket.py
+++ b/docker/utils/socket.py
@@ -69,7 +69,11 @@ def frames_iter(socket):
     """
     Returns a generator of frames read from socket
     """
-    n = next_frame_size(socket)
-    while n > 0:
-        yield read(socket, n)
+    while True:
         n = next_frame_size(socket)
+        if n == 0:
+            break
+        while n > 0:
+            result = read(socket, n)
+            n -= len(result)
+            yield result
diff --git a/docker/utils/utils.py b/docker/utils/utils.py
index b107f22e..823894c3 100644
--- a/docker/utils/utils.py
+++ b/docker/utils/utils.py
@@ -735,9 +735,9 @@ def create_host_config(binds=None, port_bindings=None, lxc_conf=None,
 
         host_config['ShmSize'] = shm_size
 
-    if pid_mode not in (None, 'host'):
-        raise host_config_value_error('pid_mode', pid_mode)
-    elif pid_mode:
+    if pid_mode:
+        if version_lt(version, '1.24') and pid_mode != 'host':
+            raise host_config_value_error('pid_mode', pid_mode)
         host_config['PidMode'] = pid_mode
 
     if ipc_mode:
@@ -1052,7 +1052,11 @@ def parse_env_file(env_file):
             if line[0] == '#':
                 continue
 
-            parse_line = line.strip().split('=', 1)
+            line = line.strip()
+            if not line:
+                continue
+
+            parse_line = line.split('=', 1)
             if len(parse_line) == 2:
                 k, v = parse_line
                 environment[k] = v
diff --git a/setup.py b/setup.py
index edf4b0e5..3f2e3c4a 100644
--- a/setup.py
+++ b/setup.py
@@ -1,4 +1,5 @@
 #!/usr/bin/env python
+import codecs
 import os
 import sys
 
@@ -35,7 +36,7 @@ with open('./test-requirements.txt') as test_reqs_txt:
 
 long_description = ''
 try:
-    with open('./README.rst') as readme_rst:
+    with codecs.open('./README.rst', encoding='utf-8') as readme_rst:
         long_description = readme_rst.read()
 except IOError:
     # README.rst is only generated on release. Its absence should not prevent


Test patch:
diff --git a/tests/integration/api_build_test.py b/tests/integration/api_build_test.py
index 9ae74f4d..3dac0e93 100644
--- a/tests/integration/api_build_test.py
+++ b/tests/integration/api_build_test.py
@@ -15,7 +15,6 @@ class BuildTest(BaseAPIIntegrationTest):
     def test_build_streaming(self):
         script = io.BytesIO('\n'.join([
             'FROM busybox',
-            'MAINTAINER docker-py',
             'RUN mkdir -p /tmp/test',
             'EXPOSE 8080',
             'ADD https://dl.dropboxusercontent.com/u/20637798/silence.tar.gz'
@@ -32,7 +31,6 @@ class BuildTest(BaseAPIIntegrationTest):
             return
         script = io.StringIO(six.text_type('\n').join([
             'FROM busybox',
-            'MAINTAINER docker-py',
             'RUN mkdir -p /tmp/test',
             'EXPOSE 8080',
             'ADD https://dl.dropboxusercontent.com/u/20637798/silence.tar.gz'
@@ -54,7 +52,6 @@ class BuildTest(BaseAPIIntegrationTest):
         with open(os.path.join(base_dir, 'Dockerfile'), 'w') as f:
             f.write("\n".join([
                 'FROM busybox',
-                'MAINTAINER docker-py',
                 'ADD . /test',
             ]))
 
@@ -182,7 +179,6 @@ class BuildTest(BaseAPIIntegrationTest):
         with open(os.path.join(base_dir, 'Dockerfile'), 'w') as f:
             f.write("\n".join([
                 'FROM busybox',
-                'MAINTAINER docker-py',
                 'ADD . /test',
             ]))
 
diff --git a/tests/integration/api_container_test.py b/tests/integration/api_container_test.py
index a5be6e76..f09e75ad 100644
--- a/tests/integration/api_container_test.py
+++ b/tests/integration/api_container_test.py
@@ -361,13 +361,6 @@ class CreateContainerTest(BaseAPIIntegrationTest):
         host_config = inspect['HostConfig']
         self.assertIn('MemorySwappiness', host_config)
 
-    def test_create_host_config_exception_raising(self):
-        self.assertRaises(TypeError,
-                          self.client.create_host_config, mem_swappiness='40')
-
-        self.assertRaises(ValueError,
-                          self.client.create_host_config, pid_mode='40')
-
     def test_create_with_environment_variable_no_value(self):
         container = self.client.create_container(
             BUSYBOX,
diff --git a/tests/unit/api_build_test.py b/tests/unit/api_build_test.py
index 8146fee7..927aa974 100644
--- a/tests/unit/api_build_test.py
+++ b/tests/unit/api_build_test.py
@@ -11,7 +11,6 @@ class BuildTest(BaseAPIClientTest):
     def test_build_container(self):
         script = io.BytesIO('\n'.join([
             'FROM busybox',
-            'MAINTAINER docker-py',
             'RUN mkdir -p /tmp/test',
             'EXPOSE 8080',
             'ADD https://dl.dropboxusercontent.com/u/20637798/silence.tar.gz'
@@ -23,7 +22,6 @@ class BuildTest(BaseAPIClientTest):
     def test_build_container_pull(self):
         script = io.BytesIO('\n'.join([
             'FROM busybox',
-            'MAINTAINER docker-py',
             'RUN mkdir -p /tmp/test',
             'EXPOSE 8080',
             'ADD https://dl.dropboxusercontent.com/u/20637798/silence.tar.gz'
@@ -35,7 +33,6 @@ class BuildTest(BaseAPIClientTest):
     def test_build_container_stream(self):
         script = io.BytesIO('\n'.join([
             'FROM busybox',
-            'MAINTAINER docker-py',
             'RUN mkdir -p /tmp/test',
             'EXPOSE 8080',
             'ADD https://dl.dropboxusercontent.com/u/20637798/silence.tar.gz'
@@ -47,7 +44,6 @@ class BuildTest(BaseAPIClientTest):
     def test_build_container_custom_context(self):
         script = io.BytesIO('\n'.join([
             'FROM busybox',
-            'MAINTAINER docker-py',
             'RUN mkdir -p /tmp/test',
             'EXPOSE 8080',
             'ADD https://dl.dropboxusercontent.com/u/20637798/silence.tar.gz'
@@ -60,7 +56,6 @@ class BuildTest(BaseAPIClientTest):
     def test_build_container_custom_context_gzip(self):
         script = io.BytesIO('\n'.join([
             'FROM busybox',
-            'MAINTAINER docker-py',
             'RUN mkdir -p /tmp/test',
             'EXPOSE 8080',
             'ADD https://dl.dropboxusercontent.com/u/20637798/silence.tar.gz'
diff --git a/tests/unit/utils_test.py b/tests/unit/utils_test.py
index 57aa226d..19d52c9f 100644
--- a/tests/unit/utils_test.py
+++ b/tests/unit/utils_test.py
@@ -205,6 +205,19 @@ class HostConfigTest(unittest.TestCase):
                 version='1.24', isolation={'isolation': 'hyperv'}
             )
 
+    def test_create_host_config_pid_mode(self):
+        with pytest.raises(ValueError):
+            create_host_config(version='1.23', pid_mode='baccab125')
+
+        config = create_host_config(version='1.23', pid_mode='host')
+        assert config.get('PidMode') == 'host'
+        config = create_host_config(version='1.24', pid_mode='baccab125')
+        assert config.get('PidMode') == 'baccab125'
+
+    def test_create_host_config_invalid_mem_swappiness(self):
+        with pytest.raises(TypeError):
+            create_host_config(version='1.24', mem_swappiness='40')
+
 
 class UlimitTest(unittest.TestCase):
     def test_create_host_config_dict_ulimit(self):
@@ -465,10 +478,18 @@ class ParseEnvFileTest(unittest.TestCase):
     def test_parse_env_file_commented_line(self):
         env_file = self.generate_tempfile(
             file_content='USER=jdoe\n#PASS=secret')
-        get_parse_env_file = parse_env_file((env_file))
+        get_parse_env_file = parse_env_file(env_file)
         self.assertEqual(get_parse_env_file, {'USER': 'jdoe'})
         os.unlink(env_file)
 
+    def test_parse_env_file_newline(self):
+        env_file = self.generate_tempfile(
+            file_content='\nUSER=jdoe\n\n\nPASS=secret')
+        get_parse_env_file = parse_env_file(env_file)
+        self.assertEqual(get_parse_env_file,
+                         {'USER': 'jdoe', 'PASS': 'secret'})
+        os.unlink(env_file)
+
     def test_parse_env_file_invalid_line(self):
         env_file = self.generate_tempfile(
             file_content='USER jdoe')