From f0062d3057ddd5b4c1c13acb7e06963c791c37b3 Mon Sep 17 00:00:00 2001 From: Ioannis Tziakos Date: Sat, 8 Nov 2025 12:40:55 +0000 Subject: [PATCH 1/7] Augment testing for trait_documenter Add tests for class and subclass attributes --- traits/util/tests/test_trait_documenter.py | 76 +++++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/traits/util/tests/test_trait_documenter.py b/traits/util/tests/test_trait_documenter.py index fcef1a6e5..df122c5e0 100644 --- a/traits/util/tests/test_trait_documenter.py +++ b/traits/util/tests/test_trait_documenter.py @@ -96,6 +96,11 @@ def not_a_trait(self): I'm a regular property, not a trait. """ +class MySubClass(MyTestClass): + + #: A new attribute. + foo = Bool(True) + @requires_sphinx class TestTraitDocumenter(unittest.TestCase): @@ -128,7 +133,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 +213,76 @@ 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', ''), + (' :no-index:', ''), + (' :no-index-entry:', ''), + (' :module: traits.util.tests.test_trait_documenter', ''), + (' :annotation: = Int(42, desc=""" First line …', '')] # noqa + 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', ''), + (' :no-index:', ''), + (' :no-index-entry:', ''), + (' :module: traits.util.tests.test_trait_documenter', ''), + (' :annotation: = Int(42, desc=""" First line …', '')] # noqa + 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', ''), + (' :no-index:', ''), + (' :no-index-entry:', ''), + (' :module: traits.util.tests.test_trait_documenter', ''), + (' :annotation: = Bool(True)', '')] # noqa + 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): """ From 1312b395be4798fee8b30668794404e4c22600bd Mon Sep 17 00:00:00 2001 From: Ioannis Tziakos Date: Sat, 8 Nov 2025 12:41:33 +0000 Subject: [PATCH 2/7] Rework TraitDocumenter to fix subclass trait support Look into the subclasses if the trait is not found in the current class. --- traits/util/trait_documenter.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/traits/util/trait_documenter.py b/traits/util/trait_documenter.py index 00048249c..7a7630c13 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,14 +115,17 @@ 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: + classes = [self.parent] + list(types.resolve_bases(self.parent.__bases__)) + 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), From 3a7030d25c8169e0792d00745ed4430e4c45ec34 Mon Sep 17 00:00:00 2001 From: Ioannis Tziakos Date: Sat, 8 Nov 2025 12:55:18 +0000 Subject: [PATCH 3/7] Flake8 cleanup --- traits/util/tests/test_trait_documenter.py | 7 ++++--- traits/util/trait_documenter.py | 6 ++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/traits/util/tests/test_trait_documenter.py b/traits/util/tests/test_trait_documenter.py index df122c5e0..23f8c58a2 100644 --- a/traits/util/tests/test_trait_documenter.py +++ b/traits/util/tests/test_trait_documenter.py @@ -96,6 +96,7 @@ def not_a_trait(self): I'm a regular property, not a trait. """ + class MySubClass(MyTestClass): #: A new attribute. @@ -232,7 +233,7 @@ def test_class(self): ('.. py:attribute:: MyTestClass.bar', ''), (' :no-index:', ''), (' :no-index-entry:', ''), - (' :module: traits.util.tests.test_trait_documenter', ''), + (' :module: traits.util.tests.test_trait_documenter', ''), # noqa (' :annotation: = Int(42, desc=""" First line …', '')] # noqa calls = documenter.add_line.call_args_list for index, line in enumerate(expected): @@ -257,7 +258,7 @@ def test_subclass(self): ('.. py:attribute:: MySubClass.bar', ''), (' :no-index:', ''), (' :no-index-entry:', ''), - (' :module: traits.util.tests.test_trait_documenter', ''), + (' :module: traits.util.tests.test_trait_documenter', ''), # noqa (' :annotation: = Int(42, desc=""" First line …', '')] # noqa calls = documenter.add_line.call_args_list for index, line in enumerate(expected): @@ -277,7 +278,7 @@ def test_subclass(self): ('.. py:attribute:: MySubClass.foo', ''), (' :no-index:', ''), (' :no-index-entry:', ''), - (' :module: traits.util.tests.test_trait_documenter', ''), + (' :module: traits.util.tests.test_trait_documenter', ''), # noqa (' :annotation: = Bool(True)', '')] # noqa calls = documenter.add_line.call_args_list for index, line in enumerate(expected): diff --git a/traits/util/trait_documenter.py b/traits/util/trait_documenter.py index 7a7630c13..62f3fc38a 100644 --- a/traits/util/trait_documenter.py +++ b/traits/util/trait_documenter.py @@ -116,7 +116,9 @@ def add_directive_header(self, sig): """ ClassLevelDocumenter.add_directive_header(self, sig) # Look into the class and parent classes: - classes = [self.parent] + list(types.resolve_bases(self.parent.__bases__)) + parent = self.parent + classes = list(types.resolve_bases(parent.__bases__)) + classes.insert(0, parent) for cls in classes: try: definition = trait_definition( @@ -128,7 +130,7 @@ def add_directive_header(self, sig): 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 From 8a085a82fa38e2a609dac075055d73526f7c31eb Mon Sep 17 00:00:00 2001 From: Ioannis Tziakos Date: Sat, 8 Nov 2025 13:03:52 +0000 Subject: [PATCH 4/7] Support older sphinx in tests --- traits/util/tests/test_trait_documenter.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/traits/util/tests/test_trait_documenter.py b/traits/util/tests/test_trait_documenter.py index 23f8c58a2..2bd916469 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) + + # Configuration file content for testing. CONF_PY = """\ @@ -232,9 +235,10 @@ def test_class(self): expected = [ ('.. py:attribute:: MyTestClass.bar', ''), (' :no-index:', ''), - (' :no-index-entry:', ''), (' :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) @@ -257,9 +261,10 @@ def test_subclass(self): expected = [ ('.. py:attribute:: MySubClass.bar', ''), (' :no-index:', ''), - (' :no-index-entry:', ''), (' :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) @@ -277,9 +282,10 @@ def test_subclass(self): expected = [ ('.. py:attribute:: MySubClass.foo', ''), (' :no-index:', ''), - (' :no-index-entry:', ''), (' :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) From 77b348f7419719d447484ab6432db5992755f9de Mon Sep 17 00:00:00 2001 From: Ioannis Tziakos Date: Sat, 8 Nov 2025 13:05:38 +0000 Subject: [PATCH 5/7] flake8 cleanup --- traits/util/tests/test_trait_documenter.py | 1 - 1 file changed, 1 deletion(-) diff --git a/traits/util/tests/test_trait_documenter.py b/traits/util/tests/test_trait_documenter.py index 2bd916469..79b5b65da 100644 --- a/traits/util/tests/test_trait_documenter.py +++ b/traits/util/tests/test_trait_documenter.py @@ -44,7 +44,6 @@ no_index_entry = sphinx.version_info >= (8, 2) - # Configuration file content for testing. CONF_PY = """\ extensions = ['sphinx.ext.autodoc'] From b5fecea3727d928105f473751b4a99577015a5fc Mon Sep 17 00:00:00 2001 From: Ioannis Tziakos Date: Sat, 8 Nov 2025 13:12:30 +0000 Subject: [PATCH 6/7] More check for older sphinx versions --- traits/util/tests/test_trait_documenter.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/traits/util/tests/test_trait_documenter.py b/traits/util/tests/test_trait_documenter.py index 79b5b65da..f55bc87e0 100644 --- a/traits/util/tests/test_trait_documenter.py +++ b/traits/util/tests/test_trait_documenter.py @@ -42,6 +42,7 @@ 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. @@ -233,7 +234,7 @@ def test_class(self): self.assertEqual(documenter.directive.warn.call_args_list, []) expected = [ ('.. py:attribute:: MyTestClass.bar', ''), - (' :no-index:', ''), + (f' :{no_index}:', ''), (' :module: traits.util.tests.test_trait_documenter', ''), # noqa (' :annotation: = Int(42, desc=""" First line …', '')] # noqa if no_index_entry: @@ -259,10 +260,10 @@ def test_subclass(self): self.assertEqual(documenter.directive.warn.call_args_list, []) expected = [ ('.. py:attribute:: MySubClass.bar', ''), - (' :no-index:', ''), + (f' :{no_index}:', ''), (' :module: traits.util.tests.test_trait_documenter', ''), # noqa (' :annotation: = Int(42, desc=""" First line …', '')] # noqa - if no_index_entry: + if no_index: expected.insert(2, (' :no-index-entry:', '')) calls = documenter.add_line.call_args_list for index, line in enumerate(expected): @@ -280,7 +281,7 @@ def test_subclass(self): self.assertEqual(documenter.directive.warn.call_args_list, []) expected = [ ('.. py:attribute:: MySubClass.foo', ''), - (' :no-index:', ''), + (f' :{no_index}:', ''), (' :module: traits.util.tests.test_trait_documenter', ''), # noqa (' :annotation: = Bool(True)', '')] # noqa if no_index_entry: From 496f8fc48bb3ffdc2d525299350cdfbd6baa40a4 Mon Sep 17 00:00:00 2001 From: Ioannis Tziakos Date: Sat, 8 Nov 2025 13:15:12 +0000 Subject: [PATCH 7/7] Fix variable name no_index -> no_index_entry --- traits/util/tests/test_trait_documenter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/traits/util/tests/test_trait_documenter.py b/traits/util/tests/test_trait_documenter.py index f55bc87e0..172a4b4d0 100644 --- a/traits/util/tests/test_trait_documenter.py +++ b/traits/util/tests/test_trait_documenter.py @@ -263,7 +263,7 @@ def test_subclass(self): (f' :{no_index}:', ''), (' :module: traits.util.tests.test_trait_documenter', ''), # noqa (' :annotation: = Int(42, desc=""" First line …', '')] # noqa - if no_index: + if no_index_entry: expected.insert(2, (' :no-index-entry:', '')) calls = documenter.add_line.call_args_list for index, line in enumerate(expected):