Replay._handle()   F
last analyzed

Complexity

Conditions 13

Size

Total Lines 33

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 13
dl 0
loc 33
rs 2.7716

How to fix   Complexity   

Complexity

Complex classes like Replay._handle() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
from collections import defaultdict
2
from collections import namedtuple
3
from difflib import unified_diff
4
from functools import partial
5
from functools import wraps
6
from inspect import isclass
7
from logging import getLevelName
8
from logging import getLogger
9
from sys import _getframe
10
from traceback import format_stack
11
12
from aspectlib import ALL_METHODS
13
from aspectlib import mimic
14
from aspectlib import weave
15
16
from .utils import Sentinel
17
from .utils import camelcase_to_underscores
18
from .utils import container
19
from .utils import logf
20
from .utils import qualname
21
from .utils import repr_ex
22
23
try:
24
    from logging import _levelNames as nameToLevel
25
except ImportError:
26
    from logging import _nameToLevel as nameToLevel
27
try:
28
    from dummy_thread import allocate_lock
29
except ImportError:
30
    from _dummy_thread import allocate_lock
31
try:
32
    from collections import OrderedDict
33
except ImportError:
34
    from .py2ordereddict import OrderedDict
35
try:
36
    from collections import ChainMap
37
except ImportError:
38
    from .py2chainmap import ChainMap
39
40
__all__ = 'mock', 'record', "Story"
41
42
logger = getLogger(__name__)
43
logexception = logf(logger.exception)
44
45
Call = namedtuple('Call', ('self', 'args', 'kwargs'))
46
CallEx = namedtuple('CallEx', ('self', 'name', 'args', 'kwargs'))
47
Result = namedtuple('Result', ('self', 'args', 'kwargs', 'result', 'exception'))
48
ResultEx = namedtuple('ResultEx', ('self', 'name', 'args', 'kwargs', 'result', 'exception'))
49
_INIT = Sentinel("INIT")
50
51
52
def mock(return_value, call=False):
53
    """
54
    Factory for a decorator that makes the function return a given `return_value`.
55
56
    Args:
57
        return_value: Value to return from the wrapper.
58
        call (bool): If ``True``, call the decorated function. (default: ``False``)
59
60
    Returns:
61
        A decorator.
62
    """
63
64
    def mock_decorator(func):
65
        @wraps(func)
66
        def mock_wrapper(*args, **kwargs):
67
            if call:
68
                func(*args, **kwargs)
69
            return return_value
70
71
        return mock_wrapper
72
73
    return mock_decorator
74
75
76
class LogCapture(object):
77
    """
78
    Records all log messages made on the given logger. Assumes the logger has a ``_log`` method.
79
80
    Example::
81
82
        >>> import logging
83
        >>> logger = logging.getLogger('mylogger')
84
        >>> with LogCapture(logger, level='INFO') as logs:
85
        ...     logger.debug("Message from debug: %s", 'somearg')
86
        ...     logger.info("Message from info: %s", 'somearg')
87
        ...     logger.error("Message from error: %s", 'somearg')
88
        >>> logs.calls
89
        [('Message from info: %s', ('somearg',), 'INFO'), ('Message from error: %s', ('somearg',), 'ERROR')]
90
        >>> logs.messages
91
        [('INFO', 'Message from info: somearg'), ('ERROR', 'Message from error: somearg')]
92
        >>> logs.has('Message from info: %s')
93
        True
94
        >>> logs.has('Message from info: somearg')
95
        True
96
        >>> logs.has('Message from info: %s', 'badarg')
97
        False
98
        >>> logs.has('Message from debug: %s')
99
        False
100
        >>> logs.assertLogged('Message from error: %s')
101
        >>> logs.assertLogged('Message from error: %s')
102
        >>> logs.assertLogged('Message from error: %s')
103
104
    .. versionchanged:: 1.3.0
105
106
        Added ``messages`` property.
107
        Changed ``calls`` to retrun the level as a string (instead of int).
108
    """
109
    def __init__(self, logger, level='DEBUG'):
110
        self._logger = logger
111
        self._level = nameToLevel[level]
112
        self._calls = []
113
        self._rollback = None
114
115
    def __enter__(self):
116
        self._rollback = weave(
117
            self._logger,
118
            record(callback=self._callback, extended=True, iscalled=True),
119
            methods='_log$'
120
        )
121
        return self
122
123
    def __exit__(self, *exc):
124
        self._rollback()
125
126
    def _callback(self, _binding, _qualname, args, _kwargs):
127
        level, message, args = args
128
        if level >= self._level:
129
            self._calls.append((
130
                message % args if args else message,
131
                message,
132
                args,
133
                getLevelName(level)
134
            ))
135
136
    @property
137
    def calls(self):
138
        return [i[1:] for i in self._calls]
139
140
    @property
141
    def messages(self):
142
        return [(i[-1], i[0]) for i in self._calls]
143
144
    def has(self, message, *args, **kwargs):
145
        level = kwargs.pop('level', None)
146
        assert not kwargs, "Unexpected arguments: %s" % kwargs
147
        for call_final_message, call_message, call_args, call_level in self._calls:
148
            if level is None or level == call_level:
149
                if (
150
                    message == call_message and args == call_args
151
                    if args else
152
                    message == call_final_message or message == call_message
153
                ):
154
                    return True
155
        return False
156
157
    def assertLogged(self, message, *args, **kwargs):
158
        if not self.has(message, *args, **kwargs):
159
            raise AssertionError("There's no such message %r (with args %r) logged on %s. Logged messages where: %s" % (
160
                message, args, self._logger, self.calls
161
            ))
162
163
164
class _RecordingFunctionWrapper(object):
165
    """
166
    Function wrapper that records calls and can be used as an weaver context manager.
167
168
    See :obj:`aspectlib.test.record` for arguments.
169
    """
170
171
    def __init__(self, wrapped, iscalled=True, calls=None, callback=None, extended=False, results=False,
172
                 recurse_lock=None, binding=None):
173
        assert not results or iscalled, "`iscalled` must be True if `results` is True"
174
        mimic(self, wrapped)
175
        self.__wrapped = wrapped
176
        self.__entanglement = None
177
        self.__iscalled = iscalled
178
        self.__binding = binding
179
        self.__callback = callback
180
        self.__extended = extended
181
        self.__results = results
182
        self.__recurse_lock = recurse_lock
183
        self.calls = [] if not callback and calls is None else calls
184
185
    def __call__(self, *args, **kwargs):
186
        record = not self.__recurse_lock or self.__recurse_lock.acquire(False)
187
        try:
188
            if self.__results:
189
                try:
190
                    result = self.__wrapped(*args, **kwargs)
191
                except Exception as exc:
192
                    if record:
193
                        self.__record(args, kwargs, None, exc)
194
                    raise
195
                else:
196
                    if record:
197
                        self.__record(args, kwargs, result, None)
198
                    return result
199
            else:
200
                if record:
201
                    self.__record(args, kwargs)
202
                if self.__iscalled:
203
                    return self.__wrapped(*args, **kwargs)
204
        finally:
205
            if record and self.__recurse_lock:
206
                self.__recurse_lock.release()
207
208
    def __record(self, args, kwargs, *response):
209
        if self.__callback is not None:
210
            self.__callback(self.__binding, qualname(self), args, kwargs, *response)
211
        if self.calls is not None:
212
            if self.__extended:
213
                self.calls.append((ResultEx if response else CallEx)(
214
                    self.__binding, qualname(self), args, kwargs, *response
215
                ))
216
            else:
217
                self.calls.append((Result if response else Call)(
218
                    self.__binding, args, kwargs, *response
219
                ))
220
221
    def __get__(self, instance, owner):
222
        return _RecordingFunctionWrapper(
223
            self.__wrapped.__get__(instance, owner),
224
            iscalled=self.__iscalled,
225
            calls=self.calls,
226
            callback=self.__callback,
227
            extended=self.__extended,
228
            results=self.__results,
229
            binding=instance,
230
        )
231
232
    def __enter__(self):
233
        self.__entanglement = weave(self.__wrapped, lambda _: self)
234
        return self
235
236
    def __exit__(self, *args):
237
        self.__entanglement.rollback()
238
239
240
def record(func=None, recurse_lock_factory=allocate_lock, **options):
241
    """
242
    Factory or decorator (depending if `func` is initially given).
243
244
    Args:
245
        callback (list):
246
            An a callable that is to be called with ``instance, function, args, kwargs``.
247
        calls (list):
248
            An object where the `Call` objects are appended. If not given and ``callback`` is not specified then a new list
249
            object will be created.
250
        iscalled (bool):
251
            If ``True`` the `func` will be called. (default: ``False``)
252
        extended (bool):
253
            If ``True`` the `func`'s ``__name__`` will also be included in the call list. (default: ``False``)
254
        results (bool):
255
            If ``True`` the results (and exceptions) will also be included in the call list. (default: ``False``)
256
257
    Returns:
258
        A wrapper that records all calls made to `func`. The history is available as a ``call``
259
        property. If access to the function is too hard then you need to specify the history manually.
260
261
    Example:
262
263
        >>> @record
264
        ... def a(x, y, a, b):
265
        ...     pass
266
        >>> a(1, 2, 3, b='c')
267
        >>> a.calls
268
        [Call(self=None, args=(1, 2, 3), kwargs={'b': 'c'})]
269
270
271
    Or, with your own history list::
272
273
        >>> calls = []
274
        >>> @record(calls=calls)
275
        ... def a(x, y, a, b):
276
        ...     pass
277
        >>> a(1, 2, 3, b='c')
278
        >>> a.calls
279
        [Call(self=None, args=(1, 2, 3), kwargs={'b': 'c'})]
280
        >>> calls is a.calls
281
        True
282
283
284
    .. versionchanged:: 0.9.0
285
286
        Renamed `history` option to `calls`.
287
        Renamed `call` option to `iscalled`.
288
        Added `callback` option.
289
        Added `extended` option.
290
    """
291
    if func:
292
        return _RecordingFunctionWrapper(
293
            func,
294
            recurse_lock=recurse_lock_factory(),
295
            **options
296
        )
297
    else:
298
        return partial(record, **options)
299
300
301
class StoryResultWrapper(object):
302
    __slots__ = '__recorder__'
303
304
    def __init__(self, recorder):
305
        self.__recorder__ = recorder
306
307
    def __eq__(self, result):
308
        self.__recorder__(_Returns(result))
309
310
    def __pow__(self, exception):
311
        if not (isinstance(exception, BaseException) or isclass(exception) and issubclass(exception, BaseException)):
312
            raise RuntimeError("Value %r must be an exception type or instance." % exception)
313
        self.__recorder__(_Raises(exception))
314
315
    def __unsupported__(self, *args):
316
        raise TypeError("Unsupported operation. Only `==` (for results) and `**` (for exceptions) can be used.")
317
318
    for mm in (
319
        '__add__', '__sub__', '__mul__', '__floordiv__', '__mod__', '__divmod__', '__lshift__', '__rshift__', '__and__',
320
        '__xor__', '__or__', '__div__', '__truediv__', '__radd__', '__rsub__', '__rmul__', '__rdiv__', '__rtruediv__',
321
        '__rfloordiv__', '__rmod__', '__rdivmod__', '__rpow__', '__rlshift__', '__rrshift__', '__rand__', '__rxor__',
322
        '__ror__', '__iadd__', '__isub__', '__imul__', '__idiv__', '__itruediv__', '__ifloordiv__', '__imod__',
323
        '__ipow__', '__ilshift__', '__irshift__', '__iand__', '__ixor__', '__ior__', '__neg__', '__pos__', '__abs__',
324
        '__invert__', '__complex__', '__int__', '__long__', '__float__', '__oct__', '__hex__', '__index__',
325
        '__coerce__', '__getslice__', '__setslice__', '__delslice__', '__len__', '__getitem__', '__reversed__',
326
        '__contains__', '__call__', '__lt__', '__le__', '__ne__', '__gt__', '__ge__', '__cmp__', '__rcmp__',
327
        '__nonzero__',
328
    ):
329
        exec("%s = __unsupported__" % mm)
330
331
332
class _StoryFunctionWrapper(object):
333
    def __init__(self, wrapped, handle, binding=None, owner=None):
334
        self._wrapped = wrapped
335
        self._name = wrapped.__name__
336
        self._handle = handle
337
        self._binding = binding
338
        self._owner = owner
339
340
    @property
341
    def _qualname(self):
342
        return qualname(self)
343
344
    def __call__(self, *args, **kwargs):
345
        if self._binding is None:
346
            return StoryResultWrapper(partial(self._handle, None, self._qualname, args, kwargs))
347
        else:
348
            if self._name == '__init__':
349
                self._handle(None, qualname(self._owner), args, kwargs, _Binds(self._binding))
350
            else:
351
                return StoryResultWrapper(partial(self._handle, self._binding, self._name, args, kwargs))
352
353
    def __get__(self, binding, owner):
354
        return mimic(type(self)(
355
            self._wrapped.__get__(binding, owner) if hasattr(self._wrapped, '__get__') else self._wrapped,
356
            handle=self._handle,
357
            binding=binding,
358
            owner=owner,
359
        ), self)
360
361
362
class _ReplayFunctionWrapper(_StoryFunctionWrapper):
363
    def __call__(self, *args, **kwargs):
364
        if self._binding is None:
365
            return self._handle(None, self._qualname, args, kwargs, self._wrapped)
366
        else:
367
            if self._name == '__init__':
368
                self._handle(None, qualname(self._owner), args, kwargs, self._wrapped, _Binds(self._binding))
369
            else:
370
                return self._handle(self._binding, self._name, args, kwargs, self._wrapped)
371
372
373
class _RecordingBase(object):
374
    _target = None
375
    _options = None
376
377
    def __init__(self, target, **options):
378
        self._target = target
379
        self._options = options
380
        self._calls = OrderedDict()
381
        self._ids = {}
382
        self._instances = defaultdict(int)
383
384
    def _make_key(self, binding, name, args, kwargs):
385
        if binding is not None:
386
            binding, _ = self._ids[id(binding)]
387
        return (
388
            binding,
389
            name,
390
            ', '.join(repr_ex(i) for i in args),
391
            ', '.join("%s=%s" % (k, repr_ex(v)) for k, v in kwargs.items())
392
        )
393
394
    def _tag_result(self, name, result):
395
        if isinstance(result, _Binds):
396
            instance_name = camelcase_to_underscores(name.rsplit('.', 1)[-1])
397
            self._instances[instance_name] += 1
398
            instance_name = "%s_%s" % (instance_name, self._instances[instance_name])
399
            self._ids[id(result.value)] = instance_name, result.value
400
            result.value = instance_name
401
        else:
402
            result.value = repr_ex(result.value, self._ids)
403
        return result
404
405
    def _handle(self, binding, name, args, kwargs, result):
406
        pk = self._make_key(binding, name, args, kwargs)
407
        result = self._tag_result(name, result)
408
        assert pk not in self._calls or self._calls[pk] == result, (
409
            "Story creation inconsistency. There is already a result cached for "
410
            "binding:%r name:%r args:%r kwargs:%r and it's: %r." % (
411
                binding, name, args, kwargs, self._calls[pk]
412
            )
413
        )
414
        self._calls[pk] = result
415
416
    def __enter__(self):
417
        self._options.setdefault('methods', ALL_METHODS)
418
        self.__entanglement = weave(
419
            self._target,
420
            partial(self._FunctionWrapper, handle=self._handle),
421
            **self._options
422
        )
423
        return self
424
425
    def __exit__(self, *args):
426
        self.__entanglement.rollback()
427
        del self._ids
428
429
430
_Raises = container("Raises")
431
_Returns = container("Returns")
432
_Binds = container("Binds")
433
434
435
class Story(_RecordingBase):
436
    """
437
        This a simple yet flexible tool that can do "capture-replay mocking" or "test doubles" [1]_. It leverages
438
        ``aspectlib``'s powerful :obj:`weaver <aspectlib.weave>`.
439
440
        Args:
441
            target (same as for :obj:`aspectlib.weave`):
442
                Targets to weave in the `story`/`replay` transactions.
443
            subclasses (bool):
444
                If ``True``, subclasses of target are weaved. *Only available for classes*
445
            aliases (bool):
446
                If ``True``, aliases of target are replaced.
447
            lazy (bool):
448
                If ``True`` only target's ``__init__`` method is patched, the rest of the methods are patched after ``__init__``
449
                is called. *Only available for classes*.
450
            methods (list or regex or string): Methods from target to patch. *Only available for classes*
451
452
        The ``Story`` allows some testing patterns that are hard to do with other tools:
453
454
        * **Proxied mocks**: partially mock `objects` and `modules` so they are called normally if the request is unknown.
455
        * **Stubs**: completely mock `objects` and `modules`. Raise errors if the request is unknown.
456
457
        The ``Story`` works in two of transactions:
458
459
        *   **The story**: You describe what calls you want to mocked. Initially you don't need to write this. Example:
460
461
            ::
462
463
                >>> import mymod
464
                >>> with Story(mymod) as story:
465
                ...     mymod.func('some arg') == 'some result'
466
                ...     mymod.func('bad arg') ** ValueError("can't use this")
467
468
        *   **The replay**: You run the code uses the interfaces mocked in the `story`. The :obj:`replay
469
            <aspectlib.test.Story.replay>` always starts from a `story` instance.
470
471
        .. versionchanged:: 0.9.0
472
473
            Added in.
474
475
        .. [1] http://www.martinfowler.com/bliki/TestDouble.html
476
    """
477
    _FunctionWrapper = _StoryFunctionWrapper
478
479
    def __init__(self, *args, **kwargs):
480
        super(Story, self).__init__(*args, **kwargs)
481
        frame = _getframe(1)
482
        self._context = frame.f_globals, frame.f_locals
483
484
    def replay(self, **options):
485
        """
486
        Args:
487
            proxy (bool):
488
                If ``True`` then unexpected uses are allowed (will use the real functions) but they are collected for later
489
                use. Default: ``True``.
490
            strict (bool):
491
                If ``True`` then an ``AssertionError`` is raised when there were `unexpected calls` or there were `missing
492
                calls` (specified in the story but not called). Default: ``True``.
493
            dump (bool):
494
                If ``True`` then the `unexpected`/`missing calls` will be printed (to ``sys.stdout``). Default: ``True``.
495
496
        Returns:
497
            A :obj:`aspectlib.test.Replay` object.
498
499
        Example:
500
501
            >>> import mymod
502
            >>> with Story(mymod) as story:
503
            ...     mymod.func('some arg') == 'some result'
504
            ...     mymod.func('other arg') == 'other result'
505
            >>> with story.replay(strict=False):
506
            ...     print(mymod.func('some arg'))
507
            ...     mymod.func('bogus arg')
508
            some result
509
            Got bogus arg in the real code!
510
            STORY/REPLAY DIFF:
511
                --- expected...
512
                +++ actual...
513
                @@ -1,2 +1,2 @@
514
                 mymod.func('some arg') == 'some result'  # returns
515
                -mymod.func('other arg') == 'other result'  # returns
516
                +mymod.func('bogus arg') == None  # returns
517
            ACTUAL:
518
                mymod.func('some arg') == 'some result'  # returns
519
                mymod.func('bogus arg') == None  # returns
520
            <BLANKLINE>
521
        """
522
        options.update(self._options)
523
        return Replay(self, **options)
524
525
ReplayPair = namedtuple("ReplayPair", ('expected', 'actual'))
526
527
528
def logged_eval(value, context):
529
    try:
530
        return eval(value, *context)
531
    except:
532
        logexception("Failed to evaluate %r.\nContext:\n%s", value, ''.join(format_stack(
533
            f=_getframe(1),
534
            limit=15
535
        )))
536
        raise
537
538
539
class Replay(_RecordingBase):
540
    """
541
    Object implementing the `replay transaction`.
542
543
    This object should be created by :obj:`Story <aspectlib.test.Story>`'s :obj:`replay <aspectlib.test.Story.replay>`
544
    method.
545
    """
546
    _FunctionWrapper = _ReplayFunctionWrapper
547
548
    def __init__(self, play, proxy=True, strict=True, dump=True, recurse_lock=False, **options):
549
        super(Replay, self).__init__(play._target, **options)
550
        self._calls, self._expected, self._actual = ChainMap(self._calls, play._calls), play._calls, self._calls
551
552
        self._proxy = proxy
553
        self._strict = strict
554
        self._dump = dump
555
        self._context = play._context
556
        self._recurse_lock = allocate_lock() if recurse_lock is True else (recurse_lock and recurse_lock())
557
558
    def _handle(self, binding, name, args, kwargs, wrapped, bind=None):
559
        pk = self._make_key(binding, name, args, kwargs)
560
        if pk in self._expected:
561
            result = self._actual[pk] = self._expected[pk]
562
            if isinstance(result, _Binds):
563
                self._tag_result(name, bind)
564
            elif isinstance(result, _Returns):
565
                return logged_eval(result.value, self._context)
566
            elif isinstance(result, _Raises):
567
                raise logged_eval(result.value, self._context)
568
            else:
569
                raise RuntimeError('Internal failure - unknown result: %r' % result)  # pragma: no cover
570
        else:
571
            if self._proxy:
572
                shouldrecord = not self._recurse_lock or self._recurse_lock.acquire(False)
573
                try:
574
                    try:
575
                        if bind:
576
                            bind = self._tag_result(name, bind)
577
                        result = wrapped(*args, **kwargs)
578
                    except Exception as exc:
579
                        if shouldrecord:
580
                            self._calls[pk] = self._tag_result(name, _Raises(exc))
581
                        raise
582
                    else:
583
                        if shouldrecord:
584
                            self._calls[pk] = bind or self._tag_result(name, _Returns(result))
585
                        return result
586
                finally:
587
                    if shouldrecord and self._recurse_lock:
588
                        self._recurse_lock.release()
589
            else:
590
                raise AssertionError("Unexpected call to %s/%s with args:%s kwargs:%s" % pk)
591
592
    def _unexpected(self, _missing=False):
593
        if _missing:
594
            expected, actual = self._actual, self._expected
595
        else:
596
            actual, expected = self._actual, self._expected
597
        return ''.join(_format_calls(OrderedDict(
598
            (pk, val) for pk, val in actual.items()
599
            if pk not in expected or val != expected.get(pk)
600
        )))
601
602
    @property
603
    def unexpected(self):
604
        """
605
        Returns a pretty text representation of just the unexpected calls.
606
607
        The output should be usable directly in the story (just copy-paste it). Example::
608
609
            >>> import mymod
610
            >>> with Story(mymod) as story:
611
            ...     pass
612
            >>> with story.replay(strict=False, dump=False) as replay:
613
            ...     mymod.func('some arg')
614
            ...     try:
615
            ...         mymod.badfunc()
616
            ...     except ValueError as exc:
617
            ...         print(exc)
618
            Got some arg in the real code!
619
            boom!
620
            >>> print(replay.unexpected)
621
            mymod.func('some arg') == None  # returns
622
            mymod.badfunc() ** ValueError('boom!',)  # raises
623
            <BLANKLINE>
624
625
        We can just take the output and paste in the story::
626
627
            >>> import mymod
628
            >>> with Story(mymod) as story:
629
            ...     mymod.func('some arg') == None  # returns
630
            ...     mymod.badfunc() ** ValueError('boom!')  # raises
631
            >>> with story.replay():
632
            ...     mymod.func('some arg')
633
            ...     try:
634
            ...         mymod.badfunc()
635
            ...     except ValueError as exc:
636
            ...         print(exc)
637
            boom!
638
639
        """
640
        return self._unexpected()
641
642
    @property
643
    def missing(self):
644
        """
645
        Returns a pretty text representation of just the missing calls.
646
        """
647
        return self._unexpected(_missing=True)
648
649
    @property
650
    def diff(self):
651
        """
652
        Returns a pretty text representation of the unexpected and missing calls.
653
654
        Most of the time you don't need to directly use this. This is useful when you run the `replay` in
655
        ``strict=False`` mode and want to do custom assertions.
656
657
        """
658
        actual = list(_format_calls(self._actual))
659
        expected = list(_format_calls(self._expected))
660
        return ''.join(unified_diff(expected, actual, fromfile='expected', tofile='actual'))
661
662
    @property
663
    def actual(self):
664
        return ''.join(_format_calls(self._actual))
665
666
    @property
667
    def expected(self):
668
        return ''.join(_format_calls(self._expected))
669
670
    def __exit__(self, *exception):
671
        super(Replay, self).__exit__()
672
        if self._strict or self._dump:
673
            diff = self.diff
674
            if diff:
675
                if exception or self._dump:
676
                    print('STORY/REPLAY DIFF:')
677
                    print('    ' + '\n    '.join(diff.splitlines()))
678
                    print('ACTUAL:')
679
                    print('    ' + '    '.join(_format_calls(self._actual)))
680
                if not exception and self._strict:
681
                    raise AssertionError(diff)
682
683
684
def _format_calls(calls):
685
    for (binding, name, args, kwargs), result in calls.items():
686
        sig = '%s(%s%s%s)' % (name, args, ', ' if kwargs and args else '', kwargs)
687
688
        if isinstance(result, _Binds):
689
            yield '%s = %s\n' % (result.value, sig)
690
        elif isinstance(result, _Returns):
691
            if binding is None:
692
                yield '%s == %s  # returns\n' % (sig, result.value)
693
            else:
694
                yield '%s.%s == %s  # returns\n' % (binding, sig, result.value)
695
        elif isinstance(result, _Raises):
696
            if binding is None:
697
                yield '%s ** %s  # raises\n' % (sig, result.value)
698
            else:
699
                yield '%s.%s ** %s  # raises\n' % (binding, sig, result.value)
700