From 412e6753db055a4feb1cf0dcbd97d0feb9f1d09a Mon Sep 17 00:00:00 2001 From: Ioannis Tziakos Date: Mon, 2 Feb 2026 16:40:33 +0100 Subject: [PATCH 01/11] Update tests to also test subclasses --- traits/util/tests/test_trait_documenter.py | 46 ++++++++++++++++++---- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/traits/util/tests/test_trait_documenter.py b/traits/util/tests/test_trait_documenter.py index bebaf31ff..881c2d4ed 100644 --- a/traits/util/tests/test_trait_documenter.py +++ b/traits/util/tests/test_trait_documenter.py @@ -100,12 +100,18 @@ def not_a_trait(self): """ -class MySubClass(MyTestClass): +class MySubClassAppend(MyTestClass): #: A new attribute. foo = Bool(True) +class MySubClassReplace(MyTestClass): + + #: Replace attribute. + bar = Int(1) + + @requires_sphinx class TestTraitDocumenter(unittest.TestCase): """ Tests for the trait documenter. """ @@ -243,12 +249,12 @@ def test_class(self): for index, line in enumerate(expected): self.assertEqual(calls[index][0], line) - def test_subclass(self): + def test_subclass_append(self): # given documenter = TraitDocumenter(mock.Mock(), 'test') documenter.object_name = 'bar' - documenter.objpath = ['MySubClass', 'bar'] - documenter.parent = MySubClass + documenter.objpath = ['MySubClassAppend', 'bar'] + documenter.parent = MySubClassAppend documenter.modname = 'traits.util.tests.test_trait_documenter' documenter.get_sourcename = mock.Mock(return_value='') documenter.add_line = mock.Mock() @@ -259,7 +265,7 @@ def test_subclass(self): # then self.assertEqual(documenter.directive.warn.call_args_list, []) expected = [ - ('.. py:attribute:: MySubClass.bar', ''), + ('.. py:attribute:: MySubClassAppend.bar', ''), (f' :{no_index}:', ''), (' :module: traits.util.tests.test_trait_documenter', ''), # noqa (' :annotation: = Int(42, desc=""" First line …', '')] # noqa @@ -271,7 +277,7 @@ def test_subclass(self): # given documenter.object_name = 'foo' - documenter.objpath = ['MySubClass', 'foo'] + documenter.objpath = ['MySubClassAppend', 'foo'] documenter.add_line = mock.Mock() # when @@ -280,7 +286,7 @@ def test_subclass(self): # then self.assertEqual(documenter.directive.warn.call_args_list, []) expected = [ - ('.. py:attribute:: MySubClass.foo', ''), + ('.. py:attribute:: MySubClassAppend.foo', ''), (f' :{no_index}:', ''), (' :module: traits.util.tests.test_trait_documenter', ''), # noqa (' :annotation: = Bool(True)', '')] # noqa @@ -290,6 +296,32 @@ def test_subclass(self): for index, line in enumerate(expected): self.assertEqual(calls[index][0], line) + def test_subclass_replace(self): + # given + documenter = TraitDocumenter(mock.Mock(), 'test') + documenter.object_name = 'bar' + documenter.objpath = ['MySubClassReplace', 'bar'] + documenter.parent = MySubClassReplace + 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:: MySubClassReplace.bar', ''), + (f' :{no_index}:', ''), + (' :module: traits.util.tests.test_trait_documenter', ''), # noqa + (' :annotation: = Int(1)', '')] # 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): """ From 540ef1e3c8a9732273c5abbb63ce9569f7622663 Mon Sep 17 00:00:00 2001 From: Ioannis Tziakos Date: Sat, 7 Feb 2026 12:33:21 +0100 Subject: [PATCH 02/11] Use the import_module function for sphinx --- traits/util/trait_documenter.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/traits/util/trait_documenter.py b/traits/util/trait_documenter.py index 7eaca2d53..53faee4d9 100644 --- a/traits/util/trait_documenter.py +++ b/traits/util/trait_documenter.py @@ -13,7 +13,6 @@ (Subclassed from the autodoc ClassLevelDocumenter) """ -from importlib import import_module import inspect import io import types @@ -21,7 +20,7 @@ import tokenize import traceback -from sphinx.ext.autodoc import ClassLevelDocumenter +from sphinx.ext.autodoc import ClassLevelDocumenter, import_module from sphinx.util import logging from traits.has_traits import MetaHasTraits From 088e32a98f685c0cc2e74da199a942fee4bcdbc9 Mon Sep 17 00:00:00 2001 From: Ioannis Tziakos Date: Sat, 7 Feb 2026 12:35:25 +0100 Subject: [PATCH 03/11] Use super to properly call the class parent --- traits/util/trait_documenter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/traits/util/trait_documenter.py b/traits/util/trait_documenter.py index 53faee4d9..f90d80e82 100644 --- a/traits/util/trait_documenter.py +++ b/traits/util/trait_documenter.py @@ -113,7 +113,7 @@ def add_directive_header(self, sig): option set to the trait definition. """ - ClassLevelDocumenter.add_directive_header(self, sig) + super().add_directive_header(sig) # Look into the class and parent classes: parent = self.parent classes = list(types.resolve_bases(parent.__bases__)) From 055179995e6a9f35b970f848859f14018a7734a5 Mon Sep 17 00:00:00 2001 From: Ioannis Tziakos Date: Sat, 7 Feb 2026 12:39:29 +0100 Subject: [PATCH 04/11] Make sure that autodoc is setup before atting the TraitDocumenter --- traits/util/trait_documenter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/traits/util/trait_documenter.py b/traits/util/trait_documenter.py index f90d80e82..e0b281a80 100644 --- a/traits/util/trait_documenter.py +++ b/traits/util/trait_documenter.py @@ -234,10 +234,10 @@ def _get_definition_tokens(tokens): ) definition_tokens.append(item) - return definition_tokens def setup(app): """ Add the TraitDocumenter in the current sphinx autodoc instance. """ + app.setup_extension('sphinx.ext.autodoc') # Require autodoc extension app.add_autodocumenter(TraitDocumenter) From 25dfee90e22c7c5ddf7764a07b2ef57b437de63a Mon Sep 17 00:00:00 2001 From: Ioannis Tziakos Date: Sat, 7 Feb 2026 12:40:43 +0100 Subject: [PATCH 05/11] Use a for else instead of a while loop --- traits/util/trait_documenter.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/traits/util/trait_documenter.py b/traits/util/trait_documenter.py index e0b281a80..663cfaf6a 100644 --- a/traits/util/trait_documenter.py +++ b/traits/util/trait_documenter.py @@ -180,21 +180,14 @@ class MyModel(HasStrictTraits) # find the trait definition start trait_found = False name_found = False - while not trait_found: - item = next(tokens, None) - if item is None: - break + for item in tokens: if name_found and item[:2] == (token.OP, "="): - trait_found = True - continue + break if item[:2] == (token.NAME, trait_name): name_found = True - - if not trait_found: - raise ValueError( - "No trait definition for {!r} found in {!r}".format( - trait_name, cls) - ) + else: + message = "No trait definition for {!r} found in {!r}" + raise ValueError(message.format(trait_name, cls)) # Retrieve the trait definition. definition_tokens = _get_definition_tokens(tokens) From e83517b30bcc6746e7427661d39e27d837e76cf2 Mon Sep 17 00:00:00 2001 From: Ioannis Tziakos Date: Sat, 7 Feb 2026 12:41:04 +0100 Subject: [PATCH 06/11] minor cleanup --- traits/util/trait_documenter.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/traits/util/trait_documenter.py b/traits/util/trait_documenter.py index 663cfaf6a..b8cd2315b 100644 --- a/traits/util/trait_documenter.py +++ b/traits/util/trait_documenter.py @@ -137,7 +137,6 @@ def add_directive_header(self, sig): # throw away all lines after the first. if "\n" in definition: definition = definition.partition("\n")[0] + " …" - self.add_line(" :annotation: = {0}".format(definition), "") @@ -200,8 +199,8 @@ def _get_definition_tokens(tokens): Parameters ---------- - tokens : iterator - An iterator producing tokens. + tokens : iteratable + An iteratable producing tokens. Returns ------- From dda50de0ea99deff06bdf8fc36fb085027ed74ff Mon Sep 17 00:00:00 2001 From: Ioannis Tziakos Date: Sat, 7 Feb 2026 12:49:32 +0100 Subject: [PATCH 07/11] Subclass TraitDocumenter to fix documenting parent attributes --- traits/util/trait_documenter.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/traits/util/trait_documenter.py b/traits/util/trait_documenter.py index b8cd2315b..2dd35104c 100644 --- a/traits/util/trait_documenter.py +++ b/traits/util/trait_documenter.py @@ -10,7 +10,7 @@ """ A Trait Documenter - (Subclassed from the autodoc ClassLevelDocumenter) + (Subclassed from the autodoc AttributeDocumenter) """ import inspect @@ -20,7 +20,7 @@ import tokenize import traceback -from sphinx.ext.autodoc import ClassLevelDocumenter, import_module +from sphinx.ext.autodoc import AttributeDocumenter, import_module from sphinx.util import logging from traits.has_traits import MetaHasTraits @@ -41,7 +41,7 @@ def _is_class_trait(name, cls): ) -class TraitDocumenter(ClassLevelDocumenter): +class TraitDocumenter(AttributeDocumenter): """ Specialized Documenter subclass for trait attributes. The class defines a new documenter that recovers the trait definition From d19e4b49457a4d122c7b166c137febdc5b4cd25b Mon Sep 17 00:00:00 2001 From: Ioannis Tziakos Date: Sat, 7 Feb 2026 12:57:11 +0100 Subject: [PATCH 08/11] Fix flake8 issue --- traits/util/trait_documenter.py | 1 - 1 file changed, 1 deletion(-) diff --git a/traits/util/trait_documenter.py b/traits/util/trait_documenter.py index 2dd35104c..843cfb331 100644 --- a/traits/util/trait_documenter.py +++ b/traits/util/trait_documenter.py @@ -177,7 +177,6 @@ class MyModel(HasStrictTraits) tokens = tokenize.generate_tokens(string_io.readline) # find the trait definition start - trait_found = False name_found = False for item in tokens: if name_found and item[:2] == (token.OP, "="): From 2edcaf26631344b4d9b9d690168f3573ac9982bd Mon Sep 17 00:00:00 2001 From: Ioannis Tziakos Date: Sat, 14 Mar 2026 12:01:42 +0000 Subject: [PATCH 09/11] Minor cleanup --- traits/util/trait_documenter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/traits/util/trait_documenter.py b/traits/util/trait_documenter.py index 843cfb331..a074dcfa9 100644 --- a/traits/util/trait_documenter.py +++ b/traits/util/trait_documenter.py @@ -113,7 +113,6 @@ def add_directive_header(self, sig): option set to the trait definition. """ - super().add_directive_header(sig) # Look into the class and parent classes: parent = self.parent classes = list(types.resolve_bases(parent.__bases__)) @@ -137,7 +136,8 @@ def add_directive_header(self, sig): # throw away all lines after the first. if "\n" in definition: definition = definition.partition("\n")[0] + " …" - self.add_line(" :annotation: = {0}".format(definition), "") + self.options.annotation = f'= {definition}' + super().add_directive_header(sig) def trait_definition(*, cls, trait_name): From adb3e73373f032ef14344b312e95bd07490733a5 Mon Sep 17 00:00:00 2001 From: Ioannis Tziakos Date: Sat, 14 Mar 2026 12:17:10 +0000 Subject: [PATCH 10/11] Update testing for the test documenter --- traits/util/tests/test_trait_documenter.py | 24 +++------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/traits/util/tests/test_trait_documenter.py b/traits/util/tests/test_trait_documenter.py index 881c2d4ed..916b78f23 100644 --- a/traits/util/tests/test_trait_documenter.py +++ b/traits/util/tests/test_trait_documenter.py @@ -119,8 +119,8 @@ class TestTraitDocumenter(unittest.TestCase): def setUp(self): self.source = """ depth_interval = Property(Tuple(Float, Float), - depends_on="_depth_interval") -""" + depends_on="_depth_interval")\n + """ string_io = io.StringIO(self.source) tokens = tokenize.generate_tokens(string_io.readline) self.tokens = tokens @@ -142,24 +142,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", " ") - documenter.object_name = "test_attribute" - documenter.parent = Fake - - with mock.patch( - ( - "traits.util.trait_documenter.ClassLevelDocumenter" - ".add_directive_header" - ) - ): - documenter.add_directive_header("") - - self.assertEqual( - len(documenter.directive.result.append.mock_calls), 1) - def test_abbreviated_annotations(self): # Regression test for enthought/traits#493. with self.create_directive() as directive: @@ -289,7 +271,7 @@ def test_subclass_append(self): ('.. py:attribute:: MySubClassAppend.foo', ''), (f' :{no_index}:', ''), (' :module: traits.util.tests.test_trait_documenter', ''), # noqa - (' :annotation: = Bool(True)', '')] # noqa + (' :annotation: = Bool(True)', '')] if no_index_entry: expected.insert(2, (' :no-index-entry:', '')) calls = documenter.add_line.call_args_list From 41a6005f68af96d60c5f7f41e178482dc4fbe93d Mon Sep 17 00:00:00 2001 From: Ioannis Tziakos Date: Tue, 17 Mar 2026 17:44:12 +0000 Subject: [PATCH 11/11] Update traits/util/trait_documenter.py Co-authored-by: Mark Dickinson --- traits/util/trait_documenter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/traits/util/trait_documenter.py b/traits/util/trait_documenter.py index a074dcfa9..7f7ad262f 100644 --- a/traits/util/trait_documenter.py +++ b/traits/util/trait_documenter.py @@ -198,8 +198,8 @@ def _get_definition_tokens(tokens): Parameters ---------- - tokens : iteratable - An iteratable producing tokens. + tokens : iterable + An iterable producing tokens. Returns -------