Repository: docker/docker-py

Issue:
Returning image build logs in higher-level API?
I'm currently working with building images and would love for build logs to be returned in the higher-level APIs so these can be used for logging errors. Could the generator or the full stream from the low-level API perhaps be returned by the `BuildError` exception? The logs when running a container are also available and I'd say both logs are equally as important when automating applications with docker. When adding the information to the `BuildError` exception, it wouldn't break backwards-compatibility either.

Could this be discussed? Was there a reason why the logs were not passed along with the higher-level API, or was it just an oversight perhaps? I can make a PR of what is discussed.


Discussion:

Resolving patch:
diff --git a/docker/errors.py b/docker/errors.py
--- a/docker/errors.py
+++ b/docker/errors.py
@@ -140,8 +140,11 @@ def __init__(self, reason):
         self.msg = reason
 
 
-class BuildError(Exception):
-    pass
+class BuildError(DockerException):
+    def __init__(self, reason, build_log):
+        super(BuildError, self).__init__(reason)
+        self.msg = reason
+        self.build_log = build_log
 
 
 class ImageLoadError(DockerException):
diff --git a/docker/models/images.py b/docker/models/images.py
--- a/docker/models/images.py
+++ b/docker/models/images.py
@@ -1,3 +1,4 @@
+import itertools
 import re
 
 import six
@@ -160,7 +161,9 @@ def build(self, **kwargs):
             platform (str): Platform in the format ``os[/arch[/variant]]``.
 
         Returns:
-            (:py:class:`Image`): The built image.
+            (tuple): The first item is the :py:class:`Image` object for the
+                image that was build. The second item is a generator of the
+                build logs as JSON-decoded objects.
 
         Raises:
             :py:class:`docker.errors.BuildError`
@@ -175,9 +178,10 @@ def build(self, **kwargs):
             return self.get(resp)
         last_event = None
         image_id = None
-        for chunk in json_stream(resp):
+        result_stream, internal_stream = itertools.tee(json_stream(resp))
+        for chunk in internal_stream:
             if 'error' in chunk:
-                raise BuildError(chunk['error'])
+                raise BuildError(chunk['error'], result_stream)
             if 'stream' in chunk:
                 match = re.search(
                     r'(^Successfully built |sha256:)([0-9a-f]+)$',
@@ -187,8 +191,8 @@ def build(self, **kwargs):
                     image_id = match.group(2)
             last_event = chunk
         if image_id:
-            return self.get(image_id)
-        raise BuildError(last_event or 'Unknown')
+            return (self.get(image_id), result_stream)
+        raise BuildError(last_event or 'Unknown', result_stream)
 
     def get(self, name):
         """


Test patch:
diff --git a/tests/integration/models_images_test.py b/tests/integration/models_images_test.py
--- a/tests/integration/models_images_test.py
+++ b/tests/integration/models_images_test.py
@@ -10,27 +10,30 @@ class ImageCollectionTest(BaseIntegrationTest):
 
     def test_build(self):
         client = docker.from_env(version=TEST_API_VERSION)
-        image = client.images.build(fileobj=io.BytesIO(
+        image, _ = client.images.build(fileobj=io.BytesIO(
             "FROM alpine\n"
             "CMD echo hello world".encode('ascii')
         ))
         self.tmp_imgs.append(image.id)
         assert client.containers.run(image) == b"hello world\n"
 
-    @pytest.mark.xfail(reason='Engine 1.13 responds with status 500')
+    # @pytest.mark.xfail(reason='Engine 1.13 responds with status 500')
     def test_build_with_error(self):
         client = docker.from_env(version=TEST_API_VERSION)
         with self.assertRaises(docker.errors.BuildError) as cm:
             client.images.build(fileobj=io.BytesIO(
                 "FROM alpine\n"
-                "NOTADOCKERFILECOMMAND".encode('ascii')
+                "RUN exit 1".encode('ascii')
             ))
-        assert str(cm.exception) == ("Unknown instruction: "
-                                     "NOTADOCKERFILECOMMAND")
+        print(cm.exception)
+        assert str(cm.exception) == (
+            "The command '/bin/sh -c exit 1' returned a non-zero code: 1"
+        )
+        assert cm.exception.build_log
 
     def test_build_with_multiple_success(self):
         client = docker.from_env(version=TEST_API_VERSION)
-        image = client.images.build(
+        image, _ = client.images.build(
             tag='some-tag', fileobj=io.BytesIO(
                 "FROM alpine\n"
                 "CMD echo hello world".encode('ascii')
@@ -41,7 +44,7 @@ def test_build_with_multiple_success(self):
 
     def test_build_with_success_build_output(self):
         client = docker.from_env(version=TEST_API_VERSION)
-        image = client.images.build(
+        image, _ = client.images.build(
             tag='dup-txt-tag', fileobj=io.BytesIO(
                 "FROM alpine\n"
                 "CMD echo Successfully built abcd1234".encode('ascii')