Add workarounds for spying on slippery functions.

Review Request #10984 — Created April 1, 2020 and submitted — Latest diff uploaded

Information

kgb
master

Reviewers

kgb

A slippery function (coined here) is a function that KGB can't by
normally spy on without that spy appearing to fall off. A function is
slippery when its attribute on a class dynamically generates and returns
the function. This can happen if wrapping a method using a
poorly-written decorator that decorates at attribute access time and not
at declaration time. An example of this would be the Stripe Python
module's stripe.Customer.delete method, which will be a different
instance every time it's accessed.

In these cases, the function returned is not bound or unbound, it's just
a standard function, so we have no idea what its owner is. The
workaround involves passing an instance as the owner when calling
spy_on() and then having FunctionSig check to see if it gets a new
function each time it's accessed. If so, it records the function as
slippery and fix up its function type. This will in turn trigger some
behavior to staple a new method to the class and to handle calls
differently.

Since slippery functions are caused by descriptors (classes with
__get__ methods), this also introduces the beginning of support for
storing state on descriptors in FunctionSig. Since these descriptors
are only accessible via a class dictionary (accessing their attribute in
any way on an object just calls __get__()), we also now have
functionality for looking up the actual defined function/descriptor
based on the function name and preserving that.

It's important to note that KGB has no way to reliably determine if a
function is slippery by default. Callers who are noticing that a spy
does not stick to a function should pass the instance as the owner in
order to enable slippery function detection.

It's also important to note that currently, we are unable to spy on an
unbound method that generates slippery functions. We can't safely
replace this on a class without new problems surfacing, and therefore
can't intercept calls on any instances from that class. For now, we
produce an error if trying to spy on an unbound method that generates
slippery functions.

KGB, Djblets, Review Board, and RBCommons unit tests (including new
ones testing against Stripe) pass with all supported versions of Python.

Commits

Files