A small addendum to the previous four parts of my journey down the Python debugger rabbit hole (part 1, part 2, part 3, and part 4).
I tried the debugger I finished last week on a small sample application for my upcoming talk at the PyData Südwest meet-up, and it failed. The problem is related to running the file passed to the debugger. Consider that we debug the following program:
def main():
print("Hi")
if __name__ == "__main__":
main()
We now set the breakpoint in the main
method when starting the debugger and continuing with the execution of the program. The problem: It never hits the breakpoint. But why? Because it never calls the main
method.
The cause of this problem is that the __name__
variable is set to dbg2.py
(the file containing the code is compiling and running the script). But how do we run the script? We use the following (based on a Real Python article):
_globals = globals().copy()
# ...
class Dbg:
# ...
def run(self, file: Path):
""" Run a given file with the debugger """
self._main_file = file
# see https://realpython.com/python-exec/#using-python-for-configuration-files
compiled = compile(file.read_text(), filename=str(file), mode='exec')
sys.argv.pop(0)
sys.breakpointhook = self._breakpoint
self._process_compiled_code(compiled)
exec(compiled, _globals)
This code uses the compile
method to compile the code, telling this method that the file belongs to the program file.
The mode argument specifies what kind of code must be compiled; it can be 'exec'
if source consists of a sequence of statements, 'eval'
if it consists of a single expression, or 'single'
if it consists of a single interactive statement (in the latter case, expression statements that evaluate to something other than None
will be printed).
Python Documentation for The Mode Argument of the Compile Method
We then remove the first argument of the program because it is the debugged file in the case of the debugger and run some post-processing on the compiled code object. This is the reason why we can’t just use eval
. Finally, we use exec
to execute the compiled code with the global variables that we had before creating the Dbg
class and others.
The problem is that exec it doesn’t set the import-related module attributes, such as __name__
and __file__
properly. So we have to emulate these by adding global variables:
exec(compiled, _globals |
{"__name__": "__main__", "__file__": str(file)})
It makes of course sense that exec
behaves this way, as it is normally used to evaluate code in the current context.
With this now fixed, it is possible to debug normal applications like the line counter that I use in my upcoming talk at the 16th November in Karlsruhe.
I hope you liked this short addendum and see you next time with a blog post on something more Java-related.