diff --git a/python/lib/sift_client/_internal/pytest_plugin/report.py b/python/lib/sift_client/_internal/pytest_plugin/report.py index e0b3351d9..af28cfb84 100644 --- a/python/lib/sift_client/_internal/pytest_plugin/report.py +++ b/python/lib/sift_client/_internal/pytest_plugin/report.py @@ -9,6 +9,7 @@ from __future__ import annotations +import inspect import logging import os import warnings @@ -568,8 +569,11 @@ def step_impl( # erroring out a perfectly valid test. ``getattr``'s default only # suppresses ``AttributeError``; the try/except catches everything else # (RuntimeError from a misbehaving ``__doc__`` descriptor, etc.). + # ``inspect.getdoc`` cleans the docstring (dedents interior lines, trims + # surrounding blank lines) and walks inheritance. try: - existing_docstring = getattr(getattr(node, "obj", None), "__doc__", None) or None + obj = getattr(node, "obj", None) + existing_docstring = (inspect.getdoc(obj) or None) if obj is not None else None except Exception: existing_docstring = None # Attach the leaf under the parent ``_sift_parents`` resolved for this item diff --git a/python/lib/sift_client/_internal/pytest_plugin/steps.py b/python/lib/sift_client/_internal/pytest_plugin/steps.py index eb710bd79..0dfcb816c 100644 --- a/python/lib/sift_client/_internal/pytest_plugin/steps.py +++ b/python/lib/sift_client/_internal/pytest_plugin/steps.py @@ -13,6 +13,7 @@ from __future__ import annotations +import inspect import logging import warnings from typing import TYPE_CHECKING, Any, List, Optional, Tuple @@ -422,9 +423,10 @@ def build_hierarchy_chain( node = node.parent continue try: - doc = ( - (getattr(node, "obj", None) and getattr(node.obj, "__doc__", None)) or "" - ).strip() or None + obj = getattr(node, "obj", None) + # ``inspect.getdoc`` cleans the docstring (dedents interior lines, + # trims surrounding blank lines) and walks inheritance. + doc = (inspect.getdoc(obj) or None) if obj is not None else None except Exception: doc = None chain.append((node.nodeid, node.name, doc, rendered, scope)) diff --git a/python/lib/sift_client/_tests/util/test_test_results_utils.py b/python/lib/sift_client/_tests/util/test_test_results_utils.py index c41587314..b682e6190 100644 --- a/python/lib/sift_client/_tests/util/test_test_results_utils.py +++ b/python/lib/sift_client/_tests/util/test_test_results_utils.py @@ -1,3 +1,4 @@ +import inspect from datetime import datetime, timezone import numpy as np @@ -32,8 +33,22 @@ def test_docstring_description_setup(self, step): Args: step: The step to test. """ - expected_description = self.test_docstring_description_setup.__doc__ + # The plugin stores the cleaned docstring via ``inspect.getdoc``, not + # the raw ``__doc__``. The raw form keeps the source indentation (the + # ``step:`` line sits at 12 spaces); the stored form must be dedented + # (interior indentation reduced to 4) with the trailing blank trimmed. + raw_docstring = self.test_docstring_description_setup.__doc__ + assert raw_docstring is not None + assert "\n step: The step to test." in raw_docstring + expected_description = ( + "Test that the description of a step is set to the docstring of the test function.\n" + "\n" + "Args:\n" + " step: The step to test." + ) assert step.current_step.description == expected_description + # Guard against a regression to raw ``__doc__``: cleaning must change it. + assert step.current_step.description != raw_docstring def helper_function(_step: NewStep): """Helper function description.""" @@ -45,7 +60,7 @@ def helper_function(_step: NewStep): def test_docstring_description_override(self, step): """This description can still be overridden.""" - current_desc = self.test_docstring_description_override.__doc__ + current_desc = inspect.getdoc(self.test_docstring_description_override) assert step.current_step.description == current_desc new_desc = "Manually updated description." step.current_step.update({"description": new_desc})