Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Document typing.NewType as a class #10700

Merged
merged 12 commits into from Jan 2, 2023
191 changes: 78 additions & 113 deletions sphinx/ext/autodoc/__init__.py
Expand Up @@ -8,6 +8,7 @@
from __future__ import annotations

import re
import sys
from inspect import Parameter, Signature
from types import ModuleType
from typing import (TYPE_CHECKING, Any, Callable, Iterator, List, Sequence, Tuple, TypeVar,
Expand Down Expand Up @@ -1420,6 +1421,11 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
'class-doc-from': class_doc_from_option,
}

# Must be higher than FunctionDocumenter, ClassDocumenter, and
# AttributeDocumenter as NewType can be an attribute and is a class
# after Python 3.10. Before 3.10 it is a kind of function object
priority = 15

_signature_class: Any = None
_signature_method_name: str = None

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

def import_object(self, raiseerror: bool = False) -> bool:
ret = super().import_object(raiseerror)
Expand All @@ -1452,9 +1459,19 @@ def import_object(self, raiseerror: bool = False) -> bool:
self.doc_as_attr = (self.objpath[-1] != self.object.__name__)
else:
self.doc_as_attr = True
if inspect.isNewType(self.object) or isinstance(self.object, TypeVar):
modname = getattr(self.object, '__module__', self.modname)
if modname != self.modname and self.modname.startswith(modname):
bases = self.modname[len(modname):].strip('.').split('.')
self.objpath = bases + self.objpath
self.modname = modname
return ret

def _get_signature(self) -> tuple[Any | None, str | None, Signature | None]:
if inspect.isNewType(self.object) or isinstance(self.object, TypeVar):
# Supress signature
return None, None, None

def get_user_defined_function_or_method(obj: Any, attr: str) -> Any:
""" Get the `attr` function or method from `obj`, if it is user-defined. """
if inspect.is_builtin_class_method(obj, attr):
Expand Down Expand Up @@ -1635,11 +1652,15 @@ def add_directive_header(self, sig: str) -> None:
self.directivetype = 'attribute'
super().add_directive_header(sig)

if inspect.isNewType(self.object) or isinstance(self.object, TypeVar):
return

if self.analyzer and '.'.join(self.objpath) in self.analyzer.finals:
self.add_line(' :final:', sourcename)

canonical_fullname = self.get_canonical_fullname()
if not self.doc_as_attr and canonical_fullname and self.fullname != canonical_fullname:
if (not self.doc_as_attr and not inspect.isNewType(self.object)
and canonical_fullname and self.fullname != canonical_fullname):
self.add_line(' :canonical: %s' % canonical_fullname, sourcename)

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

def get_doc(self) -> list[list[str]] | None:
if isinstance(self.object, TypeVar):
if self.object.__doc__ == TypeVar.__doc__:
return []
if sys.version_info[:2] < (3, 10):
if inspect.isNewType(self.object) or isinstance(self.object, TypeVar):
parts = self.modname.strip('.').split('.')
orig_objpath = self.objpath
for i in range(len(parts)):
new_modname = '.'.join(parts[:len(parts) - i])
new_objpath = parts[len(parts) - i:] + orig_objpath
try:
analyzer = ModuleAnalyzer.for_module(new_modname)
analyzer.analyze()
key = ('', new_objpath[-1])
comment = list(analyzer.attr_docs.get(key, []))
if comment:
self.objpath = new_objpath
self.modname = new_modname
return [comment]
except PycodeError:
pass
if self.doc_as_attr:
# Don't show the docstring of the class when it is an alias.
comment = self.get_variable_comment()
Expand Down Expand Up @@ -1751,6 +1793,35 @@ def get_variable_comment(self) -> list[str] | None:
return None

def add_content(self, more_content: StringList | None) -> None:
if inspect.isNewType(self.object):
if self.config.autodoc_typehints_format == "short":
supertype = restify(self.object.__supertype__, "smart")
else:
supertype = restify(self.object.__supertype__)

more_content = StringList([_('alias of %s') % supertype, ''], source='')
if isinstance(self.object, TypeVar):
attrs = [repr(self.object.__name__)]
for constraint in self.object.__constraints__:
if self.config.autodoc_typehints_format == "short":
attrs.append(stringify_annotation(constraint, "smart"))
else:
attrs.append(stringify_annotation(constraint))
if self.object.__bound__:
if self.config.autodoc_typehints_format == "short":
bound = restify(self.object.__bound__, "smart")
else:
bound = restify(self.object.__bound__)
attrs.append(r"bound=\ " + bound)
if self.object.__covariant__:
attrs.append("covariant=True")
if self.object.__contravariant__:
attrs.append("contravariant=True")

more_content = StringList(
[_('alias of TypeVar(%s)') % ", ".join(attrs), ''],
source=''
)
if self.doc_as_attr and self.modname != self.get_real_modname():
try:
# override analyzer to obtain doccomment around its definition.
Expand Down Expand Up @@ -1801,7 +1872,7 @@ class ExceptionDocumenter(ClassDocumenter):
member_order = 10

# needs a higher priority than ClassDocumenter
priority = 10
priority = ClassDocumenter.priority + 5

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

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


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


class NewTypeMixin(DataDocumenterMixinBase):
"""
Mixin for DataDocumenter and AttributeDocumenter to provide the feature for
supporting NewTypes.
"""

def should_suppress_directive_header(self) -> bool:
return (inspect.isNewType(self.object) or
super().should_suppress_directive_header())

def update_content(self, more_content: StringList) -> None:
if inspect.isNewType(self.object):
if self.config.autodoc_typehints_format == "short":
supertype = restify(self.object.__supertype__, "smart")
else:
supertype = restify(self.object.__supertype__)

more_content.append(_('alias of %s') % supertype, '')
more_content.append('', '')

super().update_content(more_content)


class TypeVarMixin(DataDocumenterMixinBase):
"""
Mixin for DataDocumenter and AttributeDocumenter to provide the feature for
supporting TypeVars.
"""

def should_suppress_directive_header(self) -> bool:
return (isinstance(self.object, TypeVar) or
super().should_suppress_directive_header())

def get_doc(self) -> list[list[str]] | None:
if isinstance(self.object, TypeVar):
if self.object.__doc__ != TypeVar.__doc__:
return super().get_doc() # type: ignore
else:
return []
else:
return super().get_doc() # type: ignore

def update_content(self, more_content: StringList) -> None:
if isinstance(self.object, TypeVar):
attrs = [repr(self.object.__name__)]
for constraint in self.object.__constraints__:
if self.config.autodoc_typehints_format == "short":
attrs.append(stringify_annotation(constraint, "smart"))
else:
attrs.append(stringify_annotation(constraint,
"fully-qualified-except-typing"))
if self.object.__bound__:
if self.config.autodoc_typehints_format == "short":
bound = restify(self.object.__bound__, "smart")
else:
bound = restify(self.object.__bound__)
attrs.append(r"bound=\ " + bound)
if self.object.__covariant__:
attrs.append("covariant=True")
if self.object.__contravariant__:
attrs.append("contravariant=True")

more_content.append(_('alias of TypeVar(%s)') % ", ".join(attrs), '')
more_content.append('', '')

super().update_content(more_content)


class UninitializedGlobalVariableMixin(DataDocumenterMixinBase):
"""
Mixin for DataDocumenter to provide the feature for supporting uninitialized
Expand Down Expand Up @@ -1963,7 +1966,7 @@ def get_doc(self) -> list[list[str]] | None:
return super().get_doc() # type: ignore


class DataDocumenter(GenericAliasMixin, NewTypeMixin, TypeVarMixin,
class DataDocumenter(GenericAliasMixin,
UninitializedGlobalVariableMixin, ModuleLevelDocumenter):
"""
Specialized Documenter subclass for data items.
Expand Down Expand Up @@ -2083,24 +2086,6 @@ def add_content(self, more_content: StringList | None) -> None:
super().add_content(more_content)


class NewTypeDataDocumenter(DataDocumenter):
"""
Specialized Documenter subclass for NewTypes.

Note: This must be invoked before FunctionDocumenter because NewType is a kind of
function object.
"""

objtype = 'newtypedata'
directivetype = 'data'
priority = FunctionDocumenter.priority + 1

@classmethod
def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any
) -> bool:
return inspect.isNewType(member) and isattr


class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: ignore
"""
Specialized Documenter subclass for methods (normal, static and class).
Expand Down Expand Up @@ -2520,8 +2505,8 @@ def get_doc(self) -> list[list[str]] | None:
return super().get_doc() # type: ignore


class AttributeDocumenter(GenericAliasMixin, NewTypeMixin, SlotsMixin, # type: ignore
TypeVarMixin, RuntimeInstanceAttributeMixin,
class AttributeDocumenter(GenericAliasMixin, SlotsMixin, # type: ignore
RuntimeInstanceAttributeMixin,
UninitializedInstanceAttributeMixin, NonDataDescriptorMixin,
DocstringStripSignatureMixin, ClassLevelDocumenter):
"""
Expand Down Expand Up @@ -2759,24 +2744,6 @@ def add_directive_header(self, sig: str) -> None:
return None


class NewTypeAttributeDocumenter(AttributeDocumenter):
"""
Specialized Documenter subclass for NewTypes.

Note: This must be invoked before MethodDocumenter because NewType is a kind of
function object.
"""

objtype = 'newvarattribute'
directivetype = 'attribute'
priority = MethodDocumenter.priority + 1

@classmethod
def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any
) -> bool:
return not isinstance(parent, ModuleDocumenter) and inspect.isNewType(member)


def autodoc_attrgetter(app: Sphinx, obj: Any, name: str, *defargs: Any) -> Any:
"""Alternative getattr() for types"""
for typ, func in app.registry.autodoc_attrgettrs.items():
Expand All @@ -2791,13 +2758,11 @@ def setup(app: Sphinx) -> dict[str, Any]:
app.add_autodocumenter(ClassDocumenter)
app.add_autodocumenter(ExceptionDocumenter)
app.add_autodocumenter(DataDocumenter)
app.add_autodocumenter(NewTypeDataDocumenter)
app.add_autodocumenter(FunctionDocumenter)
app.add_autodocumenter(DecoratorDocumenter)
app.add_autodocumenter(MethodDocumenter)
app.add_autodocumenter(AttributeDocumenter)
app.add_autodocumenter(PropertyDocumenter)
app.add_autodocumenter(NewTypeAttributeDocumenter)

app.add_config_value('autoclass_content', 'class', True, ENUM('both', 'class', 'init'))
app.add_config_value('autodoc_member_order', 'alphabetical', True,
Expand Down
5 changes: 2 additions & 3 deletions sphinx/ext/autosummary/generate.py
Expand Up @@ -82,12 +82,11 @@ def setup_documenters(app: Any) -> None:
from sphinx.ext.autodoc import (AttributeDocumenter, ClassDocumenter, DataDocumenter,
DecoratorDocumenter, ExceptionDocumenter,
FunctionDocumenter, MethodDocumenter, ModuleDocumenter,
NewTypeAttributeDocumenter, NewTypeDataDocumenter,
PropertyDocumenter)
documenters: list[type[Documenter]] = [
ModuleDocumenter, ClassDocumenter, ExceptionDocumenter, DataDocumenter,
FunctionDocumenter, MethodDocumenter, NewTypeAttributeDocumenter,
NewTypeDataDocumenter, AttributeDocumenter, DecoratorDocumenter, PropertyDocumenter,
FunctionDocumenter, MethodDocumenter,
AttributeDocumenter, DecoratorDocumenter, PropertyDocumenter,
]
for documenter in documenters:
app.registry.add_documenter(documenter.objtype, documenter)
Expand Down
16 changes: 8 additions & 8 deletions tests/test_ext_autodoc.py
Expand Up @@ -1911,63 +1911,63 @@ def test_autodoc_TypeVar(app):
' :module: target.typevar',
'',
'',
' .. py:attribute:: Class.T1',
' .. py:class:: Class.T1',
' :module: target.typevar',
'',
' T1',
'',
" alias of TypeVar('T1')",
'',
'',
' .. py:attribute:: Class.T6',
' .. py:class:: Class.T6',
' :module: target.typevar',
'',
' T6',
'',
' alias of :py:class:`~datetime.date`',
'',
'',
'.. py:data:: T1',
'.. py:class:: T1',
' :module: target.typevar',
'',
' T1',
'',
" alias of TypeVar('T1')",
'',
'',
'.. py:data:: T3',
'.. py:class:: T3',
' :module: target.typevar',
'',
' T3',
'',
" alias of TypeVar('T3', int, str)",
'',
'',
'.. py:data:: T4',
'.. py:class:: T4',
' :module: target.typevar',
'',
' T4',
'',
" alias of TypeVar('T4', covariant=True)",
'',
'',
'.. py:data:: T5',
'.. py:class:: T5',
' :module: target.typevar',
'',
' T5',
'',
" alias of TypeVar('T5', contravariant=True)",
'',
'',
'.. py:data:: T6',
'.. py:class:: T6',
' :module: target.typevar',
'',
' T6',
'',
' alias of :py:class:`~datetime.date`',
'',
'',
'.. py:data:: T7',
'.. py:class:: T7',
' :module: target.typevar',
'',
' T7',
Expand Down