Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pass keyword argument only to __new__() and never further it to __init__()?

Tags:

python

mocking

Part 1

I have a setup where I have a set of classes that I want to mock, my idea was that in the cases where I want to do this I pass a mock keyword argument into the constructor and in __new__ intercept this and instead pass back a mocked version of that object.

It looks like this (Edited the keyword lookup after @mgilsons suggestion):

class RealObject(object):
    def __new__(cls, *args, **kwargs):
        if kwargs.pop('mock', None):
            return MockRealObject()
        return super(RealObect, cls).__new__(cls, *args, **kwargs)

    def __init__(self, whatever = None):
        '''
        Constructor
        '''
        #stuff happens

I then call the constructor like this:

ro = RealObject(mock = bool)

The issue I have here is that I get the following error when bool is False:

TypeError: __init__() got an unexpected keyword argument 'mock'

This works if I add mock as a keyword argument to __init__ but what I am asking if this is possible to avoid. I even pop the mock from the kwargs dict.

This is also a question about the design. Is there a better way to do this? (of course!) I wanted to try doing it this way, without using a factory or a superclass or anything. But still, should I use another keyword maybe? __call__?

Part 2 based on jsbueno's answer

So I wanted to extract the metaclass and the __new__ function into a separate module. I did this:

class Mockable(object):

    def __new__(cls, *args, **kwargs):

        if kwargs.pop('mock', None):
            mock_cls = eval('{0}{1}'.format('Mock',cls.__name__))
            return super(mock_cls, mock_cls).__new__(mock_cls)

        return super(cls, cls).__new__(cls,*args, **kwargs)


class MockableMetaclass(type):

    def __call__(self, *args, **kwargs):
        obj = self.__new__(self, *args, **kwargs)
        if "mock" in kwargs:
            del kwargs["mock"]

        obj.__init__(*args, **kwargs)

        return obj

And I have defined in a separate module the classes RealObject and MockRealObject. I have two problems now:

  1. If MockableMetaclass and Mockable are not in the same module as the RealObject class the eval will raise a NameError if I provide mock = True.
  2. If mock = False the code will enter into an endless recursion that ends in an impressive RuntimeError: maximum recursion depth exceeded while calling a Python objec. I'm guessing this is due to RealObject's superclass no longer being object but instead Mockable.

How can I fix these problems? is my approach incorrect? Should I instead have Mockable as a decorator? I tried that but that didn't seem to work since __new__ of an instance is only read-only it seems.

like image 651
Parham Avatar asked Feb 07 '13 16:02

Parham


People also ask

How do you pass a keyword argument in Python?

In Python, we can pass a variable number of arguments to a function using special symbols. There are two special symbols: *args (Non Keyword Arguments) **kwargs (Keyword Arguments)

What is keyword only argument?

Keyword-only arguments are another attribute of Python functions that have been available since Python 3.0. These arguments are specified using the '*' marker. They prompt the user to state the keyword used in the already defined function when making a call to the same function.

What is __ new __ in Python?

__new__ is static class method, while __init__ is instance method. __new__ has to create the instance first, so __init__ can initialize it. Note that __init__ takes self as parameter. Until you create instance there is no self . Now, I gather, that you're trying to implement singleton pattern in Python.

What is unexpected keyword argument?

Unexpected keyword argument %r in %s call. Description: Used when a function call passes a keyword argument that doesn't correspond to one of the function's parameter names.


2 Answers

This is a job for the metaclass! :-)

The code responsible to call both __new__ and __init__ when instantiating a Python new-style object lies in the __call__method for the class metaclass. (or the semantically equivalent to that).

In other words - when you do:

RealObject() - what is really called is the RealObject.__class__.__call__ method. Since without declaring a explicit metaclass, the metaclass is type, it is type.__call__ which is called.

Most recipes around dealing with metaclasses deal with subclassing the __new__ method - automating actions when the class is created. But overriding __call__ we can take actions when the class is instantiated, instead.

In this case, all that is needed is to remove the "mock" keyword parameter, if any, before calling __init__:

class MetaMock(type):
    def __call__(cls, *args, **kw):
       obj = cls.__new__(cls, *args, **kw)
       if "mock" in kw:
           del kw["mock"]
       obj.__init__(*args, **kw)
       return obj

class RealObject(metaclass=MetaMock):
    ...
like image 157
jsbueno Avatar answered Sep 30 '22 17:09

jsbueno


A subclass is pretty much essential, since __new__ always passes the arguments to the constructor call to the __init__ method. If you add a subclass via a class decorator as a mixin then you can intercept the mock argument in the subclass __init__:

def mock_with(mock_cls):
    class MockMixin(object):
        def __new__(cls, *args, **kwargs):
            if kwargs.pop('mock'):
                return mock_cls()
            return super(MockMixin, cls).__new__(cls, *args, **kwargs)
        def __init__(self, *args, **kwargs):
            kwargs.pop('mock')
            super(MockMixin, self).__init__(*args, **kwargs)
    def decorator(real_cls):
        return type(real_cls.__name__, (MockMixin, real_cls), {})
    return decorator

class MockRealObject(object):
    pass

@mock_with(MockRealObject)
class RealObject(object):
    def __init__(self, whatever=None):
        pass

r = RealObject(mock=False)
assert isinstance(r, RealObject)
m = RealObject(mock=True)
assert isinstance(m, MockRealObject)

The alternative is for the subclass __new__ method to return RealObject(cls, *args, **kwargs); in that case, since the returned object isn't an instance of the subclass. However in that case the isinstance check will fail.

like image 27
ecatmur Avatar answered Sep 30 '22 19:09

ecatmur