NotableQuestioner
NotableQuestioner

Reputation: 73

__call__ from metaclass shadows signature of __init__

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.

Upvotes: 6

Views: 960

Answers (5)

Fernando Lobo
Fernando Lobo

Reputation: 31

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

Upvotes: 0

Alexander McFarlane
Alexander McFarlane

Reputation: 11293

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

Upvotes: 1

Tomasz Urbaszek
Tomasz Urbaszek

Reputation: 808

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__.

Upvotes: 0

johnbaltis
johnbaltis

Reputation: 1588

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.

Upvotes: 2

jsbueno
jsbueno

Reputation: 110516

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://stackoverflow.com/a/33112180/108205 )

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.

Upvotes: 0

Related Questions