Usage Guide
This guide provides comprehensive information on how to use dowhen for instrumentation.
Basic Concepts
An instrumentation is basically a callback on a trigger. You can think of do as a callback, and when as a trigger.
Triggers
when
when takes an entity, optional positional identifiers and an optional keyword-only condition.
entity- a function, method, code object, class, module orNoneidentifiers- something to locate a specific line or a special eventcondition- an expression or a function to determine whether the trigger should fire
Entity
You need to specify an entity to instrument. This can be a function, method, code object, class, module or None.
If you pass a class or module, dowhen will instrument all functions and methods in that class or module.
If you pass None, dowhen will instrument globally, which means every code object will be instrumented.
This will introduce an overhead at the beginning, but the unnecessary events will be disabled while the
program is running.
Identifiers
Line
To locate a line, you can use the absolute line number, a string starting with + as
the relative line number, the prefix of the line or a regex pattern.
Notice that the indentation of the line is stripped before matching.
from dowhen import when
def f(x):
return x # line 4
# These will all locate line 4
when(f, 4) # absolute line number
when(f, "+1") # relative to function start
when(f, "return x") # exact match of the line content
when(f, "ret") # prefix of the line
when(f, re.compile(r"return.*")) # regex
If an identifier matches multiple lines, the callback will trigger on all of them.
def f(x):
x += 0
x += 0
return x
do("x += 1").when(f, "x +=") # triggers on both lines
assert f(0) == 2
Special Events
Besides locating lines, you can also use special events as identifiers:
"<start>"- when the function is called"<return>"- when the function returns
when(f, "<start>") # triggers when f is called
when(f, "<return>") # triggers when f returns
Combination of Identifiers
You can combine multiple identifiers to make the trigger more specific:
when(f, ("return x", "+1")) # triggers on `return x` only when it's the +1 line
Multiple identifiers
You can also specify multiple identifiers to trigger on:
def f(x):
for i in range(100):
x += i
return x
do("print(x)").when(f, "return x", "<start>") # triggers on both `return x` and when f is called
Conditions
You can add conditions to triggers to make them more specific:
from dowhen import when
def f(x):
return x
when(f, "return x", condition="x == 0").do("x = 1")
assert f(0) == 1 # x is set to 1 when x is 0
assert f(2) == 2 # x is not modified when x is not 0
You can also use a function as a condition:
from dowhen import when
def f(x):
return x
def check(x):
return x == 0
when(f, "return x", condition=check).do("x = 1")
assert f(0) == 1 # x is set to 1 when x is 0
assert f(2) == 2 # x is not modified when x is not 0
If the condition function returns dowhen.DISABLE, the trigger will not fire anymore.
from dowhen import when, DISABLE
def f(x):
return x
def check(x):
if x == 0:
return True
return DISABLE
when(f, "return x", condition=check).do("x = 1")
assert f(0) == 1 # x is set to 1 when x is 0
assert f(2) == 2 # x is not modified and the trigger is disabled
assert f(0) == 0 # x is not modified anymore
Source Hash
If you need to confirm that the source code of the function has not changed,
you can use the source_hash argument.
from dowhen import when, get_source_hash
def f(x):
return x
# Calculate this once and use the constant in your code
source_hash = get_source_hash(f)
# This will raise an error if the source code of f changes
when(f, "return x", source_hash=source_hash).do("x = 1")
source_hash is not a security feature. It is just a sanity check to ensure
that the source code of the function has not changed so your instrumentation
is still valid. It’s just a piece of the md5 hash of the source code of the function.
Callbacks
do
do executes code when the trigger fires, it can be a string or a function.
from dowhen import do
def f(x):
return x
do("x = 1").when(f, "return x")
assert f(0) == 1
If you are using a function for do, the local variables that match the function arguments
will be automatically passed to the function.
Special arguments:
_frame- when used, the current frame object is passed._retval- when used, the return value of the function is passed. Only valid for<return>triggers.
If you want to change the value of the local variables, you need to return a dictionary with the variable names as keys and the new values as values.
You can also return dowhen.DISABLE to disable the trigger.
from dowhen import do
def f(x):
return x
def callback(x):
return {"x": 1}
do(callback).when(f, "return x")
assert f(0) == 1
def callback_special(_frame, _retval):
assert _frame.f_locals["x"] == 1
assert _retval == 1
do(callback_special).when(f, "<return>")
assert f(0) == 1
bp
bp enters pdb at the trigger.
from dowhen import bp
def f(x):
return x
# Equivalent to setting a breakpoint at f
bp().when(f, "<start>")
goto
goto can modify execution flow.
from dowhen import goto
def f(x):
x = 1
return x
# This skips the line `x = 1` and goes directly to `return x`
goto("return x").when(f, "x = 1")
assert f(0) == 0
You can pass an absolute line number or a source line to goto, similar to identifier
in when. goto also takes a relative line number, but it is relative to the executing line.
Therefore, it can take both +<line_number> and -<line_number>.
Handlers
When you combine a trigger with a callback, you create a handler.
from dowhen import when, do
def f(x):
return x
# This creates a handler
handler = when(f, "return x").do("x = 1")
assert f(0) == 1 # x is set to 1 when f is called
# You can temporarily disable the handler
handler.disable()
assert f(0) == 0 # x is not modified anymore
# You can re-enable the handler
handler.enable()
assert f(0) == 1 # x is set to 1 again
# You can also remove the handler permanently
handler.remove()
assert f(0) == 0 # x is not modified anymore
You can use with statement to create a handler that is automatically removed after the block:
from dowhen import do
def f(x):
return x
with do("x = 1").when(f, "return x"):
assert f(0) == 1
assert f(0) == 0
Handler can use do, bp, and goto as well, which allows you to
chain multiple callbacks together:
from dowhen import when
def f(x):
x += 100
return x
when(f, "x += 100").goto("return x").do("x += 1")
assert f(0) == 1
Utilities
clear_all
You can clear all handlers set by dowhen using clear_all.
from dowhen import clear_all
clear_all()