On Thu, Dec 2, 2021 at 7:36 PM Paul Moore <p.f.moore@gmail.com> wrote:
Actually, Chris - does functools.wraps work properly in your implementation when wrapping functions with late-bound defaults?
def dec(f): ... @wraps(f) ... def inner(*args, **kw): ... print("Calling") ... return f(*args, **kw) ... return inner ... @dec ... def g(a => []): ... return len(a)
Yes, it does. There are two things happening here which, from a technical standpoint, are actually orthogonal; the function *call* is simply using *a,**kw notation, so it's passing along all the positional and keyword parameters untouched; but the function *documentation* just says "hey look over there for the real signature".
g([1,2,3]) Calling 3 g() Calling 0
(In your example there's no proof that it's late-bound, but it is.)
help(g) Help on function g in module __main__:
g(a=>[])
g.__wrapped__ <function g at 0x7fb5581efa00> g <function g at 0x7fb5581efab0>
When you do "inner = wraps(f)(inner)", what happens is that the function's name and qualname get updated, and then __wrapped__ gets added as a pointer saying "hey, assume that I have the signature of that guy over there". There's unfortunately no way to merge signatures (you can't do something like "def f(*a, timeout=500, **kw):" and then use the timeout parameter in your wrapper and pass the rest on - help(f) will just show the base function's signature), and nothing is actually aware of the underlying details. But on the plus side, this DOES work correctly. Documentation for late-bound defaults is done by a (compile-time) string snapshot of the AST that defined the default, so it's about as accurate as for early-bound defaults. One thing I'm not 100% happy with in the reference implementation is that the string for help() is stored on the function object, but the behaviour of the late-bound default is inherent to the code object. I'm not sure of a way around that, but it also doesn't seem terribly problematic in practice. ChrisA