diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index b5cae374b0e..45e4cad65cc 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -2438,8 +2438,8 @@ def get_doc(self) -> list[list[str]] | None: if self.object is SLOTSATTR: try: parent___slots__ = inspect.getslots(self.parent) - if parent___slots__ and parent___slots__.get(self.objpath[-1]): - docstring = prepare_docstring(parent___slots__[self.objpath[-1]]) + if parent___slots__ and (docstring := parent___slots__.get(self.objpath[-1])): + docstring = prepare_docstring(docstring) return [docstring] else: return [] diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 654419f9b88..70938131fd6 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -11,41 +11,45 @@ import sys import types import typing -from collections.abc import Mapping, Sequence +from collections.abc import Mapping from functools import cached_property, partial, partialmethod, singledispatchmethod from importlib import import_module -from inspect import ( # NoQA: F401 - Parameter, - isasyncgenfunction, - isclass, - ismethod, - ismethoddescriptor, - ismodule, -) +from inspect import Parameter, Signature from io import StringIO -from types import ( - ClassMethodDescriptorType, - MethodDescriptorType, - MethodType, - ModuleType, - WrapperDescriptorType, -) -from typing import Any, Callable, cast +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).""" + """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: @@ -54,13 +58,27 @@ def unwrap(obj: Any) -> Any: 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. """ - Get an original object from wrapped object (unwrapping partials, wrapped - functions, and other decorators). - """ + 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 stop and stop(obj): - return obj if ispartial(obj): obj = obj.func elif inspect.isroutine(obj) and hasattr(obj, '__wrapped__'): @@ -72,10 +90,11 @@ def unwrap_all(obj: Any, *, stop: Callable[[Any], bool] | None = None) -> Any: def getall(obj: Any) -> Sequence[str] | None: - """Get __all__ attribute of the module as dict. + """Get the ``__all__`` attribute of an object as sequence. - Return None if given *obj* does not have __all__. - Raises ValueError if given *obj* have invalid __all__. + 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: @@ -86,7 +105,7 @@ def getall(obj: Any) -> Sequence[str] | None: def getannotations(obj: Any) -> Mapping[str, Any]: - """Get __annotations__ from given *obj* safely.""" + """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: @@ -96,31 +115,32 @@ def getannotations(obj: Any) -> Mapping[str, Any]: __annotations__ = __dict__.get('__annotations__', None) if isinstance(__annotations__, Mapping): return __annotations__ - else: - return {} + return {} def getglobals(obj: Any) -> Mapping[str, Any]: - """Get __globals__ from given *obj* safely.""" + """Safely get :attr:`obj.__globals__ `.""" __globals__ = safe_getattr(obj, '__globals__', None) if isinstance(__globals__, Mapping): return __globals__ - else: - return {} + return {} def getmro(obj: Any) -> tuple[type, ...]: - """Get __mro__ from given *obj* safely.""" + """Safely get :attr:`obj.__mro__ `.""" __mro__ = safe_getattr(obj, '__mro__', None) if isinstance(__mro__, tuple): return __mro__ - else: - return () + return () def getorigbases(obj: Any) -> tuple[Any, ...] | None: - """Get __orig_bases__ from *obj* safely.""" - if not inspect.isclass(obj): + """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__. @@ -129,18 +149,17 @@ def getorigbases(obj: Any) -> tuple[Any, ...] | None: __orig_bases__ = __dict__.get('__orig_bases__') if isinstance(__orig_bases__, tuple) and len(__orig_bases__) > 0: return __orig_bases__ - else: - return None + return None -def getslots(obj: Any) -> dict[str, Any] | None: - """Get __slots__ attribute of the class as dict. +def getslots(obj: Any) -> dict[str, Any] | dict[str, None] | None: + """Safely get :term:`obj.__slots__ <__slots__>` as a dictionary if any. - Return None if gienv *obj* does not have __slots__. - Raises TypeError if given *obj* is not a class. - Raises ValueError if given *obj* have invalid __slots__. + - 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 inspect.isclass(obj): + if not isclass(obj): raise TypeError __slots__ = safe_getattr(obj, '__slots__', None) @@ -157,7 +176,7 @@ def getslots(obj: Any) -> dict[str, Any] | None: def isNewType(obj: Any) -> bool: - """Check the if object is a kind of NewType.""" + """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) @@ -166,72 +185,69 @@ def isNewType(obj: Any) -> bool: def isenumclass(x: Any) -> bool: - """Check if the object is subclass of enum.""" - return inspect.isclass(x) and issubclass(x, enum.Enum) + """Check if the object is an :class:`enumeration class `.""" + return isclass(x) and issubclass(x, enum.Enum) def isenumattribute(x: Any) -> bool: - """Check if the object is attribute of enum.""" + """Check if the object is an enumeration attribute.""" return isinstance(x, enum.Enum) def unpartial(obj: Any) -> Any: - """Get an original object from partial object. + """Get an original object from a partial-like object. + + If *obj* is not a partial object, it is returned as is. - This returns given object itself if not partial. + .. seealso:: :func:`ispartial` """ while ispartial(obj): obj = obj.func - return obj def ispartial(obj: Any) -> bool: - """Check if the object is partial.""" + """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 classmethod.""" + """Check if the object is a :class:`classmethod`.""" if isinstance(obj, classmethod): return True - if inspect.ismethod(obj) and obj.__self__ is not None and isclass(obj.__self__): + if ismethod(obj) and obj.__self__ is not None and isclass(obj.__self__): return True if cls and name: - placeholder = object() + # trace __mro__ if the method is defined in parent class for basecls in getmro(cls): - meth = basecls.__dict__.get(name, placeholder) - if meth is not placeholder: + meth = basecls.__dict__.get(name) + if meth is not None: return isclassmethod(meth) - return False def isstaticmethod(obj: Any, cls: Any = None, name: str | None = None) -> bool: - """Check if the object is staticmethod.""" + """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 - # - # .. note:: This only works well with new style classes. for basecls in getattr(cls, '__mro__', [cls]): meth = basecls.__dict__.get(name) - if meth: + if meth is not None: return isinstance(meth, staticmethod) return False def isdescriptor(x: Any) -> bool: - """Check if the object is some kind of descriptor.""" + """Check if the object is a :external+python:term:`descriptor`.""" return any( - callable(safe_getattr(x, item, None)) - for item in ('__get__', '__set__', '__delete__') + callable(safe_getattr(x, item, None)) for item in ('__get__', '__set__', '__delete__') ) def isabstractmethod(obj: Any) -> bool: - """Check if the object is an abstractmethod.""" + """Check if the object is an :func:`abstractmethod`.""" return safe_getattr(obj, '__isabstractmethod__', False) is True @@ -248,84 +264,106 @@ def is_cython_function_or_method(obj: Any) -> bool: return False +_DESCRIPTOR_LIKE: Final[tuple[type, ...]] = ( + ClassMethodDescriptorType, + MethodDescriptorType, + WrapperDescriptorType, +) + + def isattributedescriptor(obj: Any) -> bool: - """Check if the object is an attribute like descriptor.""" + """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 inspect.ismethod(unwrapped): + 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 inspect.isclass(unwrapped): + if isclass(unwrapped): # attribute must not be a class return False - if isinstance(unwrapped, (ClassMethodDescriptorType, - MethodDescriptorType, - WrapperDescriptorType)): + 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 type(unwrapped).__name__ != 'instancemethod' return False def is_singledispatch_function(obj: Any) -> bool: - """Check if the object is singledispatch function.""" - return (inspect.isfunction(obj) and - hasattr(obj, 'dispatch') and - hasattr(obj, 'register') and - obj.dispatch.__module__ == 'functools') + """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 singledispatch method.""" + """Check if the object is a :class:`~functools.singledispatchmethod`.""" return isinstance(obj, singledispatchmethod) def isfunction(obj: Any) -> bool: - """Check if the object is function.""" + """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 function.""" + """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 is any kind of function or method.""" + """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 coroutine-function.""" - def iswrappedcoroutine(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__') - - obj = unwrap_all(obj, stop=iswrappedcoroutine) + """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.""" + """Check if the object is property (possibly cached).""" return isinstance(obj, (property, cached_property)) def isgenericalias(obj: Any) -> bool: - """Check if the object is GenericAlias.""" - return isinstance( - obj, (types.GenericAlias, typing._BaseGenericAlias)) # type: ignore[attr-defined] + """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: @@ -366,8 +404,10 @@ def object_description(obj: Any, *, _seen: frozenset[int] = frozenset()) -> str: # 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) + 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: @@ -388,8 +428,9 @@ def object_description(obj: Any, *, _seen: frozenset[int] = frozenset()) -> str: 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) + 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) @@ -419,16 +460,18 @@ def object_description(obj: Any, *, _seen: frozenset[int] = frozenset()) -> str: def is_builtin_class_method(obj: Any, attr_name: str) -> bool: - """If attr_name is implemented at builtin class, return True. + """Check whether *attr_name* is implemented on a builtin class. >>> is_builtin_class_method(int, '__init__') True - Why this function needed? CPython implements int.__init__ by Descriptor - but PyPy implements it by pure Python code. + + This function is needed since CPython implements ``int.__init__`` via + descriptors, but PyPy implementation is written in pure Python code. """ + mro = getmro(obj) + try: - mro = getmro(obj) cls = next(c for c in mro if attr_name in safe_getattr(c, '__dict__', {})) except StopIteration: return False @@ -455,9 +498,9 @@ def __repr__(self) -> str: class TypeAliasForwardRef: - """Pseudo typing class for autodoc_type_aliases. + """Pseudo typing class for :confval:`autodoc_type_aliases`. - This avoids the error on evaluating the type inside `get_type_hints()`. + This avoids the error on evaluating the type inside :func:`typing.get_type_hints()`. """ def __init__(self, name: str) -> None: @@ -478,9 +521,9 @@ def __repr__(self) -> str: class TypeAliasModule: - """Pseudo module class for autodoc_type_aliases.""" + """Pseudo module class for :confval:`autodoc_type_aliases`.""" - def __init__(self, modname: str, mapping: dict[str, str]) -> None: + def __init__(self, modname: str, mapping: Mapping[str, str]) -> None: self.__modname = modname self.__mapping = mapping @@ -511,12 +554,13 @@ def __getattr__(self, name: str) -> Any: class TypeAliasNamespace(dict[str, Any]): - """Pseudo namespace class for autodoc_type_aliases. + """Pseudo namespace class for :confval:`autodoc_type_aliases`. - This enables to look up nested modules and classes like `mod1.mod2.Class`. + Useful for looking up nested objects via ``namespace.foo.bar.Class``. """ - def __init__(self, mapping: dict[str, str]) -> None: + def __init__(self, mapping: Mapping[str, str]) -> None: + super().__init__() self.__mapping = mapping def __getitem__(self, key: str) -> Any: @@ -533,17 +577,21 @@ def __getitem__(self, key: str) -> Any: raise KeyError -def _should_unwrap(subject: Callable) -> bool: +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__) + return ( + __globals__.get('__name__') == 'contextlib' + and __globals__.get('__file__') == contextlib.__file__ + ) def signature( - subject: Callable, bound_method: bool = False, type_aliases: dict[str, str] | None = None, -) -> inspect.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 @@ -596,41 +644,17 @@ def signature( # # For example, this helps a function having a default value `inspect._empty`. # refs: https://github.com/sphinx-doc/sphinx/issues/7935 - return inspect.Signature(parameters, return_annotation=return_annotation, - __validate_parameters__=False) + return Signature( + parameters, return_annotation=return_annotation, __validate_parameters__=False + ) -def evaluate_signature(sig: inspect.Signature, globalns: dict[str, Any] | None = None, - localns: dict[str, Any] | None = None, - ) -> inspect.Signature: +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.""" - 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 - if globalns is None: globalns = {} if localns is None: @@ -639,20 +663,56 @@ def evaluate( parameters = list(sig.parameters.values()) for i, param in enumerate(parameters): if param.annotation: - annotation = evaluate(param.annotation, globalns, localns) + 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_annotation = _evaluate(return_annotation, globalns, localns) return sig.replace(parameters=parameters, return_annotation=return_annotation) -def stringify_signature(sig: inspect.Signature, show_annotation: bool = True, - show_return_annotation: bool = True, - unqualified_typehints: bool = False) -> str: - """Stringify a Signature object. +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 @@ -664,31 +724,35 @@ def stringify_signature(sig: inspect.Signature, show_annotation: bool = True, else: mode = 'fully-qualified' + EMPTY = Parameter.empty + args = [] last_kind = None for param in sig.parameters.values(): - if param.kind != param.POSITIONAL_ONLY and last_kind == param.POSITIONAL_ONLY: + if param.kind != Parameter.POSITIONAL_ONLY and last_kind == Parameter.POSITIONAL_ONLY: # PEP-570: Separator for Positional Only Parameter: / args.append('/') - if param.kind == param.KEYWORD_ONLY and last_kind in (param.POSITIONAL_OR_KEYWORD, - param.POSITIONAL_ONLY, - None): + 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 == param.VAR_POSITIONAL: + if param.kind is Parameter.VAR_POSITIONAL: arg.write('*' + param.name) - elif param.kind == param.VAR_KEYWORD: + elif param.kind is Parameter.VAR_KEYWORD: arg.write('**' + param.name) else: arg.write(param.name) - if show_annotation and param.annotation is not param.empty: + if show_annotation and param.annotation is not EMPTY: arg.write(': ') arg.write(stringify_annotation(param.annotation, mode)) - if param.default is not param.empty: - if show_annotation and param.annotation is not param.empty: + if param.default is not EMPTY: + if show_annotation and param.annotation is not EMPTY: arg.write(' = ') else: arg.write('=') @@ -697,91 +761,86 @@ def stringify_signature(sig: inspect.Signature, show_annotation: bool = True, args.append(arg.getvalue()) last_kind = param.kind - if last_kind == Parameter.POSITIONAL_ONLY: + 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 Parameter.empty or - show_annotation is False or - show_return_annotation is False): + if sig.return_annotation is EMPTY or not show_annotation or not show_return_annotation: return f'({concatenated_args})' else: - annotation = stringify_annotation(sig.return_annotation, mode) - return f'({concatenated_args}) -> {annotation}' + retann = stringify_annotation(sig.return_annotation, mode) + return f'({concatenated_args}) -> {retann}' -def signature_from_str(signature: str) -> inspect.Signature: - """Create a Signature object from string.""" +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 = cast(ast.FunctionDef, module.body[0]) + function = typing.cast(ast.FunctionDef, module.body[0]) return signature_from_ast(function, code) -def signature_from_ast(node: ast.FunctionDef, code: str = '') -> inspect.Signature: - """Create a Signature object from AST *node*.""" - args = node.args - defaults = list(args.defaults) - params = [] - if hasattr(args, "posonlyargs"): - posonlyargs = len(args.posonlyargs) - positionals = posonlyargs + len(args.args) - else: - posonlyargs = 0 - positionals = len(args.args) - - for _ in range(len(defaults), positionals): - defaults.insert(0, Parameter.empty) # type: ignore[arg-type] - - if hasattr(args, "posonlyargs"): - for i, arg in enumerate(args.posonlyargs): - if defaults[i] is Parameter.empty: - default = Parameter.empty - else: - default = DefaultValue( - ast_unparse(defaults[i], code)) # type: ignore[assignment] - - annotation = ast_unparse(arg.annotation, code) or Parameter.empty - params.append(Parameter(arg.arg, Parameter.POSITIONAL_ONLY, - default=default, annotation=annotation)) - - for i, arg in enumerate(args.args): - if defaults[i + posonlyargs] is Parameter.empty: - default = Parameter.empty - else: - default = DefaultValue( - ast_unparse(defaults[i + posonlyargs], code), # type: ignore[assignment] - ) - - annotation = ast_unparse(arg.annotation, code) or Parameter.empty - params.append(Parameter(arg.arg, Parameter.POSITIONAL_OR_KEYWORD, - default=default, annotation=annotation)) +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: - annotation = ast_unparse(args.vararg.annotation, code) or Parameter.empty - params.append(Parameter(args.vararg.arg, Parameter.VAR_POSITIONAL, - annotation=annotation)) + params.append(_define(Parameter.VAR_POSITIONAL, args.vararg, code, defexpr=None)) - for i, arg in enumerate(args.kwonlyargs): - if args.kw_defaults[i] is None: - default = Parameter.empty - else: - default = DefaultValue( - ast_unparse(args.kw_defaults[i], code)) # type: ignore[arg-type,assignment] - annotation = ast_unparse(arg.annotation, code) or Parameter.empty - params.append(Parameter(arg.arg, Parameter.KEYWORD_ONLY, default=default, - annotation=annotation)) + # 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: - annotation = ast_unparse(args.kwarg.annotation, code) or Parameter.empty - params.append(Parameter(args.kwarg.arg, Parameter.VAR_KEYWORD, - annotation=annotation)) + 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) + - return_annotation = ast_unparse(node.returns, code) or Parameter.empty +def _define( + kind: _ParameterKind, + arg: ast.arg, + code: str, + *, + defexpr: ast.expr | None, +) -> Parameter: + EMPTY = Parameter.empty - return inspect.Signature(params, return_annotation=return_annotation) + 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( @@ -799,15 +858,6 @@ def getdoc( * inherited docstring * inherited decorated methods """ - 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 - else: - return None - if cls and name and isclassmethod(obj, cls, name): for basecls in getmro(cls): meth = basecls.__dict__.get(name) @@ -816,7 +866,7 @@ def getdoc_internal( if doc is not None or not allow_inherited: return doc - doc = getdoc_internal(obj) + doc = _getdoc_internal(obj) if ispartial(obj) and doc == obj.__class__.__doc__: return getdoc(obj.func) elif doc is None and allow_inherited: @@ -825,7 +875,7 @@ def getdoc_internal( for basecls in getmro(cls): meth = safe_getattr(basecls, name, None) if meth is not None: - doc = getdoc_internal(meth) + doc = _getdoc_internal(meth) if doc is not None: break @@ -842,3 +892,12 @@ def getdoc_internal( 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