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 to
s. 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)>