Hunter is a flexible code tracing toolkit, not for measuring coverage, but for debugging, logging, inspection and other nefarious purposes. It has a simple Python API, a convenient terminal API and a CLI tool to attach to processes.

Overview

Basic use involves passing various filters to the trace option. An example:

import hunter hunter . trace ( module = 'posixpath' , action = hunter . CallPrinter ) import os os . path . join ( 'a' , 'b' )

That would result in:

>>> os . path . join ( 'a' , 'b' ) /usr/lib/python3.6/posixpath.py:75 call => join(a='a') /usr/lib/python3.6/posixpath.py:80 line a = os.fspath(a) /usr/lib/python3.6/posixpath.py:81 line sep = _get_sep(a) /usr/lib/python3.6/posixpath.py:41 call => _get_sep(path='a') /usr/lib/python3.6/posixpath.py:42 line if isinstance(path, bytes): /usr/lib/python3.6/posixpath.py:45 line return '/' /usr/lib/python3.6/posixpath.py:45 return <= _get_sep: '/' /usr/lib/python3.6/posixpath.py:82 line path = a /usr/lib/python3.6/posixpath.py:83 line try: /usr/lib/python3.6/posixpath.py:84 line if not p: /usr/lib/python3.6/posixpath.py:86 line for b in map(os.fspath, p): /usr/lib/python3.6/posixpath.py:87 line if b.startswith(sep): /usr/lib/python3.6/posixpath.py:89 line elif not path or path.endswith(sep): /usr/lib/python3.6/posixpath.py:92 line path += sep + b /usr/lib/python3.6/posixpath.py:86 line for b in map(os.fspath, p): /usr/lib/python3.6/posixpath.py:96 line return path /usr/lib/python3.6/posixpath.py:96 return <= join: 'a/b' 'a/b'

In a terminal it would look like:

Actions Output format can be controlled with “actions”. There’s an alternative CodePrinter action that doesn’t handle nesting (it was the default action until Hunter 2.0). If filters match then action will be run. Example: import hunter hunter . trace ( module = 'posixpath' , action = hunter . CodePrinter ) import os os . path . join ( 'a' , 'b' ) That would result in: >>> os . path . join ( 'a' , 'b' ) /usr/lib/python3.6/posixpath.py:75 call def join(a, *p): /usr/lib/python3.6/posixpath.py:80 line a = os.fspath(a) /usr/lib/python3.6/posixpath.py:81 line sep = _get_sep(a) /usr/lib/python3.6/posixpath.py:41 call def _get_sep(path): /usr/lib/python3.6/posixpath.py:42 line if isinstance(path, bytes): /usr/lib/python3.6/posixpath.py:45 line return '/' /usr/lib/python3.6/posixpath.py:45 return return '/' ... return value: '/' /usr/lib/python3.6/posixpath.py:82 line path = a /usr/lib/python3.6/posixpath.py:83 line try: /usr/lib/python3.6/posixpath.py:84 line if not p: /usr/lib/python3.6/posixpath.py:86 line for b in map(os.fspath, p): /usr/lib/python3.6/posixpath.py:87 line if b.startswith(sep): /usr/lib/python3.6/posixpath.py:89 line elif not path or path.endswith(sep): /usr/lib/python3.6/posixpath.py:92 line path += sep + b /usr/lib/python3.6/posixpath.py:86 line for b in map(os.fspath, p): /usr/lib/python3.6/posixpath.py:96 line return path /usr/lib/python3.6/posixpath.py:96 return return path ... return value: 'a/b' 'a/b' or in a terminal: Another useful action is the VarsPrinter : import hunter # note that this kind of invocation will also use the default `CallPrinter` action hunter . trace ( hunter . Q ( module = 'posixpath' , action = hunter . VarsPrinter ( 'path' ))) import os os . path . join ( 'a' , 'b' ) That would result in: >>> os . path . join ( 'a' , 'b' ) /usr/lib/python3.6/posixpath.py:75 call => join(a='a') /usr/lib/python3.6/posixpath.py:80 line a = os.fspath(a) /usr/lib/python3.6/posixpath.py:81 line sep = _get_sep(a) /usr/lib/python3.6/posixpath.py:41 call [path => 'a'] /usr/lib/python3.6/posixpath.py:41 call => _get_sep(path='a') /usr/lib/python3.6/posixpath.py:42 line [path => 'a'] /usr/lib/python3.6/posixpath.py:42 line if isinstance(path, bytes): /usr/lib/python3.6/posixpath.py:45 line [path => 'a'] /usr/lib/python3.6/posixpath.py:45 line return '/' /usr/lib/python3.6/posixpath.py:45 return [path => 'a'] /usr/lib/python3.6/posixpath.py:45 return <= _get_sep: '/' /usr/lib/python3.6/posixpath.py:82 line path = a /usr/lib/python3.6/posixpath.py:83 line [path => 'a'] /usr/lib/python3.6/posixpath.py:83 line try: /usr/lib/python3.6/posixpath.py:84 line [path => 'a'] /usr/lib/python3.6/posixpath.py:84 line if not p: /usr/lib/python3.6/posixpath.py:86 line [path => 'a'] /usr/lib/python3.6/posixpath.py:86 line for b in map(os.fspath, p): /usr/lib/python3.6/posixpath.py:87 line [path => 'a'] /usr/lib/python3.6/posixpath.py:87 line if b.startswith(sep): /usr/lib/python3.6/posixpath.py:89 line [path => 'a'] /usr/lib/python3.6/posixpath.py:89 line elif not path or path.endswith(sep): /usr/lib/python3.6/posixpath.py:92 line [path => 'a'] /usr/lib/python3.6/posixpath.py:92 line path += sep + b /usr/lib/python3.6/posixpath.py:86 line [path => 'a/b'] /usr/lib/python3.6/posixpath.py:86 line for b in map(os.fspath, p): /usr/lib/python3.6/posixpath.py:96 line [path => 'a/b'] /usr/lib/python3.6/posixpath.py:96 line return path /usr/lib/python3.6/posixpath.py:96 return [path => 'a/b'] /usr/lib/python3.6/posixpath.py:96 return <= join: 'a/b' 'a/b' In a terminal it would look like: You can give it a tree-like configuration where you can optionally configure specific actions for parts of the tree (like dumping variables or a pdb set_trace): from hunter import trace , Q , Debugger from pdb import Pdb trace ( # drop into a Pdb session if ``foo.bar()`` is called Q ( module = "foo" , function = "bar" , kind = "call" , action = Debugger ( klass = Pdb )) | # or Q ( # show code that contains "mumbo.jumbo" on the current line lambda event : event . locals . get ( "mumbo" ) == "jumbo" , # and it's not in Python's stdlib stdlib = False , # and it contains "mumbo" on the current line source__contains = "mumbo" ) ) import foo foo . func () With a foo.py like this: def bar (): execution_will_get_stopped # cause we get a Pdb session here def func (): mumbo = 1 mumbo = "jumbo" print ( "not shown in trace" ) print ( mumbo ) mumbo = 2 print ( mumbo ) # not shown in trace bar () We get: >>> foo . func () not shown in trace /home/ionel/osp/python-hunter/foo.py:8 line print(mumbo) jumbo /home/ionel/osp/python-hunter/foo.py:9 line mumbo = 2 2 /home/ionel/osp/python-hunter/foo.py:1 call def bar(): > /home/ionel/osp/python-hunter/foo.py(2)bar() -> execution_will_get_stopped # cause we get a Pdb session here (Pdb) In a terminal it would look like:

Tracing processes In similar fashion to strace Hunter can trace other processes, eg: hunter-trace --gdb -p 123 If you wanna play it safe (no messy GDB) then add this in your code: from hunter import remote remote.install() Then you can do: hunter-trace -p 123 See docs on the remote feature. Note: Windows ain’t supported.

Environment variable activation For your convenience environment variable activation is available. Just run your app like this: PYTHONHUNTER="module='os.path'" python yourapp.py On Windows you’d do something like: set PYTHONHUNTER=module='os.path' python yourapp.py The activation works with a clever .pth file that checks for that env var presence and before your app runs does something like this: from hunter import * trace(<whatever-you-had-in-the-PYTHONHUNTER-env-var>) Note that Hunter is activated even if the env var is empty, eg: PYTHONHUNTER="" . Environment variable configuration Sometimes you always use the same options (like stdlib=False or force_colors=True ). To save typing you can set something like this in your environment: PYTHONHUNTERCONFIG="stdlib=False,force_colors=True" This is the same as PYTHONHUNTER="stdlib=False,action=CallPrinter(force_colors=True)" . Notes: Setting PYTHONHUNTERCONFIG alone doesn’t activate hunter.

All the options for the builtin actions are supported.

Although using predicates is supported it can be problematic. Example of setup that won’t trace anything: PYTHONHUNTERCONFIG="Q(module_startswith='django')" PYTHONHUNTER="Q(module_startswith='celery')" which is the equivalent of: PYTHONHUNTER="Q(module_startswith='django'),Q(module_startswith='celery')" which is the equivalent of: PYTHONHUNTER="Q(module_startswith='django')&Q(module_startswith='celery')"