Repository: docker/docker-py

Issue:
Exception retrieving untagged images on api >= 1.24
Docker API >= 1.24 will return a null object if image tags DNE instead of not including it in the response. This makes the dict.get fail to catch the null case and the list comprehension to iterate over a non-iterable.

```
  File "<stdin>", line 1, in <module>
  File "docker/models/images.py", line 16, in __repr__
    return "<%s: '%s'>" % (self.__class__.__name__, "', '".join(self.tags))
  File "docker/models/images.py", line 34, in tags
    tag for tag in self.attrs.get('RepoTags', [])
TypeError: 'NoneType' object is not iterable
```

This is similar to an issue seen in [salt](https://github.com/saltstack/salt/pull/35447/commits/b833b5f9587534d3b843a026ef91abc4ec929d0f)

Was able to get things working with a pretty quick change:
```
diff --git a/docker/models/images.py b/docker/models/images.py
index 32068e6..39a640d 100644
--- a/docker/models/images.py
+++ b/docker/models/images.py
@@ -30,9 +30,11 @@ class Image(Model):
         """
         The image's tags.
         """
+        tags = self.attrs.get('RepoTags', [])
+        if tags is None:
+            return []
         return [
-            tag for tag in self.attrs.get('RepoTags', [])
-            if tag != '<none>:<none>'
+            tag for tag in tags if tag != '<none>:<none>'
         ]
 
     def history(self):
```

Comments:

Patch:
diff --git a/docker/api/client.py b/docker/api/client.py
index a9fe7d08..22c32b44 100644
--- a/docker/api/client.py
+++ b/docker/api/client.py
@@ -18,16 +18,20 @@ from .service import ServiceApiMixin
 from .swarm import SwarmApiMixin
 from .volume import VolumeApiMixin
 from .. import auth
-from ..constants import (DEFAULT_TIMEOUT_SECONDS, DEFAULT_USER_AGENT,
-                         IS_WINDOWS_PLATFORM, DEFAULT_DOCKER_API_VERSION,
-                         STREAM_HEADER_SIZE_BYTES, DEFAULT_NUM_POOLS,
-                         MINIMUM_DOCKER_API_VERSION)
-from ..errors import (DockerException, TLSParameterError,
-                      create_api_error_from_http_exception)
+from ..constants import (
+    DEFAULT_TIMEOUT_SECONDS, DEFAULT_USER_AGENT, IS_WINDOWS_PLATFORM,
+    DEFAULT_DOCKER_API_VERSION, STREAM_HEADER_SIZE_BYTES, DEFAULT_NUM_POOLS,
+    MINIMUM_DOCKER_API_VERSION
+)
+from ..errors import (
+    DockerException, TLSParameterError,
+    create_api_error_from_http_exception
+)
 from ..tls import TLSConfig
 from ..transport import SSLAdapter, UnixAdapter
 from ..utils import utils, check_resource, update_headers
 from ..utils.socket import frames_iter
+from ..utils.json_stream import json_stream
 try:
     from ..transport import NpipeAdapter
 except ImportError:
@@ -274,27 +278,20 @@ class APIClient(
 
     def _stream_helper(self, response, decode=False):
         """Generator for data coming from a chunked-encoded HTTP response."""
+
         if response.raw._fp.chunked:
-            reader = response.raw
-            while not reader.closed:
-                # this read call will block until we get a chunk
-                data = reader.read(1)
-                if not data:
-                    break
-                if reader._fp.chunk_left:
-                    data += reader.read(reader._fp.chunk_left)
-                if decode:
-                    if six.PY3:
-                        data = data.decode('utf-8')
-                    # remove the trailing newline
-                    data = data.strip()
-                    # split the data at any newlines
-                    data_list = data.split("\r\n")
-                    # load and yield each line seperately
-                    for data in data_list:
-                        data = json.loads(data)
-                        yield data
-                else:
+            if decode:
+                for chunk in json_stream(self._stream_helper(response, False)):
+                    yield chunk
+            else:
+                reader = response.raw
+                while not reader.closed:
+                    # this read call will block until we get a chunk
+                    data = reader.read(1)
+                    if not data:
+                        break
+                    if reader._fp.chunk_left:
+                        data += reader.read(reader._fp.chunk_left)
                     yield data
         else:
             # Response isn't chunked, meaning we probably
diff --git a/docker/models/images.py b/docker/models/images.py
index 32068e69..6f8f4fe2 100644
--- a/docker/models/images.py
+++ b/docker/models/images.py
@@ -30,10 +30,10 @@ class Image(Model):
         """
         The image's tags.
         """
-        return [
-            tag for tag in self.attrs.get('RepoTags', [])
-            if tag != '<none>:<none>'
-        ]
+        tags = self.attrs.get('RepoTags')
+        if tags is None:
+            tags = []
+        return [tag for tag in tags if tag != '<none>:<none>']
 
     def history(self):
         """
diff --git a/docker/models/resource.py b/docker/models/resource.py
index 95712aef..ed3900af 100644
--- a/docker/models/resource.py
+++ b/docker/models/resource.py
@@ -23,6 +23,9 @@ class Model(object):
     def __eq__(self, other):
         return isinstance(other, self.__class__) and self.id == other.id
 
+    def __hash__(self):
+        return hash("%s:%s" % (self.__class__.__name__, self.id))
+
     @property
     def id(self):
         """


Test patch:
diff --git a/tests/unit/models_images_test.py b/tests/unit/models_images_test.py
index 392c58d7..efb21166 100644
--- a/tests/unit/models_images_test.py
+++ b/tests/unit/models_images_test.py
@@ -83,6 +83,11 @@ class ImageTest(unittest.TestCase):
         })
         assert image.tags == []
 
+        image = Image(attrs={
+            'RepoTags': None
+        })
+        assert image.tags == []
+
     def test_history(self):
         client = make_fake_client()
         image = client.images.get(FAKE_IMAGE_ID)
diff --git a/tests/unit/models_resources_test.py b/tests/unit/models_resources_test.py
index 25c6a3ed..5af24ee6 100644
--- a/tests/unit/models_resources_test.py
+++ b/tests/unit/models_resources_test.py
@@ -12,3 +12,17 @@ class ModelTest(unittest.TestCase):
         container.reload()
         assert client.api.inspect_container.call_count == 2
         assert container.attrs['Name'] == "foobar"
+
+    def test_hash(self):
+        client = make_fake_client()
+        container1 = client.containers.get(FAKE_CONTAINER_ID)
+        my_set = set([container1])
+        assert len(my_set) == 1
+
+        container2 = client.containers.get(FAKE_CONTAINER_ID)
+        my_set.add(container2)
+        assert len(my_set) == 1
+
+        image1 = client.images.get(FAKE_CONTAINER_ID)
+        my_set.add(image1)
+        assert len(my_set) == 2