Add support for spy operations.

Review Request #11153 — Created Aug. 30, 2020 and submitted — Latest diff uploaded

Information

kgb
master

Reviewers

kgb

A spy operation is an action or set of possible actions that are
performed when a function is called. They're an alternative to
overriding a function explicitly, and instead work as reusable shortcuts
for common patterns used when spying. Examples would be raising an
exception, or dispatching a function based on the arguments provided in
the call.

These can be thought of as glorified functions, and in fact they
basically are, under the hood, but are designed to be extensible so that
projects can more easily maintain common spy patterns.

There are some built-in operations provided by kgb:

  • SpyOpMatchAny allows a caller to provide a list of all possible sets
    of arguments that may be in one or more calls, triggering spy behavior
    for the particular match (allowing call_original/call_fake to be
    conditional on the arguments). Any call not provided in the list will
    raise an UnexpectedCallError assertion.

  • SpyOpMatchInOrder is similar to SpyOpMatchAny, but the calls
    must be in the order specified (which is useful for ensuring an order
    of operations).

  • SpyOpRaise takes an exception instance and raises it when the
    function is called (preventing a caller from having to define a
    wrapping function).

  • SpyOpReturn takes a return value and returns it when the function is
    called (similar to defining a simple lambda, but better specifying the
    intent).

These are set with an op= argument, instead of a call_fake=. For
example::

spy_on(pen.emit_poison, op=kgb.SpyOpRaise(PoisonEmptyError()))

Or, for one of the more complex examples:

spy_on(traps.trigger, op=kgb.SpyOpMatchAny([
    {
        'args': ('hallway_lasers',),
        'call_fake': _send_wolves,
    },
    {
        'args': ('trap_tile',),
        'call_fake': _spill_hot_oil,
    },
    {
        'args': ('infrared_camera',),
        'kwargs': {
            'sector': 'underground_passage',
        },
        'call_original': False,
    },
]))

Unit tests pass on all supported versions of Python.

Plugged each of these operations into some real-world code and made sure
they worked as expected.

Commits

Files