__call__ from metaclass shadows signature of __init__
Asked Answered
E

5

6

I would like to have in the code underneath that when i type instance_of_A = A(, that the name of the supposed arguments is init_argumentA and not *meta_args, **meta_kwargs. But unfortunatally, the arguments of the __call__ method of the metaclass are shown.

class Meta(type):    
    def __call__(cls,*meta_args,**meta_kwargs):
        # Something here
        return super().__call__(*meta_args, **meta_kwargs)

class A(metaclass = Meta):
    def __init__(self,init_argumentA):
        # something here 

class B(metaclass = Meta):
    def __init__(self,init_argumentB):
        # something here

I have searched for a solution and found the question How to dynamically change signatures of method in subclass? and Signature-changing decorator: properly documenting additional argument. But none, seem to be completely what I want. The first link uses inspect to change the amount of variables given to a function, but i can't seem to let it work for my case and I think there has to be a more obvious solution. The second one isn't completely what I want, but something in that way might be a good alternative.

Edit: I am working in Spyder. I want this because I have thousands of classes of the Meta type and each class have different arguments, which is impossible to remember, so the idea is that the user can remember it when seeing the correct arguments show up.

Excise answered 9/4, 2018 at 19:28 Comment(4)
You have to be more specific about when they should be shown. Are you working in a Python shell, IDLE, IPython, Jupyter, Visual Studio code, something else? Do you want them to show in the IDE at all or is this about help() or documentation builds?Casta
Thanks for the feedback, i updated the question and hope that it is more clear nowExcise
this seems to be a problem with IDE, not with Python itselfExeter
okay, I cannot reproduce that. If I use your code (with pass inside the __init__s) in spyder it shows the signature of A.__init__ and B.__init__ when I print A( or B(. Could you add further information, such as the spyder version, the python version, a working code sample (your code produces an IndentationError) and maybe a screenshot of the incorrect suggestion?Casta
F
0

Ok - even though the reason for you to want that seems to be equivocated, as any "honest" Python inspecting tool should show the __init__ signature, what is needed for what you ask is that for each class you generate a dynamic metaclass, for which the __call__ method has the same signature of the class's own __init__ method.

For faking the __init__ signature on __call__ we can simply use functools.wraps. (but you might want to check the answers at https://mcmap.net/q/346373/-set-function-signature-in-python )

And for dynamically creating an extra metaclass, that can be done on the __metaclass__.__new__ itself, with just some care to avoud infinite recursion on the __new__ method - threads.Lock can help with that in a more consistent way than a simple global flag.

from functools import wraps
creation_locks = {} 

class M(type):
    def __new__(metacls, name, bases, namespace):
        lock = creation_locks.setdefault(name, Lock())
        if lock.locked():
            return super().__new__(metacls, name, bases, namespace)
        with lock:
            def __call__(cls, *args, **kwargs):
                return super().__call__(*args, **kwargs)
            new_metacls = type(metacls.__name__ + "_sigfix", (metacls,), {"__call__": __call__}) 
            cls = new_metacls(name, bases, namespace)
            wraps(cls.__init__)(__call__)
        del creation_locks[name]
        return cls

I initially thought of using a named parameter to the metaclass __new__ argument to control recursion, but then it would be passed to the created class' __init_subclass__ method (which will result in an error) - so the Lock use.

Fuge answered 10/4, 2018 at 1:13 Comment(0)
P
2

Using the code you provided, you can change the Meta class

class Meta(type):
    def __call__(cls, *meta_args, **meta_kwargs):
        # Something here
        return super().__call__(*meta_args, **meta_kwargs)


class A(metaclass=Meta):
    def __init__(self, x):
        pass

to

import inspect

class Meta(type):
    def __call__(cls, *meta_args, **meta_kwargs):
        # Something here

        # Restore the signature of __init__
        sig = inspect.signature(cls.__init__)
        parameters = tuple(sig.parameters.values())
        cls.__signature__ = sig.replace(parameters=parameters[1:])

        return super().__call__(*meta_args, **meta_kwargs)

Now IPython or some IDE will show you the correct signature.

Parthenia answered 17/2, 2020 at 12:14 Comment(0)
K
1

I found that the answer of @johnbaltis was 99% there but not quite what was needed to ensure the signatures were in place.

If we use __init__ rather than __call__ as below we get the desired behaviour

import inspect

class Meta(type):
    def __init__(cls, clsname, bases, attrs):

        # Restore the signature
        sig = inspect.signature(cls.__init__)
        parameters = tuple(sig.parameters.values())
        cls.__signature__ = sig.replace(parameters=parameters[1:])

        return super().__init__(clsname, bases, attrs)

    def __call__(cls, *args, **kwargs):
        super().__call__(*args, **kwargs)
        print(f'Instanciated: {cls.__name__}')

class A(metaclass=Meta):
    def __init__(self, x: int, y: str):
        pass

which will correctly give:

In [12]: A?
Init signature: A(x: int, y: str)
Docstring:      <no docstring>
Type:           Meta
Subclasses:     

In [13]: A(0, 'y')
Instanciated: A
Knitter answered 20/12, 2020 at 22:43 Comment(0)
F
0

Ok - even though the reason for you to want that seems to be equivocated, as any "honest" Python inspecting tool should show the __init__ signature, what is needed for what you ask is that for each class you generate a dynamic metaclass, for which the __call__ method has the same signature of the class's own __init__ method.

For faking the __init__ signature on __call__ we can simply use functools.wraps. (but you might want to check the answers at https://mcmap.net/q/346373/-set-function-signature-in-python )

And for dynamically creating an extra metaclass, that can be done on the __metaclass__.__new__ itself, with just some care to avoud infinite recursion on the __new__ method - threads.Lock can help with that in a more consistent way than a simple global flag.

from functools import wraps
creation_locks = {} 

class M(type):
    def __new__(metacls, name, bases, namespace):
        lock = creation_locks.setdefault(name, Lock())
        if lock.locked():
            return super().__new__(metacls, name, bases, namespace)
        with lock:
            def __call__(cls, *args, **kwargs):
                return super().__call__(*args, **kwargs)
            new_metacls = type(metacls.__name__ + "_sigfix", (metacls,), {"__call__": __call__}) 
            cls = new_metacls(name, bases, namespace)
            wraps(cls.__init__)(__call__)
        del creation_locks[name]
        return cls

I initially thought of using a named parameter to the metaclass __new__ argument to control recursion, but then it would be passed to the created class' __init_subclass__ method (which will result in an error) - so the Lock use.

Fuge answered 10/4, 2018 at 1:13 Comment(0)
I
0

Not sure if this helps the author but in my case I needed to change inspect.signature(Klass) to inspect.signature(Klass.__init__) to get signature of class __init__ instead of metaclass __call__.

Interdependent answered 9/6, 2020 at 13:55 Comment(0)
Z
0

Although this question is from 6 years ago, I'd like to point out that this much simpler implementation also works:

from functools import wraps

class Meta(type):

    @wraps(type.__call__)
    def __call__(cls, *meta_args, **meta_kwargs):
        # Something here
        return super().__call__(*meta_args, **meta_kwargs)


class A(metaclass=Meta):
    def __init__(self, x):
    """Documentation for 'A.__init__'"""
        pass
Zeba answered 26/9 at 7:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.