pythonspydermetaclass

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


Solution

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