Skip to content

Commit ce845f2

Browse files
committed
Add py:type directive and role
Closes #7896
1 parent 67493fc commit ce845f2

File tree

7 files changed

+150
-16
lines changed

7 files changed

+150
-16
lines changed

CHANGES.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,9 @@ Features added
166166
to annotate the return type of their ``setup`` function.
167167
Patch by Chris Sewell.
168168

169+
* #7896: Python Domain: Add a :rst:dir:`py:type` directive for documenting
170+
type aliases, and a :rst:role:`py:type` role for linking to them.
171+
169172
Bugs fixed
170173
----------
171174

doc/usage/domains/python.rst

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,9 @@ The following directives are provided for module and class contents:
124124
.. rst:directive:: .. py:data:: name
125125
126126
Describes global data in a module, including both variables and values used
127-
as "defined constants." Class and object attributes are not documented
128-
using this environment.
127+
as "defined constants." Class and object attributes are documented
128+
with :rst:dir:`py:attribute`. Type aliases are documented with
129+
:rst:dir:`py:type`.
129130

130131
.. rubric:: options
131132

@@ -258,7 +259,7 @@ The following directives are provided for module and class contents:
258259
259260
Describes an object data attribute. The description should include
260261
information about the type of the data to be expected and whether it may be
261-
changed directly.
262+
changed directly. Type aliases are documented with :rst:dir:`py:type`.
262263

263264
.. rubric:: options
264265

@@ -315,6 +316,19 @@ The following directives are provided for module and class contents:
315316
Describe the location where the object is defined. The default value is
316317
the module specified by :rst:dir:`py:currentmodule`.
317318
319+
.. rst:directive:: .. py:type:: name
320+
321+
Describes a :ref:`type alias <python:type-aliases>`. A description of
322+
the type alias, such as the docstring can be, placed in the body of
323+
the directive.
324+
325+
.. versionadded:: 7.3
326+
327+
.. rubric:: options
328+
329+
.. rst:directive:option:: type: the type that the alias represents
330+
:type: text
331+
318332
.. rst:directive:: .. py:method:: name(parameters)
319333
.. py:method:: name[type parameters](parameters)
320334
@@ -649,6 +663,10 @@ a matching identifier is found:
649663

650664
.. note:: The role is also able to refer to property.
651665

666+
.. rst:role:: py:type
667+
668+
Reference a type alias.
669+
652670
.. rst:role:: py:exc
653671
654672
Reference an exception. A dotted name may be used.

sphinx/domains/python/__init__.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,46 @@ def get_index_text(self, modname: str, name_cls: tuple[str, str]) -> str:
390390
return _('%s (%s property)') % (attrname, clsname)
391391

392392

393+
class PyTypeAlias(PyObject):
394+
"""Description of a type alias."""
395+
396+
option_spec: OptionSpec = PyObject.option_spec.copy()
397+
option_spec.update({
398+
'value': directives.unchanged,
399+
})
400+
401+
def get_signature_prefix(self, sig: str) -> list[nodes.Node]:
402+
return [nodes.Text('type'), addnodes.desc_sig_space()]
403+
404+
def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str]:
405+
fullname, prefix = super().handle_signature(sig, signode)
406+
407+
value = self.options.get('value')
408+
if value:
409+
annotations = _parse_annotation(value, self.env)
410+
signode += addnodes.desc_annotation(value, '',
411+
addnodes.desc_sig_space(),
412+
addnodes.desc_sig_punctuation('', '='),
413+
addnodes.desc_sig_space(),
414+
*annotations)
415+
416+
return fullname, prefix
417+
418+
def get_index_text(self, modname: str, name_cls: tuple[str, str]) -> str:
419+
name, cls = name_cls
420+
try:
421+
clsname, attrname = name.rsplit('.', 1)
422+
if modname and self.env.config.add_module_names:
423+
clsname = f'{modname}.{clsname}'
424+
except ValueError:
425+
if modname:
426+
return _('%s (in module %s)') % (name, modname)
427+
else:
428+
return name
429+
430+
return _('%s (type alias in %s)') % (attrname, clsname)
431+
432+
393433
class PyModule(SphinxDirective):
394434
"""
395435
Directive to mark description of a new module.
@@ -594,6 +634,7 @@ class PythonDomain(Domain):
594634
'staticmethod': ObjType(_('static method'), 'meth', 'obj'),
595635
'attribute': ObjType(_('attribute'), 'attr', 'obj'),
596636
'property': ObjType(_('property'), 'attr', '_prop', 'obj'),
637+
'type': ObjType(_('type alias'), 'type', 'obj'),
597638
'module': ObjType(_('module'), 'mod', 'obj'),
598639
}
599640

@@ -607,6 +648,7 @@ class PythonDomain(Domain):
607648
'staticmethod': PyStaticMethod,
608649
'attribute': PyAttribute,
609650
'property': PyProperty,
651+
'type': PyTypeAlias,
610652
'module': PyModule,
611653
'currentmodule': PyCurrentModule,
612654
'decorator': PyDecoratorFunction,
@@ -619,6 +661,7 @@ class PythonDomain(Domain):
619661
'class': PyXRefRole(),
620662
'const': PyXRefRole(),
621663
'attr': PyXRefRole(),
664+
'type': PyXRefRole(),
622665
'meth': PyXRefRole(fix_parens=True),
623666
'mod': PyXRefRole(),
624667
'obj': PyXRefRole(),

tests/roots/test-domain-py/module.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,6 @@ module
6464

6565
.. py:data:: test2
6666
:type: typing.Literal[-2]
67+
68+
.. py:type:: MyType1
69+
:value: list[int | str]

tests/roots/test-domain-py/roles.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,19 @@ roles
55
66
.. py:method:: top_level
77
8+
.. py:type:: TopLevelType
9+
810
* :py:class:`TopLevel`
911
* :py:meth:`top_level`
12+
* :py:type:`TopLevelType`
1013

1114

1215
.. py:class:: NestedParentA
1316
1417
* Link to :py:meth:`child_1`
1518

19+
.. py:type:: NestedTypeA
20+
1621
.. py:method:: child_1()
1722
1823
* Link to :py:meth:`NestedChildA.subchild_2`
@@ -46,3 +51,4 @@ roles
4651
* Link to :py:class:`NestedParentB`
4752

4853
* :py:class:`NestedParentA.NestedChildA`
54+
* :py:type:`NestedParentA.NestedTypeA`

tests/test_domains/test_domain_py.py

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -92,19 +92,21 @@ def assert_refnode(node, module_name, class_name, target, reftype=None,
9292
refnodes = list(doctree.findall(pending_xref))
9393
assert_refnode(refnodes[0], None, None, 'TopLevel', 'class')
9494
assert_refnode(refnodes[1], None, None, 'top_level', 'meth')
95-
assert_refnode(refnodes[2], None, 'NestedParentA', 'child_1', 'meth')
96-
assert_refnode(refnodes[3], None, 'NestedParentA', 'NestedChildA.subchild_2', 'meth')
97-
assert_refnode(refnodes[4], None, 'NestedParentA', 'child_2', 'meth')
98-
assert_refnode(refnodes[5], False, 'NestedParentA', 'any_child', domain='')
99-
assert_refnode(refnodes[6], None, 'NestedParentA', 'NestedChildA', 'class')
100-
assert_refnode(refnodes[7], None, 'NestedParentA.NestedChildA', 'subchild_2', 'meth')
101-
assert_refnode(refnodes[8], None, 'NestedParentA.NestedChildA',
95+
assert_refnode(refnodes[2], None, None, 'TopLevelType', 'type')
96+
assert_refnode(refnodes[3], None, 'NestedParentA', 'child_1', 'meth')
97+
assert_refnode(refnodes[4], None, 'NestedParentA', 'NestedChildA.subchild_2', 'meth')
98+
assert_refnode(refnodes[5], None, 'NestedParentA', 'child_2', 'meth')
99+
assert_refnode(refnodes[6], False, 'NestedParentA', 'any_child', domain='')
100+
assert_refnode(refnodes[7], None, 'NestedParentA', 'NestedChildA', 'class')
101+
assert_refnode(refnodes[8], None, 'NestedParentA.NestedChildA', 'subchild_2', 'meth')
102+
assert_refnode(refnodes[9], None, 'NestedParentA.NestedChildA',
102103
'NestedParentA.child_1', 'meth')
103-
assert_refnode(refnodes[9], None, 'NestedParentA', 'NestedChildA.subchild_1', 'meth')
104-
assert_refnode(refnodes[10], None, 'NestedParentB', 'child_1', 'meth')
105-
assert_refnode(refnodes[11], None, 'NestedParentB', 'NestedParentB', 'class')
106-
assert_refnode(refnodes[12], None, None, 'NestedParentA.NestedChildA', 'class')
107-
assert len(refnodes) == 13
104+
assert_refnode(refnodes[10], None, 'NestedParentA', 'NestedChildA.subchild_1', 'meth')
105+
assert_refnode(refnodes[11], None, 'NestedParentB', 'child_1', 'meth')
106+
assert_refnode(refnodes[12], None, 'NestedParentB', 'NestedParentB', 'class')
107+
assert_refnode(refnodes[13], None, None, 'NestedParentA.NestedChildA', 'class')
108+
assert_refnode(refnodes[14], None, None, 'NestedParentA.NestedTypeA', 'type')
109+
assert len(refnodes) == 15
108110

109111
doctree = app.env.get_doctree('module')
110112
refnodes = list(doctree.findall(pending_xref))
@@ -135,7 +137,10 @@ def assert_refnode(node, module_name, class_name, target, reftype=None,
135137
assert_refnode(refnodes[15], False, False, 'index', 'doc', domain='std')
136138
assert_refnode(refnodes[16], False, False, 'typing.Literal', 'obj', domain='py')
137139
assert_refnode(refnodes[17], False, False, 'typing.Literal', 'obj', domain='py')
138-
assert len(refnodes) == 18
140+
assert_refnode(refnodes[18], False, False, 'list', 'class', domain='py')
141+
assert_refnode(refnodes[19], False, False, 'int', 'class', domain='py')
142+
assert_refnode(refnodes[20], False, False, 'str', 'class', domain='py')
143+
assert len(refnodes) == 21
139144

140145
doctree = app.env.get_doctree('module_option')
141146
refnodes = list(doctree.findall(pending_xref))
@@ -191,7 +196,9 @@ def test_domain_py_objects(app, status, warning):
191196

192197
assert objects['TopLevel'][2] == 'class'
193198
assert objects['top_level'][2] == 'method'
199+
assert objects['TopLevelType'][2] == 'type'
194200
assert objects['NestedParentA'][2] == 'class'
201+
assert objects['NestedParentA.NestedTypeA'][2] == 'type'
195202
assert objects['NestedParentA.child_1'][2] == 'method'
196203
assert objects['NestedParentA.any_child'][2] == 'method'
197204
assert objects['NestedParentA.NestedChildA'][2] == 'class'
@@ -233,6 +240,9 @@ def find_obj(modname, prefix, obj_name, obj_type, searchmode=0):
233240
assert (find_obj(None, None, 'NONEXISTANT', 'class') == [])
234241
assert (find_obj(None, None, 'NestedParentA', 'class') ==
235242
[('NestedParentA', ('roles', 'NestedParentA', 'class', False))])
243+
assert (find_obj(None, None, 'NestedParentA.NestedTypeA', 'type') ==
244+
[('NestedParentA.NestedTypeA',
245+
('roles', 'NestedParentA.NestedTypeA', 'type', False))])
236246
assert (find_obj(None, None, 'NestedParentA.NestedChildA', 'class') ==
237247
[('NestedParentA.NestedChildA',
238248
('roles', 'NestedParentA.NestedChildA', 'class', False))])

tests/test_domains/test_domain_py_pyobject.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,57 @@ def test_pyproperty(app):
362362
assert domain.objects['Class.prop2'] == ('index', 'Class.prop2', 'property', False)
363363

364364

365+
def test_pytypealias(app):
366+
text = (".. py:module:: example\n"
367+
".. py:type:: Alias1\n"
368+
" :value: list[str | int]\n"
369+
"\n"
370+
".. py:class:: Class\n"
371+
"\n"
372+
" .. py:type:: Alias2\n"
373+
" :value: int\n")
374+
domain = app.env.get_domain('py')
375+
doctree = restructuredtext.parse(app, text)
376+
assert_node(doctree, (addnodes.index,
377+
addnodes.index,
378+
nodes.target,
379+
[desc, ([desc_signature, ([desc_annotation, ('type', desc_sig_space)],
380+
[desc_addname, 'example.'],
381+
[desc_name, 'Alias1'],
382+
[desc_annotation, (desc_sig_space,
383+
[desc_sig_punctuation, '='],
384+
desc_sig_space,
385+
[pending_xref, 'list'],
386+
[desc_sig_punctuation, '['],
387+
[pending_xref, 'str'],
388+
desc_sig_space,
389+
[desc_sig_punctuation, '|'],
390+
desc_sig_space,
391+
[pending_xref, 'int'],
392+
[desc_sig_punctuation, ']'],
393+
)])],
394+
[desc_content, ()])],
395+
addnodes.index,
396+
[desc, ([desc_signature, ([desc_annotation, ('class', desc_sig_space)],
397+
[desc_addname, 'example.'],
398+
[desc_name, 'Class'])],
399+
[desc_content, (addnodes.index,
400+
desc)])]))
401+
assert_node(doctree[5][1][0], addnodes.index,
402+
entries=[('single', 'Alias2 (type alias in example.Class)', 'example.Class.Alias2', '', None)])
403+
assert_node(doctree[5][1][1], ([desc_signature, ([desc_annotation, ('type', desc_sig_space)],
404+
[desc_name, 'Alias2'],
405+
[desc_annotation, (desc_sig_space,
406+
[desc_sig_punctuation, '='],
407+
desc_sig_space,
408+
[pending_xref, 'int'])])],
409+
[desc_content, ()]))
410+
assert 'example.Alias1' in domain.objects
411+
assert domain.objects['example.Alias1'] == ('index', 'example.Alias1', 'type', False)
412+
assert 'example.Class.Alias2' in domain.objects
413+
assert domain.objects['example.Class.Alias2'] == ('index', 'example.Class.Alias2', 'type', False)
414+
415+
365416
def test_pydecorator_signature(app):
366417
text = ".. py:decorator:: deco"
367418
domain = app.env.get_domain('py')

0 commit comments

Comments
 (0)