Source code for dol.sig_builtins

"""Signatures for the callables that ``inspect.signature`` can't handle.

Many builtins (``print``, ``map``, ``itertools.product``, ...) have no signature that
``inspect`` can read. This module holds hand-made ones, and the function ``Sig`` uses
to get a signature for any callable, falling back to those:

- ``_robust_signature_of_callable``: ``inspect.signature``, with the fallbacks below
- ``sigs_for_builtins``, ``sigs_for_builtin_modules``, ``sigs_for_type_name``: the
  hand-made signatures, by name (``sigs_for_sigless_builtin_name`` merges the first two)
- ``resolve_function``, ``dict_of_attribute_signatures``: helpers

It depends on nothing else in ``dol``. It used to be part of ``dol.signatures``, which
still re-exports all of it (i2mint/dol#70).

    >>> from dol.sig_builtins import _robust_signature_of_callable
    >>> _robust_signature_of_callable(isinstance)
    <Signature (obj, class_or_tuple, /)>
"""

import sys
from inspect import Signature, signature
from typing import Any, KT, VT, T
from collections.abc import Callable, Iterable, Iterator, Mapping as MappingType
from functools import cached_property, partial, partialmethod

# A default signature of (*no_sig_args, **no_sig_kwargs)
DFLT_SIGNATURE = signature(lambda *no_sig_args, **no_sig_kwargs: ...)


#########################################################################################
# Manual construction of missing signatures
# ############################################################################


# TODO: Might want to monkey-patch inspect._signature_from_callable to use
#  sigs_for_sigless_builtin_name
def _robust_signature_of_callable(callable_obj: Callable) -> Signature:
    r"""Get the signature of a Callable, returning a custom made one for those
    builtins that don't have one

    >>> _robust_signature_of_callable(
    ...     _robust_signature_of_callable
    ... )  # has a normal signature
    <Signature (callable_obj: ...Callable) -> inspect.Signature>
    >>> s = _robust_signature_of_callable(print)  # has one that this module provides
    >>> assert isinstance(s, Signature)
    >>> # Will be: <Signature (*value, sep=' ', end='\n', file=<_io.TextIOWrapper
    name='<stdout>' mode='w' encoding='utf-8'>, flush=False)>
    >>> _robust_signature_of_callable(
    ...     slice
    ... )  # doesn't have one, so will return a blanket one
    <Signature (*no_sig_args, **no_sig_kwargs)>
    """
    # First check if we have a custom signature for this type/object
    # This is important for operator instances that might have generic signatures in Python 3.12+
    obj_name = getattr(callable_obj, "__name__", None)
    if obj_name in sigs_for_sigless_builtin_name:
        return sigs_for_sigless_builtin_name[obj_name] or DFLT_SIGNATURE

    type_name = getattr(type(callable_obj), "__name__", None)
    if type_name in sigs_for_type_name:
        return sigs_for_type_name[type_name] or DFLT_SIGNATURE

    # Try to get the signature normally
    try:
        return signature(callable_obj)
    except ValueError:
        # if all attempts fail, return the default signature
        return DFLT_SIGNATURE


[docs] def resolve_function(obj: T) -> T | Callable: """Get the underlying function of a property or cached_property Note that if all conditions fail, the object itself is returned. The problem this function solves is that sometimes there's a function behind an object, but it's not always easy to get to it. For example, in a class, you might want to get the source of the code decorated with ``@property``, a ``@cached_property``, or a ``partial`` function. Consider the following example: >>> from functools import cached_property, partial >>> class C: ... @property ... def prop(self): ... pass ... @cached_property ... def cached_prop(self): ... pass ... partial_func = partial(partial) Note that ``prop`` is not callable, and you can't get its source. >>> import inspect >>> callable(C.prop) False >>> inspect.getsource(C.prop) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... TypeError: <property object at 0x...> is not a module, class, method, function, traceback, frame, or code object But if you grab the underlying function, you can get the source: >>> func = resolve_function(C.prop) >>> callable(func) True >>> isinstance(inspect.getsource(func), str) True Same goes with ``cached_property`` and ``partial``: >>> isinstance(inspect.getsource(resolve_function(C.cached_prop)), str) True >>> isinstance(inspect.getsource(resolve_function(C.partial_func)), str) True """ if isinstance(obj, cached_property): return obj.func elif isinstance(obj, property): return obj.fget elif isinstance(obj, (partial, partialmethod)): return obj.func elif not callable(obj) and callable(wrapped := getattr(obj, "__wrapped__", None)): # If obj is not callable, but has a __wrapped__ attribute that is, return that return wrapped else: # if not just return obj return obj
[docs] def dict_of_attribute_signatures(cls: type) -> dict[str, Signature]: """ A function that extracts the signatures of all callable attributes of a class. :param cls: The class that holds the the ``(name, func)`` pairs we want to extract. :return: A dict of ``(name, signature(func))`` pairs extracted from class. One of the intended applications is to use ``dict_of_attribute_signatures`` as a decorator, like so: >>> @dict_of_attribute_signatures ... class names_and_signatures: ... def foo(x: str, *, y=2) -> tuple: ... ... def bar(z, /) -> float: ... >>> names_and_signatures {'foo': <Signature (x: str, *, y=2) -> tuple>, 'bar': <Signature (z, /) -> float>} """ def gen(): object_attr_names = set(vars(object)) for attr_name, attr_val in vars(cls).items(): if callable(attr_val): if attr_name not in object_attr_names: # if the attr is a callable attribute that's not in all objects... yield attr_name, signature(attr_val) return dict(gen())
@dict_of_attribute_signatures class sigs_for_builtins: def __import__(name, globals=None, locals=None, fromlist=(), level=0): """__import__(name, globals=None, locals=None, fromlist=(), level=0) -> module""" def filter(function, iterable, /): """filter(function or None, iterable) --> filter object""" def map(func, iterable, /, *iterables): """``map(func, *iterables) --> map object``""" def print(*value, sep=" ", end="\n", file=sys.stdout, flush=False): r"""``print(value, ..., sep=' ', end='\n', file=sys.stdout, flush=False)``""" def zip(*iterables): """ ``zip(*iterables)`` --> A zip object yielding tuples until an input is exhausted. """ def bool(x: Any, /) -> bool: ... def bytearray(iterable_of_ints: Iterable[int], /): ... def classmethod(function: Callable, /): ... def int(x, base=10, /): ... def iter(callable: Callable, sentinel=None, /): ... def next(iterator: Iterator, default=None, /): ... def staticmethod(function: Callable, /): ... def str(bytes_or_buffer, encoding=None, errors=None, /): ... def super(type_, obj=None, /): ... # def type(name, bases=None, dict=None, /): # ... sigs_for_builtins = dict( sigs_for_builtins, **{ "__build_class__": None, # __build_class__(func, name, /, *bases, [metaclass], **kwds) -> class # "bool": None, # bool(x) -> bool "breakpoint": None, # breakpoint(*args, **kws) # "bytearray": None, # bytearray(iterable_of_ints) -> bytearray # bytearray(string, encoding[, errors]) -> bytearray # bytearray(bytes_or_buffer) -> mutable copy of bytes_or_buffer # bytearray(int) -> bytes array of size given by the parameter initialized with # null bytes # bytearray() -> empty bytes array "bytes": None, # bytes(iterable_of_ints) -> bytes # bytes(string, encoding[, errors]) -> bytes # bytes(bytes_or_buffer) -> immutable copy of bytes_or_buffer # bytes(int) -> bytes object of size given by the parameter initialized with null # bytes # bytes() -> empty bytes object # "classmethod": None, # classmethod(function) -> method "dict": None, # dict() -> new empty dictionary # dict(mapping) -> new dictionary initialized from a mapping object's # dict(iterable) -> new dictionary initialized as if via: # dict(**kwargs) -> new dictionary initialized with the name=value pairs "dir": None, # dir([object]) -> list of strings "frozenset": None, # frozenset() -> empty frozenset object # frozenset(iterable) -> frozenset object "getattr": None, # getattr(object, name[, default]) -> value # "int": None, # int([x]) -> integer # int(x, base=10) -> integer # "iter": None, # iter(iterable) -> iterator # iter(callable, sentinel) -> iterator "max": None, # max(iterable, *[, default=obj, key=func]) -> value # max(arg1, arg2, *args, *[, key=func]) -> value "min": None, # min(iterable, *[, default=obj, key=func]) -> value # min(arg1, arg2, *args, *[, key=func]) -> value # "next": None, # next(iterator[, default]) "range": None, # range(stop) -> range object # range(start, stop[, step]) -> range object "set": None, # set() -> new empty set object # set(iterable) -> new set object "slice": None, # slice(stop) # slice(start, stop[, step]) # "staticmethod": None, # staticmethod(function) -> method # "str": None, # str(object='') -> str # str(bytes_or_buffer[, encoding[, errors]]) -> str # "super": None, # super() -> same as super(__class__, <first argument>) # super(type) -> unbound super object # super(type, obj) -> bound super object; requires isinstance(obj, type) # super(type, type2) -> bound super object; requires issubclass(type2, type) # "type": None, # type(object_or_name, bases, dict) # type(object) -> the object's type # type(name, bases, dict) -> a new type "vars": None, # vars([object]) -> dictionary }, ) # # Remove the None-valued elements (No, don't, because we distinguish # # functions we listed but didn't associate a default signature, with those functions # # we don't list at all. # sigs_for_builtins = { # k: v for k, v in sigs_for_builtins.items() if v is not None # } # TODO: itemgetter, attrgetter and methodcaller use KT as their first argument, but # in reality both attrgetter and methodcaller are more restrictive: They need to be # valid attributes, therefore valid python identifiers. Any better typing for that? # TODO: We take care of the MutableMapping dunders below, but some of these dunders # are not specific to MutableMapping. The signature used below is somewhat, but not # completely, specific to MutableMapping. For example, __contains__ is also defined # for the `set` type, but it's input is not called key, nor would the KT annotation # be completely correct. The signatures were sometimes made to be more general (such # as __setitem__ and __delitem__ returning an Any instead of None), but could be # made more (for example, annotating return of __iter__ as Iterator instead of # Iterator[KT]). We hope that the fact that all the signatures are positional-only # will at least mitigate the problem as far as name differences go. @dict_of_attribute_signatures class sigs_for_builtin_modules: """ Below are the signatures, manually created to match those callables of the python standard library that don't have signatures (through ``inspect.signature``), """ def __eq__(self, other, /) -> bool: """self.__eq__(other) <==> self==other""" def __ne__(self, other, /) -> bool: """self.__ne__(other) <==> self!=other""" def __iter__(self, /) -> Iterator[KT]: """self.__iter__() <==> iter(self)""" def __getitem__(self, key: KT, /) -> VT: """self.__getitem__(key) <==> self[key]""" def __len__(self, /) -> int: """self.__len__() <==> len(self)""" def __contains__(self, key: KT, /) -> bool: """self.__contains__(key) <==> key in self""" def __setitem__(self, key: KT, value: VT, /) -> Any: """self.__setitem__(key, value) <==> self[key] = value""" def __delitem__(self, key: KT, /) -> Any: """self.__delitem__(key) <==> del self[key]""" def itemgetter(item, /, *items) -> Callable[[Iterable[VT]], VT | tuple[VT]]: """itemgetter(item, ...) --> itemgetter object,""" def attrgetter(attr, /, *attrs) -> Callable[[Iterable[VT]], VT | tuple[VT]]: """attrgetter(item, ...) --> attrgetter object,""" def methodcaller( name: KT, /, *args: Iterable[VT], **kwargs: MappingType[str, Any] ) -> Callable[[Any], Any]: """methodcaller(name, ...) --> methodcaller object""" def partial(func: Callable, *args, **keywords) -> Callable: """``partial(func, *args, **keywords)`` - new function with partial application of the given arguments and keywords.""" def partialmethod(func: Callable, *args, **keywords) -> Callable: """``functools.partialmethod(func, *args, **keywords)``""" # Merge sigs_for_builtin_modules and sigs_for_builtins sigs_for_sigless_builtin_name = dict(sigs_for_builtin_modules, **sigs_for_builtins) @dict_of_attribute_signatures class sigs_for_type_name: """ Below are the signatures, manually created to match callable objects that are output by builtin functions or are instances of builtin classes, and that have no signatures (through ``inspect.signature``), """ def itemgetter(iterable: Iterable[VT], /) -> VT | tuple[VT]: ... def attrgetter(iterable: Iterable[VT], /) -> VT | tuple[VT]: ... def methodcaller(obj: Any) -> Any: ...