diff options
Diffstat (limited to 'sphinx/util')
-rw-r--r-- | sphinx/util/docstrings.py | 23 | ||||
-rw-r--r-- | sphinx/util/inspect.py | 87 | ||||
-rw-r--r-- | sphinx/util/texescape.py | 2 |
3 files changed, 104 insertions, 8 deletions
diff --git a/sphinx/util/docstrings.py b/sphinx/util/docstrings.py index 46bb5b9b8..d81d7dd99 100644 --- a/sphinx/util/docstrings.py +++ b/sphinx/util/docstrings.py @@ -11,26 +11,28 @@ import re import sys import warnings -from typing import Dict, List +from typing import Dict, List, Tuple from docutils.parsers.rst.states import Body -from sphinx.deprecation import RemovedInSphinx50Warning +from sphinx.deprecation import RemovedInSphinx50Warning, RemovedInSphinx60Warning field_list_item_re = re.compile(Body.patterns['field_marker']) -def extract_metadata(s: str) -> Dict[str, str]: - """Extract metadata from docstring.""" +def separate_metadata(s: str) -> Tuple[str, Dict[str, str]]: + """Separate docstring into metadata and others.""" in_other_element = False metadata: Dict[str, str] = {} + lines = [] if not s: - return metadata + return s, metadata for line in prepare_docstring(s): if line.strip() == '': in_other_element = False + lines.append(line) else: matched = field_list_item_re.match(line) if matched and not in_other_element: @@ -38,9 +40,20 @@ def extract_metadata(s: str) -> Dict[str, str]: if field_name.startswith('meta '): name = field_name[5:].strip() metadata[name] = line[matched.end():].strip() + else: + lines.append(line) else: in_other_element = True + lines.append(line) + + return '\n'.join(lines), metadata + + +def extract_metadata(s: str) -> Dict[str, str]: + warnings.warn("extract_metadata() is deprecated.", + RemovedInSphinx60Warning, stacklevel=2) + docstring, metadata = separate_metadata(s) return metadata diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 7c9adb0bf..f216e8797 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -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. diff --git a/sphinx/util/texescape.py b/sphinx/util/texescape.py index 417a963a7..8dcc08a9b 100644 --- a/sphinx/util/texescape.py +++ b/sphinx/util/texescape.py @@ -29,6 +29,8 @@ tex_replacements = [ # map special Unicode characters to TeX commands ('✓', r'\(\checkmark\)'), ('✔', r'\(\pmb{\checkmark}\)'), + ('✕', r'\(\times\)'), + ('✖', r'\(\pmb{\times}\)'), # used to separate -- in options ('', r'{}'), # map some special Unicode characters to similar ASCII ones |