Package: watchpoints
watchpointswatchpoints is an easy-to-use, intuitive variable/object monitor tool for python that behaves similar to watchpoints in gdb.Installpip install watchpointsUsagewatchSimplywatchthe variables you need to monitor!fromwatchpointsimportwatcha=0watch(a)a=1will generate====== Watchpoints Triggered ======
Call Stack (most recent call last):
  <module> (my_script.py:5):
>   a = 1
a:
0
->
1It works on both variable change and object changefromwatchpointsimportwatcha=[]watch(a)a.append(1)# Triggera={}# TriggerEven better, it can track the changes of the object after the changes of the variablefromwatchpointsimportwatcha=[]watch(a)a={}# Triggera["a"]=2# TriggerWithout doubts, it works whenever the object is changed, even if it's not in the same scopefromwatchpointsimportwatchdeffunc(var):var["a"]=1a={}watch(a)func(a)====== Watchpoints Triggered ======
Call Stack (most recent call last):
  <module> (my_script.py:8):
>   func(a)
  func (my_script.py:4):
>   var["a"] = 1
a:
{}
->
{'a': 1}As you can imagine, you can monitor attributes of an object, or a specific element of a list or a dictfromwatchpointsimportwatchclassMyObj:def__init__(self):self.a=0obj=MyObj()d={"a":0}watch(obj.a,d["a"])# Yes you can do thisobj.a=1# Triggerd["a"]=1# TriggerAlso, watchpoints supports nativethreadinglibrary for multi-threading. It will tell you which thread is changing the
value as well.====== Watchpoints Triggered ======
---- Thread-1 ----
Call Stack (most recent call last):
  _bootstrap (/usr/lib/python3.8/threading.py:890):
>   self._bootstrap_inner()
  _bootstrap_inner (/usr/lib/python3.8/threading.py:932):
>   self.run()
  run (my_script.py:15):
>   a[0] = i
a:
[0]
->
[1]watchpoints will try to guess what you want to monitor, and monitor it as you expect(well most of the time)unwatchWhen you are done with the variable, you can unwatch it.fromwatchpointsimportwatch,unwatcha=0watch(a)a=1unwatch(a)a=2# nothing will happenOr you can unwatch everything by passing no argument to itunwatch()# unwatch everythingprint to different streamLike theprintfunction, you can choose the output stream for watch print usingfileargument. The default
value issys.stderr.f=open("watch.log","w")a=0watch(a,file=f)a=1f.close()Be aware thatthe stream needs to be available when the variable is changed! So the following codeWON'T WORK:a=0withopen("watch.log","w")asf:watch(a,file=f)a=1Or you could just give a filename towatch. It will append to the file.watch(a,file="watch.log")Use config if you want to make it globalwatch.config(file="watch.log")customize printerYou can use your own printer function to print the object, instead of the defaultobjprintwithcustom_printer# This will use built-in print function for the objectswatch(a,custom_printer=print)Use config if you want to make it globalwatch.config(custom_printer=print)aliasYou can give an alias to a monitored variable, so you can unwatch it anywhere. And the alias will be printed instead of the variable namefromwatchpointsimportwatch,unwatchwatch(a,alias="james")# Many other stuff, scope changesunwatch("james")conditional callbackYou can give an extra condition filter to do "conditional watchpoints". Pass a functionfunc(obj)which returnsTrueif you want to trigger the callback towhenofwatcha=0watch(a,when=lambdax:x>0)a=-1# Won't triggera=1# Triggervariable vs objectWhen you dowatch()on an object, you are actually tracking both the object and the variable holding it. In most cases, that's what
you want anyways. However, you can configure precisely which you want to track.a=[]watch(a,track="object")a.append(1)# Triggera={}# Won't trigger because the list object does not changea=[]watch(a,track="variable")a.append(1)#  Won't trigger, because "a" still holds the same objecta={}# Triggerobject compare and deepcopyNested object comparison is tricky. It's hard to find a solid standard to compare complicated customized objects.
By default, watchpoints will do a shallow copy of the object. You can override this behavior by passingdeepcopy=Truetowatch()watch(a,deepcopy=True)watchpoints will honor__eq__method for user-defined classes first. If__eq__is not implemented, watchpoints will compare__dict__(basically attibures) of the object if using shallow copy, and raise anNotImplementedErrorif using deepcopy.The reason behind this is, if you deepcopied a complicated structure, there's no way for watchpoints to figure out if it's the same object
without user defined__eq__function.customize copy and compareFor your own data structures, you can provide a customized copy and/or customized compare function for watchpoints to better suit your need.watchpoints will use the copy function you provide to copy the object for reference, and use your compare function to check if that
object is changed. If copy function or compare function is not provided, it falls to default as mentioned above.cmpargument takes a function that will take two objects as arguments and return abooleanrepresenting whether the objects
aredifferentdefmy_cmp(obj1,obj2):returnobj1.id!=obj2.idwatch(a,cmp=my_cmp)copyargument takes a function that will take a object and return a copy of itdefmy_copy(obj):returnMyObj(id=obj.id)watch(a,copy=my_copy)stack limitYou can specify the call stack limit printed usingwatch.config(). The default value is5, any positive integer is accepted.
You can useNonefor unlimited call stack, which means it will prints out all the frames.watch.config(stack_limit=10)You can also set different stack limits for each monitored variable by passingstack_limitargument towatch# This will only change stack_limit for awatch(a,stack_limit=10)customize callbackOf course sometimes you want to print in your own format, or even do something more than print. You can use your own callback for monitored variableswatch(a,callback=my_callback)The callback function takes three argumentsdefmy_callback(frame,elem,exec_info)frameis the current frame when a change is detected.elemis aWatchElementobject that I'm too lazy to describe for now.exec_infois a tuple of(funcname, filename, lineno)of the line that changed the variableYou can also set change the callback function globally bywatch.config(callback=my_callback)Userestore()to restore the default callbackwatch.restore()Integrating with pdbwatchpoints can be used with pdb with ease. You can trigger pdb just like usingbreakpoint()when
your monitored variable is changed. Simply dowatch.config(pdb=True)When you are in pdb, useq(uit)command to exit pdb, and the next change on the variable will trigger the pdb again.Avoid importSometimes it's a hassle having to import the function in every single file. You can install the watch function to builtins
and be able to call it in any files:watch.install()# or watch.install("func_name") and use it as func_name()# Remove it from the builtinswatch.uninstall()# if installed with a name, pass it to uninstall() as wellLimitationswatchpoints usessys.settrace()so it is not compatible with other libraries that use the same function.watchpoints will slow down your program significantly, like other debuggers, so use it for debugging purpose onlywatch()needs to be used by itself, not nested in other functions, to be correctly parsedat this point, there might be other issues because it's still in development phaseBugs/RequestsPlease send bug reports and feature requests throughgithub issue tracker.LicenseCopyright Tian Gao, 2020.Distributed under the terms of theApache 2.0 license.