mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
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:
commit
110fe1797c
3
CHANGES
3
CHANGES
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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
|
@ -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'
|
||||
|
@ -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():
|
||||
|
Loading…
Reference in New Issue
Block a user