Child Class from MagicMock object has weird spec='str' and can't use or mock methods of the class
Question:
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.
Answers:
Interesting…
The issue you’re facing is because the MagicMock object has a "spec" attribute, which only allows methods that are part of the specified object. In this case, the "spec" is set to "str", so only methods of the "str" class are allowed.
To resolve this, you can either create the MagicMock object without a "spec" attribute, or set the "spec" attribute to "None", which will allow all methods:
from unittest.mock import MagicMock
a = MagicMock(spec=None) # or 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())
This should allow the "c.x()" method to be called without raising an "AttributeError". Also be careful of misconfigured mocks!
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
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.
Interesting…
The issue you’re facing is because the MagicMock object has a "spec" attribute, which only allows methods that are part of the specified object. In this case, the "spec" is set to "str", so only methods of the "str" class are allowed.
To resolve this, you can either create the MagicMock object without a "spec" attribute, or set the "spec" attribute to "None", which will allow all methods:
from unittest.mock import MagicMock
a = MagicMock(spec=None) # or 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())
This should allow the "c.x()" method to be called without raising an "AttributeError". Also be careful of misconfigured mocks!
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