Add support for spy operations.

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

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.

Summary ID
Add support for spy operations.
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, }, ]))
7222f42482f4d8206ba3258681d01f4dcf543a85
Description From Last Updated

F401 'kgb.calls.SpyCall' imported but unused

reviewbotreviewbot

E501 line too long (82 > 79 characters)

reviewbotreviewbot

F401 'kgb.agency.SpyAgency' imported but unused

reviewbotreviewbot

F841 local variable 'spy' is assigned to but never used

reviewbotreviewbot

F841 local variable 'spy' is assigned to but never used

reviewbotreviewbot

F841 local variable 'spy' is assigned to but never used

reviewbotreviewbot

F841 local variable 'spy' is assigned to but never used

reviewbotreviewbot

F841 local variable 'spy' is assigned to but never used

reviewbotreviewbot

F841 local variable 'spy' is assigned to but never used

reviewbotreviewbot

F841 local variable 'spy' is assigned to but never used

reviewbotreviewbot

F841 local variable 'spy' is assigned to but never used

reviewbotreviewbot

F841 local variable 'spy' is assigned to but never used

reviewbotreviewbot

F841 local variable 'spy' is assigned to but never used

reviewbotreviewbot

F841 local variable 'spy' is assigned to but never used

reviewbotreviewbot

F841 local variable 'spy' is assigned to but never used

reviewbotreviewbot

F841 local variable 'spy' is assigned to but never used

reviewbotreviewbot

F841 local variable 'spy' is assigned to but never used

reviewbotreviewbot

F841 local variable 'spy' is assigned to but never used

reviewbotreviewbot

F841 local variable 'spy' is assigned to but never used

reviewbotreviewbot

F841 local variable 'spy' is assigned to but never used

reviewbotreviewbot

F841 local variable 'spy' is assigned to but never used

reviewbotreviewbot

F841 local variable 'spy' is assigned to but never used

reviewbotreviewbot
Checks run (1 failed, 1 succeeded)
flake8 failed.
JSHint passed.

flake8

chipx86
david
  1. Ship It!
  2. 
      
chipx86
Review request changed
Status:
Completed
Change Summary:
Pushed to master (743d50e)