mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Add `py:type
` directive and role for documenting type aliases (#11989)
Co-authored-by: Adam Turner <9087854+aa-turner@users.noreply.github.com>
This commit is contained in:
parent
91c5cd3abd
commit
e38a60d3f2
@ -74,6 +74,9 @@ Features added
|
||||
* #11592: Add :confval:`coverage_modules` to the coverage builder
|
||||
to allow explicitly specifying which modules should be documented.
|
||||
Patch by Stephen Finucane.
|
||||
* #7896, #11989: Add a :rst:dir:`py:type` directiv for documenting type aliases,
|
||||
and a :rst:role:`py:type` role for linking to them.
|
||||
Patch by Ashley Whetter.
|
||||
|
||||
Bugs fixed
|
||||
----------
|
||||
|
@ -124,8 +124,9 @@ The following directives are provided for module and class contents:
|
||||
.. rst:directive:: .. py:data:: name
|
||||
|
||||
Describes global data in a module, including both variables and values used
|
||||
as "defined constants." Class and object attributes are not documented
|
||||
using this environment.
|
||||
as "defined constants."
|
||||
Consider using :rst:dir:`py:type` for type aliases instead
|
||||
and :rst:dir:`py:attribute` for class variables and instance attributes.
|
||||
|
||||
.. rubric:: options
|
||||
|
||||
@ -259,6 +260,7 @@ The following directives are provided for module and class contents:
|
||||
Describes an object data attribute. The description should include
|
||||
information about the type of the data to be expected and whether it may be
|
||||
changed directly.
|
||||
Type aliases should be documented with :rst:dir:`py:type`.
|
||||
|
||||
.. rubric:: options
|
||||
|
||||
@ -315,6 +317,55 @@ The following directives are provided for module and class contents:
|
||||
Describe the location where the object is defined. The default value is
|
||||
the module specified by :rst:dir:`py:currentmodule`.
|
||||
|
||||
.. rst:directive:: .. py:type:: name
|
||||
|
||||
Describe a :ref:`type alias <python:type-aliases>`.
|
||||
|
||||
The type that the alias represents should be described
|
||||
with the :rst:dir:`!canonical` option.
|
||||
This directive supports an optional description body.
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: rst
|
||||
|
||||
.. py:type:: UInt64
|
||||
|
||||
Represent a 64-bit positive integer.
|
||||
|
||||
will be rendered as follows:
|
||||
|
||||
.. py:type:: UInt64
|
||||
:no-contents-entry:
|
||||
:no-index-entry:
|
||||
|
||||
Represent a 64-bit positive integer.
|
||||
|
||||
.. rubric:: options
|
||||
|
||||
.. rst:directive:option:: canonical
|
||||
:type: text
|
||||
|
||||
The canonical type represented by this alias, for example:
|
||||
|
||||
.. code-block:: rst
|
||||
|
||||
.. py:type:: StrPattern
|
||||
:canonical: str | re.Pattern[str]
|
||||
|
||||
Represent a regular expression or a compiled pattern.
|
||||
|
||||
This is rendered as:
|
||||
|
||||
.. py:type:: StrPattern
|
||||
:no-contents-entry:
|
||||
:no-index-entry:
|
||||
:canonical: str | re.Pattern[str]
|
||||
|
||||
Represent a regular expression or a compiled pattern.
|
||||
|
||||
.. versionadded:: 7.4
|
||||
|
||||
.. rst:directive:: .. py:method:: name(parameters)
|
||||
.. py:method:: name[type parameters](parameters)
|
||||
|
||||
@ -649,6 +700,10 @@ a matching identifier is found:
|
||||
|
||||
.. note:: The role is also able to refer to property.
|
||||
|
||||
.. rst:role:: py:type
|
||||
|
||||
Reference a type alias.
|
||||
|
||||
.. rst:role:: py:exc
|
||||
|
||||
Reference an exception. A dotted name may be used.
|
||||
|
@ -389,6 +389,45 @@ class PyProperty(PyObject):
|
||||
return _('%s (%s property)') % (attrname, clsname)
|
||||
|
||||
|
||||
class PyTypeAlias(PyObject):
|
||||
"""Description of a type alias."""
|
||||
|
||||
option_spec: ClassVar[OptionSpec] = PyObject.option_spec.copy()
|
||||
option_spec.update({
|
||||
'canonical': directives.unchanged,
|
||||
})
|
||||
|
||||
def get_signature_prefix(self, sig: str) -> list[nodes.Node]:
|
||||
return [nodes.Text('type'), addnodes.desc_sig_space()]
|
||||
|
||||
def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str]:
|
||||
fullname, prefix = super().handle_signature(sig, signode)
|
||||
if canonical := self.options.get('canonical'):
|
||||
canonical_annotations = _parse_annotation(canonical, self.env)
|
||||
signode += addnodes.desc_annotation(
|
||||
canonical, '',
|
||||
addnodes.desc_sig_space(),
|
||||
addnodes.desc_sig_punctuation('', '='),
|
||||
addnodes.desc_sig_space(),
|
||||
*canonical_annotations,
|
||||
)
|
||||
return fullname, prefix
|
||||
|
||||
def get_index_text(self, modname: str, name_cls: tuple[str, str]) -> str:
|
||||
name, cls = name_cls
|
||||
try:
|
||||
clsname, attrname = name.rsplit('.', 1)
|
||||
if modname and self.env.config.add_module_names:
|
||||
clsname = f'{modname}.{clsname}'
|
||||
except ValueError:
|
||||
if modname:
|
||||
return _('%s (in module %s)') % (name, modname)
|
||||
else:
|
||||
return name
|
||||
|
||||
return _('%s (type alias in %s)') % (attrname, clsname)
|
||||
|
||||
|
||||
class PyModule(SphinxDirective):
|
||||
"""
|
||||
Directive to mark description of a new module.
|
||||
@ -590,6 +629,7 @@ class PythonDomain(Domain):
|
||||
'staticmethod': ObjType(_('static method'), 'meth', 'obj'),
|
||||
'attribute': ObjType(_('attribute'), 'attr', 'obj'),
|
||||
'property': ObjType(_('property'), 'attr', '_prop', 'obj'),
|
||||
'type': ObjType(_('type alias'), 'type', 'obj'),
|
||||
'module': ObjType(_('module'), 'mod', 'obj'),
|
||||
}
|
||||
|
||||
@ -603,6 +643,7 @@ class PythonDomain(Domain):
|
||||
'staticmethod': PyStaticMethod,
|
||||
'attribute': PyAttribute,
|
||||
'property': PyProperty,
|
||||
'type': PyTypeAlias,
|
||||
'module': PyModule,
|
||||
'currentmodule': PyCurrentModule,
|
||||
'decorator': PyDecoratorFunction,
|
||||
@ -615,6 +656,7 @@ class PythonDomain(Domain):
|
||||
'class': PyXRefRole(),
|
||||
'const': PyXRefRole(),
|
||||
'attr': PyXRefRole(),
|
||||
'type': PyXRefRole(),
|
||||
'meth': PyXRefRole(fix_parens=True),
|
||||
'mod': PyXRefRole(),
|
||||
'obj': PyXRefRole(),
|
||||
|
@ -8,3 +8,4 @@ test-domain-py
|
||||
module_option
|
||||
abbr
|
||||
canonical
|
||||
type_alias
|
||||
|
@ -64,3 +64,6 @@ module
|
||||
|
||||
.. py:data:: test2
|
||||
:type: typing.Literal[-2]
|
||||
|
||||
.. py:type:: MyType1
|
||||
:canonical: list[int | str]
|
||||
|
@ -5,14 +5,19 @@ roles
|
||||
|
||||
.. py:method:: top_level
|
||||
|
||||
.. py:type:: TopLevelType
|
||||
|
||||
* :py:class:`TopLevel`
|
||||
* :py:meth:`top_level`
|
||||
* :py:type:`TopLevelType`
|
||||
|
||||
|
||||
.. py:class:: NestedParentA
|
||||
|
||||
* Link to :py:meth:`child_1`
|
||||
|
||||
.. py:type:: NestedTypeA
|
||||
|
||||
.. py:method:: child_1()
|
||||
|
||||
* Link to :py:meth:`NestedChildA.subchild_2`
|
||||
@ -46,3 +51,4 @@ roles
|
||||
* Link to :py:class:`NestedParentB`
|
||||
|
||||
* :py:class:`NestedParentA.NestedChildA`
|
||||
* :py:type:`NestedParentA.NestedTypeA`
|
||||
|
15
tests/roots/test-domain-py/type_alias.rst
Normal file
15
tests/roots/test-domain-py/type_alias.rst
Normal file
@ -0,0 +1,15 @@
|
||||
Type Alias
|
||||
==========
|
||||
|
||||
.. py:module:: module_two
|
||||
|
||||
.. py:class:: SomeClass
|
||||
|
||||
:py:type:`.MyAlias`
|
||||
:any:`MyAlias`
|
||||
:any:`module_one.MyAlias`
|
||||
|
||||
.. py:module:: module_one
|
||||
|
||||
.. py:type:: MyAlias
|
||||
:canonical: list[int | module_two.SomeClass]
|
@ -92,19 +92,21 @@ def test_domain_py_xrefs(app, status, warning):
|
||||
refnodes = list(doctree.findall(pending_xref))
|
||||
assert_refnode(refnodes[0], None, None, 'TopLevel', 'class')
|
||||
assert_refnode(refnodes[1], None, None, 'top_level', 'meth')
|
||||
assert_refnode(refnodes[2], None, 'NestedParentA', 'child_1', 'meth')
|
||||
assert_refnode(refnodes[3], None, 'NestedParentA', 'NestedChildA.subchild_2', 'meth')
|
||||
assert_refnode(refnodes[4], None, 'NestedParentA', 'child_2', 'meth')
|
||||
assert_refnode(refnodes[5], False, 'NestedParentA', 'any_child', domain='')
|
||||
assert_refnode(refnodes[6], None, 'NestedParentA', 'NestedChildA', 'class')
|
||||
assert_refnode(refnodes[7], None, 'NestedParentA.NestedChildA', 'subchild_2', 'meth')
|
||||
assert_refnode(refnodes[8], None, 'NestedParentA.NestedChildA',
|
||||
assert_refnode(refnodes[2], None, None, 'TopLevelType', 'type')
|
||||
assert_refnode(refnodes[3], None, 'NestedParentA', 'child_1', 'meth')
|
||||
assert_refnode(refnodes[4], None, 'NestedParentA', 'NestedChildA.subchild_2', 'meth')
|
||||
assert_refnode(refnodes[5], None, 'NestedParentA', 'child_2', 'meth')
|
||||
assert_refnode(refnodes[6], False, 'NestedParentA', 'any_child', domain='')
|
||||
assert_refnode(refnodes[7], None, 'NestedParentA', 'NestedChildA', 'class')
|
||||
assert_refnode(refnodes[8], None, 'NestedParentA.NestedChildA', 'subchild_2', 'meth')
|
||||
assert_refnode(refnodes[9], None, 'NestedParentA.NestedChildA',
|
||||
'NestedParentA.child_1', 'meth')
|
||||
assert_refnode(refnodes[9], None, 'NestedParentA', 'NestedChildA.subchild_1', 'meth')
|
||||
assert_refnode(refnodes[10], None, 'NestedParentB', 'child_1', 'meth')
|
||||
assert_refnode(refnodes[11], None, 'NestedParentB', 'NestedParentB', 'class')
|
||||
assert_refnode(refnodes[12], None, None, 'NestedParentA.NestedChildA', 'class')
|
||||
assert len(refnodes) == 13
|
||||
assert_refnode(refnodes[10], None, 'NestedParentA', 'NestedChildA.subchild_1', 'meth')
|
||||
assert_refnode(refnodes[11], None, 'NestedParentB', 'child_1', 'meth')
|
||||
assert_refnode(refnodes[12], None, 'NestedParentB', 'NestedParentB', 'class')
|
||||
assert_refnode(refnodes[13], None, None, 'NestedParentA.NestedChildA', 'class')
|
||||
assert_refnode(refnodes[14], None, None, 'NestedParentA.NestedTypeA', 'type')
|
||||
assert len(refnodes) == 15
|
||||
|
||||
doctree = app.env.get_doctree('module')
|
||||
refnodes = list(doctree.findall(pending_xref))
|
||||
@ -135,7 +137,10 @@ def test_domain_py_xrefs(app, status, warning):
|
||||
assert_refnode(refnodes[15], False, False, 'index', 'doc', domain='std')
|
||||
assert_refnode(refnodes[16], False, False, 'typing.Literal', 'obj', domain='py')
|
||||
assert_refnode(refnodes[17], False, False, 'typing.Literal', 'obj', domain='py')
|
||||
assert len(refnodes) == 18
|
||||
assert_refnode(refnodes[18], False, False, 'list', 'class', domain='py')
|
||||
assert_refnode(refnodes[19], False, False, 'int', 'class', domain='py')
|
||||
assert_refnode(refnodes[20], False, False, 'str', 'class', domain='py')
|
||||
assert len(refnodes) == 21
|
||||
|
||||
doctree = app.env.get_doctree('module_option')
|
||||
refnodes = list(doctree.findall(pending_xref))
|
||||
@ -191,7 +196,9 @@ def test_domain_py_objects(app, status, warning):
|
||||
|
||||
assert objects['TopLevel'][2] == 'class'
|
||||
assert objects['top_level'][2] == 'method'
|
||||
assert objects['TopLevelType'][2] == 'type'
|
||||
assert objects['NestedParentA'][2] == 'class'
|
||||
assert objects['NestedParentA.NestedTypeA'][2] == 'type'
|
||||
assert objects['NestedParentA.child_1'][2] == 'method'
|
||||
assert objects['NestedParentA.any_child'][2] == 'method'
|
||||
assert objects['NestedParentA.NestedChildA'][2] == 'class'
|
||||
@ -233,6 +240,9 @@ def test_domain_py_find_obj(app, status, warning):
|
||||
assert (find_obj(None, None, 'NONEXISTANT', 'class') == [])
|
||||
assert (find_obj(None, None, 'NestedParentA', 'class') ==
|
||||
[('NestedParentA', ('roles', 'NestedParentA', 'class', False))])
|
||||
assert (find_obj(None, None, 'NestedParentA.NestedTypeA', 'type') ==
|
||||
[('NestedParentA.NestedTypeA',
|
||||
('roles', 'NestedParentA.NestedTypeA', 'type', False))])
|
||||
assert (find_obj(None, None, 'NestedParentA.NestedChildA', 'class') ==
|
||||
[('NestedParentA.NestedChildA',
|
||||
('roles', 'NestedParentA.NestedChildA', 'class', False))])
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from docutils import nodes
|
||||
|
||||
from sphinx import addnodes
|
||||
@ -362,6 +363,76 @@ def test_pyproperty(app):
|
||||
assert domain.objects['Class.prop2'] == ('index', 'Class.prop2', 'property', False)
|
||||
|
||||
|
||||
def test_py_type_alias(app):
|
||||
text = (".. py:module:: example\n"
|
||||
".. py:type:: Alias1\n"
|
||||
" :canonical: list[str | int]\n"
|
||||
"\n"
|
||||
".. py:class:: Class\n"
|
||||
"\n"
|
||||
" .. py:type:: Alias2\n"
|
||||
" :canonical: int\n")
|
||||
domain = app.env.get_domain('py')
|
||||
doctree = restructuredtext.parse(app, text)
|
||||
assert_node(doctree, (addnodes.index,
|
||||
addnodes.index,
|
||||
nodes.target,
|
||||
[desc, ([desc_signature, ([desc_annotation, ('type', desc_sig_space)],
|
||||
[desc_addname, 'example.'],
|
||||
[desc_name, 'Alias1'],
|
||||
[desc_annotation, (desc_sig_space,
|
||||
[desc_sig_punctuation, '='],
|
||||
desc_sig_space,
|
||||
[pending_xref, 'list'],
|
||||
[desc_sig_punctuation, '['],
|
||||
[pending_xref, 'str'],
|
||||
desc_sig_space,
|
||||
[desc_sig_punctuation, '|'],
|
||||
desc_sig_space,
|
||||
[pending_xref, 'int'],
|
||||
[desc_sig_punctuation, ']'],
|
||||
)])],
|
||||
[desc_content, ()])],
|
||||
addnodes.index,
|
||||
[desc, ([desc_signature, ([desc_annotation, ('class', desc_sig_space)],
|
||||
[desc_addname, 'example.'],
|
||||
[desc_name, 'Class'])],
|
||||
[desc_content, (addnodes.index,
|
||||
desc)])]))
|
||||
assert_node(doctree[5][1][0], addnodes.index,
|
||||
entries=[('single', 'Alias2 (type alias in example.Class)', 'example.Class.Alias2', '', None)])
|
||||
assert_node(doctree[5][1][1], ([desc_signature, ([desc_annotation, ('type', desc_sig_space)],
|
||||
[desc_name, 'Alias2'],
|
||||
[desc_annotation, (desc_sig_space,
|
||||
[desc_sig_punctuation, '='],
|
||||
desc_sig_space,
|
||||
[pending_xref, 'int'])])],
|
||||
[desc_content, ()]))
|
||||
assert 'example.Alias1' in domain.objects
|
||||
assert domain.objects['example.Alias1'] == ('index', 'example.Alias1', 'type', False)
|
||||
assert 'example.Class.Alias2' in domain.objects
|
||||
assert domain.objects['example.Class.Alias2'] == ('index', 'example.Class.Alias2', 'type', False)
|
||||
|
||||
|
||||
@pytest.mark.sphinx('html', testroot='domain-py', freshenv=True)
|
||||
def test_domain_py_type_alias(app, status, warning):
|
||||
app.build(force_all=True)
|
||||
|
||||
content = (app.outdir / 'type_alias.html').read_text(encoding='utf8')
|
||||
assert ('<em class="property"><span class="pre">type</span><span class="w"> </span></em>'
|
||||
'<span class="sig-prename descclassname"><span class="pre">module_one.</span></span>'
|
||||
'<span class="sig-name descname"><span class="pre">MyAlias</span></span>'
|
||||
'<em class="property"><span class="w"> </span><span class="p"><span class="pre">=</span></span>'
|
||||
'<span class="w"> </span><span class="pre">list</span>'
|
||||
'<span class="p"><span class="pre">[</span></span>'
|
||||
'<span class="pre">int</span><span class="w"> </span>'
|
||||
'<span class="p"><span class="pre">|</span></span><span class="w"> </span>'
|
||||
'<a class="reference internal" href="#module_two.SomeClass" title="module_two.SomeClass">'
|
||||
'<span class="pre">module_two.SomeClass</span></a>'
|
||||
'<span class="p"><span class="pre">]</span></span></em>' in content)
|
||||
assert warning.getvalue() == ''
|
||||
|
||||
|
||||
def test_pydecorator_signature(app):
|
||||
text = ".. py:decorator:: deco"
|
||||
domain = app.env.get_domain('py')
|
||||
|
Loading…
Reference in New Issue
Block a user