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)>