Skip to content

Commit e3334d7

Browse files
authored
Fix/subclass support in trait documenter (#1866)
- Augment tests for trait_documenter - Update code to document traits in subclasses fixes #1865
1 parent b901580 commit e3334d7

File tree

2 files changed

+97
-10
lines changed

2 files changed

+97
-10
lines changed

traits/util/tests/test_trait_documenter.py

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@
4141
else:
4242
from pathlib import Path
4343

44+
no_index_entry = sphinx.version_info >= (8, 2)
45+
no_index = 'noindex' if sphinx.version_info < (7, 2) else 'no-index'
46+
4447

4548
# Configuration file content for testing.
4649
CONF_PY = """\
@@ -97,6 +100,12 @@ def not_a_trait(self):
97100
"""
98101

99102

103+
class MySubClass(MyTestClass):
104+
105+
#: A new attribute.
106+
foo = Bool(True)
107+
108+
100109
@requires_sphinx
101110
class TestTraitDocumenter(unittest.TestCase):
102111
""" Tests for the trait documenter. """
@@ -128,7 +137,6 @@ def test_get_definition_tokens(self):
128137
self.assertEqual(src.rstrip(), string)
129138

130139
def test_add_line(self):
131-
132140
mocked_directive = mock.MagicMock()
133141

134142
documenter = TraitDocumenter(mocked_directive, "test", " ")
@@ -209,6 +217,79 @@ def test_can_document_member(self):
209217
)
210218
)
211219

220+
def test_class(self):
221+
# given
222+
documenter = TraitDocumenter(mock.Mock(), 'test')
223+
documenter.parent = MyTestClass
224+
documenter.object_name = 'bar'
225+
documenter.modname = 'traits.util.tests.test_trait_documenter'
226+
documenter.get_sourcename = mock.Mock(return_value='<autodoc>')
227+
documenter.objpath = ['MyTestClass', 'bar']
228+
documenter.add_line = mock.Mock()
229+
230+
# when
231+
documenter.add_directive_header('')
232+
233+
# then
234+
self.assertEqual(documenter.directive.warn.call_args_list, [])
235+
expected = [
236+
('.. py:attribute:: MyTestClass.bar', '<autodoc>'),
237+
(f' :{no_index}:', '<autodoc>'),
238+
(' :module: traits.util.tests.test_trait_documenter', '<autodoc>'), # noqa
239+
(' :annotation: = Int(42, desc=""" First line …', '<autodoc>')] # noqa
240+
if no_index_entry:
241+
expected.insert(2, (' :no-index-entry:', '<autodoc>'))
242+
calls = documenter.add_line.call_args_list
243+
for index, line in enumerate(expected):
244+
self.assertEqual(calls[index][0], line)
245+
246+
def test_subclass(self):
247+
# given
248+
documenter = TraitDocumenter(mock.Mock(), 'test')
249+
documenter.object_name = 'bar'
250+
documenter.objpath = ['MySubClass', 'bar']
251+
documenter.parent = MySubClass
252+
documenter.modname = 'traits.util.tests.test_trait_documenter'
253+
documenter.get_sourcename = mock.Mock(return_value='<autodoc>')
254+
documenter.add_line = mock.Mock()
255+
256+
# when
257+
documenter.add_directive_header('')
258+
259+
# then
260+
self.assertEqual(documenter.directive.warn.call_args_list, [])
261+
expected = [
262+
('.. py:attribute:: MySubClass.bar', '<autodoc>'),
263+
(f' :{no_index}:', '<autodoc>'),
264+
(' :module: traits.util.tests.test_trait_documenter', '<autodoc>'), # noqa
265+
(' :annotation: = Int(42, desc=""" First line …', '<autodoc>')] # noqa
266+
if no_index_entry:
267+
expected.insert(2, (' :no-index-entry:', '<autodoc>'))
268+
calls = documenter.add_line.call_args_list
269+
for index, line in enumerate(expected):
270+
self.assertEqual(calls[index][0], line)
271+
272+
# given
273+
documenter.object_name = 'foo'
274+
documenter.objpath = ['MySubClass', 'foo']
275+
documenter.add_line = mock.Mock()
276+
277+
# when
278+
documenter.add_directive_header('')
279+
280+
# then
281+
self.assertEqual(documenter.directive.warn.call_args_list, [])
282+
expected = [
283+
('.. py:attribute:: MySubClass.foo', '<autodoc>'),
284+
(f' :{no_index}:', '<autodoc>'),
285+
(' :module: traits.util.tests.test_trait_documenter', '<autodoc>'), # noqa
286+
(' :annotation: = Bool(True)', '<autodoc>')] # noqa
287+
if no_index_entry:
288+
expected.insert(2, (' :no-index-entry:', '<autodoc>'))
289+
calls = documenter.add_line.call_args_list
290+
for index, line in enumerate(expected):
291+
self.assertEqual(calls[index][0], line)
292+
212293
@contextlib.contextmanager
213294
def create_directive(self):
214295
"""

traits/util/trait_documenter.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from importlib import import_module
1717
import inspect
1818
import io
19+
import types
1920
import token
2021
import tokenize
2122
import traceback
@@ -114,17 +115,22 @@ def add_directive_header(self, sig):
114115
115116
"""
116117
ClassLevelDocumenter.add_directive_header(self, sig)
117-
try:
118-
definition = trait_definition(
119-
cls=self.parent,
120-
trait_name=self.object_name,
121-
)
122-
except ValueError:
123-
# Without this, a failure to find the trait definition aborts
124-
# the whole documentation build.
118+
# Look into the class and parent classes:
119+
parent = self.parent
120+
classes = list(types.resolve_bases(parent.__bases__))
121+
classes.insert(0, parent)
122+
for cls in classes:
123+
try:
124+
definition = trait_definition(
125+
cls=cls, trait_name=self.object_name)
126+
except ValueError:
127+
continue
128+
else:
129+
break
130+
else:
125131
logger.warning(
126132
"No definition for the trait {!r} could be found in "
127-
"class {!r}.".format(self.object_name, self.parent),
133+
"class {!r}.".format(self.object_name, parent),
128134
exc_info=True)
129135
return
130136

0 commit comments

Comments
 (0)