This repository provides User Manual for setting up a Docker environment tailored for testing DGTD code.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

905 lines
30 KiB

"""Helpers for inspecting Python modules."""
from __future__ import annotations
import ast
import builtins
import contextlib
import enum
import inspect
import re
import sys
import types
import typing
from collections.abc import Mapping
from functools import cached_property, partial, partialmethod, singledispatchmethod
from importlib import import_module
from inspect import Parameter, Signature
from io import StringIO
from types import ClassMethodDescriptorType, MethodDescriptorType, WrapperDescriptorType
from typing import TYPE_CHECKING, Any
from sphinx.pycode.ast import unparse as ast_unparse
from sphinx.util import logging
from sphinx.util.typing import ForwardRef, stringify_annotation
if TYPE_CHECKING:
from collections.abc import Callable, Sequence
from inspect import _ParameterKind
from types import MethodType, ModuleType
from typing import Final
logger = logging.getLogger(__name__)
memory_address_re = re.compile(r' at 0x[0-9a-f]{8,16}(?=>)', re.IGNORECASE)
# re-export as is
isasyncgenfunction = inspect.isasyncgenfunction
ismethod = inspect.ismethod
ismethoddescriptor = inspect.ismethoddescriptor
isclass = inspect.isclass
ismodule = inspect.ismodule
def unwrap(obj: Any) -> Any:
"""Get an original object from wrapped object (wrapped functions).
Mocked objects are returned as is.
"""
if hasattr(obj, '__sphinx_mock__'):
# Skip unwrapping mock object to avoid RecursionError
return obj
try:
return inspect.unwrap(obj)
except ValueError:
# might be a mock object
return obj
def unwrap_all(obj: Any, *, stop: Callable[[Any], bool] | None = None) -> Any:
"""Get an original object from wrapped object.
Unlike :func:`unwrap`, this unwraps partial functions, wrapped functions,
class methods and static methods.
When specified, *stop* is a predicate indicating whether an object should
be unwrapped or not.
"""
if callable(stop):
while not stop(obj):
if ispartial(obj):
obj = obj.func
elif inspect.isroutine(obj) and hasattr(obj, '__wrapped__'):
obj = obj.__wrapped__
elif isclassmethod(obj) or isstaticmethod(obj):
obj = obj.__func__
else:
return obj
return obj # in case the while loop never starts
while True:
if ispartial(obj):
obj = obj.func
elif inspect.isroutine(obj) and hasattr(obj, '__wrapped__'):
obj = obj.__wrapped__
elif isclassmethod(obj) or isstaticmethod(obj):
obj = obj.__func__
else:
return obj
def getall(obj: Any) -> Sequence[str] | None:
"""Get the ``__all__`` attribute of an object as sequence.
This returns ``None`` if the given ``obj.__all__`` does not exist and
raises :exc:`ValueError` if ``obj.__all__`` is not a list or tuple of
strings.
"""
__all__ = safe_getattr(obj, '__all__', None)
if __all__ is None:
return None
if isinstance(__all__, (list, tuple)) and all(isinstance(e, str) for e in __all__):
return __all__
raise ValueError(__all__)
def getannotations(obj: Any) -> Mapping[str, Any]:
"""Safely get the ``__annotations__`` attribute of an object."""
if sys.version_info >= (3, 10, 0) or not isinstance(obj, type):
__annotations__ = safe_getattr(obj, '__annotations__', None)
else:
# Workaround for bugfix not available until python 3.10 as recommended by docs
# https://docs.python.org/3.10/howto/annotations.html#accessing-the-annotations-dict-of-an-object-in-python-3-9-and-older
__dict__ = safe_getattr(obj, '__dict__', {})
__annotations__ = __dict__.get('__annotations__', None)
if isinstance(__annotations__, Mapping):
return __annotations__
return {}
def getglobals(obj: Any) -> Mapping[str, Any]:
"""Safely get :attr:`obj.__globals__ <function.__globals__>`."""
__globals__ = safe_getattr(obj, '__globals__', None)
if isinstance(__globals__, Mapping):
return __globals__
return {}
def getmro(obj: Any) -> tuple[type, ...]:
"""Safely get :attr:`obj.__mro__ <class.__mro__>`."""
__mro__ = safe_getattr(obj, '__mro__', None)
if isinstance(__mro__, tuple):
return __mro__
return ()
def getorigbases(obj: Any) -> tuple[Any, ...] | None:
"""Safely get ``obj.__orig_bases__``.
This returns ``None`` if the object is not a class or if ``__orig_bases__``
is not well-defined (e.g., a non-tuple object or an empty sequence).
"""
if not isclass(obj):
return None
# Get __orig_bases__ from obj.__dict__ to avoid accessing the parent's __orig_bases__.
# refs: https://github.com/sphinx-doc/sphinx/issues/9607
__dict__ = safe_getattr(obj, '__dict__', {})
__orig_bases__ = __dict__.get('__orig_bases__')
if isinstance(__orig_bases__, tuple) and len(__orig_bases__) > 0:
return __orig_bases__
return None
def getslots(obj: Any) -> dict[str, Any] | dict[str, None] | None:
"""Safely get :term:`obj.__slots__ <__slots__>` as a dictionary if any.
- This returns ``None`` if ``obj.__slots__`` does not exist.
- This raises a :exc:`TypeError` if *obj* is not a class.
- This raises a :exc:`ValueError` if ``obj.__slots__`` is invalid.
"""
if not isclass(obj):
raise TypeError
__slots__ = safe_getattr(obj, '__slots__', None)
if __slots__ is None:
return None
elif isinstance(__slots__, dict):
return __slots__
elif isinstance(__slots__, str):
return {__slots__: None}
elif isinstance(__slots__, (list, tuple)):
return dict.fromkeys(__slots__)
else:
raise ValueError
def isNewType(obj: Any) -> bool:
"""Check the if object is a kind of :class:`~typing.NewType`."""
if sys.version_info[:2] >= (3, 10):
return isinstance(obj, typing.NewType)
__module__ = safe_getattr(obj, '__module__', None)
__qualname__ = safe_getattr(obj, '__qualname__', None)
return __module__ == 'typing' and __qualname__ == 'NewType.<locals>.new_type'
def isenumclass(x: Any) -> bool:
"""Check if the object is an :class:`enumeration class <enum.Enum>`."""
return isclass(x) and issubclass(x, enum.Enum)
def isenumattribute(x: Any) -> bool:
"""Check if the object is an enumeration attribute."""
return isinstance(x, enum.Enum)
def unpartial(obj: Any) -> Any:
"""Get an original object from a partial-like object.
If *obj* is not a partial object, it is returned as is.
.. seealso:: :func:`ispartial`
"""
while ispartial(obj):
obj = obj.func
return obj
def ispartial(obj: Any) -> bool:
"""Check if the object is a partial function or method."""
return isinstance(obj, (partial, partialmethod))
def isclassmethod(obj: Any, cls: Any = None, name: str | None = None) -> bool:
"""Check if the object is a :class:`classmethod`."""
if isinstance(obj, classmethod):
return True
if ismethod(obj) and obj.__self__ is not None and isclass(obj.__self__):
return True
if cls and name:
# trace __mro__ if the method is defined in parent class
sentinel = object()
for basecls in getmro(cls):
meth = basecls.__dict__.get(name, sentinel)
if meth is not sentinel:
return isclassmethod(meth)
return False
def isstaticmethod(obj: Any, cls: Any = None, name: str | None = None) -> bool:
"""Check if the object is a :class:`staticmethod`."""
if isinstance(obj, staticmethod):
return True
if cls and name:
# trace __mro__ if the method is defined in parent class
sentinel = object()
for basecls in getattr(cls, '__mro__', [cls]):
meth = basecls.__dict__.get(name, sentinel)
if meth is not sentinel:
return isinstance(meth, staticmethod)
return False
def isdescriptor(x: Any) -> bool:
"""Check if the object is a :external+python:term:`descriptor`."""
return any(
callable(safe_getattr(x, item, None)) for item in ('__get__', '__set__', '__delete__')
)
def isabstractmethod(obj: Any) -> bool:
"""Check if the object is an :func:`abstractmethod`."""
return safe_getattr(obj, '__isabstractmethod__', False) is True
def isboundmethod(method: MethodType) -> bool:
"""Check if the method is a bound method."""
return safe_getattr(method, '__self__', None) is not None
def is_cython_function_or_method(obj: Any) -> bool:
"""Check if the object is a function or method in cython."""
try:
return obj.__class__.__name__ == 'cython_function_or_method'
except AttributeError:
return False
_DESCRIPTOR_LIKE: Final[tuple[type, ...]] = (
ClassMethodDescriptorType,
MethodDescriptorType,
WrapperDescriptorType,
)
def isattributedescriptor(obj: Any) -> bool:
"""Check if the object is an attribute-like descriptor."""
if inspect.isdatadescriptor(obj):
# data descriptor is kind of attribute
return True
if isdescriptor(obj):
# non data descriptor
unwrapped = unwrap(obj)
if isfunction(unwrapped) or isbuiltin(unwrapped) or ismethod(unwrapped):
# attribute must not be either function, builtin and method
return False
if is_cython_function_or_method(unwrapped):
# attribute must not be either function and method (for cython)
return False
if isclass(unwrapped):
# attribute must not be a class
return False
if isinstance(unwrapped, _DESCRIPTOR_LIKE):
# attribute must not be a method descriptor
return False
# attribute must not be an instancemethod (C-API)
return type(unwrapped).__name__ != 'instancemethod'
return False
def is_singledispatch_function(obj: Any) -> bool:
"""Check if the object is a :func:`~functools.singledispatch` function."""
return (
inspect.isfunction(obj)
and hasattr(obj, 'dispatch')
and hasattr(obj, 'register')
and obj.dispatch.__module__ == 'functools'
)
def is_singledispatch_method(obj: Any) -> bool:
"""Check if the object is a :class:`~functools.singledispatchmethod`."""
return isinstance(obj, singledispatchmethod)
def isfunction(obj: Any) -> bool:
"""Check if the object is a user-defined function.
Partial objects are unwrapped before checking them.
.. seealso:: :external+python:func:`inspect.isfunction`
"""
return inspect.isfunction(unpartial(obj))
def isbuiltin(obj: Any) -> bool:
"""Check if the object is a built-in function or method.
Partial objects are unwrapped before checking them.
.. seealso:: :external+python:func:`inspect.isbuiltin`
"""
return inspect.isbuiltin(unpartial(obj))
def isroutine(obj: Any) -> bool:
"""Check if the object is a kind of function or method.
Partial objects are unwrapped before checking them.
.. seealso:: :external+python:func:`inspect.isroutine`
"""
return inspect.isroutine(unpartial(obj))
def iscoroutinefunction(obj: Any) -> bool:
"""Check if the object is a :external+python:term:`coroutine` function."""
obj = unwrap_all(obj, stop=_is_wrapped_coroutine)
return inspect.iscoroutinefunction(obj)
def _is_wrapped_coroutine(obj: Any) -> bool:
"""Check if the object is wrapped coroutine-function."""
if isstaticmethod(obj) or isclassmethod(obj) or ispartial(obj):
# staticmethod, classmethod and partial method are not a wrapped coroutine-function
# Note: Since 3.10, staticmethod and classmethod becomes a kind of wrappers
return False
return hasattr(obj, '__wrapped__')
def isproperty(obj: Any) -> bool:
"""Check if the object is property (possibly cached)."""
return isinstance(obj, (property, cached_property))
def isgenericalias(obj: Any) -> bool:
"""Check if the object is a generic alias."""
return isinstance(obj, (types.GenericAlias, typing._BaseGenericAlias)) # type: ignore[attr-defined]
def safe_getattr(obj: Any, name: str, *defargs: Any) -> Any:
"""A getattr() that turns all exceptions into AttributeErrors."""
try:
return getattr(obj, name, *defargs)
except Exception as exc:
# sometimes accessing a property raises an exception (e.g.
# NotImplementedError), so let's try to read the attribute directly
try:
# In case the object does weird things with attribute access
# such that accessing `obj.__dict__` may raise an exception
return obj.__dict__[name]
except Exception:
pass
# this is a catch-all for all the weird things that some modules do
# with attribute access
if defargs:
return defargs[0]
raise AttributeError(name) from exc
def object_description(obj: Any, *, _seen: frozenset[int] = frozenset()) -> str:
"""A repr() implementation that returns text safe to use in reST context.
Maintains a set of 'seen' object IDs to detect and avoid infinite recursion.
"""
seen = _seen
if isinstance(obj, dict):
if id(obj) in seen:
return 'dict(...)'
seen |= {id(obj)}
try:
sorted_keys = sorted(obj)
except TypeError:
# Cannot sort dict keys, fall back to using descriptions as a sort key
sorted_keys = sorted(obj, key=lambda k: object_description(k, _seen=seen))
items = (
(object_description(key, _seen=seen), object_description(obj[key], _seen=seen))
for key in sorted_keys
)
return '{%s}' % ', '.join(f'{key}: {value}' for (key, value) in items)
elif isinstance(obj, set):
if id(obj) in seen:
return 'set(...)'
seen |= {id(obj)}
try:
sorted_values = sorted(obj)
except TypeError:
# Cannot sort set values, fall back to using descriptions as a sort key
sorted_values = sorted(obj, key=lambda x: object_description(x, _seen=seen))
return '{%s}' % ', '.join(object_description(x, _seen=seen) for x in sorted_values)
elif isinstance(obj, frozenset):
if id(obj) in seen:
return 'frozenset(...)'
seen |= {id(obj)}
try:
sorted_values = sorted(obj)
except TypeError:
# Cannot sort frozenset values, fall back to using descriptions as a sort key
sorted_values = sorted(obj, key=lambda x: object_description(x, _seen=seen))
return 'frozenset({%s})' % ', '.join(
object_description(x, _seen=seen) for x in sorted_values
)
elif isinstance(obj, enum.Enum):
if obj.__repr__.__func__ is not enum.Enum.__repr__: # type: ignore[attr-defined]
return repr(obj)
return f'{obj.__class__.__name__}.{obj.name}'
elif isinstance(obj, tuple):
if id(obj) in seen:
return 'tuple(...)'
seen |= frozenset([id(obj)])
return '({}{})'.format(
', '.join(object_description(x, _seen=seen) for x in obj),
',' * (len(obj) == 1),
)
elif isinstance(obj, list):
if id(obj) in seen:
return 'list(...)'
seen |= {id(obj)}
return '[%s]' % ', '.join(object_description(x, _seen=seen) for x in obj)
try:
s = repr(obj)
except Exception as exc:
raise ValueError from exc
# Strip non-deterministic memory addresses such as
# ``<__main__.A at 0x7f68cb685710>``
s = memory_address_re.sub('', s)
return s.replace('\n', ' ')
def is_builtin_class_method(obj: Any, attr_name: str) -> bool:
"""Check whether *attr_name* is implemented on a builtin class.
>>> is_builtin_class_method(int, '__init__')
True
This function is needed since CPython implements ``int.__init__`` via
descriptors, but PyPy implementation is written in pure Python code.
"""
mro = getmro(obj)
try:
cls = next(c for c in mro if attr_name in safe_getattr(c, '__dict__', {}))
except StopIteration:
return False
try:
name = safe_getattr(cls, '__name__')
except AttributeError:
return False
return getattr(builtins, name, None) is cls
class DefaultValue:
"""A simple wrapper for default value of the parameters of overload functions."""
def __init__(self, value: str) -> None:
self.value = value
def __eq__(self, other: object) -> bool:
return self.value == other
def __repr__(self) -> str:
return self.value
class TypeAliasForwardRef:
"""Pseudo typing class for :confval:`autodoc_type_aliases`.
This avoids the error on evaluating the type inside :func:`typing.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
def __hash__(self) -> int:
return hash(self.name)
def __repr__(self) -> str:
return self.name
class TypeAliasModule:
"""Pseudo module class for :confval:`autodoc_type_aliases`."""
def __init__(self, modname: str, mapping: Mapping[str, str]) -> None:
self.__modname = modname
self.__mapping = mapping
self.__module: ModuleType | None = 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 :confval:`autodoc_type_aliases`.
Useful for looking up nested objects via ``namespace.foo.bar.Class``.
"""
def __init__(self, mapping: Mapping[str, str]) -> None:
super().__init__()
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[..., Any]) -> bool:
"""Check the function should be unwrapped on getting signature."""
__globals__ = getglobals(subject)
# contextmanger should be unwrapped
return (
__globals__.get('__name__') == 'contextlib'
and __globals__.get('__file__') == contextlib.__file__
)
def signature(
subject: Callable[..., Any],
bound_method: bool = False,
type_aliases: Mapping[str, str] | None = None,
) -> Signature:
"""Return a Signature object for the given *subject*.
:param bound_method: Specify *subject* is a bound method or not
"""
if type_aliases is None:
type_aliases = {}
try:
if _should_unwrap(subject):
signature = inspect.signature(subject)
else:
signature = inspect.signature(subject, follow_wrapped=True)
except ValueError:
# follow built-in wrappers up (ex. functools.lru_cache)
signature = inspect.signature(subject)
parameters = list(signature.parameters.values())
return_annotation = signature.return_annotation
try:
# Resolve annotations using ``get_type_hints()`` and 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:
annotation = annotations[param.name]
if isinstance(annotation, TypeAliasForwardRef):
annotation = annotation.name
parameters[i] = param.replace(annotation=annotation)
if 'return' in annotations:
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.
pass
if bound_method:
if inspect.ismethod(subject):
# ``inspect.signature()`` considers the subject is a bound method and removes
# first argument from signature. Therefore no skips are needed here.
pass
else:
if len(parameters) > 0:
parameters.pop(0)
# To allow to create signature object correctly for pure python functions,
# pass an internal parameter __validate_parameters__=False to Signature
#
# For example, this helps a function having a default value `inspect._empty`.
# refs: https://github.com/sphinx-doc/sphinx/issues/7935
return Signature(
parameters, return_annotation=return_annotation, __validate_parameters__=False
)
def evaluate_signature(
sig: Signature,
globalns: dict[str, Any] | None = None,
localns: dict[str, Any] | None = None,
) -> Signature:
"""Evaluate unresolved type annotations in a signature object."""
if globalns is None:
globalns = {}
if localns is None:
localns = globalns
parameters = list(sig.parameters.values())
for i, param in enumerate(parameters):
if param.annotation:
annotation = _evaluate(param.annotation, globalns, localns)
parameters[i] = param.replace(annotation=annotation)
return_annotation = sig.return_annotation
if return_annotation:
return_annotation = _evaluate(return_annotation, globalns, localns)
return sig.replace(parameters=parameters, return_annotation=return_annotation)
def _evaluate_forwardref(
ref: ForwardRef,
globalns: dict[str, Any] | None,
localns: dict[str, Any] | None,
) -> Any:
"""Evaluate a forward reference."""
return ref._evaluate(globalns, localns, frozenset())
def _evaluate(
annotation: Any,
globalns: dict[str, Any],
localns: dict[str, Any],
) -> Any:
"""Evaluate unresolved type annotation."""
try:
if isinstance(annotation, str):
ref = ForwardRef(annotation, True)
annotation = _evaluate_forwardref(ref, globalns, localns)
if isinstance(annotation, ForwardRef):
annotation = _evaluate_forwardref(ref, globalns, localns)
elif isinstance(annotation, str):
# might be a ForwardRef'ed annotation in overloaded functions
ref = ForwardRef(annotation, True)
annotation = _evaluate_forwardref(ref, globalns, localns)
except (NameError, TypeError):
# failed to evaluate type. skipped.
pass
return annotation
def stringify_signature(
sig: Signature,
show_annotation: bool = True,
show_return_annotation: bool = True,
unqualified_typehints: bool = False,
) -> str:
"""Stringify a :class:`~inspect.Signature` object.
:param show_annotation: If enabled, show annotations on the signature
:param show_return_annotation: If enabled, show annotation of the return value
:param unqualified_typehints: If enabled, show annotations as unqualified
(ex. io.StringIO -> StringIO)
"""
if unqualified_typehints:
mode = 'smart'
else:
mode = 'fully-qualified'
EMPTY = Parameter.empty
args = []
last_kind = None
for param in sig.parameters.values():
if param.kind != Parameter.POSITIONAL_ONLY and last_kind == Parameter.POSITIONAL_ONLY:
# PEP-570: Separator for Positional Only Parameter: /
args.append('/')
if param.kind == Parameter.KEYWORD_ONLY and last_kind in (
Parameter.POSITIONAL_OR_KEYWORD,
Parameter.POSITIONAL_ONLY,
None,
):
# PEP-3102: Separator for Keyword Only Parameter: *
args.append('*')
arg = StringIO()
if param.kind is Parameter.VAR_POSITIONAL:
arg.write('*' + param.name)
elif param.kind is Parameter.VAR_KEYWORD:
arg.write('**' + param.name)
else:
arg.write(param.name)
if show_annotation and param.annotation is not EMPTY:
arg.write(': ')
arg.write(stringify_annotation(param.annotation, mode))
if param.default is not EMPTY:
if show_annotation and param.annotation is not EMPTY:
arg.write(' = ')
else:
arg.write('=')
arg.write(object_description(param.default))
args.append(arg.getvalue())
last_kind = param.kind
if last_kind is Parameter.POSITIONAL_ONLY:
# PEP-570: Separator for Positional Only Parameter: /
args.append('/')
concatenated_args = ', '.join(args)
if sig.return_annotation is EMPTY or not show_annotation or not show_return_annotation:
return f'({concatenated_args})'
else:
retann = stringify_annotation(sig.return_annotation, mode)
return f'({concatenated_args}) -> {retann}'
def signature_from_str(signature: str) -> Signature:
"""Create a :class:`~inspect.Signature` object from a string."""
code = 'def func' + signature + ': pass'
module = ast.parse(code)
function = typing.cast(ast.FunctionDef, module.body[0])
return signature_from_ast(function, code)
def signature_from_ast(node: ast.FunctionDef, code: str = '') -> Signature:
"""Create a :class:`~inspect.Signature` object from an AST node."""
EMPTY = Parameter.empty
args: ast.arguments = node.args
defaults: tuple[ast.expr | None, ...] = tuple(args.defaults)
pos_only_offset = len(args.posonlyargs)
defaults_offset = pos_only_offset + len(args.args) - len(defaults)
# The sequence ``D = args.defaults`` contains non-None AST expressions,
# so we can use ``None`` as a sentinel value for that to indicate that
# there is no default value for a specific parameter.
#
# Let *p* be the number of positional-only and positional-or-keyword
# arguments. Note that ``0 <= len(D) <= p`` and ``D[0]`` is the default
# value corresponding to a positional-only *or* a positional-or-keyword
# argument. Since a non-default argument cannot follow a default argument,
# the sequence *D* can be completed on the left by adding None sentinels
# so that ``len(D) == p`` and ``D[i]`` is the *i*-th default argument.
defaults = (None,) * defaults_offset + defaults
# construct the parameter list
params: list[Parameter] = []
# positional-only arguments (introduced in Python 3.8)
for arg, defexpr in zip(args.posonlyargs, defaults):
params.append(_define(Parameter.POSITIONAL_ONLY, arg, code, defexpr=defexpr))
# normal arguments
for arg, defexpr in zip(args.args, defaults[pos_only_offset:]):
params.append(_define(Parameter.POSITIONAL_OR_KEYWORD, arg, code, defexpr=defexpr))
# variadic positional argument (no possible default expression)
if args.vararg:
params.append(_define(Parameter.VAR_POSITIONAL, args.vararg, code, defexpr=None))
# keyword-only arguments
for arg, defexpr in zip(args.kwonlyargs, args.kw_defaults):
params.append(_define(Parameter.KEYWORD_ONLY, arg, code, defexpr=defexpr))
# variadic keyword argument (no possible default expression)
if args.kwarg:
params.append(_define(Parameter.VAR_KEYWORD, args.kwarg, code, defexpr=None))
return_annotation = ast_unparse(node.returns, code) or EMPTY
return Signature(params, return_annotation=return_annotation)
def _define(
kind: _ParameterKind,
arg: ast.arg,
code: str,
*,
defexpr: ast.expr | None,
) -> Parameter:
EMPTY = Parameter.empty
default = EMPTY if defexpr is None else DefaultValue(ast_unparse(defexpr, code))
annotation = ast_unparse(arg.annotation, code) or EMPTY
return Parameter(arg.arg, kind, default=default, annotation=annotation)
def getdoc(
obj: Any,
attrgetter: Callable = safe_getattr,
allow_inherited: bool = False,
cls: Any = None,
name: str | None = None,
) -> str | None:
"""Get the docstring for the object.
This tries to obtain the docstring for some kind of objects additionally:
* partial functions
* inherited docstring
* inherited decorated methods
"""
if cls and name and isclassmethod(obj, cls, name):
for basecls in getmro(cls):
meth = basecls.__dict__.get(name)
if meth and hasattr(meth, '__func__'):
doc: str | None = getdoc(meth.__func__)
if doc is not None or not allow_inherited:
return doc
doc = _getdoc_internal(obj)
if ispartial(obj) and doc == obj.__class__.__doc__:
return getdoc(obj.func)
elif doc is None and allow_inherited:
if cls and name:
# Check a docstring of the attribute or method from super classes.
for basecls in getmro(cls):
meth = safe_getattr(basecls, name, None)
if meth is not None:
doc = _getdoc_internal(meth)
if doc is not None:
break
if doc is None:
# retry using `inspect.getdoc()`
for basecls in getmro(cls):
meth = safe_getattr(basecls, name, None)
if meth is not None:
doc = inspect.getdoc(meth)
if doc is not None:
break
if doc is None:
doc = inspect.getdoc(obj)
return doc
def _getdoc_internal(
obj: Any, attrgetter: Callable[[Any, str, Any], Any] = safe_getattr
) -> str | None:
doc = attrgetter(obj, '__doc__', None)
if isinstance(doc, str):
return doc
return None