Skip to content
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

Closed
wants to merge 2 commits into from

Conversation

Kontakter
Copy link

@Kontakter Kontakter commented Mar 11, 2020

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.

@Kontakter Kontakter changed the title Ignat/add patch argspec Adding patch_argspec Mar 11, 2020
@micheles
Copy link
Owner

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.

@Kontakter
Copy link
Author

Actually it is almost everything what I do.

My use case is following – I have a library that written as collection of free function:

def func1(..., context):
    ...

def func2(..., context):
    ...
    func1(..., context)
    ...

This context holds HTTP session and some library specific settings.

I want to build class that holds context and have all these free functions as methods, since it is more pythonic way.

Moreover, simple solution (see below) loses all docs, defaults and other properties of original functions from library, so I want to use decoration.

class MyClient(object):
    def func1(self, ....):
          return func1(..., context=self.context)
    ...

@micheles
Copy link
Owner

I see, things like wrapping a C-style functional API into an OOP API.

@Kontakter
Copy link
Author

So, should I fix something in this PR before merge?

@micheles
Copy link
Owner

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

@micheles micheles added wontfix and removed wontfix labels Mar 15, 2020
@micheles
Copy link
Owner

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

@Kontakter
Copy link
Author

Unfortunately, both solutions have drawbacks.

The first one is not friendly to non-trivial default arguments since inspect.Parameter represent it as repr(self._default), the second one is not python2-friendly (my library used both by python2 and python3 users).

@micheles
Copy link
Owner

micheles commented Mar 16, 2020

Could you give an example of the problem with the default arguments?

@Kontakter
Copy link
Author

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 create_class_method – is my attempt to adapt your solution to my actual needs.

@smarie
Copy link
Contributor

smarie commented Mar 24, 2020

Hi @Kontakter and @micheles , I guess that this use case of signature modification can be easily handled by makefun

@Kontakter
Copy link
Author

Looks interesting, I would try makefun instead of decorator

@smarie
Copy link
Contributor

smarie commented Mar 24, 2020

Note that it has been greatly inspired by decorator - it is "just" a generalization of decorator's engine

@micheles micheles closed this Jul 21, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants