Tips and tricks
Auditing class attributes
Suppose we have a module example that defines the following class for
representing a point in a two-dimensional space:
# example/__init__.py
import math
class Point2D:
def __init__(self, x, y):
self.x = x
self.y = y
def norm(self):
return math.sqrt(self.x**2 + self.y**2)
@property
def tuple(self):
return (self.x, self.y)
It’s fairly straightforward to audit methods of this class, like norm. We
simply create a new method using auditor.wrap, then replace the old method
with the new one:
>>> from example import Point2D
>>> from seagrass import Auditor
>>> from seagrass.base import ProtoHook
>>> auditor = Auditor()
>>> class PrintEventHook(ProtoHook[None]):
... def prehook(self, event, args, kwargs):
... print(f"{self.__class__.__name__}: {event} triggered")
... def posthook(self, event, result, context):
... pass
>>> hooks = [PrintEventHook()]
>>> aunorm = auditor.audit("event.norm", Point2D.norm, hooks=hooks)
>>> Point2D.norm = aunorm
>>> with auditor.start_auditing():
... p = Point2D(3, 4)
... print(f"p.norm()={p.norm()}")
PrintEventHook: event.norm triggered
p.norm()=5.0
>>> auditor.toggle_event("event.norm", False)
However, what if we want to audit a class member that isn’t a method? For
instance, maybe we want to know in what parts of the code the attribute x
gets accessed or modified. A little trick we can use for this is to redefine
x as being a property of the class Point2D, and then wrap the
getter, setter, and deleter methods for that property.
>>> getter_hooks = setter_hooks = deleter_hooks = [PrintEventHook()]
>>> @auditor.audit("point2d.get_x", hooks=getter_hooks)
... def get_x(self):
... return self.__x
>>> @auditor.audit("point2d.set_x", hooks=setter_hooks)
... def set_x(self, val):
... self.__x = val
>>> @auditor.audit("point2d.del_x", hooks=deleter_hooks)
... def del_x(self):
... del self.__x
>>> setattr(Point2D, "x", property(fget=get_x, fset=set_x, fdel=del_x))
>>> auditor.toggle_auditing(True)
>>> p = Point2D(3, 4)
PrintEventHook: point2d.set_x triggered
>>> p.norm()
PrintEventHook: point2d.get_x triggered
5.0
>>> p.x += 1
PrintEventHook: point2d.get_x triggered
PrintEventHook: point2d.set_x triggered
>>> auditor.toggle_auditing(False)
>>> for func in ("get_x", "set_x", "del_x"):
... auditor.toggle_event(f"point2d.{func}", False)
Finally, what if we want to audit an attribute that’s already a property, like
tuple? In that case, we just need to create a new property that wraps the
getter, setter, and/or deleter methods of the old property.
1
>>> isinstance(Point2D.tuple, property)
True
>>> aufget = auditor.audit("tuple_getter", Point2D.tuple.fget, hooks=hooks)
>>> new_prop = property(
... fget=aufget, fset=Point2D.tuple.fset, fdel=Point2D.tuple.fdel,
... )
>>> setattr(Point2D, "tuple", new_prop)
>>> with auditor.start_auditing():
... p = Point2D(3, 4)
... print(p.tuple)
PrintEventHook: tuple_getter triggered
(3, 4)
Footnotes
- 1
It’s tempting to try directly overriding the attributes of the original property by redefining
Point2D.tuple.fget. However,fgetis a read-only attribute of a property likePoint2D.tuple, and you will get anAttributeErrorif you try to do this:>>> aufget = auditor.audit("tuple_getter", Point2D.tuple.fget, hooks=hooks) >>> setattr(Point2D.tuple, "fget", aufget) Traceback (most recent call last): AttributeError: readonly attribute
As a result, we have to take the more indirect route of defining a new property that uses the wrapped getter method, and then override the original
tupleproperty with the new one.
Automatically naming events
If you’re trying to instrument a large number of functions, it can be a hassle
to try and create event names for all of them. To work around this, you can get
Seagrass to automatically create names using the seagrass.auto()
function:
>>> import time >>> from seagrass import Auditor, auto >>> from seagrass.base import ProtoHook >>> class PrintHook(ProtoHook): ... def prehook(self, event, args, kwargs): ... print(f"event={event!r} raised") >>> auditor = Auditor() >>> ausleep = auditor.audit(auto, time.sleep, hooks=[PrintHook()]) >>> ausleep.__event_name__ 'time.sleep' >>> with auditor.start_auditing(): ... ausleep(0.0) event='time.sleep' raised
As you can see, Seagrass will automatically create a new event whose name is based on the name and module of the instrumented function.
You can customize how functions are named by modifying the function you pass in
to audit(). For instance, if you wanted to have the
name of the event be "event:" plus the qualified path to the function, you
could do
>>> event_name = lambda func: "event:" + auto(func) >>> ausleep = auditor.audit(event_name, time.sleep, hooks=[PrintHook()]) >>> ausleep.__event_name__ 'event:time.sleep' >>> with auditor.start_auditing(): ... ausleep(0.0) event='event:time.sleep' raised