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.

Asked By: arrmansa

||

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!

Answered By: Megatom

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

Demo: https://replit.com/@blhsing/HotAcademicCases

Answered By: blhsing