Pass the keyword argument only for __new __ () and never continue until __init __ ()?
Part 1
I have a setup where I have a set of classes that I want to mock, I thought that in cases where I want to do this, I pass the keyword argument mock
to the constructor and to __new__
intercept this and instead put back the mocked version of this object.
It looks like this ( Edited keyword search 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
Then I call the constructor like this:
ro = RealObject(mock = bool)
The problem I am having here is that I am getting the following error when bool
there is False
:
TypeError: __init__() got an unexpected keyword argument 'mock'
It works if I add the mock
keyword to as an argument __init__
, but I ask if this can be avoided. I even deduce mock
from kwargs
dict
.
It's also a design question. Is there a better way to do this? (of course!) I wanted to try to do it this way, without using a factory or superclass or anything else. But still, should I use a different keyword? __call__
?
Part 2 based on jsbueno answer
So, I wanted to extract the metaclass and function __new__
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:
- If
MockableMetaclass
and areMockable
not in the same module as the classRealObject
, iteval
will raiseNameError
if I have providedmock = True
. - If
mock = False
, the code goes into infinite recursion that ends up impressiveRuntimeError: maximum recursion depth exceeded while calling a Python objec
. I assume this is because theRealObject
superclass is notobject
, but insteadMockable
.
How can I fix these problems? is my approach wrong? Should I Mockable
be a decorator instead ? I tried this but it doesn't seem to work as the __new__
instance is read-only.
source to share
This assignment for __metaclass__
: -)
The code responsible for both calling __new__
and __init__
creating a new Python style object lies in the method __call__
for the class metaclass. (or semantically equivalent to this).
In other words - when you do:
RealObject()
- what is really called is a method RealObject.__class__.__call__
. Because without declaring an explicit metaclass, metaclass type
, it is called type.__call__
.
Most of the recipes associated with metaclasses are related to subclassing a method __new__
- automating actions when creating a class. But by overriding __call__
, we can take action when the class is instantiated.
In this case, all that is needed is to remove the "mock" 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(object):
__metaclass__ = MetaMock
...
source to share
The subclass is very important because it __new__
always passes arguments to a constructor call to a method __init__
. If you add the subclass via the class decorator as a mixin, you can intercept the argument mock
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 for a subclass method __new__
is to return RealObject(cls, *args, **kwargs)
; in this case, because the returned object is not an instance of the subclass. However, in this case, the check isinstance
will fail.
source to share