New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adding patch_argspec #85
Conversation
I am curious to know what is your real use case, since I am sure you are doing more than just adding self to a function signature. |
Actually it is almost everything what I do. My use case is following – I have a library that written as collection of free function:
This I want to build class that holds Moreover, simple solution (see below) loses all docs, defaults and other properties of original functions from library, so I want to use decoration.
|
I see, things like wrapping a C-style functional API into an OOP API. |
So, should I fix something in this PR before merge? |
I am not going to merge your PR since what you want to do can be done already without changing the decorator module at all. But perhaps I could update the documentation explaining how this can be done. Here is the solution: import inspect
from decorator import FunctionMaker
def to_method(f):
"""
Takes a function with signature (..., context) and returns a new
function with signature (self, ...) to be used a a method in a
class with a .context attribute.
"""
sig = inspect.signature(f)
params = list(sig.parameters.values())
assert params[-1].name == 'context'
self = inspect.Parameter('self', inspect.Parameter.POSITIONAL_OR_KEYWORD)
params.insert(0, self) # insert self
del params[-1] # remove context
newsig = sig.replace(parameters=params)
return FunctionMaker.create(
'%s%s' % (f.__name__, newsig),
'context = self.context; return _func_%s' % sig,
dict(_func_=f))
def foo(x, context):
return x
def bar(x, y, context):
return x + y
class Client:
def __init__(self, context):
self.context = context
foo = to_method(foo)
bar = to_method(bar)
c = Client(None)
assert c.foo(1) == 1
assert c.bar(1, 2) == 3 |
BTW, here is a solution not using the decorator module at all: def to_method(f):
sig = inspect.signature(f)
params = list(sig.parameters.values())
assert params[-1].name == 'context'
self = inspect.Parameter('self', inspect.Parameter.POSITIONAL_OR_KEYWORD)
params.insert(0, self) # insert self
del params[-1] # remove context
def meth(self, *args):
allargs = args + (self.context,)
return f(*allargs)
meth.__signature__ = sig.replace(parameters=params)
return meth |
Unfortunately, both solutions have drawbacks. The first one is not friendly to non-trivial default arguments since inspect.Parameter represent it as |
Could you give an example of the problem with the default arguments? |
Sure, it looks like this: import inspect
from funcsigs import signature, Parameter
from decorator import FunctionMaker
def create_class_method(func):
is_class = False
if inspect.isclass(func):
func = func.__dict__["__init__"]
is_class = True
sig = signature(func)
params = list(sig.parameters.values())
client_index = None
for index, param in enumerate(params):
if param.name == "client":
client_index = index
if client_index is not None:
del params[client_index]
if not is_class:
self = Parameter("self", Parameter.POSITIONAL_OR_KEYWORD)
params.insert(0, self)
newsig = sig.replace(parameters=params)
return FunctionMaker.create(
"%s%s" % (func.__name__, newsig),
"client = self; return _func_%s" % sig,
dict(_func_=func))
class _KwargSentinelClass(object):
pass
_KWARG_SENTINEL = _KwargSentinelClass()
def my_method(param=_KWARG_SENTINEL, client=None):
if param is _KWARG_SENTINEL:
print("KWARG_SENTINEL")
else:
print(param)
create_class_method(my_method) Function |
Hi @Kontakter and @micheles , I guess that this use case of signature modification can be easily handled by |
Looks interesting, I would try |
Note that it has been greatly inspired by |
I have a task where I need to perform complex decoration with patching function spec. But this library is still very useful in my case and allow to not reimplement a lot of decoration logic.