Package: ptera
PteraPtera is a powerful way to instrument your code for logging, debugging and testing purposes. With a simple call toptera.probing(), you can:Obtain a stream of the values taken by any variable.Probe multiple variables from multiple functions in multiple scopes.Apply maps, filters, reductions, and much more to the streams.Override the values of variables based on complex conditions.Createexternal assertsorconditional breakpoints.Writecomplex, focused tests.Et cetera :)📖Read the documentationInstallpipinstallpteraExampleYou can use Ptera to observe assignments to any variable in your program:frompteraimportprobingdeff(x):y=10foriinrange(1,x+1):y=y+ireturnywithprobing("f > y").values()asvalues:f(3)# These are all the values taken by the y variable in f.assertvalues==[{"y":10},{"y":11},{"y":13},{"y":16},]In the above,Weselectthe variableyof functionfusing the selectorf > y.We use thevalues()method to obtain a list in which the values ofywill be progressively accumulated.Whenfis called within theprobingblock, assignments toyare intercepted and appended to the list.When theprobingblock finishes, the instrumentation is removed andfreverts to its normal behavior.Creating probesptera.probing: Probe variables inside awithblock.ptera.global_probe: Activate a global probe.Using probesThe interface for Ptera's probes is inspired from functional reactive programming and is identical to the interface ofgiving(itself based onrx).See here for a complete list of operators.You can always usewith probing(...).values()as in the example at the top if you want to keep it simple and just obtain a list of values. You can also usewith probing(...).display()to print the values instead.Beyond that, you can also define complex data processing pipelines. For example:withprobing("f > x")asprobe:probe["x"].map(abs).max().print()f(1234)The above defines a pipeline that extracts the value ofx, applies theabsfunction on every element, takes the maximum of these absolute values, and then prints it out. Keep in mind that this statement doesn't reallydoanything at the moment it is executed, it onlydeclaresa pipeline that will be activated whenever a probed variable is set afterwards. That is whyfis called after and not before.More examplesPtera is all about providing new ways to inspect what your programs are doing, so all examples will be based on this simple binary search function:frompteraimportglobal_probe,probingdeff(arr,key):lo=-1hi=len(arr)whilelo<hi-1:mid=lo+(hi-lo)//2if(elem:=arr[mid])>key:hi=midelse:lo=midreturnlo+1############################### THE PROBING CODE GOES HERE ###############################f(list(range(1,350,7)),136)To get the output listed in the right column of the table below, the code in the left column should be inserted before the call tof, where the big comment is. Most of the methods onglobal_probedefine the pipeline through which the probed values will be routed (the interface is inspired from functional reactive programming), so it is important to define them before the instrumented functions are called.CodeOutputThedisplaymethod provides a simple way to log values.global_probe("f > mid").display()mid: 24
mid: 11
mid: 17
mid: 20
mid: 18
mid: 19Theprintmethod lets you specify a format string.global_probe("f(mid) > elem").print("arr[{mid}] =={elem}")arr[24] == 169
arr[11] == 78
arr[17] == 120
arr[20] == 141
arr[18] == 127
arr[19] == 134Reductions are easy: extract the key and usemin,max, etc.global_probe("f > lo")["lo"].max().print("max(lo) ={}")global_probe("f > hi")["hi"].min().print("min(hi) ={}")max(lo) = 19
min(hi) = 20Define assertions withfail()(for debugging, also try.breakpoint()!)defunordered(xs):returnany(x>yforx,yinzip(xs[:-1],xs[1:]))probe=global_probe("f > arr")["arr"]probe.filter(unordered).fail("List is unordered:{}")f([1,6,30,7],18)Traceback (most recent call last):
  ...
  File "test.py", line 30, in <module>
    f([1, 6, 30, 7], 18)
  File "<string>", line 3, in f__ptera_redirect
  File "test.py", line 3, in f
    def f(arr, key):
giving.gvn.Failure: List is unordered: [1, 6, 30, 7]Accumulate into a list:results=global_probe("f > mid")["mid"].accum()f(list(range(1,350,7)),136)print(results)ORwithprobing("f > mid")["mid"].values()asresults:f(list(range(1,350,7)),136)print(results)[24, 11, 17, 20, 18, 19]probingUsage:with ptera.probing(selector) as probe: ...Theselectoris a specification of which variables in which functions we want to stream through the probe. One of the variables must be thefocusof the selector, meaning that the probe is triggered whenthatvariable is set. The focus may be indicated either asf(!x)orf > x(the focus isxin both cases).Theprobeis a wrapper aroundrx.Observableand supports a large number ofoperatorssuch asmap,filter,min,average,throttle, etc. (the interface is the same as ingiving).Example 1: intermediate variablesPtera is capable of capturing any variable in a function, not just inputs and return values:deffact(n):curr=1foriinrange(n):curr=curr*(i+1)returncurrwithprobing("fact(i, !curr)").print():fact(3)# {'curr': 1}# {'curr': 1, 'i': 0}# {'curr': 2, 'i': 1}# {'curr': 6, 'i': 2}The "!" in the selector above means that the focus iscurr. This means it is triggered whencurris set. This is why the first result does not have a value fori. You can use the selectorfact(!i, curr)to focus oniinstead:withprobing("fact(!i, curr)").print():fact(3)# {'i': 0, 'curr': 1}# {'i': 1, 'curr': 1}# {'i': 2, 'curr': 2}You can see that the associations are different (curr is 2 when i is 2, whereas it was 6 with the other selector), but this is simply because they are now triggered wheniis set.Example 2: multiple scopesA selector may act on several nested scopes in a call graph. For example, the selectorf(x) > g(y) > h > zwould capture variablesx,yandzfrom the scopes of three different functions, but only whenfcallsgandgcallsh(either directly or indirectly).deff(x):returng(x+1)*g(-x-1)defg(x):returnx*2# Use "as" to rename a variable if there is a name conflictwithprobing("f(x) > g > x as gx").print():f(5)# {'gx': 6, 'x': 5}# {'gx': -6, 'x': 5}g(10)# Prints nothingExample 3: overriding variablesIt is also possible to override the value of a variable with theoverride(orkoverride) methods:defadd_ct(x):ct=1returnx+ctwithprobing("add_ct(x) > ct",overridable=True)asprobe:# The value of other variables can be used to compute the new value of ctprobe.override(lambdadata:data["x"])# You can also use koverride, which calls func(**data)# probe.koverride(lambda x: x)print(add_ct(3))# sets ct = x = 3; prints 6print(add_ct(10))# sets ct = x = 20; prints 20Important:override() only overrides thefocus variable. As explained earlier, the focus variable is the one to the right of>, or the one prefixed with!. A Ptera selector is only triggered when the focus variable is set, so realistically it is the only one that it makes sense to override.This is worth keeping in mind, because otherwise it's not always obvious what override is doing. For example:withprobing("add_ct(x) > ct",overridable=True)asprobe:# The focus is ct, so override will always set ct# Therefore, this sets ct = 10 when x == 3:probe.where(x=3).override(10)print(add_ct(3))# sets ct = 10; prints 13print(add_ct(10))# does not override anything; prints 11