xdbg - Tutorial

Home

To use xdbg, you will first need to start an IPython session. This could be in the form of a Jupyter notebook (as presented here), but xdbg will work in most IPython sessions including the ipython command-line console. Your text editor may also have a way of interacting with IPython (an example would be Atom with the hydrogen package).

To begin, you need to to ensure that xdbg is loaded into your current session.

In [1]:
%load_ext xdbg

When loaded, IPython is extended with extra debugger-related magics:

  • %break [func [lineno]]
  • %tbreak [func [lineno]]
  • %enable [bpnumber ...]
  • %disable [bpnumber ...]
  • %ignore bpnumber [count]
  • %scope name
  • %makescope name

Using breakpoints

The %break magic lets you set breakpoints inside a function, and execute interactive statements at that point.

For example:

In [2]:
def foo(n):
    pass
In [3]:
%break foo
New breakpoint 0
In [8]:
val = foo(5)
[xdbg] Entered: <__main__>.foo

The message above indicates that the scope of the interactive iterpreter has changed.

All commands now target the scope inside the function. For example, the variable n is now defined.

In [5]:
n
Out[5]:
5

Any variables defined now are also defined inside the local scope. For example:

In [6]:
x = 5
'x' in globals()
Out[6]:
False

Issuing a return statement exits the function scope

In [7]:
return 5
[xdbg] Exited: <__main__>.foo
In [9]:
val
Out[9]:
5
In [10]:
x
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-10-401b30e3b8b5> in <module>()
----> 1 x

NameError: name 'x' is not defined

As you see, execution continued (val is set), and the local variable x is no longer in scope.

xdbg also provides some typical debugger functionality, such as setting breakpoints mid-function and enabling/disabling them.

In [11]:
def foo(n):
    up_to_n = list(range(n))
    print(up_to_n)
In [12]:
%break foo ?
1   def foo(n):
2       up_to_n = list(range(n))
3       print(up_to_n)

In [13]:
%break foo 3
New breakpoint 1
In [14]:
%disable 1
Modified: 1
In [15]:
foo(5)
[0, 1, 2, 3, 4]
In [16]:
%enable 1
Modified: 1
In [20]:
foo(5)
[xdbg] Entered: <__main__>.foo
In [18]:
up_to_n
Out[18]:
[0, 1, 2, 3, 4]
In [19]:
return
[xdbg] Exited: <__main__>.foo

Note that xdbg does not provide commands for stepping through the execution of a function or walking up/down the stack. Instead it drops you straight into an interactive REPL at the location of the breakpoint.

However, the IPython interactive environment is much more powerful than a typical debugger, because it lets you execute a multitude of Python commands. You can simulate stepping through the execution of a function by pasting th source code of that function into the REPL. You can also inspect the stack using the inspect module. Commands for stepping through execution are of course very convenient, so they may be added to future versions of xdbg.

Working with modules

In addition to debugging functions, xdbg also lets you move the interpreter scope inside any module.

We are currently in the main scope, and the function xdbg.bar is not defined

In [21]:
__name__
Out[21]:
'__main__'
In [22]:
import xdbg
xdbg.bar
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-22-c3b967afe602> in <module>()
      1 import xdbg
----> 2 xdbg.bar

AttributeError: module 'xdbg' has no attribute 'bar'

Now we can switch into the xdbg module and define foo.

In [23]:
%scope xdbg
In [24]:
__name__
Out[24]:
'xdbg'
In [25]:
x = 5

def bar():
    print('x is', x)
In [26]:
# With no arguments, scope returns to the main module
%scope
In [27]:
__name__
Out[27]:
'__main__'
In [28]:
xdbg.bar()
x is 5

Note how in the definition of bar we used the variable x, which is not defined in the main module. This is the main advantage of using %scope: you can write code exactly the way it would appear in the module's file. This allows you to copy-paste code between your debugging session and the module without changing it (or even avoid copy-pasting entirely by using a Jupyter-enabled editor such as Atom with the hydrogen package).

Sometimes you may need to enter another file's scope before the module is fully imported -- for example, if the toplevel code in the file contains a bug. Consider the following situation:

In [29]:
%%file util.py
raise NotImplementedError("Code here is not ready yet!")
Overwriting util.py
In [30]:
import util
---------------------------------------------------------------------------
NotImplementedError                       Traceback (most recent call last)
<ipython-input-30-6822737b3752> in <module>()
----> 1 import util

/Users/kitaev/dev/xdbg/doc/util.py in <module>()
----> 1 raise NotImplementedError("Code here is not ready yet!")

NotImplementedError: Code here is not ready yet!

The util module here cannot be imported, so there is no scope to jump to

In [31]:
%scope util
Module not found: util

In this situation, you can use %makescope, which makes the scope available while not populating it with any code.

In [32]:
%makescope util

Note how this leads to the creation of a dummy module that stands in for util:

In [33]:
import util
util.__file__
Out[33]:
'/Users/kitaev/dev/xdbg/doc/util.py'

It is then possible to move the interpreter into that module to populate its contents:

In [34]:
%scope util
In [35]:
x = 1
In [36]:
%scope
In [37]:
util.x
Out[37]:
1