Skip to content

Commit 29e12ec

Browse files
authored
Document typing.NewType as a class (#10700)
1 parent ec26c2f commit 29e12ec

File tree

7 files changed

+153
-189
lines changed

7 files changed

+153
-189
lines changed

sphinx/ext/autodoc/__init__.py

Lines changed: 78 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from __future__ import annotations
99

1010
import re
11+
import sys
1112
from inspect import Parameter, Signature
1213
from types import ModuleType
1314
from typing import (TYPE_CHECKING, Any, Callable, Iterator, List, Sequence, Tuple, TypeVar,
@@ -1420,6 +1421,11 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
14201421
'class-doc-from': class_doc_from_option,
14211422
}
14221423

1424+
# Must be higher than FunctionDocumenter, ClassDocumenter, and
1425+
# AttributeDocumenter as NewType can be an attribute and is a class
1426+
# after Python 3.10. Before 3.10 it is a kind of function object
1427+
priority = 15
1428+
14231429
_signature_class: Any = None
14241430
_signature_method_name: str = None
14251431

@@ -1441,7 +1447,8 @@ def __init__(self, *args: Any) -> None:
14411447
@classmethod
14421448
def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any
14431449
) -> bool:
1444-
return isinstance(member, type)
1450+
return isinstance(member, type) or (
1451+
isattr and (inspect.isNewType(member) or isinstance(member, TypeVar)))
14451452

14461453
def import_object(self, raiseerror: bool = False) -> bool:
14471454
ret = super().import_object(raiseerror)
@@ -1452,9 +1459,19 @@ def import_object(self, raiseerror: bool = False) -> bool:
14521459
self.doc_as_attr = (self.objpath[-1] != self.object.__name__)
14531460
else:
14541461
self.doc_as_attr = True
1462+
if inspect.isNewType(self.object) or isinstance(self.object, TypeVar):
1463+
modname = getattr(self.object, '__module__', self.modname)
1464+
if modname != self.modname and self.modname.startswith(modname):
1465+
bases = self.modname[len(modname):].strip('.').split('.')
1466+
self.objpath = bases + self.objpath
1467+
self.modname = modname
14551468
return ret
14561469

14571470
def _get_signature(self) -> tuple[Any | None, str | None, Signature | None]:
1471+
if inspect.isNewType(self.object) or isinstance(self.object, TypeVar):
1472+
# Supress signature
1473+
return None, None, None
1474+
14581475
def get_user_defined_function_or_method(obj: Any, attr: str) -> Any:
14591476
""" Get the `attr` function or method from `obj`, if it is user-defined. """
14601477
if inspect.is_builtin_class_method(obj, attr):
@@ -1635,11 +1652,15 @@ def add_directive_header(self, sig: str) -> None:
16351652
self.directivetype = 'attribute'
16361653
super().add_directive_header(sig)
16371654

1655+
if inspect.isNewType(self.object) or isinstance(self.object, TypeVar):
1656+
return
1657+
16381658
if self.analyzer and '.'.join(self.objpath) in self.analyzer.finals:
16391659
self.add_line(' :final:', sourcename)
16401660

16411661
canonical_fullname = self.get_canonical_fullname()
1642-
if not self.doc_as_attr and canonical_fullname and self.fullname != canonical_fullname:
1662+
if (not self.doc_as_attr and not inspect.isNewType(self.object)
1663+
and canonical_fullname and self.fullname != canonical_fullname):
16431664
self.add_line(' :canonical: %s' % canonical_fullname, sourcename)
16441665

16451666
# add inheritance info, if wanted
@@ -1687,6 +1708,27 @@ def get_object_members(self, want_all: bool) -> tuple[bool, ObjectMembers]:
16871708
return False, [m for m in members.values() if m.class_ == self.object]
16881709

16891710
def get_doc(self) -> list[list[str]] | None:
1711+
if isinstance(self.object, TypeVar):
1712+
if self.object.__doc__ == TypeVar.__doc__:
1713+
return []
1714+
if sys.version_info[:2] < (3, 10):
1715+
if inspect.isNewType(self.object) or isinstance(self.object, TypeVar):
1716+
parts = self.modname.strip('.').split('.')
1717+
orig_objpath = self.objpath
1718+
for i in range(len(parts)):
1719+
new_modname = '.'.join(parts[:len(parts) - i])
1720+
new_objpath = parts[len(parts) - i:] + orig_objpath
1721+
try:
1722+
analyzer = ModuleAnalyzer.for_module(new_modname)
1723+
analyzer.analyze()
1724+
key = ('', new_objpath[-1])
1725+
comment = list(analyzer.attr_docs.get(key, []))
1726+
if comment:
1727+
self.objpath = new_objpath
1728+
self.modname = new_modname
1729+
return [comment]
1730+
except PycodeError:
1731+
pass
16901732
if self.doc_as_attr:
16911733
# Don't show the docstring of the class when it is an alias.
16921734
comment = self.get_variable_comment()
@@ -1751,6 +1793,35 @@ def get_variable_comment(self) -> list[str] | None:
17511793
return None
17521794

17531795
def add_content(self, more_content: StringList | None) -> None:
1796+
if inspect.isNewType(self.object):
1797+
if self.config.autodoc_typehints_format == "short":
1798+
supertype = restify(self.object.__supertype__, "smart")
1799+
else:
1800+
supertype = restify(self.object.__supertype__)
1801+
1802+
more_content = StringList([_('alias of %s') % supertype, ''], source='')
1803+
if isinstance(self.object, TypeVar):
1804+
attrs = [repr(self.object.__name__)]
1805+
for constraint in self.object.__constraints__:
1806+
if self.config.autodoc_typehints_format == "short":
1807+
attrs.append(stringify_annotation(constraint, "smart"))
1808+
else:
1809+
attrs.append(stringify_annotation(constraint))
1810+
if self.object.__bound__:
1811+
if self.config.autodoc_typehints_format == "short":
1812+
bound = restify(self.object.__bound__, "smart")
1813+
else:
1814+
bound = restify(self.object.__bound__)
1815+
attrs.append(r"bound=\ " + bound)
1816+
if self.object.__covariant__:
1817+
attrs.append("covariant=True")
1818+
if self.object.__contravariant__:
1819+
attrs.append("contravariant=True")
1820+
1821+
more_content = StringList(
1822+
[_('alias of TypeVar(%s)') % ", ".join(attrs), ''],
1823+
source=''
1824+
)
17541825
if self.doc_as_attr and self.modname != self.get_real_modname():
17551826
try:
17561827
# override analyzer to obtain doccomment around its definition.
@@ -1801,7 +1872,7 @@ class ExceptionDocumenter(ClassDocumenter):
18011872
member_order = 10
18021873

18031874
# needs a higher priority than ClassDocumenter
1804-
priority = 10
1875+
priority = ClassDocumenter.priority + 5
18051876

18061877
@classmethod
18071878
def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any
@@ -1827,7 +1898,7 @@ def should_suppress_value_header(self) -> bool:
18271898
return False
18281899

18291900
def update_content(self, more_content: StringList) -> None:
1830-
"""Update docstring for the NewType object."""
1901+
"""Update docstring, for example with TypeVar variance."""
18311902
pass
18321903

18331904

@@ -1854,74 +1925,6 @@ def update_content(self, more_content: StringList) -> None:
18541925
super().update_content(more_content)
18551926

18561927

1857-
class NewTypeMixin(DataDocumenterMixinBase):
1858-
"""
1859-
Mixin for DataDocumenter and AttributeDocumenter to provide the feature for
1860-
supporting NewTypes.
1861-
"""
1862-
1863-
def should_suppress_directive_header(self) -> bool:
1864-
return (inspect.isNewType(self.object) or
1865-
super().should_suppress_directive_header())
1866-
1867-
def update_content(self, more_content: StringList) -> None:
1868-
if inspect.isNewType(self.object):
1869-
if self.config.autodoc_typehints_format == "short":
1870-
supertype = restify(self.object.__supertype__, "smart")
1871-
else:
1872-
supertype = restify(self.object.__supertype__)
1873-
1874-
more_content.append(_('alias of %s') % supertype, '')
1875-
more_content.append('', '')
1876-
1877-
super().update_content(more_content)
1878-
1879-
1880-
class TypeVarMixin(DataDocumenterMixinBase):
1881-
"""
1882-
Mixin for DataDocumenter and AttributeDocumenter to provide the feature for
1883-
supporting TypeVars.
1884-
"""
1885-
1886-
def should_suppress_directive_header(self) -> bool:
1887-
return (isinstance(self.object, TypeVar) or
1888-
super().should_suppress_directive_header())
1889-
1890-
def get_doc(self) -> list[list[str]] | None:
1891-
if isinstance(self.object, TypeVar):
1892-
if self.object.__doc__ != TypeVar.__doc__:
1893-
return super().get_doc() # type: ignore
1894-
else:
1895-
return []
1896-
else:
1897-
return super().get_doc() # type: ignore
1898-
1899-
def update_content(self, more_content: StringList) -> None:
1900-
if isinstance(self.object, TypeVar):
1901-
attrs = [repr(self.object.__name__)]
1902-
for constraint in self.object.__constraints__:
1903-
if self.config.autodoc_typehints_format == "short":
1904-
attrs.append(stringify_annotation(constraint, "smart"))
1905-
else:
1906-
attrs.append(stringify_annotation(constraint,
1907-
"fully-qualified-except-typing"))
1908-
if self.object.__bound__:
1909-
if self.config.autodoc_typehints_format == "short":
1910-
bound = restify(self.object.__bound__, "smart")
1911-
else:
1912-
bound = restify(self.object.__bound__)
1913-
attrs.append(r"bound=\ " + bound)
1914-
if self.object.__covariant__:
1915-
attrs.append("covariant=True")
1916-
if self.object.__contravariant__:
1917-
attrs.append("contravariant=True")
1918-
1919-
more_content.append(_('alias of TypeVar(%s)') % ", ".join(attrs), '')
1920-
more_content.append('', '')
1921-
1922-
super().update_content(more_content)
1923-
1924-
19251928
class UninitializedGlobalVariableMixin(DataDocumenterMixinBase):
19261929
"""
19271930
Mixin for DataDocumenter to provide the feature for supporting uninitialized
@@ -1963,7 +1966,7 @@ def get_doc(self) -> list[list[str]] | None:
19631966
return super().get_doc() # type: ignore
19641967

19651968

1966-
class DataDocumenter(GenericAliasMixin, NewTypeMixin, TypeVarMixin,
1969+
class DataDocumenter(GenericAliasMixin,
19671970
UninitializedGlobalVariableMixin, ModuleLevelDocumenter):
19681971
"""
19691972
Specialized Documenter subclass for data items.
@@ -2083,24 +2086,6 @@ def add_content(self, more_content: StringList | None) -> None:
20832086
super().add_content(more_content)
20842087

20852088

2086-
class NewTypeDataDocumenter(DataDocumenter):
2087-
"""
2088-
Specialized Documenter subclass for NewTypes.
2089-
2090-
Note: This must be invoked before FunctionDocumenter because NewType is a kind of
2091-
function object.
2092-
"""
2093-
2094-
objtype = 'newtypedata'
2095-
directivetype = 'data'
2096-
priority = FunctionDocumenter.priority + 1
2097-
2098-
@classmethod
2099-
def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any
2100-
) -> bool:
2101-
return inspect.isNewType(member) and isattr
2102-
2103-
21042089
class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: ignore
21052090
"""
21062091
Specialized Documenter subclass for methods (normal, static and class).
@@ -2520,8 +2505,8 @@ def get_doc(self) -> list[list[str]] | None:
25202505
return super().get_doc() # type: ignore
25212506

25222507

2523-
class AttributeDocumenter(GenericAliasMixin, NewTypeMixin, SlotsMixin, # type: ignore
2524-
TypeVarMixin, RuntimeInstanceAttributeMixin,
2508+
class AttributeDocumenter(GenericAliasMixin, SlotsMixin, # type: ignore
2509+
RuntimeInstanceAttributeMixin,
25252510
UninitializedInstanceAttributeMixin, NonDataDescriptorMixin,
25262511
DocstringStripSignatureMixin, ClassLevelDocumenter):
25272512
"""
@@ -2759,24 +2744,6 @@ def add_directive_header(self, sig: str) -> None:
27592744
return None
27602745

27612746

2762-
class NewTypeAttributeDocumenter(AttributeDocumenter):
2763-
"""
2764-
Specialized Documenter subclass for NewTypes.
2765-
2766-
Note: This must be invoked before MethodDocumenter because NewType is a kind of
2767-
function object.
2768-
"""
2769-
2770-
objtype = 'newvarattribute'
2771-
directivetype = 'attribute'
2772-
priority = MethodDocumenter.priority + 1
2773-
2774-
@classmethod
2775-
def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any
2776-
) -> bool:
2777-
return not isinstance(parent, ModuleDocumenter) and inspect.isNewType(member)
2778-
2779-
27802747
def autodoc_attrgetter(app: Sphinx, obj: Any, name: str, *defargs: Any) -> Any:
27812748
"""Alternative getattr() for types"""
27822749
for typ, func in app.registry.autodoc_attrgettrs.items():
@@ -2791,13 +2758,11 @@ def setup(app: Sphinx) -> dict[str, Any]:
27912758
app.add_autodocumenter(ClassDocumenter)
27922759
app.add_autodocumenter(ExceptionDocumenter)
27932760
app.add_autodocumenter(DataDocumenter)
2794-
app.add_autodocumenter(NewTypeDataDocumenter)
27952761
app.add_autodocumenter(FunctionDocumenter)
27962762
app.add_autodocumenter(DecoratorDocumenter)
27972763
app.add_autodocumenter(MethodDocumenter)
27982764
app.add_autodocumenter(AttributeDocumenter)
27992765
app.add_autodocumenter(PropertyDocumenter)
2800-
app.add_autodocumenter(NewTypeAttributeDocumenter)
28012766

28022767
app.add_config_value('autoclass_content', 'class', True, ENUM('both', 'class', 'init'))
28032768
app.add_config_value('autodoc_member_order', 'alphabetical', True,

sphinx/ext/autosummary/generate.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,11 @@ def setup_documenters(app: Any) -> None:
8282
from sphinx.ext.autodoc import (AttributeDocumenter, ClassDocumenter, DataDocumenter,
8383
DecoratorDocumenter, ExceptionDocumenter,
8484
FunctionDocumenter, MethodDocumenter, ModuleDocumenter,
85-
NewTypeAttributeDocumenter, NewTypeDataDocumenter,
8685
PropertyDocumenter)
8786
documenters: list[type[Documenter]] = [
8887
ModuleDocumenter, ClassDocumenter, ExceptionDocumenter, DataDocumenter,
89-
FunctionDocumenter, MethodDocumenter, NewTypeAttributeDocumenter,
90-
NewTypeDataDocumenter, AttributeDocumenter, DecoratorDocumenter, PropertyDocumenter,
88+
FunctionDocumenter, MethodDocumenter,
89+
AttributeDocumenter, DecoratorDocumenter, PropertyDocumenter,
9190
]
9291
for documenter in documenters:
9392
app.registry.add_documenter(documenter.objtype, documenter)

tests/test_ext_autodoc.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1911,63 +1911,63 @@ def test_autodoc_TypeVar(app):
19111911
' :module: target.typevar',
19121912
'',
19131913
'',
1914-
' .. py:attribute:: Class.T1',
1914+
' .. py:class:: Class.T1',
19151915
' :module: target.typevar',
19161916
'',
19171917
' T1',
19181918
'',
19191919
" alias of TypeVar('T1')",
19201920
'',
19211921
'',
1922-
' .. py:attribute:: Class.T6',
1922+
' .. py:class:: Class.T6',
19231923
' :module: target.typevar',
19241924
'',
19251925
' T6',
19261926
'',
19271927
' alias of :py:class:`~datetime.date`',
19281928
'',
19291929
'',
1930-
'.. py:data:: T1',
1930+
'.. py:class:: T1',
19311931
' :module: target.typevar',
19321932
'',
19331933
' T1',
19341934
'',
19351935
" alias of TypeVar('T1')",
19361936
'',
19371937
'',
1938-
'.. py:data:: T3',
1938+
'.. py:class:: T3',
19391939
' :module: target.typevar',
19401940
'',
19411941
' T3',
19421942
'',
19431943
" alias of TypeVar('T3', int, str)",
19441944
'',
19451945
'',
1946-
'.. py:data:: T4',
1946+
'.. py:class:: T4',
19471947
' :module: target.typevar',
19481948
'',
19491949
' T4',
19501950
'',
19511951
" alias of TypeVar('T4', covariant=True)",
19521952
'',
19531953
'',
1954-
'.. py:data:: T5',
1954+
'.. py:class:: T5',
19551955
' :module: target.typevar',
19561956
'',
19571957
' T5',
19581958
'',
19591959
" alias of TypeVar('T5', contravariant=True)",
19601960
'',
19611961
'',
1962-
'.. py:data:: T6',
1962+
'.. py:class:: T6',
19631963
' :module: target.typevar',
19641964
'',
19651965
' T6',
19661966
'',
19671967
' alias of :py:class:`~datetime.date`',
19681968
'',
19691969
'',
1970-
'.. py:data:: T7',
1970+
'.. py:class:: T7',
19711971
' :module: target.typevar',
19721972
'',
19731973
' T7',

0 commit comments

Comments
 (0)