diff --git a/traits/util/tests/test_trait_documenter.py b/traits/util/tests/test_trait_documenter.py index fcef1a6e5..172a4b4d0 100644 --- a/traits/util/tests/test_trait_documenter.py +++ b/traits/util/tests/test_trait_documenter.py @@ -41,6 +41,9 @@ else: from pathlib import Path + no_index_entry = sphinx.version_info >= (8, 2) + no_index = 'noindex' if sphinx.version_info < (7, 2) else 'no-index' + # Configuration file content for testing. CONF_PY = """\ @@ -97,6 +100,12 @@ def not_a_trait(self): """ +class MySubClass(MyTestClass): + + #: A new attribute. + foo = Bool(True) + + @requires_sphinx class TestTraitDocumenter(unittest.TestCase): """ Tests for the trait documenter. """ @@ -128,7 +137,6 @@ def test_get_definition_tokens(self): self.assertEqual(src.rstrip(), string) def test_add_line(self): - mocked_directive = mock.MagicMock() documenter = TraitDocumenter(mocked_directive, "test", " ") @@ -209,6 +217,79 @@ def test_can_document_member(self): ) ) + def test_class(self): + # given + documenter = TraitDocumenter(mock.Mock(), 'test') + documenter.parent = MyTestClass + documenter.object_name = 'bar' + documenter.modname = 'traits.util.tests.test_trait_documenter' + documenter.get_sourcename = mock.Mock(return_value='') + documenter.objpath = ['MyTestClass', 'bar'] + documenter.add_line = mock.Mock() + + # when + documenter.add_directive_header('') + + # then + self.assertEqual(documenter.directive.warn.call_args_list, []) + expected = [ + ('.. py:attribute:: MyTestClass.bar', ''), + (f' :{no_index}:', ''), + (' :module: traits.util.tests.test_trait_documenter', ''), # noqa + (' :annotation: = Int(42, desc=""" First line …', '')] # noqa + if no_index_entry: + expected.insert(2, (' :no-index-entry:', '')) + calls = documenter.add_line.call_args_list + for index, line in enumerate(expected): + self.assertEqual(calls[index][0], line) + + def test_subclass(self): + # given + documenter = TraitDocumenter(mock.Mock(), 'test') + documenter.object_name = 'bar' + documenter.objpath = ['MySubClass', 'bar'] + documenter.parent = MySubClass + documenter.modname = 'traits.util.tests.test_trait_documenter' + documenter.get_sourcename = mock.Mock(return_value='') + documenter.add_line = mock.Mock() + + # when + documenter.add_directive_header('') + + # then + self.assertEqual(documenter.directive.warn.call_args_list, []) + expected = [ + ('.. py:attribute:: MySubClass.bar', ''), + (f' :{no_index}:', ''), + (' :module: traits.util.tests.test_trait_documenter', ''), # noqa + (' :annotation: = Int(42, desc=""" First line …', '')] # noqa + if no_index_entry: + expected.insert(2, (' :no-index-entry:', '')) + calls = documenter.add_line.call_args_list + for index, line in enumerate(expected): + self.assertEqual(calls[index][0], line) + + # given + documenter.object_name = 'foo' + documenter.objpath = ['MySubClass', 'foo'] + documenter.add_line = mock.Mock() + + # when + documenter.add_directive_header('') + + # then + self.assertEqual(documenter.directive.warn.call_args_list, []) + expected = [ + ('.. py:attribute:: MySubClass.foo', ''), + (f' :{no_index}:', ''), + (' :module: traits.util.tests.test_trait_documenter', ''), # noqa + (' :annotation: = Bool(True)', '')] # noqa + if no_index_entry: + expected.insert(2, (' :no-index-entry:', '')) + calls = documenter.add_line.call_args_list + for index, line in enumerate(expected): + self.assertEqual(calls[index][0], line) + @contextlib.contextmanager def create_directive(self): """ diff --git a/traits/util/trait_documenter.py b/traits/util/trait_documenter.py index 00048249c..62f3fc38a 100644 --- a/traits/util/trait_documenter.py +++ b/traits/util/trait_documenter.py @@ -16,6 +16,7 @@ from importlib import import_module import inspect import io +import types import token import tokenize import traceback @@ -114,17 +115,22 @@ def add_directive_header(self, sig): """ ClassLevelDocumenter.add_directive_header(self, sig) - try: - definition = trait_definition( - cls=self.parent, - trait_name=self.object_name, - ) - except ValueError: - # Without this, a failure to find the trait definition aborts - # the whole documentation build. + # Look into the class and parent classes: + parent = self.parent + classes = list(types.resolve_bases(parent.__bases__)) + classes.insert(0, parent) + for cls in classes: + try: + definition = trait_definition( + cls=cls, trait_name=self.object_name) + except ValueError: + continue + else: + break + else: logger.warning( "No definition for the trait {!r} could be found in " - "class {!r}.".format(self.object_name, self.parent), + "class {!r}.".format(self.object_name, parent), exc_info=True) return