When a class is created deriving from a MagicMock() object it has an unwanted spec='str'. Does anyone know why this happens? Does anyone know any operations that could be done to the MagicMock() object in this case such that it doesn't have the spec='str' or can use methods of the class?
from unittest.mock import MagicMock
a = MagicMock()
class b():
@staticmethod
def x():
return 1
class c(a):
@staticmethod
def x():
return 1
print(a)
print(b)
print(c)
print(a.x())
print(b.x())
print(c.x())
which returns
MagicMock id='140670188364408'>
<class '__main__.b'>
<MagicMock spec='str' id='140670220499320'>
<MagicMock name='mock.x()' id='140670220574848'>
1
Traceback (most recent call last):
File "/xyz/test.py", line 19, in <module>
print(c.x())
File "/xyz/lib/python3.7/unittest/mock.py", line 580, in _getattr_
raise AttributeError("Mock object has no attribute %r" % name)
AttributeError: Mock object has no attribute 'x'
Basically I need the AttributeError to not be here. Is there something I can do to 'a' such that c.x() is valid?
edit - the issue seems to be with _mock_add_spec in mock.py still not sure how to fix this.
In Python, classes are actually instances of the type
class. A class
statement like this:
class c(a):
@staticmethod
def x():
return 1
is really syntactic sugar of calling type
with the name of the class, the base classes and the class members:
c = type('c', (a,), {'x': staticmethod(lambda: 1)})
The above statement would go through the given base classes and call the __new__
method of the type of the first base class with the __new__
method defined, which in this case is a
. The return value gets assigned to c
to become a new class.
Normally, a
would be an actual class--an instance of type
or a subclass of type
. But in this case, a
is not an instance of type
, but rather an instance of MagicMock
, so MagicMock.__new__
, instead of type.__new__
, is called with these 3 arguments.
And here lies the problem: MagicMock
is not a subclass of type
, so its __new__
method is not meant to take the same arguments as type.__new__
. And yet, when MagicMock.__new__
is called with these 3 arguments, it takes them without complaint anyway because according to the signature of MagicMock
's constructor (which is the same as Mock
's):
class unittest.mock.Mock(spec=None, side_effect=None, return_value=DEFAULT, wraps=None, name=None, spec_set=None, unsafe=False, **kwargs)
MagicMock.__new__
would assign the 3 positional arguments as spec
, side_effect
and return_value
, respectively. As you now see, the first argument, the class name ('c'
in this case), an instance of str
, becomes spec
, which is why your class c
becomes an instance of MagicMock
with a spec
of str
.
The solution
Luckily, a magic method named __mro_entries__
was introduced since Python 3.7 that can solve this problem by providing a non-class base class with a substitute base class, so that when a
, an instance of MagicMock
, is used as a base class, we can use __mro_entries__
to force its child class to instead use a
's class, MagicMock
(or SubclassableMagicMock
in the following example), as a base class:
from unittest.mock import MagicMock
class SubclassableMagicMock(MagicMock):
def __mro_entries__(self, bases):
return self.__class__,
so that:
a = SubclassableMagicMock()
class b():
@staticmethod
def x():
return 1
class c(a):
@staticmethod
def x():
return 1
print(a)
print(b)
print(c)
print(a.x())
print(b.x())
print(c.x())
outputs:
<SubclassableMagicMock id='140127365021408'>
<class '__main__.b'>
<class '__main__.c'>
<SubclassableMagicMock name='mock.x()' id='140127351680080'>
1
1