Merge pull request #9159 from tk0miya/8588_nested_autodoc_type_aliases

Fix #8588: autodoc_type_aliases does not support dotted name
This commit is contained in:
Takeshi KOMIYA 2021-05-03 22:30:01 +09:00 committed by GitHub
commit 110fe1797c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 147 additions and 26 deletions

View File

@ -18,6 +18,9 @@ Features added
* #8107: autodoc: Add ``class-doc-from`` option to :rst:dir:`autoclass`
directive to control the content of the specific class like
:confval:`autoclass_content`
* #8588: autodoc: :confval:`autodoc_type_aliases` now supports dotted name. It
allows you to define an alias for a class with module name like
``foo.bar.BazClass``
* #9129: html search: Show search summaries when html_copy_source = False
* #9120: html theme: Eliminate prompt characters of code-block from copyable
text

View File

@ -18,8 +18,10 @@ import types
import typing
import warnings
from functools import partial, partialmethod
from importlib import import_module
from inspect import Parameter, isclass, ismethod, ismethoddescriptor, ismodule # NOQA
from io import StringIO
from types import ModuleType
from typing import Any, Callable, Dict, Mapping, Optional, Sequence, Tuple, Type, cast
from sphinx.deprecation import RemovedInSphinx50Warning
@ -501,6 +503,78 @@ class DefaultValue:
return self.value
class TypeAliasForwardRef:
"""Pseudo typing class for autodoc_type_aliases.
This avoids the error on evaluating the type inside `get_type_hints()`.
"""
def __init__(self, name: str) -> None:
self.name = name
def __call__(self) -> None:
# Dummy method to imitate special typing classes
pass
def __eq__(self, other: Any) -> bool:
return self.name == other
class TypeAliasModule:
"""Pseudo module class for autodoc_type_aliases."""
def __init__(self, modname: str, mapping: Dict[str, str]) -> None:
self.__modname = modname
self.__mapping = mapping
self.__module: Optional[ModuleType] = None
def __getattr__(self, name: str) -> Any:
fullname = '.'.join(filter(None, [self.__modname, name]))
if fullname in self.__mapping:
# exactly matched
return TypeAliasForwardRef(self.__mapping[fullname])
else:
prefix = fullname + '.'
nested = {k: v for k, v in self.__mapping.items() if k.startswith(prefix)}
if nested:
# sub modules or classes found
return TypeAliasModule(fullname, nested)
else:
# no sub modules or classes found.
try:
# return the real submodule if exists
return import_module(fullname)
except ImportError:
# return the real class
if self.__module is None:
self.__module = import_module(self.__modname)
return getattr(self.__module, name)
class TypeAliasNamespace(Dict[str, Any]):
"""Pseudo namespace class for autodoc_type_aliases.
This enables to look up nested modules and classes like `mod1.mod2.Class`.
"""
def __init__(self, mapping: Dict[str, str]) -> None:
self.__mapping = mapping
def __getitem__(self, key: str) -> Any:
if key in self.__mapping:
# exactly matched
return TypeAliasForwardRef(self.__mapping[key])
else:
prefix = key + '.'
nested = {k: v for k, v in self.__mapping.items() if k.startswith(prefix)}
if nested:
# sub modules or classes found
return TypeAliasModule(key, nested)
else:
raise KeyError
def _should_unwrap(subject: Callable) -> bool:
"""Check the function should be unwrapped on getting signature."""
__globals__ = getglobals(subject)
@ -549,12 +623,19 @@ def signature(subject: Callable, bound_method: bool = False, follow_wrapped: boo
try:
# Resolve annotations using ``get_type_hints()`` and type_aliases.
annotations = typing.get_type_hints(subject, None, type_aliases)
localns = TypeAliasNamespace(type_aliases)
annotations = typing.get_type_hints(subject, None, localns)
for i, param in enumerate(parameters):
if param.name in annotations:
parameters[i] = param.replace(annotation=annotations[param.name])
annotation = annotations[param.name]
if isinstance(annotation, TypeAliasForwardRef):
annotation = annotation.name
parameters[i] = param.replace(annotation=annotation)
if 'return' in annotations:
return_annotation = annotations['return']
if isinstance(annotations['return'], TypeAliasForwardRef):
return_annotation = annotations['return'].name
else:
return_annotation = annotations['return']
except Exception:
# ``get_type_hints()`` does not support some kind of objects like partial,
# ForwardRef and so on.

View File

@ -1,5 +1,6 @@
from __future__ import annotations
import io
from typing import overload
myint = int
@ -11,6 +12,10 @@ variable: myint
variable2 = None # type: myint
def read(r: io.BytesIO) -> io.StringIO:
"""docstring"""
def sum(x: myint, y: myint) -> myint:
"""docstring"""
return x + y

View File

@ -792,27 +792,27 @@ def test_autodoc_typehints_description_for_invalid_node(app):
def test_autodoc_type_aliases(app):
# default
options = {"members": None}
actual = do_autodoc(app, 'module', 'target.annotations', options)
actual = do_autodoc(app, 'module', 'target.autodoc_type_aliases', options)
assert list(actual) == [
'',
'.. py:module:: target.annotations',
'.. py:module:: target.autodoc_type_aliases',
'',
'',
'.. py:class:: Foo()',
' :module: target.annotations',
' :module: target.autodoc_type_aliases',
'',
' docstring',
'',
'',
' .. py:attribute:: Foo.attr1',
' :module: target.annotations',
' :module: target.autodoc_type_aliases',
' :type: int',
'',
' docstring',
'',
'',
' .. py:attribute:: Foo.attr2',
' :module: target.annotations',
' :module: target.autodoc_type_aliases',
' :type: int',
'',
' docstring',
@ -820,26 +820,32 @@ def test_autodoc_type_aliases(app):
'',
'.. py:function:: mult(x: int, y: int) -> int',
' mult(x: float, y: float) -> float',
' :module: target.annotations',
' :module: target.autodoc_type_aliases',
'',
' docstring',
'',
'',
'.. py:function:: read(r: _io.BytesIO) -> _io.StringIO',
' :module: target.autodoc_type_aliases',
'',
' docstring',
'',
'',
'.. py:function:: sum(x: int, y: int) -> int',
' :module: target.annotations',
' :module: target.autodoc_type_aliases',
'',
' docstring',
'',
'',
'.. py:data:: variable',
' :module: target.annotations',
' :module: target.autodoc_type_aliases',
' :type: int',
'',
' docstring',
'',
'',
'.. py:data:: variable2',
' :module: target.annotations',
' :module: target.autodoc_type_aliases',
' :type: int',
' :value: None',
'',
@ -848,28 +854,29 @@ def test_autodoc_type_aliases(app):
]
# define aliases
app.config.autodoc_type_aliases = {'myint': 'myint'}
actual = do_autodoc(app, 'module', 'target.annotations', options)
app.config.autodoc_type_aliases = {'myint': 'myint',
'io.StringIO': 'my.module.StringIO'}
actual = do_autodoc(app, 'module', 'target.autodoc_type_aliases', options)
assert list(actual) == [
'',
'.. py:module:: target.annotations',
'.. py:module:: target.autodoc_type_aliases',
'',
'',
'.. py:class:: Foo()',
' :module: target.annotations',
' :module: target.autodoc_type_aliases',
'',
' docstring',
'',
'',
' .. py:attribute:: Foo.attr1',
' :module: target.annotations',
' :module: target.autodoc_type_aliases',
' :type: myint',
'',
' docstring',
'',
'',
' .. py:attribute:: Foo.attr2',
' :module: target.annotations',
' :module: target.autodoc_type_aliases',
' :type: myint',
'',
' docstring',
@ -877,26 +884,32 @@ def test_autodoc_type_aliases(app):
'',
'.. py:function:: mult(x: myint, y: myint) -> myint',
' mult(x: float, y: float) -> float',
' :module: target.annotations',
' :module: target.autodoc_type_aliases',
'',
' docstring',
'',
'',
'.. py:function:: read(r: _io.BytesIO) -> my.module.StringIO',
' :module: target.autodoc_type_aliases',
'',
' docstring',
'',
'',
'.. py:function:: sum(x: myint, y: myint) -> myint',
' :module: target.annotations',
' :module: target.autodoc_type_aliases',
'',
' docstring',
'',
'',
'.. py:data:: variable',
' :module: target.annotations',
' :module: target.autodoc_type_aliases',
' :type: myint',
'',
' docstring',
'',
'',
'.. py:data:: variable2',
' :module: target.annotations',
' :module: target.autodoc_type_aliases',
' :type: myint',
' :value: None',
'',
@ -911,10 +924,10 @@ def test_autodoc_type_aliases(app):
confoverrides={'autodoc_typehints': "description",
'autodoc_type_aliases': {'myint': 'myint'}})
def test_autodoc_typehints_description_and_type_aliases(app):
(app.srcdir / 'annotations.rst').write_text('.. autofunction:: target.annotations.sum')
(app.srcdir / 'autodoc_type_aliases.rst').write_text('.. autofunction:: target.autodoc_type_aliases.sum')
app.build()
context = (app.outdir / 'annotations.txt').read_text()
assert ('target.annotations.sum(x, y)\n'
context = (app.outdir / 'autodoc_type_aliases.txt').read_text()
assert ('target.autodoc_type_aliases.sum(x, y)\n'
'\n'
' docstring\n'
'\n'

View File

@ -19,7 +19,26 @@ import _testcapi
import pytest
from sphinx.util import inspect
from sphinx.util.inspect import stringify_signature
from sphinx.util.inspect import TypeAliasNamespace, stringify_signature
def test_TypeAliasNamespace():
import logging.config
type_alias = TypeAliasNamespace({'logging.Filter': 'MyFilter',
'logging.Handler': 'MyHandler',
'logging.handlers.SyslogHandler': 'MySyslogHandler'})
assert type_alias['logging'].Filter == 'MyFilter'
assert type_alias['logging'].Handler == 'MyHandler'
assert type_alias['logging'].handlers.SyslogHandler == 'MySyslogHandler'
assert type_alias['logging'].Logger == logging.Logger
assert type_alias['logging'].config == logging.config
with pytest.raises(KeyError):
assert type_alias['log']
with pytest.raises(KeyError):
assert type_alias['unknown']
def test_signature():