Hidden Docs of Fastcore

What is hidden docs?

The docs of fastcore is very well thought and written. The more you dig into the source code, the more you will appreciate it.

However, to make docs clean and clear, it can’t explain everything in the source, so, there is always something new to learn, interesting features to discover, or even potential issues where we can help if very very lucky.

In this topic, I would like to share the interesting little things as I explore the fastcore library. You are very welcome to join and share your findings here too.

What is fastcore?
Basically, it is one of the key foundation of all fastai libraries. Read more about fastcore here.

9 Likes

Findings from fastcore.meta.delegates

from fastcore.imports import *
from fastcore.meta import *
from nbdev.showdoc import *
from fastcore.test import *
from pprint import pprint
from debuggable.utils import *
import inspect

The undocumented feature of arg keep in function delegates

The docs of delegates tells us if keep=False, **kwargs of func mid (example below) will be removed; keep=True will keep **kwargs for mid.

But do you know keep=False has an untold feature, which is keep=False will make sure the mid won’t take any more args from other tos. In other words, after delegates(low, keep=False)(mid), delegates(low2)(mid) won’t change the signature of mid. This is a useful feature of keep of delegates but not documented.

In the example below, you can’t delegates args of low2 to mid after receiving args from low.

def low(a, b=1): pass
@delegates(low)
def mid(c, d:int=1, **kwargs): pass
mid
<function __main__.mid(c, d: int = 1, *, b=1)>
mid.__delwrap__
<function __main__.low(a, b=1)>
def low2(e, f=1, **kwargs): pass
delegates(low2)(mid)
<function __main__.mid(c, d: int = 1, *, b=1)>

However, when you set keep=True, **kwargs stays with mid signature, and mid can receive arg g from low2 by using delegates again.

def low(a, b=1): pass
@delegates(low, keep=True)
def mid(c, d:int=1, **kwargs): pass
def low2(e, g=1): pass
delegates(low2)(mid)
<function __main__.mid(c, d: int = 1, *, b=1, g=1)>

The hidden or unexpected feature of keep of delegates

When keep=False, show_doc can tell you where b come from in the example below.

When keep=True, show_doc can’t show the address info of b.

So, you are out of luck, if you want to keep both **kwargs and display the address info of b at the same time.

Note: see how f.__delwrap__ help showdoc to generate the address info of b from here.

def low(a, b=1): pass
@delegates(low, keep=False)
def mid(c, d=1, **kwargs): pass
show_doc(mid)

mid

 mid (c, d=1, b=1)
Type Default Details
c
d int 1
b int 1 Argument passed to low
@delegates(mid, keep=False)
def high(e, f=1, **kwargs): pass
show_doc(high)

high

 high (e, f=1, d=1, b=1)
Type Default Details
e
f int 1
d int 1 Argument passed to mid
b int 1 Argument passed to mid

You can keep **kwargs, but you can’t have the address info of b for mid, and d and b for high.

def low(a, b=1): pass
@delegates(low, keep=True)
def mid(c, d=1, **kwargs): pass
show_doc(mid)

mid

 mid (c, d=1, b=1, **kwargs)
@delegates(mid, keep=True)
def high(e, f=1, **kwargs): pass
show_doc(high)

high

 high (e, f=1, d=1, b=1, **kwargs)

Is there a legitimate use case for keeping both **kwargs and the address info of b?

I think so, and here is a case example below.

The function low need **kwargs to override y from lower and mid needs **kwargs to override b from low. Therefore, **kwargs should be kept using keep=True.

The signature of mid does not tell us where b is from, it would be useful to show us the address info of b. But the current official delegates doesn’t allow you to have both.

If you agree this use case is legitimate, then the unexpected feature here may be an issue need to be resolved, right? @jeremy @Moody

def lower(x, y=1): return x + y
def low(a, b=1, **kwargs): return lower(a, **kwargs) + b
@delegates(low, keep=True)
def mid(c, d=1, **kwargs): return low(c, **kwargs) + d
mid
<function __main__.mid(c, d=1, *, b=1, **kwargs)>
mid(1, 1, b=1, y=2)
5
show_doc(mid)

mid

 mid (c, d=1, b=1, **kwargs)

Can we enable delegates to allow both keeping **kwargs and display address info of args from to?

The cause for disallowing the use case above is keep=True and from_f.__delwrap__ are tied together. The problem can be solved if we untie them, and keep from_f.__delwrap__ always available to from_f.

Signature: delegates(to: function = None, keep=False, but: list = None, verbose=True)
Source:   
def delegates(to:FunctionType=None, # Delegatee
              keep=False, # Keep `kwargs` in decorated function?
              but:list=None, # Exclude these parameters from signature
              verbose=True): # Include `to` in docments?
    "Decorator: replace `**kwargs` in signature with params from `to`"
    if but is None: but = []
    def _f(f):
        if to is None: to_f,from_f = f.__base__.__init__,f.__init__
        else:          to_f,from_f = to.__init__ if isinstance(to,type) else to,f
        from_f = getattr(from_f,'__func__',from_f)
        to_f = getattr(to_f,'__func__',to_f)
        if hasattr(from_f,'__delwrap__'): return f
        sig = inspect.signature(from_f)
        sigd = dict(sig.parameters)
        k = sigd.pop('kwargs')
        s2 = {k:v.replace(kind=inspect.Parameter.KEYWORD_ONLY) for k,v in inspect.signature(to_f).parameters.items()
              if v.default != inspect.Parameter.empty and k not in sigd and k not in but}
        sigd.update(s2)
        if keep: sigd['kwargs'] = k
        else: from_f.__delwrap__ = to_f
        from_f.__delopts__ = dict(verbose=verbose)
        from_f.__signature__ = sig.replace(parameters=sigd.values())
        return f
    return _f
File:      ~/Documents/fastcore/fastcore/meta.py
Type:      function

The two lines of code need alteration which are marked with ‘###’ below.

def delegates(to=None, # Delegatee
              keep=False, # Keep `kwargs` in decorated function?
              but:list=None, # Exclude these parameters from signature
              verbose=True): # Include `to` in docments?
    "Decorator: replace `**kwargs` in signature with params from `to`"
    if but is None: but = []
    def _f(f):
        if to is None: to_f,from_f = f.__base__.__init__,f.__init__
        else:          to_f,from_f = to.__init__ if isinstance(to,type) else to,f
        from_f = getattr(from_f,'__func__',from_f)
        to_f = getattr(to_f,'__func__',to_f)
        if hasattr(from_f,'__delwrap__') and keep==False: return f ### if you don't want `f` to run delegates again
        sig = inspect.signature(from_f)
        sigd = dict(sig.parameters)
        k = sigd.pop('kwargs')
        s2 = {k:v.replace(kind=inspect.Parameter.KEYWORD_ONLY) for k,v in inspect.signature(to_f).parameters.items()
              if v.default != inspect.Parameter.empty and k not in sigd and k not in but}
        sigd.update(s2)
        if keep: sigd['kwargs'] = k 
        from_f.__delwrap__ = to_f ### enable show_doc to display the address info for args of `to`
        
        from_f.__delopts__ = dict(verbose=verbose)
        from_f.__signature__ = sig.replace(parameters=sigd.values())
        return f
    return _f

Now, you can use keep=True to have both **kwargs and show_doc displaying address info on b

def lower(x, y=1): return x + y
def low(a, b=1, **kwargs): return lower(a, **kwargs) + b
@delegates(low, keep=True)
def mid(c, d=1, **kwargs): return low(c, **kwargs) + d
mid
<function __main__.mid(c, d=1, *, b=1, **kwargs)>
mid(1, 1, b=1, y=2)
5
show_doc(mid)

mid

 mid (c, d=1, b=1, **kwargs)
Type Default Details
c
d int 1
b int 1 Argument passed to low
kwargs Argument passed to low

Now, you can use keep=False to remove **kwargs but show_doc can still display address info of b.

@delegates(mid, keep=False)
def high(e, f=1, **kwargs): pass
high
<function __main__.high(e, f=1, *, d=1, b=1)>
show_doc(high)

high

 high (e, f=1, d=1, b=1)
Type Default Details
e
f int 1
d int 1 Argument passed to mid
b int 1 Argument passed to mid

Now, you can use keep=False to not only remove **kwargs but also refuse to use delegates again.

delegates(lower, keep=False)(high)
<function __main__.high(e, f=1, *, d=1, b=1)>

6 Likes

Great to see, this new thread you are creating. Will be following it along.

3 Likes

A personal docs on delegates

I have been developing a workflow of creating my own docs for fastcore. Here is what I have so far.

1st step: read and run examples from the official docs to understand how you should use it.

2nd step: write your own examples to test your understanding of the usages and explore the boundaries to cause errors.

3rd step: read the source and find out what from the source caused the errors using debugging tools like print, pdb.set_trace, %debug, jupyterlab’s visual debugger etc.

To keep my docs here readable, I will only present the second step and one or two examples of the third step.

pprint(inspect.getsource(delegates))
('def delegates(to:FunctionType=None, # Delegatee\n'
 '              keep=False, # Keep `kwargs` in decorated function?\n'
 '              but:list=None): # Exclude these parameters from signature\n'
 '    "Decorator: replace `**kwargs` in signature with params from `to`"\n'
 '    if but is None: but = []\n'
 '    def _f(f):\n'
 '        if to is None: to_f,from_f = f.__base__.__init__,f.__init__\n'
 '        else:          to_f,from_f = to.__init__ if isinstance(to,type) else '
 'to,f\n'
 "        from_f = getattr(from_f,'__func__',from_f)\n"
 "        to_f = getattr(to_f,'__func__',to_f)\n"
 "        if hasattr(from_f,'__delwrap__'): return f\n"
 '        sig = inspect.signature(from_f)\n'
 '        sigd = dict(sig.parameters)\n'
 "        k = sigd.pop('kwargs')\n"
 '        s2 = {k:v.replace(kind=inspect.Parameter.KEYWORD_ONLY) for k,v in '
 'inspect.signature(to_f).parameters.items()\n'
 '              if v.default != inspect.Parameter.empty and k not in sigd and '
 'k not in but}\n'
 '        anno = {k:v for k,v in to_f.__annotations__.items() if k not in sigd '
 'and k not in but}\n'
 '        sigd.update(s2)\n'
 "        if keep: sigd['kwargs'] = k\n"
 '        else: from_f.__delwrap__ = to_f\n'
 '        from_f.__signature__ = sig.replace(parameters=sigd.values())\n'
 "        if hasattr(from_f, '__annotations__'): "
 'from_f.__annotations__.update(anno)\n'
 '        return f\n'
 '    return _f\n')

When to use it?

When f want to replace its **kwargs param with unique kwargs (with their annotations) from to.

Who are to and f?

They can be functions, instance methods, classmethods, and even classes.

What is **kwargs?

It’s a param of f, to receive unknown keyword args (i.e., not sure how many and what they are) with values. In general, to can tell us what those unkown keyword args for f.

Additional features with keep and but

When to use keep=False?

f is happy with all the params it got, no more expectation of other kwargs, so remove **kwargs from its signature.

when to use keep=True?

f is not content with all the params it got, and still expect other kwargs in the future when needed.

when to use but=['d']?

f does not want a list of specified kwargs from to, such as d here.

Creating your own examples to test the boundary of usages

def low(a, b=1, c=1, **kwargs): pass # to receive unexpected kwargs, `to` need `**kwargs` too.
@delegates(low, keep=True, but=['b'])
def mid(a, d=1, **kwargs): pass
test_sig(mid, '(a, d=1, *, c=1, **kwargs)')
mid(a=1, d=1, c=1, e=1) # mid is open to other unexpected kwargs without error
def low(a, b=1): pass 
@delegates(low) # to as func
def mid(c, d=1, **kwargs): pass # f as func
test_sig(mid, '(c, d=1, *, b=1)')
class Foo():

    @delegates(low) # to as func
    @classmethod # classmethod() run before delegates()
    def clsmid1(cls, c, d=1, **kwargs): pass # f as classmethod


    @delegates(clsmid1) # to as classmethod
    @classmethod
    def clsmid2(cls, c, e=1, **kwargs): pass # f as classmethod

test_sig(Foo.clsmid1, '(c, d=1, *, b=1)')
test_sig(Foo.clsmid2, '(c, e=1, *, d=1, b=1)')
class Foo():
    
	@classmethod
	@delegates(low) # to as func
	def clsmid1(cls, c, d=1, **kwargs): pass # f as func, not classmethod

	@classmethod 
	@delegates(clsmid1)  # to as classmethod 
	def clsmid2(cls, c, e=1, **kwargs): pass # f as func, not classmethod

test_sig(Foo.clsmid1, '(c, d=1, *, b=1)')
test_sig(Foo.clsmid2, '(c, e=1, *, d=1, b=1)')
class Foo1():

    @delegates(Foo.clsmid2) # to as classmethod
    def instmid1(self, c, f=1, **kwargs): pass # f as func

    @delegates(instmid1) # to as func type
    def instmid2(self, c, g=1, **kwargs): pass # f as func 

f = Foo1()
test_sig(f.instmid1, '(c, f=1, *, e=1, d=1, b=1)')
test_sig(f.instmid2, '(c, g=1, *, f=1, e=1, d=1, b=1)')
class Foo():
    @classmethod
    def f(cls, a=1, c=2): pass

@delegates(Foo.f) # `to` is a classmethod
def low(a, b=1, **kwargs): pass # `f` is a function
test_sig(low, '(a, b=1, *, c=2)')
class Foo():
	def __init__(a, b=1, **kwargs): pass

@delegates(Foo) # `to` is a class
def low(c, d=1, **kwargs): pass # `f` as function

test_sig(low, '(c, d=1, *, b=1)')
class Base(): 
    def __init__(self, a, b=1): pass 
class Other():
    def __init__(self, e=1): pass

@delegates(Other)
class Foo():    
    def __new__(self, g, f=1, **kwargs): pass # f as a class is ok with either __new__ or __init__
test_sig(Foo, '(g, f=1, *, e=1)')
@delegates(Other) # as a normal class, it must have __init__, and __new__ won't do
class Foo():    
    def __init__(self, c, d=1, **kwargs): pass
test_sig(Foo, '(c, d=1, *, e=1)')
try: 
    @delegates(Other)
    class Foo(): pass # f as a class at least needs to have a signature with **kwargs inside
except: 
    pprint(inspect.signature(Foo))
<Signature (c, d=1, *, e=1)>
@delegates()
class Subcls(Base): # to as a superclass (Base),  to must have __init__, and __new__ won't do. Let's debug the source to see why
    def __init__(self, c, d=1, **kwargs): pass # f as class, should have __init__
test_sig(Subcls, '(c, d=1, *, b=1)')

debugging the error to understand the source


1 Like

Where does the problem which FixSigMeta is designed to solve come from?

What is the problem that FixSigMeta solves?

When you inherit from a class that defines __new__ , or a metaclass that defines __call__ , the signature of your __init__ method is obfuscated such that tab completion no longer works. FixSigMeta fixes this issue and restores signatures.

to read the docs to understand the problem

The questions I have when I read the docs about the problem above

  1. How a class Foo get signature from its init`
  2. Why ‘When you inherit from a class that defines new’ can cause a problem? (problem 1)
  3. Why ‘a metaclass that defines call’ can cause a problem? (problem 2)
  4. Why inspect with python 3.9 won’t have the problem 1?

How a class borrow signature from its __init__

A demo: Foo borrows its signature from its __init__

import inspect
class Foo:
    def __init__(self, a, b, c): pass

inspect.signature(Foo)

Which func perform the job

inspect.signature??
inspect.Signature.from_callable??
inspect._signature_from_callable??

Reading the source with visual debugger

from inspect import *
from inspect import _signature_from_callable
from inspect import _signature_is_functionlike, _signature_is_builtin, _signature_get_user_defined_method, _signature_from_function, _signature_bound_method
def _signature_from_callable(obj, *,
                             follow_wrapper_chains=True,
                             skip_bound_arg=True,
                             sigcls):

    """Private helper function to get signature for arbitrary
    callable objects.
    """

    _get_signature_of = functools.partial(_signature_from_callable,
                                follow_wrapper_chains=follow_wrapper_chains,
                                skip_bound_arg=skip_bound_arg,
                                sigcls=sigcls)

    if not callable(obj):
        raise TypeError('{!r} is not a callable object'.format(obj))

    if isinstance(obj, types.MethodType):
        # In this case we skip the first parameter of the underlying
        # function (usually `self` or `cls`).
        sig = _get_signature_of(obj.__func__)

        if skip_bound_arg:
            return _signature_bound_method(sig)
        else:
            return sig

    # Was this function wrapped by a decorator?
    if follow_wrapper_chains:
        obj = unwrap(obj, stop=(lambda f: hasattr(f, "__signature__")))
        if isinstance(obj, types.MethodType):
            # If the unwrapped object is a *method*, we might want to
            # skip its first parameter (self).
            # See test_signature_wrapped_bound_method for details.
            return _get_signature_of(obj)

    try:
        sig = obj.__signature__
    except AttributeError:
        pass
    else:
        if sig is not None:
            if not isinstance(sig, Signature):
                raise TypeError(
                    'unexpected object {!r} in __signature__ '
                    'attribute'.format(sig))
            return sig

    try:
        partialmethod = obj._partialmethod
    except AttributeError:
        pass
    else:
        if isinstance(partialmethod, functools.partialmethod):
            # Unbound partialmethod (see functools.partialmethod)
            # This means, that we need to calculate the signature
            # as if it's a regular partial object, but taking into
            # account that the first positional argument
            # (usually `self`, or `cls`) will not be passed
            # automatically (as for boundmethods)

            wrapped_sig = _get_signature_of(partialmethod.func)

            sig = _signature_get_partial(wrapped_sig, partialmethod, (None,))
            first_wrapped_param = tuple(wrapped_sig.parameters.values())[0]
            if first_wrapped_param.kind is Parameter.VAR_POSITIONAL:
                # First argument of the wrapped callable is `*args`, as in
                # `partialmethod(lambda *args)`.
                return sig
            else:
                sig_params = tuple(sig.parameters.values())
                assert (not sig_params or
                        first_wrapped_param is not sig_params[0])
                new_params = (first_wrapped_param,) + sig_params
                return sig.replace(parameters=new_params)

    if isfunction(obj) or _signature_is_functionlike(obj):
        # If it's a pure Python function, or an object that is duck type
        # of a Python function (Cython functions, for instance), then:
        return _signature_from_function(sigcls, obj,
                                        skip_bound_arg=skip_bound_arg)

    if _signature_is_builtin(obj):
        return _signature_from_builtin(sigcls, obj,
                                       skip_bound_arg=skip_bound_arg)

    if isinstance(obj, functools.partial):
        wrapped_sig = _get_signature_of(obj.func)
        return _signature_get_partial(wrapped_sig, obj)

    sig = None
    if isinstance(obj, type):
        # obj is a class or a metaclass

        # First, let's see if it has an overloaded __call__ defined
        # in its metaclass
        call = _signature_get_user_defined_method(type(obj), '__call__')
        if call is not None:
            sig = _get_signature_of(call)
        else:
            factory_method = None
            new = _signature_get_user_defined_method(obj, '__new__')
            init = _signature_get_user_defined_method(obj, '__init__')
            # Now we check if the 'obj' class has an own '__new__' method
            if '__new__' in obj.__dict__:
                factory_method = new
            # or an own '__init__' method
            elif '__init__' in obj.__dict__:
                factory_method = init
            # If not, we take inherited '__new__' or '__init__', if present
            elif new is not None:
                factory_method = new
            elif init is not None:
                factory_method = init

            if factory_method is not None:
                sig = _get_signature_of(factory_method)

        if sig is None:
            # At this point we know, that `obj` is a class, with no user-
            # defined '__init__', '__new__', or class-level '__call__'

            for base in obj.__mro__[:-1]:
                # Since '__text_signature__' is implemented as a
                # descriptor that extracts text signature from the
                # class docstring, if 'obj' is derived from a builtin
                # class, its own '__text_signature__' may be 'None'.
                # Therefore, we go through the MRO (except the last
                # class in there, which is 'object') to find the first
                # class with non-empty text signature.
                try:
                    text_sig = base.__text_signature__
                except AttributeError:
                    pass
                else:
                    if text_sig:
                        # If 'base' class has a __text_signature__ attribute:
                        # return a signature based on it
                        return _signature_fromstr(sigcls, base, text_sig)

            # No '__text_signature__' was found for the 'obj' class.
            # Last option is to check if its '__init__' is
            # object.__init__ or type.__init__.
            if type not in obj.__mro__:
                # We have a class (not metaclass), but no user-defined
                # __init__ or __new__ for it
                if (obj.__init__ is object.__init__ and
                    obj.__new__ is object.__new__):
                    # Return a signature of 'object' builtin.
                    return sigcls.from_callable(object)
                else:
                    raise ValueError(
                        'no signature found for builtin type {!r}'.format(obj))

    elif not isinstance(obj, _NonUserDefinedCallables):
        # An object with __call__
        # We also check that the 'obj' is not an instance of
        # _WrapperDescriptor or _MethodWrapper to avoid
        # infinite recursion (and even potential segfault)
        call = _signature_get_user_defined_method(type(obj), '__call__')
        if call is not None:
            try:
                sig = _get_signature_of(call)
            except ValueError as ex:
                msg = 'no signature found for {!r}'.format(obj)
                raise ValueError(msg) from ex

    if sig is not None:
        # For classes and objects we skip the first parameter of their
        # __call__, __new__, or __init__ methods
        if skip_bound_arg:
            return _signature_bound_method(sig)
        else:
            return sig

    if isinstance(obj, types.BuiltinFunctionType):
        # Raise a nicer error message for builtins
        msg = 'no signature found for builtin function {!r}'.format(obj)
        raise ValueError(msg)

    raise ValueError('callable {!r} is not supported by signature'.format(obj))
class Foo:
    def __init__(self, a, b, c): pass

_signature_from_callable(Foo, sigcls=inspect.Signature, follow_wrapper_chains=True) # debug the function above through this example

How exactly Foo get signatures from __init__

with visual debugger, we can run through the example above and find out that Foo’s signature is indeeded borrowed from its __init__ signature with the block of code copied below.

The answer is that

if our Foo is a metaclass which has its own __call__, then Foo will get signature from __call__;

if our Foo is a class which has its own __new__ that can be found in its __dict__, then Foo will gets signature from __new__;

if our Foo is a class which only has its own __init__ which can be found in its __dict__, then Foo will get its signature from __new__ instead of __init__.

    if isinstance(obj, type):
            # obj is a class or a metaclass

            # First, let's see if it has an overloaded __call__ defined
            # in its metaclass
            call = _signature_get_user_defined_method(type(obj), '__call__')     ###############################
            if call is not None:
                sig = _get_signature_of(call)
            else:
                factory_method = None
                new = _signature_get_user_defined_method(obj, '__new__')         ###############################
                init = _signature_get_user_defined_method(obj, '__init__')
                # Now we check if the 'obj' class has an own '__new__' method
                if '__new__' in obj.__dict__:                                    ###############################
                    factory_method = new
                # or an own '__init__' method
                elif '__init__' in obj.__dict__:                                 ###############################
                    factory_method = init
                # If not, we take inherited '__new__' or '__init__', if present
                elif new is not None:
                    factory_method = new
                elif init is not None:
                    factory_method = init

                if factory_method is not None:
                    sig = _get_signature_of(factory_method)
                
...


    if sig is not None:
        # For classes and objects we skip the first parameter of their
        # __call__, __new__, or __init__ methods
        if skip_bound_arg:
            return _signature_bound_method(sig)
        else:
            return sig

Why Base.__new__ won’t affect Foo get signature from __init__

But how could overwritting __new__ mess up with the process of getting signature from __init__?

With visual debugger and source code above, we can find out that though Foo has __new__ from Base, but it does not have its own __new__ in its __dict__, so Foo will still get signature ffrom its own __init__.

class Base: # pass
    def __new__(self, **args): pass  # defines a __new__ 

class Foo(Base):
    def __init__(self, d, e, f): pass
    
_signature_from_callable(Foo, sigcls=inspect.Signature, follow_wrapper_chains=True) # debug the function above through this example

Why python 3.7 with inspect can’t handle the example above?

Because based on the code below, Foo will take Base.__new__'s signature even though it’s not Foo’s own __new__.


sig = None
    if isinstance(obj, type):
        # obj is a class or a metaclass

        # First, let's see if it has an overloaded __call__ defined
        # in its metaclass
        call = _signature_get_user_defined_method(type(obj), '__call__')  ###############################
        if call is not None:
            sig = _signature_from_callable(
                call,
                follow_wrapper_chains=follow_wrapper_chains,
                skip_bound_arg=skip_bound_arg,
                sigcls=sigcls)
        else:
            # Now we check if the 'obj' class has a '__new__' method
            new = _signature_get_user_defined_method(obj, '__new__')      ###############################
            if new is not None:                                           ###############################
                sig = _signature_from_callable(                           ###############################
                    new,
                    follow_wrapper_chains=follow_wrapper_chains,
                    skip_bound_arg=skip_bound_arg,
                    sigcls=sigcls)
            else:
                # Finally, we should have at least __init__ implemented
                init = _signature_get_user_defined_method(obj, '__init__')
                if init is not None:
                    sig = _signature_from_callable(
                        init,
                        follow_wrapper_chains=follow_wrapper_chains,
                        skip_bound_arg=skip_bound_arg,
                        sigcls=sigcls)
                    
...

if sig is not None:
        # For classes and objects we skip the first parameter of their
        # __call__, __new__, or __init__ methods
        if skip_bound_arg:
            return _signature_bound_method(sig)
        else:
            return sig
1 Like

The “step 1-7” shows how Foo gets signature from Foo.__init__ using _signature_from_callable.

code example

class Foo:
    def __init__(self, a, b, c): pass

    @classmethod
    def clsmed(): pass
    
inspect.signature(Foo)
def _signature_from_callable(obj, *,======================================================(0)       
                             follow_wrapper_chains=True,==================================(1)       
                             skip_bound_arg=True,=========================================(2)       
                             sigcls):=====================================================(3)       
                                                                                                                                                        (4)
    """Private helper function to get signature for arbitrary=============================(5)       
    callable objects.=====================================================================(6)       
    """===================================================================================(7)       
                                                                                                                                                        (8)
    _get_signature_of = functools.partial(_signature_from_callable,=======================(9) # so that it can use in itself
                                follow_wrapper_chains=follow_wrapper_chains,==============(10)      
                                skip_bound_arg=skip_bound_arg,============================(11)      
                                sigcls=sigcls)============================================(12)      
                                                                                                                                                        (13)
    if not callable(obj):=================================================================(14) # obj must be callable
        raise TypeError('{!r} is not a callable object'.format(obj))======================(15)      
                                                                                                                                                        (16)
    if isinstance(obj, types.MethodType):=================================================(17) # obj can be a classmethod
        # In this case we skip the first parameter of the underlying======================(18)      
        # function (usually `self` or `cls`).=============================================(19)      
        sig = _get_signature_of(obj.__func__)=============================================(20)      
                                                                                                                                                        (21)
        if skip_bound_arg:================================================================(22)      
            return _signature_bound_method(sig)===========================================(23)      
        else:=============================================================================(24)      
            return sig====================================================================(25)      
                                                                                                                                                        (26)
    # Was this function wrapped by a decorator?===========================================(27)      
    if follow_wrapper_chains:=============================================================(28) # try @delegates
        obj = unwrap(obj, stop=(lambda f: hasattr(f, "__signature__")))===================(29)      
        if isinstance(obj, types.MethodType):=============================================(30)      
            # If the unwrapped object is a *method*, we might want to=====================(31)      
            # skip its first parameter (self).============================================(32)      
            # See test_signature_wrapped_bound_method for details.========================(33)      
            return _get_signature_of(obj)=================================================(34)      
                                                                                                                                                        (35)
    try:==================================================================================(36)      
        sig = obj.__signature__===========================================================(37) # check __signature__
    except AttributeError:================================================================(38)      
        pass==============================================================================(39)      
    else:=================================================================================(40)      
        if sig is not None:===============================================================(41)      
            if not isinstance(sig, Signature):============================================(42)      
                raise TypeError(==========================================================(43)      
                    'unexpected object {!r} in __signature__ '============================(44)      
                    'attribute'.format(sig))==============================================(45)      
            return sig====================================================================(46)      
                                                                                                                                                        (47)
    try:==================================================================================(48)      
        partialmethod = obj._partialmethod================================================(49)      
    except AttributeError:================================================================(50)      
        pass==============================================================================(51)      
    else:=================================================================================(52)      
        if isinstance(partialmethod, functools.partialmethod):============================(53)      
            # Unbound partialmethod (see functools.partialmethod)=========================(54)      
            # This means, that we need to calculate the signature=========================(55)      
            # as if it's a regular partial object, but taking into========================(56)      
            # account that the first positional argument==================================(57)      
            # (usually `self`, or `cls`) will not be passed===============================(58)      
            # automatically (as for boundmethods)=========================================(59)      
                                                                                                                                                        (60)
            wrapped_sig = _get_signature_of(partialmethod.func)===========================(61)      
                                                                                                                                                        (62)
            sig = _signature_get_partial(wrapped_sig, partialmethod, (None,))=============(63)      
            first_wrapped_param = tuple(wrapped_sig.parameters.values())[0]===============(64)      
            if first_wrapped_param.kind is Parameter.VAR_POSITIONAL:======================(65)      
                # First argument of the wrapped callable is `*args`, as in================(66)      
                # `partialmethod(lambda *args)`.==========================================(67)      
                return sig================================================================(68)      
            else:=========================================================================(69)      
                sig_params = tuple(sig.parameters.values())===============================(70)      
                assert (not sig_params or=================================================(71)      
                        first_wrapped_param is not sig_params[0])=========================(72)      
                new_params = (first_wrapped_param,) + sig_params==========================(73)      
                return sig.replace(parameters=new_params)=================================(74)      
                                                                                                                                                        (75)
    if isfunction(obj) or _signature_is_functionlike(obj):================================(76) # step 7: run on itself will run here
        # If it's a pure Python function, or an object that is duck type==================(77)      
        # of a Python function (Cython functions, for instance), then:====================(78)      
        return _signature_from_function(sigcls, obj,======================================(79) # step 8: get sig with a different func
                                        skip_bound_arg=skip_bound_arg)====================(80)      
                                                                                                                                                        (81)
    if _signature_is_builtin(obj):========================================================(82)      
        return _signature_from_builtin(sigcls, obj,=======================================(83)      
                                       skip_bound_arg=skip_bound_arg)=====================(84)      
                                                                                                                                                        (85)
    if isinstance(obj, functools.partial):================================================(86)      
        wrapped_sig = _get_signature_of(obj.func)=========================================(87)      
        return _signature_get_partial(wrapped_sig, obj)===================================(88)      
                                                                                                                                                        (89)
    sig = None============================================================================(90)      
    if isinstance(obj, type):=============================================================(91) # step 1: obj is a class?
        # obj is a class or a metaclass===================================================(92)      
                                                                                                                                                        (93)
        # First, let's see if it has an overloaded __call__ defined=======================(94)      
        # in its metaclass================================================================(95)      
        call = _signature_get_user_defined_method(type(obj), '__call__')==================(96) # step 2: define its own __call__?
        if call is not None:==============================================================(97)      
            sig = _get_signature_of(call)=================================================(98)      
        else:=============================================================================(99)      
            factory_method = None=========================================================(100)     
            new = _signature_get_user_defined_method(obj, '__new__')======================(101) # step 3: define its own __new__?
            init = _signature_get_user_defined_method(obj, '__init__')====================(102) # step 4: define its own __init__?
            # Now we check if the 'obj' class has an own '__new__' method=================(103)     
            if '__new__' in obj.__dict__:=================================================(104)     
                factory_method = new======================================================(105)     
            # or an own '__init__' method=================================================(106)     
            elif '__init__' in obj.__dict__:==============================================(107) # __init__ is inside obj.__dict__?
                factory_method = init=====================================================(108) # step 5: __init__ is inside obj.__dict__?
            # If not, we take inherited '__new__' or '__init__', if present===============(109)     
            elif new is not None:=========================================================(110)     
                factory_method = new======================================================(111)     
            elif init is not None:========================================================(112)     
                factory_method = init=====================================================(113)     
                                                                                                                                                        (114)
            if factory_method is not None:================================================(115)     
                sig = _get_signature_of(factory_method)===================================(116) # step 6: run on itself using functools.partial
                                                                                                                                                        (117)
        if sig is None:===================================================================(118)     
            # At this point we know, that `obj` is a class, with no user-=================(119)     
            # defined '__init__', '__new__', or class-level '__call__'====================(120)     
                                                                                                                                                        (121)
            for base in obj.__mro__[:-1]:=================================================(122)     
                # Since '__text_signature__' is implemented as a==========================(123)     
                # descriptor that extracts text signature from the========================(124)     
                # class docstring, if 'obj' is derived from a builtin=====================(125)     
                # class, its own '__text_signature__' may be 'None'.======================(126)     
                # Therefore, we go through the MRO (except the last=======================(127)     
                # class in there, which is 'object') to find the first====================(128)     
                # class with non-empty text signature.====================================(129)     
                try:======================================================================(130)     
                    text_sig = base.__text_signature__====================================(131)     
                except AttributeError:====================================================(132)     
                    pass==================================================================(133)     
                else:=====================================================================(134)     
                    if text_sig:==========================================================(135)     
                        # If 'base' class has a __text_signature__ attribute:=============(136)     
                        # return a signature based on it==================================(137)     
                        return _signature_fromstr(sigcls, base, text_sig)=================(138)     
                                                                                                                                                        (139)
            # No '__text_signature__' was found for the 'obj' class.======================(140)     
            # Last option is to check if its '__init__' is================================(141)     
            # object.__init__ or type.__init__.===========================================(142)     
            if type not in obj.__mro__:===================================================(143)     
                # We have a class (not metaclass), but no user-defined====================(144)     
                # __init__ or __new__ for it==============================================(145)     
                if (obj.__init__ is object.__init__ and===================================(146)     
                    obj.__new__ is object.__new__):=======================================(147)     
                    # Return a signature of 'object' builtin.=============================(148)     
                    return sigcls.from_callable(object)===================================(149)     
                else:=====================================================================(150)     
                    raise ValueError(=====================================================(151)     
                        'no signature found for builtin type {!r}'.format(obj))===========(152)     
                                                                                                                                                        (153)
    elif not isinstance(obj, _NonUserDefinedCallables):===================================(154)     
        # An object with __call__=========================================================(155)     
        # We also check that the 'obj' is not an instance of==============================(156)     
        # _WrapperDescriptor or _MethodWrapper to avoid===================================(157)     
        # infinite recursion (and even potential segfault)================================(158)     
        call = _signature_get_user_defined_method(type(obj), '__call__')==================(159)     
        if call is not None:==============================================================(160)     
            try:==========================================================================(161)     
                sig = _get_signature_of(call)=============================================(162)     
            except ValueError as ex:======================================================(163)     
                msg = 'no signature found for {!r}'.format(obj)===========================(164)     
                raise ValueError(msg) from ex=============================================(165)     
                                                                                                                                                        (166)
    if sig is not None:===================================================================(167)     
        # For classes and objects we skip the first parameter of their====================(168)     
        # __call__, __new__, or __init__ methods==========================================(169)     
        if skip_bound_arg:================================================================(170)     
            return _signature_bound_method(sig)===========================================(171)     
        else:=============================================================================(172)     
            return sig====================================================================(173)     
                                                                                                                                                        (174)
    if isinstance(obj, types.BuiltinFunctionType):========================================(175)     
        # Raise a nicer error message for builtins========================================(176)     
        msg = 'no signature found for builtin function {!r}'.format(obj)==================(177)     
        raise ValueError(msg)=============================================================(178)     
                                                                                                                                                        (179)
    raise ValueError('callable {!r} is not supported by signature'.format(obj))===========(180)   

Why python 3.7 with inspect can’t help Foo get sig from Foo.__init__


@Daniel you might find this helpful for understanding delegates:

2 Likes

Thank you Jeremy for the recommendation. I will read this post today.

I know I have stayed with delegates and FixSigMeta for a while now :sweat_smile: (I think I understand them well now :slight_smile: ), that’s because I have been writing a little debugging library to replace pdb when I explore fastcore source code with jupyter lab in literal programming approach. I have completed the second version of the library yesterday. I am testing it to see how efficiently can it help me explore the fastcore library and more. Hopefully, I can speed up the exploration of fastcore with this little tool.

2 Likes

I’d love to see this!

1 Like

Thank you Jeremy! I would love to show it to you, but I think I need one more day to make the project slightly more readable.

Hi Jeremy, you are the first to see this little library. I am very much looking forward to what you think of it and how I could improve on it.

Looks cool @Daniel !

1 Like

Sorry to admit it but I don’t understand the concept of tagging @ when some developer who has closely worked with core functions and methods tries to ask admin for their views, that is considered correct, but when I ask for some help or support using @ for admin or core devs of the forum I am told that I shouldn’t do it.
So can someone explain when to use @ and when not to?
Sorry for such a silly question.

Hi @joshiharshit5077, to avoid the admin/maintainer being overwhelmed with questions that can be answered by someone else, we only at mention @ them in the case that only they can respond.

We can see above, Daniel didn’t at mention Jeremy (no @).

He did mention jeremy in the post, that’s why I got confused I saw some posts mentioning admin and other devs so that they can support with the issue.
I did understand the overwhelmed point though.

We are adviced to only use @ to Jeremy when it is something only Jeremy can deal with. I rarely use @ to Jeremy, because most of my questions if they are answerable, they all have been answered by Jeremy or other members of the community. This is a very supportive community.

I understand you want all your questions be answered, I share the feelings. My own experience is that instead of worrying people not reading my questions, I should pay more attention to whether my questions are readable and answerable, i.e., showing people what have I done to solve the problem myself, and where exactly I stuck, so that it can be slightly easier for people to step in and help. To do it well is difficult but very helpful to me when I post questions.

Don’t give up, keep working, keep sharing, you will get through the questions.

3 Likes

Oh, that’s understandable, thanks, Daniel. I’ll try working on my problems probing already answered similar questions.

1 Like

Also, you get back what you put in. For instance, @Daniel has put an enormous amount of work into the fast.ai community, including writing all the course notes that appear on course.fast.ai! Since he’s put in extra to help the community, I put in extra to answer his questions.

3 Likes

Wow, thank you so much Jeremy! I didn’t know my notes were shared on the course site! It’s such an honor! Thank you! Now I will have to spend some time to revise my notes to make them more readable and constructive (which I planned to do in August but get carried away by fastdebug :sweat_smile: , I think I should be able to get back to it soon).

1 Like