Repository: great-expectations/great_expectations
Pull request #6795 (2023-01-13 00:19:16)

[MAINTENANCE] Add exception message to `RenderedAtomicContent` failure renderer
Changes proposed in this pull request:
- GREAT-1590
- Add exception to failed renderer payload for debugging by client

### Definition of Done
- [X] My code follows the Great Expectations [style guide](https://docs.greatexpectations.io/docs/contributing/style_guides/code_style)
- [X] I have performed a [self-review](https://docs.greatexpectations.io/docs/contributing/contributing_checklist) of my own code
- [X] I have commented my code, particularly in hard-to-understand areas
- [X] I have made corresponding changes to the documentation
- [X] I have added [unit tests](https://docs.greatexpectations.io/docs/contributing/contributing_test#writing-unit-and-integration-tests) where applicable and made sure that new and existing tests are passing.
- [X] I have run any local integration tests and made sure that nothing is broken.

netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy Preview for *niobium-lead-7998* processing.


|  Name | Link |
|---------------------------------|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 622e66b36ab37608467589a47d1bf81b067afa72 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/niobium-lead-7998/deploys/63c0a386a1b619000c085b44 |
codesee-maps[bot]: <details>
<summary><b>👇 <a href="https://app.codesee.io/r/reviews?pr=6795&src=https%3A%2F%2Fgithub.com%2Fgreat-expectations%2Fgreat_expectations">Click on the image</a> for a new way to code review</b>
</summary>

  - Make big changes easier — review code in small groups of related files
  - Know where to start — see the whole change at a glance
  - Take a code tour — explore the change with an interactive tour
  - Make comments and review — all fully sync’ed with github

    [Try it now!](https://app.codesee.io/r/reviews?pr=6795&src=https%3A%2F%2Fgithub.com%2Fgreat-expectations%2Fgreat_expectations)
</details>

[![Review these changes using an interactive CodeSee Map](https://s3.us-east-2.amazonaws.com/maps.codesee.io/images/github/great-expectations/great_expectations/6795/54bc60df/9ef38bc879ceef3c6c81a7621d64b1dcc7bde99e.svg)](https://app.codesee.io/r/reviews?pr=6795&src=https%3A%2F%2Fgithub.com%2Fgreat-expectations%2Fgreat_expectations)

#### Legend
![CodeSee Map Legend](https://app.codesee.io/Legend_v2.png)

Patch:
diff --git a/great_expectations/expectations/expectation.py b/great_expectations/expectations/expectation.py
index 6d420fa04..ed27f9fd2 100644
--- a/great_expectations/expectations/expectation.py
+++ b/great_expectations/expectations/expectation.py
@@ -1197,8 +1197,6 @@ class Expectation(metaclass=MetaExpectation):
         data_context: Optional[AbstractDataContext] = None,
         runtime_configuration: Optional[dict] = None,
     ) -> ExpectationValidationResult:
-        include_rendered_content: bool = validator._include_rendered_content or False
-
         if not configuration:
             configuration = deepcopy(self.configuration)
 
@@ -1213,14 +1211,13 @@ class Expectation(metaclass=MetaExpectation):
         configuration.process_evaluation_parameters(
             evaluation_parameters, interactive_evaluation, data_context
         )
-        evr: ExpectationValidationResult = validator.graph_validate(
+        expectation_validation_result_list: list[
+            ExpectationValidationResult
+        ] = validator.graph_validate(
             configurations=[configuration],
             runtime_configuration=runtime_configuration,
-        )[0]
-        if include_rendered_content:
-            evr.render()
-
-        return evr
+        )
+        return expectation_validation_result_list[0]
 
     @property
     def configuration(self) -> ExpectationConfiguration:
diff --git a/great_expectations/render/components.py b/great_expectations/render/components.py
index af701e113..062e9fb58 100644
--- a/great_expectations/render/components.py
+++ b/great_expectations/render/components.py
@@ -7,6 +7,7 @@ from string import Template as pTemplate
 from typing import TYPE_CHECKING, List, Optional, Union
 
 from marshmallow import Schema, fields, post_dump, post_load
+from typing_extensions import Final
 
 from great_expectations.render.exceptions import InvalidRenderedContentError
 from great_expectations.types import DictDot
@@ -708,7 +709,7 @@ class RenderedAtomicValueSchema(Schema):
     def create_value_obj(self, data, **kwargs):
         return RenderedAtomicValue(**data)
 
-    REMOVE_KEYS_IF_NONE = [
+    REMOVE_KEYS_IF_NONE: Final[tuple[str, ...]] = (
         "header",
         "template",
         "table",
@@ -717,7 +718,7 @@ class RenderedAtomicValueSchema(Schema):
         "table",
         "graph",
         "meta_notes",
-    ]
+    )
 
     @post_dump
     def clean_null_attrs(self, data: dict, **kwargs: dict) -> dict:
@@ -742,11 +743,13 @@ class RenderedAtomicContent(RenderedContent):
         name: Union[str, AtomicDiagnosticRendererType, AtomicPrescriptiveRendererType],
         value: RenderedAtomicValue,
         value_type: Optional[str] = None,
+        exception: Optional[str] = None,
     ) -> None:
         # str conversion is performed to ensure Enum value is what is serialized
         self.name = str(name)
         self.value = value
         self.value_type = value_type
+        self.exception = exception
 
     def __repr__(self) -> str:
         return json.dumps(self.to_json_dict(), indent=2)
@@ -756,10 +759,8 @@ class RenderedAtomicContent(RenderedContent):
 
     def to_json_dict(self) -> dict:
         """Returns RenderedAtomicContent as a json dictionary."""
-        d = super().to_json_dict()
-        d["name"] = self.name
+        d = renderedAtomicContentSchema.dump(self)
         d["value"] = self.value.to_json_dict()
-        d["value_type"] = self.value_type
         return d
 
 
@@ -767,10 +768,24 @@ class RenderedAtomicContentSchema(Schema):
     name = fields.String(required=False, allow_none=True)
     value = fields.Nested(RenderedAtomicValueSchema(), required=True, allow_none=False)
     value_type = fields.String(required=True, allow_none=False)
+    exception = fields.String(required=False, allow_none=True)
+
+    REMOVE_KEYS_IF_NONE: Final[tuple[str, ...]] = ("exception",)
 
     @post_load
     def make_rendered_atomic_content(self, data, **kwargs):
         return RenderedAtomicContent(**data)
 
+    @post_dump
+    def clean_null_attrs(self, data: dict, **kwargs: dict) -> dict:
+        """Removes the attributes in RenderedAtomicContentSchema.REMOVE_KEYS_IF_NONE during serialization if
+        their values are None."""
+        data = deepcopy(data)
+        for key in RenderedAtomicContentSchema.REMOVE_KEYS_IF_NONE:
+            if key in data and data[key] is None:
+                data.pop(key)
+        return data
+
 
+renderedAtomicContentSchema = RenderedAtomicContentSchema()
 renderedAtomicValueSchema = RenderedAtomicValueSchema()
diff --git a/great_expectations/render/renderer/inline_renderer.py b/great_expectations/render/renderer/inline_renderer.py
index 966ed3a84..ba6a38e78 100644
--- a/great_expectations/render/renderer/inline_renderer.py
+++ b/great_expectations/render/renderer/inline_renderer.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 import logging
 from typing import TYPE_CHECKING, Callable, List, Optional, Union
 
@@ -121,68 +123,87 @@ class InlineRenderer(Renderer):
         renderer_rendered_content: RenderedAtomicContent
         rendered_content: List[RenderedAtomicContent] = []
         for renderer_name in try_renderer_names:
-            try:
-                renderer_rendered_content = self._get_renderer_atomic_rendered_content(
-                    render_object=render_object,
-                    renderer_name=renderer_name,
-                    expectation_type=expectation_type,
+            renderer_rendered_content = self._get_renderer_atomic_rendered_content(
+                render_object=render_object,
+                renderer_name=renderer_name,
+                expectation_type=expectation_type,
+            )
+            rendered_content.append(renderer_rendered_content)
+
+        return rendered_content
+
+    @staticmethod
+    def _get_renderer_atomic_rendered_content(
+        render_object: ExpectationConfiguration | ExpectationValidationResult,
+        renderer_name: str
+        | AtomicDiagnosticRendererType
+        | AtomicPrescriptiveRendererType,
+        expectation_type: str,
+    ) -> RenderedAtomicContent:
+        renderer_impl: Optional[RendererImpl]
+        try:
+            renderer_impl = get_renderer_impl(
+                object_name=expectation_type, renderer_type=renderer_name
+            )
+            if renderer_impl:
+                renderer_rendered_content = (
+                    InlineRenderer._get_rendered_content_from_renderer_impl(
+                        renderer_impl=renderer_impl,
+                        render_object=render_object,
+                    )
                 )
-                rendered_content.append(renderer_rendered_content)
-            except Exception as e:
-                logger.info(
-                    f'Renderer "{renderer_name}" failed to render Expectation "{expectation_type} with exception message: {str(e)}".'
+            else:
+                raise InlineRendererError(
+                    f"renderer_name: {renderer_name} was not found in the registry for expectation_type: {expectation_type}"
                 )
-                failed_renderer_type: str
-                if renderer_name.startswith(AtomicRendererType.PRESCRIPTIVE):
-                    failed_renderer_type = AtomicPrescriptiveRendererType.FAILED
-                    logger.info(
-                        f'Renderer "{failed_renderer_type}" will be used to render prescriptive content.'
-                    )
-                else:
-                    failed_renderer_type = AtomicDiagnosticRendererType.FAILED
-                    logger.info(
-                        f'Renderer "{failed_renderer_type}" will be used to render diagnostic content.'
-                    )
 
+            assert isinstance(
+                renderer_rendered_content, RenderedAtomicContent
+            ), f"The renderer: {renderer_name} for expectation: {expectation_type} should return RenderedAtomicContent."
+        except Exception as e:
+            error_message = f'Renderer "{renderer_name}" failed to render Expectation "{expectation_type} with exception message: {str(e)}".'
+            logger.info(error_message)
+
+            failure_renderer: AtomicPrescriptiveRendererType | AtomicDiagnosticRendererType
+            if renderer_name.startswith(AtomicRendererType.PRESCRIPTIVE):
+                failure_renderer = AtomicPrescriptiveRendererType.FAILED
+                failure_renderer_message = f'Renderer "{failure_renderer}" will be used to render prescriptive content.'
+            else:
+                failure_renderer = AtomicDiagnosticRendererType.FAILED
+                failure_renderer_message = f'Renderer "{failure_renderer}" will be used to render diagnostic content.'
+            logger.info(failure_renderer_message)
+
+            renderer_impl = get_renderer_impl(
+                object_name=expectation_type, renderer_type=failure_renderer
+            )
+            if renderer_impl:
                 renderer_rendered_content = (
-                    InlineRenderer._get_renderer_atomic_rendered_content(
+                    InlineRenderer._get_rendered_content_from_renderer_impl(
+                        renderer_impl=renderer_impl,
                         render_object=render_object,
-                        renderer_name=failed_renderer_type,
-                        expectation_type=expectation_type,
                     )
                 )
-                rendered_content.append(renderer_rendered_content)
+                renderer_rendered_content.exception = error_message
+            else:
+                raise InlineRendererError(
+                    f'Renderer "{failure_renderer}" was not found in the registry.'
+                )
 
-        return rendered_content
+        return renderer_rendered_content
 
     @staticmethod
-    def _get_renderer_atomic_rendered_content(
-        render_object: Union[ExpectationConfiguration, ExpectationValidationResult],
-        renderer_name: Union[
-            str, AtomicDiagnosticRendererType, AtomicPrescriptiveRendererType
-        ],
-        expectation_type: str,
+    def _get_rendered_content_from_renderer_impl(
+        renderer_impl: RendererImpl,
+        render_object: ExpectationConfiguration | ExpectationValidationResult,
     ) -> RenderedAtomicContent:
-        renderer_impl: Optional[RendererImpl] = get_renderer_impl(
-            object_name=expectation_type, renderer_type=renderer_name
-        )
-        if renderer_impl:
-            renderer_fn: Callable[
-                ..., Union[RenderedAtomicContent, RenderedContent]
-            ] = renderer_impl.renderer
-            if isinstance(render_object, ExpectationConfiguration):
-                renderer_rendered_content = renderer_fn(configuration=render_object)
-            else:
-                renderer_rendered_content = renderer_fn(result=render_object)
+        renderer_fn: Callable[
+            ..., RenderedAtomicContent | RenderedContent
+        ] = renderer_impl.renderer
+        if isinstance(render_object, ExpectationConfiguration):
+            renderer_rendered_content = renderer_fn(configuration=render_object)
         else:
-            raise InlineRendererError(
-                f"renderer_name: {renderer_name} was not found in the registry for expectation_type: {expectation_type}"
-            )
-
-        assert isinstance(
-            renderer_rendered_content, RenderedAtomicContent
-        ), f"The renderer: {renderer_name} for expectation: {expectation_type} should return RenderedAtomicContent."
-
+            renderer_rendered_content = renderer_fn(result=render_object)
+        assert isinstance(renderer_rendered_content, RenderedAtomicContent)
         return renderer_rendered_content
 
     def get_rendered_content(


Test patch:
diff --git a/tests/data_context/test_data_context.py b/tests/data_context/test_data_context.py
index d01a2cbf8..1dd88033d 100644
--- a/tests/data_context/test_data_context.py
+++ b/tests/data_context/test_data_context.py
@@ -3192,6 +3192,8 @@ def test_unrendered_and_failed_prescriptive_renderer_behavior(
                 }
             ),
             value_type="StringValueType",
+            exception='Renderer "atomic.prescriptive.custom_renderer_type" failed to render Expectation '
+            '"expect_sky_to_be_color with exception message: This renderer is broken!".',
         ),
         RenderedAtomicContent(
             name=AtomicPrescriptiveRendererType.SUMMARY,
diff --git a/tests/render/test_inline_renderer.py b/tests/render/test_inline_renderer.py
index 2586cafb9..dc2a2a718 100644
--- a/tests/render/test_inline_renderer.py
+++ b/tests/render/test_inline_renderer.py
@@ -30,7 +30,9 @@ def clean_serialized_rendered_atomic_content_graphs(
 
 
 @pytest.mark.unit
-def test_inline_renderer_error_message(basic_expectation_suite: ExpectationSuite):
+def test_inline_renderer_instantiation_error_message(
+    basic_expectation_suite: ExpectationSuite,
+):
     expectation_suite: ExpectationSuite = basic_expectation_suite
     with pytest.raises(InlineRendererError) as e:
         InlineRenderer(render_object=expectation_suite)  # type: ignore