Getting Started¶
Capturing function calls¶
The functions capture_call()
and capture_calls()
allow
capturing useful information about calls to a given function.
They are typically used as context managers, e.g.:
with nopdb.capture_call(fn) as call:
some_code_that_calls_fn()
print(call) # see details about how fn() was called
Note
Only calls to pure-Python functions can be captured. Built-in functions and C extensions are not supported.
To have a concrete example, let’s first define some simple functions to work with:
>>> def f(x, y):
... z = x + y
... return 2 * z
>>> def g(x):
... return f(x, x)
Now let’s try calling g()
and capturing the call to f()
that
will be made from there:
>>> with nopdb.capture_call(f) as call:
... g(1)
4
>>> call
CallCapture(name='f', args=OrderedDict(x=1, y=1), return_value=4)
>>> call.args['x']
1
>>> call.return_value
4
>>> call.locals
{'x': 1, 'y': 1, 'z': 2}
>>> call.print_stack()
File "<stdin>", line 2, in <module>
File "<stdin>", line 2, in g
File "<stdin>", line 1, in f
The object returned by capture_calls()
will always contain information
about the most recent call within the context manager block.
To capture all the calls, we can use capture_calls()
(in the plural):
>>> with nopdb.capture_calls(f) as calls:
... g(1)
... g(42)
4
168
>>> calls
[CallInfo(name='f', args=OrderedDict(x=1, y=1), return_value=4),
CallInfo(name='f', args=OrderedDict(x=42, y=42), return_value=168)]
Both capture_call()
and capture_calls()
support different ways of
specifying which function(s) should be considered:
We may pass a function or its name, i.e.
capture_calls(f)
orcapture_calls('f')
.Passing a method bound to an instance, as in
capture_calls(obj.f)
, will work as expected: only calls invoked on that particular instance (and not other instances of the same class) will be captured.A module, a filename or a full file path can be passed, e.g.
capture_calls('f', module=mymodule)
orcapture_calls('f', file='mymodule.py')
.If no arguments are supplied, calls to all Python functions will be captured.
Setting breakpoints¶
Like conventional debuggers, NoPdb can set breakpoints. However, because NoPdb is a non-interactive debugger, its breakpoints do not actually stop the execution of the program. Instead, they allow executing actions scheduled in advance, such as evaluating expressions.
To set a breakpoint, call the breakpoint()
function. A breakpoint object
is returned, allowing to schedule actions using its
eval()
, exec()
and debug()
methods.
Using the example from the previous section, let’s try to use a breakpoint to capture the value of a variable:
>>> with nopdb.breakpoint(f, line=3) as bp:
... z_values = bp.eval('z') # Get the value of z whenever the breakpoint is hit
...
... g(1)
... g(42)
4
168
>>> z_values
[2, 84]
Note
There are multiple ways to specify the breakpoint location (see the
reference for breakpoint()
for a detailed description of all the
parameters). Like in a classical debugger, we can pass a filename
and a
line
number. Like above, we can also pass a function
(or its name).
Note that lines are always counted from the beginning of the file or notebook cell,
and the breakpoint will be triggered just before executing the given line.
A more convenient option is to provide the source code of the desired line (ignoring surrounding whitespace), for example:
with nopdb.breakpoint(f, line='return 2 * z') as bp:
...
line
can also be omitted, in which case the breakpoint will be triggered
every time the given function is called.
A conditional breakpoint can be set using the cond
parameter.
Not only can we capture values, we can also modify them!
>>> with nopdb.breakpoint(f, line=3) as bp:
... # Get the value of z, then increment it, then get the new value
... z_before = bp.eval('z')
... bp.exec('z += 1')
... z_after = bp.eval('z')
...
... g(1) # This would normally return 4
6
>>> z_before
[2]
>>> z_after
[3]
Warning
Assigning to local variables is somewhat experimental and only supported under CPython (the most common Python implementation) and PyPy.
The NoPdb
class¶
Another way to use NoPdb is by creating a NoPdb
object. The object can either
be used as a context manager, or started and stopped explicitly using the
start()
and stop()
methods. This can be useful if we want to
set multiple breakpoints or call captures in a single context:
with nopdb.NoPdb():
f_call = nopdb.capture_call(f)
g_call = nopdb.capture_call(g)
z_val = nopdb.breakpoint(f, line=3).eval('z')
g(1)
Or alternatively:
dbg = nopdb.NoPdb()
f_call = dbg.capture_call(f)
g_call = dbg.capture_call(g)
z_val = dbg.breakpoint(f, line=3).eval('z')
dbg.start()
g(1)
dbg.stop()
Note
While it is possible to create multiple NoPdb
objects, they cannot
be active simultaneously. Starting a new instance will pause the currently active
instance.