当调用随机方法时,MagicMock如何避免抛出AttributeError?

时间:2018-10-24 22:18:04

标签: python mocking pytest magicmock

在Python中,如果调用不存在的方法,则会抛出AttributeError。例如

#! /usr/bin/python3

import argparse
import boto3
from datetime import datetime
from datetime import timezone

def build_argparser():
    parser = argparse.ArgumentParser(description='List S3 buckets by file date.')
    parser.add_argument('-p', '--profile', help='Profile to use')
    return parser

if __name__ == "__main__":
    parser = build_argparser()
    args = parser.parse_args()

    if args.profile == None:
        s3 = boto3.resource('s3')
    else:
        profile = boto3.session.Session(profile_name=args.profile)
        s3 = profile.resource('s3')

    for bucket in s3.buckets.all():
        print(bucket.name)
        latest_key = ""
        latest_datetime = datetime
        for object in bucket.objects.all():
            #print('\t' + str(object.key) + ': ' + str(object.last_modified))
            if latest_datetime == datetime or latest_datetime < object.last_modified:
                latest_key = object.key
                latest_datetime = object.last_modified

        print('\t' + str(latest_key) + ': ' + str(latest_datetime))

在下面的代码中,MagicMock类没有名为hello的函数,或者未为方法hello创建任何补丁。仍然在代码下面并不会引发AttributeError

>>> class A:
...     def yo(self):
...             print(1)
... 
>>> a = A()
>>> a.yo()
1
>>> a.hello()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'A' object has no attribute 'hello'

MagicMock如何做到这一点?当调用任何方法(可能未定义)时,如何创建可以执行操作的类?

2 个答案:

答案 0 :(得分:2)

Python数据模型记录了一个钩子__getattr__,当属性访问无法以通常的方式解析时,应调用该钩子。模仿者使用它来返回一个新的模仿实例-即,模仿将未知属性定义为factories

以更简单的方式重现模拟的实现,您只需将__getattr____call__转换为工厂函数:

class M:
    def __call__(self):
        return M()
    def __getattr__(self, name):
        return M()

用法示例:

>>> mock = M()
>>> mock.potato
<__main__.M at 0xdeadbeef>
>>> mock.potato()
<__main__.M at 0xcafef00d>
  

MagicMock如何做到这一点?

这部分不专门针对MagicMock,普通的Mock也会做到这一点(名称中的“魔术”只是指可以更好地模拟magic methods的其他功能) 。 MagicMock inherits such behavior from one of the base classes

>>> MagicMock.mro()
[unittest.mock.MagicMock,
 unittest.mock.MagicMixin,
 unittest.mock.Mock,
 unittest.mock.CallableMixin,
 unittest.mock.NonCallableMock,  # <--- this one overrides __getattr__!
 unittest.mock.Base,
 object]
  

如何创建一个可以在调用任何方法(可能未定义)时执行操作的类?

这取决于您要在普通属性访问之前还是之后。如果要排在最前面,则应定义__getattribute__,在搜索类/实例名称空间之前,将无条件调用它来实现属性访问。但是,如果您要优先考虑普通属性(即生活在对象__dict__中的属性)和descriptors的优先级,则应按照前面的讨论定义__getattr__

答案 1 :(得分:1)

我实际上不知道MagicMock的工作原理(我从未使用过,但是我听说过好东西),但是这一部分行为可以复制(可能还有其他多种可能)解决方案),通过劫持__getattr__的方式,使其返回一个可调用对象,该对象在被调用时会创建一个新的模拟实例:

class MM:
    def __init__(self, name=None):
        # store a name, TODO: random id, etc.
        self.name = name

    def __repr__(self):
        # make it pretty
        if self.name:
            r = f'<MM name={self.name}>'
        else:
            r = f'<MM>'
        return r

    def __getattr__(self, attrname):
        # we want a factory for a mock instance with a name corresponding to attrname
        def magicattr():
            return MM(name=f"'mock.{attrname}()'")
        return magicattr

执行后,我们将看到以下内容:

>>> MM()
<MM>
>>> MM().hello()
<MM name='mock.hello()'>

我并没有为定义id而过分,但基本技巧可以在上面的精简示例中看到。

以上方法的工作方式是访问.hello或任何其他属性都经过我们的自定义__getattr__,这使我们有机会即时生成一个伪造的(模拟的)方法,无论我们使用什么属性想要。据我了解,MagicMock的众多好处之一就是我们不必担心默认情况下会抛出AttributeError,它正常工作