A time machine for debugging pesky stateful errors.
It's not always clear what lines of code introduce ugly bugs. By walking through modifications to an object's state step by step, tracker makes it simpler to debug where the flaws in your logic lie.
Tracker is conveniently available via pip:
pip install trackeror installable via git clone and setup.py
git clone [email protected]:madisonmay/tracker.git
python setup.py installTo ensure Tracker is properly installed, you can run the unittest suite from the project root:
nosetests -vTracker uses class decorators to specify a list of attributes to monitor as a program executes. Let's walk through an example with a bit of code designed to implement a simple balance sheet and see how tracker might help hunt down a bug in some sample code. See the examples directory for runnable code snippets.
from tracker import tracked
@tracked('balance')
class BalanceSheet(object):
def __init__(self, balance=0):
self.balance = balance
def add_expense(self, value):
self.balance -= value
def deposit(self, value):
self.balance += valueNow let's add some simple erroneous code to demonstrate the usefulness of tracker. Let's imagine for a minute that a second developer working on the balance sheet project misunderstood how the BalanceSheet class was intended to work, added their own syntax for direct modification of the balance attribute, and also included calls to the BalanceSheet's methods. In other words, our second developer is doubling the amount of every expense and deposit.
def add_expense(sheet, value):
sheet.balance -= value
sheet.add_expense(value)
def deposit(sheet, value):
sheet.balance += value
sheet.deposit(value)So when the second developer goes to run a routine deposit, they see that the balance is not what they expected.
from balance_sheet import BalanceSheet, add_expense, deposit
sheet = BalanceSheet()
deposit(sheet, 100)
print sheet.balanceUsing tracker's replay functionality, we can rewind and see what lines of code changed the sheet's balance and help our second developer uncover their simple mistake.
for snapshot in sheet.replay():
print snapshotwhich outputs:
{'balance': 0}
--------------------------------------------------------------------------------
balance_sheet_debugging.py:3
2
3 sheet = BalanceSheet()
4 deposit(sheet, 100)
/home/m/Projects/Tracker/examples/balance_sheet.py:8
7 def __init__(self):
8 self.balance = 0
9
{'balance': 100}
--------------------------------------------------------------------------------
balance_sheet_debugging.py:4
3 sheet = BalanceSheet()
4 deposit(sheet, 100)
5 print sheet.balance
balance_sheet.py:23
22 def deposit(sheet, value):
23 sheet.balance += value
24 sheet.deposit(value)
{'balance': 200}
--------------------------------------------------------------------------------
balance_sheet_debugging.py:4
3 sheet = BalanceSheet()
4 deposit(sheet, 100)
5 print sheet.balance
balance_sheet.py:24
23 sheet.balance += value
24 sheet.deposit(value)
25
balance_sheet.py:14
13 def deposit(self, value):
14 self.balance += value
15
Each Snapshot object in sheet.replay() has a state attribute (i.e. {'balance': 200}) and a stack attribute (an array of Frame objects). Each Frame object has a file attribute and a line attribute which point to the line of code which triggered the modification. For dead simple debugging, the __str__ method of the Snapshot object outputs a human readable represenations of those values.
Now we can easily walk through the code that caused the double deposit and follow how the balance attribute changed value over time.
Tracker patches __setattr__ and __setitem__ in order to record changes to variables that you've placed on a watch list.
Whenever a change is caught, the inspect library is used to capture the lines of code that caused the modification. Tracker is barely over 100 lines of code long, so I encourage you to read the source for the gritty details.
Tracker is a work in progress, so pull requests, critical feedback, github issues, and feedback of any kind is welcome.