invocation-tree 0.0.34__tar.gz → 0.0.36__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: invocation_tree
3
- Version: 0.0.34
3
+ Version: 0.0.36
4
4
  Summary: Generates an invocation tree of functions calls.
5
5
  Author-email: Bas Terwijn <bterwijn@gmail.com>
6
6
  License-Expression: BSD-2-Clause
@@ -32,10 +32,14 @@ Run a live demo in the 👉 [**Invocation Tree Web Debugger**](https://invocatio
32
32
  - shows the invocation tree (call tree) of a program **in real time**
33
33
  - helps to **understand recursion** and its depth-first nature
34
34
 
35
+ The new `@ivt.show` [decorator interface](#decorator) now allows for **scaling** to larger code bases.
36
+
35
37
  # Topics #
36
38
 
37
39
  [Iteration and Recursion](#iteration-and-recursion)
38
40
 
41
+ [Divide and Conquer](#divide-and-conquer)
42
+
39
43
  [Permutations](#permutations)
40
44
 
41
45
  [Recursion Benefits](#recursion-benefits)
@@ -46,6 +50,8 @@ Run a live demo in the 👉 [**Invocation Tree Web Debugger**](https://invocatio
46
50
 
47
51
  [Quick Sort](#quick-sort)
48
52
 
53
+ [Mutability](#mutability)
54
+
49
55
  [Jugs Puzzle](#jugs-puzzle)
50
56
 
51
57
  [Configuration](#Configuration)
@@ -147,17 +153,27 @@ Each node in the invocation tree represents a function call, and the node's colo
147
153
 
148
154
  For every function call, the package displays its **local variables** and **return value**. Changes to the values of these variables over time are highlighted using bold text and gray shading to make them easier to track.
149
155
 
156
+ We can also visualize the execution of this program in the [Memory Graph Web Debugger](https://memory-graph.com/#codeurl=https://raw.githubusercontent.com/bterwijn/memory_graph/refs/heads/main/src/factorial.py&timestep=1.0&play):
157
+
158
+ [![factorial_mgwd](https://raw.githubusercontent.com/bterwijn/invocation_tree/main/images/factorial_mgwd.svg)](https://memory-graph.com/#codeurl=https://raw.githubusercontent.com/bterwijn/memory_graph/refs/heads/main/src/factorial.py&timestep=1.0&play)
159
+
160
+ where the **call stack** is explicit. Each function call adds a stack frame to the call stack with a reference to its local variables and when a function returns it's stack frame is removed from the call stack. But when later a function calls itself multiple times the [invocation_tree](https://github.com/bterwijn/invocation_tree?tab=readme-ov-file#installation) will give us a better visualization than [memory_graph](https://github.com/bterwijn/memory_graph?tab=readme-ov-file#installation), so we will use that here mostly.
161
+
162
+ # Divide and Conquer #
163
+
150
164
  With recursion we often use a divide and conquer strategy, splitting the problem in subproblems that are easier to solve. With factorial we split `factorial(4)` in a `4` and `factorial(3)` subproblem.
151
165
 
152
- **exercise1:** Use recursions to compute the sum of all the values in a list (hint: split for example the list `[1, 2, 3, ...]` in head `1` and tail `[2, 3, ...]`).
166
+ ### exercise1
167
+ Use recursions to compute the sum of all the values in a list (hint: split for example the list `[1, 2, 3, ...]` in head `1` and tail `[2, 3, ...]`).
153
168
  ```python
154
- def sum(values):
169
+ def sum_list(values):
155
170
  # <your recursive implementation>
156
171
 
157
- print(sum([3, 7, 4, 9, 2])) # 25
172
+ print(sum_list([3, 7, 4, 9, 2])) # 25
158
173
  ```
159
174
 
160
- **exercise2:** Rewrite this iterative implementation of decimal to binary conversion to a recursive implementation.
175
+ ### exercise2
176
+ Rewrite this iterative implementation of decimal to binary conversion to a recursive implementation.
161
177
 
162
178
  ```python
163
179
  def binary(decimal):
@@ -187,7 +203,7 @@ We can use recursion to compute all permutation of a number of elements with rep
187
203
 
188
204
  ![perms_LR3](https://raw.githubusercontent.com/bterwijn/invocation_tree/main/images/perms_LR3.png)
189
205
 
190
- This can be implemented recursively, using a divide and conquer strategy, like:
206
+ This can be implemented recursively, using a divide and conquer strategy, with a function calling itself multiple times, like:
191
207
 
192
208
  ```python
193
209
  import invocation_tree as ivt
@@ -215,7 +231,7 @@ RRR
215
231
  ![permutations](https://raw.githubusercontent.com/bterwijn/invocation_tree/main/images/permutations.gif)
216
232
  Or see it in the [Invocation Tree Web Debugger](https://invocation-tree.com/#timestep=1.0&play)
217
233
 
218
- The visualization shows the depth-first nature of recursion. In each step the first elements is chosen first, and quickly the bottom of the tree is reached. Then the permutation is printed, the function returns, one step back is made, and the next element is chosen. When each element had it's turn the function returns and another step back is made. This pattern repeats until all permutations are printed.
234
+ The visualization shows the depth-first nature of recursion. In each step the first element is chosen first, and quickly the bottom of the tree is reached. Then the permutation is printed, the function returns, one step back is made, and the next element is chosen. When each element had it's turn the function returns and another step back is made. This pattern repeats until all permutations are printed.
219
235
 
220
236
  We can also iterate over all permutations with replacement using the `product()` function of `iterools` to get the same result:
221
237
 
@@ -264,7 +280,19 @@ Or see it in the [Invocation Tree Web Debugger](https://www.invocation-tree.com/
264
280
 
265
281
  With recursion we can stop neighbors from being equal early, in contrast to iteration, where we would have had to filter out a permutation with equal neighbors after it was fully generated, which could be much slower and would require a more complex program.
266
282
 
267
- **exercise3:** Print all permutations with replacements of elements 'A', 'B', and 'C' of length 5 that are palindrome ('ABABA' is palindrome because if you read it backwards it's the same).
283
+ ### exercise3
284
+ Write function `palindromes(elems, perm, n)` that print all permutations with replacements of elements in `elems` of length `n` that are palindrome ('ABABA' is palindrome because if you read it backwards it's the same). The function call `palindromes('ABC', '', 3)` should result in these lines in any order:
285
+ ```
286
+ AAA
287
+ ABA
288
+ ACA
289
+ BAB
290
+ BBB
291
+ BCB
292
+ CAC
293
+ CBC
294
+ CCC
295
+ ```
268
296
 
269
297
  # Path Planning #
270
298
 
@@ -330,7 +358,8 @@ See it in the [Invocation Tree Web Debugger](https://www.invocation-tree.com/#co
330
358
 
331
359
  Add temporary debug prints wherever behavior isn’t clear. Experiment with what and how you print to maximize clarity.
332
360
 
333
- **exercise4:** In this larger bidirectional graph, print all the paths of length 7 that connect node `a` to node `b` where going over the same node multiple times is allowed (`avjxbxb` is one such path, there are 114 such paths in total).
361
+ ### exercise4
362
+ In this larger bidirectional graph, print all the paths of length 7 that connect node `a` to node `b` where going over the same node multiple times is allowed (`avjxbxb` is one such path, there are 114 such paths in total).
334
363
 
335
364
  ```python
336
365
  edges = [('a', 's'), ('i', 'z'), ('c', 'p'), ('d', 'p'), ('d', 'u'), ('b', 'e'), ('b', 'g'),
@@ -391,7 +420,8 @@ print(results)
391
420
  <!-- ![permutations_collect](https://raw.githubusercontent.com/bterwijn/invocation_tree/main/images/permutations_collect.gif) -->
392
421
  See it in the [Invocation Tree Web Debugger](https://www.invocation-tree.com/#codeurl=https://raw.githubusercontent.com/bterwijn/invocation_tree/refs/heads/main/src/permutations_collect.py&timestep=0.5&play)
393
422
 
394
- **exercise5:** Create a list with all paths of length 10 in the larger bidirectional graph that go from `a` to `b`, and that do go via `d` but do **not** go via `x` (`amajdjaskb` is one such path, there are 145 such paths in total).
423
+ ### exercise5
424
+ Create a list with all paths of length 10 in the larger bidirectional graph that go from `a` to `b`, and that do go via `d` but do **not** go via `x` (`amajdjaskb` is one such path, there are 145 such paths in total).
395
425
 
396
426
  Where is the best place in the code to test for `x` to make the program run fast?
397
427
 
@@ -405,9 +435,75 @@ edges = [('a', 's'), ('i', 'z'), ('c', 'p'), ('d', 'p'), ('d', 'u'), ('b', 'e')
405
435
  ```
406
436
  ![graph_big_d_x.png)](https://raw.githubusercontent.com/bterwijn/invocation_tree/main/images/graph_big_d_x.png)
407
437
 
438
+
439
+ # Mutability #
440
+
441
+ In the permutation problem we could choose to use mutable type `list` to represent a permutation instead of the immutable type `str` we used before. This can be done in two ways. One way is to use the `+` list concatenation operator to add elements to the permutation, but this is slow because this creates a whole new list each time:
442
+
443
+ ```python
444
+ def permutations(elements, perm, n):
445
+ if n == 0:
446
+ print(perm)
447
+ else:
448
+ for element in elements:
449
+ permutations(elements, perm + [element], n-1) # creates new list, SLOW!
450
+
451
+ permutations('LR', [], 3)
452
+ ```
453
+
454
+ The [Memory Graph Web Debugger](https://memory-graph.com/#codeurl=https://raw.githubusercontent.com/bterwijn/invocation_tree/refs/heads/main/src/perm_mutable_copy.py&timestep=1&play) shows that each recursive function call has it's own list copy.
455
+
456
+ The [Invocation Tree Web Debugger](https://invocation-tree.com/#codeurl=https://raw.githubusercontent.com/bterwijn/invocation_tree/refs/heads/main/src/perm_mutable_copy.py&timestep=1&play) shows that all permutation are generated in the same way as when we used immutable type `str` to represent each permutation.
457
+
458
+ A second way is to mutate the `list` value with the `+=` operator or `append()` function and then after the recursive call to undo this action to restore its original value. This way we avoid creating new lists so this is much faster. We now use the same list in each recursive function call. We couldn't do this before with immutable type `str` because a value of immutable type is always automatically copied when we change it. However, now we have to take care to correctly undo each action we take so the code can get it a bit more complex, but this generally is worth it for faster execution. This style of recursion is often called **backtracking with in-place mutation**.
459
+
460
+ ```python
461
+ def permutations(elements, perm, n):
462
+ if n == 0:
463
+ print(perm)
464
+ else:
465
+ for element in elements:
466
+ perm.append(element) # do action that mutates, FAST!
467
+ permutations(elements, perm, n-1)
468
+ perm.pop() # undo action
469
+
470
+ permutations('LR', [], 3)
471
+ ```
472
+
473
+ The [Memory Graph Web Debugger](https://memory-graph.com/#codeurl=https://raw.githubusercontent.com/bterwijn/invocation_tree/refs/heads/main/src/perm_mutable_undo.py&timestep=1&play) now shows that all function calls use the same list.
474
+
475
+ The [Invocation Tree Web Debugger](https://invocation-tree.com/#codeurl=https://raw.githubusercontent.com/bterwijn/invocation_tree/refs/heads/main/src/perm_mutable_undo.py&timestep=1&play) shows that all permutation are generated but with each action being undone so that in the end the list is empty again.
476
+
477
+ ### exercise6
478
+ Rewrite your code of **exercise5** so that it uses a list to represent each path and uses backtracking with in-place mutation so that a single list is used in each recursive function call for faster execution. For example the path `'amajdjaskb'` should now be represented as `['a', 'm', 'a', 'j', 'd', 'j', 'a', 's', 'k', 'b']`.
479
+
480
+ # Lazy Evaluation #
481
+
482
+ We can combine recursion and lazy evaluation using the `yield` and `yield from` keywords. We use `yield` to produce a value, and we use `yield from` when calling each function that (indirectly) produces a value using `yield`. Here we see an example with the `permutations()` function:
483
+
484
+ ```python
485
+ def permutations(elements, perm, n):
486
+ if n == 0:
487
+ yield perm
488
+ else:
489
+ for element in elements:
490
+ yield from permutations(elements, perm + element, n-1)
491
+
492
+ generator_function = permutations('LR', '', 3)
493
+ for perm in generator_function:
494
+ print(perm)
495
+ ```
496
+
497
+ The first call to the `permutations()` function now results in a generator_function where we can iterate over to get all permutations that are yielded.
498
+
499
+ The [Invocation Tree Web Debugger](https://invocation-tree.com/#codeurl=https://raw.githubusercontent.com/bterwijn/invocation_tree/refs/heads/main/src/perm_lazy.py&timestep=1&play) gives an idea about how lazy evaluation is implemented. When we read a value from the generator_function the `permutations()` functions get called recursively until a permutation is yielded and then all `permutations()` calls return. When we read the next value the previous state of the recursion is restored and execution continues until the next permutation is yielded. This patterns repeats until all the recursive calls are completed and the generator is used up. Unfortunately this process makes the tree difficult to read, something we might improve in the future. At least it now gives an idea about the overhead of the lazy evaluation of recursive functions, the price we pay for not having to use memory for the `results` list.
500
+
501
+ ### exercise7
502
+ Rewrite your code of **exercise6** so that `get_all_paths()` recursion is evaluated lazily.
503
+
408
504
  # Quick Sort #
409
505
 
410
- Another nice example of divide-and-conquer is the recursive quicksort algorithm. It works by choosing a pivot element and dividing the list into elements smaller than the pivot and elements larger than the pivot. Each of these sublists is then quicksorted in the same way. When we get to the point a sublist has zero or one element, it is already sorted. When returning, these sorted sublists are then combined with the pivot to produce a larger sorted lists. Here we use the return value to get the sorted result.
506
+ Another nice example of divide-and-conquer is the recursive quicksort algorithm. It works by choosing a pivot element and splitting the list into elements smaller than the pivot, equal to the pivot, and larger than the pivot. The smaller and larger sublists are then quicksorted recursively. When we get to the point a sublist has zero or one element, then it is sorted. When returning, these sorted sublists are then combined with the elements equal to the pivot to produce a larger sorted lists.
411
507
 
412
508
  ```python
413
509
  import invocation_tree as ivt
@@ -415,7 +511,7 @@ import invocation_tree as ivt
415
511
  def quick_sort(values):
416
512
  if len(values) <= 1:
417
513
  return values
418
- pivot = values[0] # choose arbitrarity the first as pivot
514
+ pivot = values[0] # choose arbitrarily the first as pivot
419
515
  smaller = [x for x in values if x < pivot]
420
516
  equal = [x for x in values if x == pivot]
421
517
  larger = [x for x in values if x > pivot]
@@ -431,10 +527,16 @@ print(' sorted values:',values)
431
527
  unsorted values: [7, 4, 10, 11, 2, 6, 9, 1, 5, 3, 8, 12]
432
528
  sorted values: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
433
529
  ```
434
- <!-- ![quick_sort](https://raw.githubusercontent.com/bterwijn/invocation_tree/main/images/quick_sort.gif) -->
530
+
435
531
  See it in the [Invocation Tree Web Debugger](https://www.invocation-tree.com/#codeurl=https://raw.githubusercontent.com/bterwijn/invocation_tree/refs/heads/main/src/quick_sort.py&timestep=0.5&play)
436
532
 
437
- **exercise6:** Rewrite this quick sort program so that we can pass in a list to collect the sorted result and we don't need to use a return value. This would make the program faster as it avoids having to use the `+` list concatenation operator that creates a new list each time we use it, whereas `+=` or `append()` only add to an existing list.
533
+ ### exercise8
534
+ Add a `key` argument so that we can use the `quick_sort(values, key=None)` function to sort each value `x` in `values` as if it was value `key(x)`, in exactly the same way as how the `sorted(iterable, key=None)` function works. For example:
535
+
536
+ - The call `quick_sort([1, 3, 4, 2], key = lambda x : -x)` should return `[4, 3, 2, 1]` because then each value is sorted by its negative value [-4, -3, -2, -1].
537
+ - The call `quick_sort(['aaa', 'bb', 'c'], key = lambda x : len(x))` should return `['c', 'bb', 'aaa']` because then each value is sorted by its length [1, 2, 3].
538
+
539
+ Sort the values as normal when `key` is `None`.
438
540
 
439
541
  # Jugs Puzzle #
440
542
 
@@ -492,14 +594,15 @@ Where:
492
594
 
493
595
  The breadth-first algorithm works and gives us the shortest path to a goal state, but to do that it uses a lot of memory to store each generation and all jugs states it has seen. Now we also want an algorithm that uses much less memory.
494
596
 
495
- **exercise7:** Write a recursive solver for the Jugs Puzzle that uses less memory by searching for the solution in a depth-first manner.
597
+ ### exercise9
598
+ Write a recursive solver for the Jugs Puzzle that uses less memory by searching for the solution in a depth-first manner.
496
599
 
497
600
  - A solution may not have the same jugs state multiple times (this also avoids infinite loops).
498
601
  - It is not necessary to find the shortest path to a goal state (like breadth-first does).
499
602
 
500
- If you want to use the Invocation Tree Web Debugger, you can look at these [configuration examples](https://invocation-tree.com/#codeurl=https://raw.githubusercontent.com/bterwijn/invocation_tree/refs/heads/main/src/config.py) to keep the tree small and readable.
603
+ Use the [decorator interface](#decorator) to visualize the execution on your system (not the Invocation Tree Web Debugger) because that allows you to easily choose which functions show up in the tree.
501
604
 
502
- **solution exercise7:** First try it yourself, we give the [solution](https://www.invocation-tree.com/#codeurl=https://raw.githubusercontent.com/bterwijn/invocation_tree/refs/heads/main/src/jugs_depth_first.py&breakpoints=136&continues=1) here for comparison.
605
+ **solution exercise8:** First try it yourself, we give the [solution](https://www.invocation-tree.com/#codeurl=https://raw.githubusercontent.com/bterwijn/invocation_tree/refs/heads/main/src/jugs_depth_first.py&breakpoints=136&continues=1) here for comparison.
503
606
 
504
607
  A harder more fun instance of this puzzle is with jugs with capacity 3, 5, 34 and 107 liter and the goal of getting to a jug with 51 liters.
505
608
 
@@ -512,7 +615,7 @@ The Invocation Tree Web Debugger gives examples of the [most important configura
512
615
 
513
616
 
514
617
  ## Hidding ##
515
- It can be useful to hide certian variables or functions to avoid unnecessary complexity. This can be done with:
618
+ It can be useful to hide certian variables to avoid unnecessary complexity. This can be done with:
516
619
 
517
620
  ```python
518
621
  tree = ivt.blocking()
@@ -542,26 +645,29 @@ tree = ivt.blocking()
542
645
  tree.ignore_calls.add(r're:namespace\..*')
543
646
  ```
544
647
 
545
- ignores all function of `namespace`.
648
+ ignores all functions starting with `namespace.`.
546
649
 
547
650
  ## Decorator ##
548
651
 
549
- A better way to hide functions is to use the `@ivt.show` decorator on only the functions you want to graph. The decorator uses the global `ivt.decorator_tree`.
652
+ A better way to hide functions is to use the `@ivt.show` decorator on only the functions you want to graph but this can only be used when running the code locally and not in the Invocation Tree Web Debugger. The decorator uses the global `ivt.decorator_tree` tree.
550
653
 
551
654
  ```python
552
655
  import invocation_tree as ivt
656
+ ivt.decorator_tree = ivt.blocking() # set tree used by decorator
657
+ #ivt.decorator_tree = ivt.blocking_each_change() # block at each change, much slower
658
+ #ivt.decorator_tree = ivt.debugger() # for VS Code or PyCharm debugger
553
659
 
554
- ivt.decorator_tree = ivt.blocking() # set tree used by decorator
555
-
556
- @ivt.show # use decorator to select which functions to graph
660
+ @ivt.show # use this decorator to select which functions to graph
557
661
  def permutations(elements, perm, n):
558
662
  if n == 0:
559
663
  print(perm)
560
664
  else:
561
665
  for element in elements:
562
- permutations(elements, perm + '\n' + element, n-1)
563
-
564
- permutations( 'LR', '', 3) # all permutations of L and R of length 3
666
+ perm.append(element) # do action that mutates
667
+ permutations(elements, perm, n-1)
668
+ perm.pop() # undo action
669
+
670
+ permutations('LR', [], 3) # all permutations of L and R of length 3
565
671
  ```
566
672
 
567
673
  ## Blocking ##
@@ -608,12 +714,6 @@ tree = ivt.Invocation_Tree()
608
714
  - if `>=0` the out filename is numbered for animated gif making
609
715
  - **tree.indent** : string
610
716
  - the string used for identing the local variables
611
- - **tree.color_active** : string
612
- - HTML color for active function
613
- - **tree.color_paused*** : string
614
- - HTML color for paused functions
615
- - **tree.color_returned***: string
616
- - HTML color for returned functions
617
717
  - **tree.to_string** : dict[str, fun]
618
718
  - mapping from type/name/id to a to_string() function for custom printing of values
619
719
  - **tree.hide_vars** : set()
@@ -627,6 +727,32 @@ tree = ivt.Invocation_Tree()
627
727
  - **tree.fontsize** : str
628
728
  - the font size used in the graph, default '14'
629
729
 
730
+ ## Functions ##
731
+
732
+ - **tree.dark_mode(b: bool = None)**
733
+ - set dark mode to 'True' or 'False', or 'None' to toggle.
734
+ - **tree.transparent_background(b: bool = None)**
735
+ - set transparent background to 'True' or 'False', or 'None' to toggle.
736
+
737
+ ## Colors ##
738
+
739
+ For light mode the colors are:
740
+
741
+ - ivt.foreground_color_light
742
+ - ivt.background_color_light
743
+ - ivt.color_paused_light
744
+ - ivt.color_active_light
745
+ - ivt.color_returned_light
746
+
747
+ For dark mode the colors are:
748
+
749
+ - ivt.foreground_color_dark
750
+ - ivt.background_color_dark
751
+ - ivt.color_paused_dark
752
+ - ivt.color_active_dark
753
+ - ivt.color_returned_dark
754
+
755
+
630
756
  # Troubleshooting #
631
757
  - Adobe Acrobat Reader [doesn't refresh a PDF file](https://community.adobe.com/t5/acrobat-reader-discussions/reload-refresh-pdfs/td-p/9632292) when it changes on disk and blocks updates which results in an `Could not open 'tree.pdf' for writing : Permission denied` error. One solution is to install a PDF reader that does refresh ([SumatraPDF](https://www.sumatrapdfreader.org/), [Okular](https://okular.kde.org/), ...) and set it as the default PDF reader. Another solution is to `render()` the graph to a different output format.
632
758
 
@@ -12,10 +12,14 @@ Run a live demo in the 👉 [**Invocation Tree Web Debugger**](https://invocatio
12
12
  - shows the invocation tree (call tree) of a program **in real time**
13
13
  - helps to **understand recursion** and its depth-first nature
14
14
 
15
+ The new `@ivt.show` [decorator interface](#decorator) now allows for **scaling** to larger code bases.
16
+
15
17
  # Topics #
16
18
 
17
19
  [Iteration and Recursion](#iteration-and-recursion)
18
20
 
21
+ [Divide and Conquer](#divide-and-conquer)
22
+
19
23
  [Permutations](#permutations)
20
24
 
21
25
  [Recursion Benefits](#recursion-benefits)
@@ -26,6 +30,8 @@ Run a live demo in the 👉 [**Invocation Tree Web Debugger**](https://invocatio
26
30
 
27
31
  [Quick Sort](#quick-sort)
28
32
 
33
+ [Mutability](#mutability)
34
+
29
35
  [Jugs Puzzle](#jugs-puzzle)
30
36
 
31
37
  [Configuration](#Configuration)
@@ -127,17 +133,27 @@ Each node in the invocation tree represents a function call, and the node's colo
127
133
 
128
134
  For every function call, the package displays its **local variables** and **return value**. Changes to the values of these variables over time are highlighted using bold text and gray shading to make them easier to track.
129
135
 
136
+ We can also visualize the execution of this program in the [Memory Graph Web Debugger](https://memory-graph.com/#codeurl=https://raw.githubusercontent.com/bterwijn/memory_graph/refs/heads/main/src/factorial.py&timestep=1.0&play):
137
+
138
+ [![factorial_mgwd](https://raw.githubusercontent.com/bterwijn/invocation_tree/main/images/factorial_mgwd.svg)](https://memory-graph.com/#codeurl=https://raw.githubusercontent.com/bterwijn/memory_graph/refs/heads/main/src/factorial.py&timestep=1.0&play)
139
+
140
+ where the **call stack** is explicit. Each function call adds a stack frame to the call stack with a reference to its local variables and when a function returns it's stack frame is removed from the call stack. But when later a function calls itself multiple times the [invocation_tree](https://github.com/bterwijn/invocation_tree?tab=readme-ov-file#installation) will give us a better visualization than [memory_graph](https://github.com/bterwijn/memory_graph?tab=readme-ov-file#installation), so we will use that here mostly.
141
+
142
+ # Divide and Conquer #
143
+
130
144
  With recursion we often use a divide and conquer strategy, splitting the problem in subproblems that are easier to solve. With factorial we split `factorial(4)` in a `4` and `factorial(3)` subproblem.
131
145
 
132
- **exercise1:** Use recursions to compute the sum of all the values in a list (hint: split for example the list `[1, 2, 3, ...]` in head `1` and tail `[2, 3, ...]`).
146
+ ### exercise1
147
+ Use recursions to compute the sum of all the values in a list (hint: split for example the list `[1, 2, 3, ...]` in head `1` and tail `[2, 3, ...]`).
133
148
  ```python
134
- def sum(values):
149
+ def sum_list(values):
135
150
  # <your recursive implementation>
136
151
 
137
- print(sum([3, 7, 4, 9, 2])) # 25
152
+ print(sum_list([3, 7, 4, 9, 2])) # 25
138
153
  ```
139
154
 
140
- **exercise2:** Rewrite this iterative implementation of decimal to binary conversion to a recursive implementation.
155
+ ### exercise2
156
+ Rewrite this iterative implementation of decimal to binary conversion to a recursive implementation.
141
157
 
142
158
  ```python
143
159
  def binary(decimal):
@@ -167,7 +183,7 @@ We can use recursion to compute all permutation of a number of elements with rep
167
183
 
168
184
  ![perms_LR3](https://raw.githubusercontent.com/bterwijn/invocation_tree/main/images/perms_LR3.png)
169
185
 
170
- This can be implemented recursively, using a divide and conquer strategy, like:
186
+ This can be implemented recursively, using a divide and conquer strategy, with a function calling itself multiple times, like:
171
187
 
172
188
  ```python
173
189
  import invocation_tree as ivt
@@ -195,7 +211,7 @@ RRR
195
211
  ![permutations](https://raw.githubusercontent.com/bterwijn/invocation_tree/main/images/permutations.gif)
196
212
  Or see it in the [Invocation Tree Web Debugger](https://invocation-tree.com/#timestep=1.0&play)
197
213
 
198
- The visualization shows the depth-first nature of recursion. In each step the first elements is chosen first, and quickly the bottom of the tree is reached. Then the permutation is printed, the function returns, one step back is made, and the next element is chosen. When each element had it's turn the function returns and another step back is made. This pattern repeats until all permutations are printed.
214
+ The visualization shows the depth-first nature of recursion. In each step the first element is chosen first, and quickly the bottom of the tree is reached. Then the permutation is printed, the function returns, one step back is made, and the next element is chosen. When each element had it's turn the function returns and another step back is made. This pattern repeats until all permutations are printed.
199
215
 
200
216
  We can also iterate over all permutations with replacement using the `product()` function of `iterools` to get the same result:
201
217
 
@@ -244,7 +260,19 @@ Or see it in the [Invocation Tree Web Debugger](https://www.invocation-tree.com/
244
260
 
245
261
  With recursion we can stop neighbors from being equal early, in contrast to iteration, where we would have had to filter out a permutation with equal neighbors after it was fully generated, which could be much slower and would require a more complex program.
246
262
 
247
- **exercise3:** Print all permutations with replacements of elements 'A', 'B', and 'C' of length 5 that are palindrome ('ABABA' is palindrome because if you read it backwards it's the same).
263
+ ### exercise3
264
+ Write function `palindromes(elems, perm, n)` that print all permutations with replacements of elements in `elems` of length `n` that are palindrome ('ABABA' is palindrome because if you read it backwards it's the same). The function call `palindromes('ABC', '', 3)` should result in these lines in any order:
265
+ ```
266
+ AAA
267
+ ABA
268
+ ACA
269
+ BAB
270
+ BBB
271
+ BCB
272
+ CAC
273
+ CBC
274
+ CCC
275
+ ```
248
276
 
249
277
  # Path Planning #
250
278
 
@@ -310,7 +338,8 @@ See it in the [Invocation Tree Web Debugger](https://www.invocation-tree.com/#co
310
338
 
311
339
  Add temporary debug prints wherever behavior isn’t clear. Experiment with what and how you print to maximize clarity.
312
340
 
313
- **exercise4:** In this larger bidirectional graph, print all the paths of length 7 that connect node `a` to node `b` where going over the same node multiple times is allowed (`avjxbxb` is one such path, there are 114 such paths in total).
341
+ ### exercise4
342
+ In this larger bidirectional graph, print all the paths of length 7 that connect node `a` to node `b` where going over the same node multiple times is allowed (`avjxbxb` is one such path, there are 114 such paths in total).
314
343
 
315
344
  ```python
316
345
  edges = [('a', 's'), ('i', 'z'), ('c', 'p'), ('d', 'p'), ('d', 'u'), ('b', 'e'), ('b', 'g'),
@@ -371,7 +400,8 @@ print(results)
371
400
  <!-- ![permutations_collect](https://raw.githubusercontent.com/bterwijn/invocation_tree/main/images/permutations_collect.gif) -->
372
401
  See it in the [Invocation Tree Web Debugger](https://www.invocation-tree.com/#codeurl=https://raw.githubusercontent.com/bterwijn/invocation_tree/refs/heads/main/src/permutations_collect.py&timestep=0.5&play)
373
402
 
374
- **exercise5:** Create a list with all paths of length 10 in the larger bidirectional graph that go from `a` to `b`, and that do go via `d` but do **not** go via `x` (`amajdjaskb` is one such path, there are 145 such paths in total).
403
+ ### exercise5
404
+ Create a list with all paths of length 10 in the larger bidirectional graph that go from `a` to `b`, and that do go via `d` but do **not** go via `x` (`amajdjaskb` is one such path, there are 145 such paths in total).
375
405
 
376
406
  Where is the best place in the code to test for `x` to make the program run fast?
377
407
 
@@ -385,9 +415,75 @@ edges = [('a', 's'), ('i', 'z'), ('c', 'p'), ('d', 'p'), ('d', 'u'), ('b', 'e')
385
415
  ```
386
416
  ![graph_big_d_x.png)](https://raw.githubusercontent.com/bterwijn/invocation_tree/main/images/graph_big_d_x.png)
387
417
 
418
+
419
+ # Mutability #
420
+
421
+ In the permutation problem we could choose to use mutable type `list` to represent a permutation instead of the immutable type `str` we used before. This can be done in two ways. One way is to use the `+` list concatenation operator to add elements to the permutation, but this is slow because this creates a whole new list each time:
422
+
423
+ ```python
424
+ def permutations(elements, perm, n):
425
+ if n == 0:
426
+ print(perm)
427
+ else:
428
+ for element in elements:
429
+ permutations(elements, perm + [element], n-1) # creates new list, SLOW!
430
+
431
+ permutations('LR', [], 3)
432
+ ```
433
+
434
+ The [Memory Graph Web Debugger](https://memory-graph.com/#codeurl=https://raw.githubusercontent.com/bterwijn/invocation_tree/refs/heads/main/src/perm_mutable_copy.py&timestep=1&play) shows that each recursive function call has it's own list copy.
435
+
436
+ The [Invocation Tree Web Debugger](https://invocation-tree.com/#codeurl=https://raw.githubusercontent.com/bterwijn/invocation_tree/refs/heads/main/src/perm_mutable_copy.py&timestep=1&play) shows that all permutation are generated in the same way as when we used immutable type `str` to represent each permutation.
437
+
438
+ A second way is to mutate the `list` value with the `+=` operator or `append()` function and then after the recursive call to undo this action to restore its original value. This way we avoid creating new lists so this is much faster. We now use the same list in each recursive function call. We couldn't do this before with immutable type `str` because a value of immutable type is always automatically copied when we change it. However, now we have to take care to correctly undo each action we take so the code can get it a bit more complex, but this generally is worth it for faster execution. This style of recursion is often called **backtracking with in-place mutation**.
439
+
440
+ ```python
441
+ def permutations(elements, perm, n):
442
+ if n == 0:
443
+ print(perm)
444
+ else:
445
+ for element in elements:
446
+ perm.append(element) # do action that mutates, FAST!
447
+ permutations(elements, perm, n-1)
448
+ perm.pop() # undo action
449
+
450
+ permutations('LR', [], 3)
451
+ ```
452
+
453
+ The [Memory Graph Web Debugger](https://memory-graph.com/#codeurl=https://raw.githubusercontent.com/bterwijn/invocation_tree/refs/heads/main/src/perm_mutable_undo.py&timestep=1&play) now shows that all function calls use the same list.
454
+
455
+ The [Invocation Tree Web Debugger](https://invocation-tree.com/#codeurl=https://raw.githubusercontent.com/bterwijn/invocation_tree/refs/heads/main/src/perm_mutable_undo.py&timestep=1&play) shows that all permutation are generated but with each action being undone so that in the end the list is empty again.
456
+
457
+ ### exercise6
458
+ Rewrite your code of **exercise5** so that it uses a list to represent each path and uses backtracking with in-place mutation so that a single list is used in each recursive function call for faster execution. For example the path `'amajdjaskb'` should now be represented as `['a', 'm', 'a', 'j', 'd', 'j', 'a', 's', 'k', 'b']`.
459
+
460
+ # Lazy Evaluation #
461
+
462
+ We can combine recursion and lazy evaluation using the `yield` and `yield from` keywords. We use `yield` to produce a value, and we use `yield from` when calling each function that (indirectly) produces a value using `yield`. Here we see an example with the `permutations()` function:
463
+
464
+ ```python
465
+ def permutations(elements, perm, n):
466
+ if n == 0:
467
+ yield perm
468
+ else:
469
+ for element in elements:
470
+ yield from permutations(elements, perm + element, n-1)
471
+
472
+ generator_function = permutations('LR', '', 3)
473
+ for perm in generator_function:
474
+ print(perm)
475
+ ```
476
+
477
+ The first call to the `permutations()` function now results in a generator_function where we can iterate over to get all permutations that are yielded.
478
+
479
+ The [Invocation Tree Web Debugger](https://invocation-tree.com/#codeurl=https://raw.githubusercontent.com/bterwijn/invocation_tree/refs/heads/main/src/perm_lazy.py&timestep=1&play) gives an idea about how lazy evaluation is implemented. When we read a value from the generator_function the `permutations()` functions get called recursively until a permutation is yielded and then all `permutations()` calls return. When we read the next value the previous state of the recursion is restored and execution continues until the next permutation is yielded. This patterns repeats until all the recursive calls are completed and the generator is used up. Unfortunately this process makes the tree difficult to read, something we might improve in the future. At least it now gives an idea about the overhead of the lazy evaluation of recursive functions, the price we pay for not having to use memory for the `results` list.
480
+
481
+ ### exercise7
482
+ Rewrite your code of **exercise6** so that `get_all_paths()` recursion is evaluated lazily.
483
+
388
484
  # Quick Sort #
389
485
 
390
- Another nice example of divide-and-conquer is the recursive quicksort algorithm. It works by choosing a pivot element and dividing the list into elements smaller than the pivot and elements larger than the pivot. Each of these sublists is then quicksorted in the same way. When we get to the point a sublist has zero or one element, it is already sorted. When returning, these sorted sublists are then combined with the pivot to produce a larger sorted lists. Here we use the return value to get the sorted result.
486
+ Another nice example of divide-and-conquer is the recursive quicksort algorithm. It works by choosing a pivot element and splitting the list into elements smaller than the pivot, equal to the pivot, and larger than the pivot. The smaller and larger sublists are then quicksorted recursively. When we get to the point a sublist has zero or one element, then it is sorted. When returning, these sorted sublists are then combined with the elements equal to the pivot to produce a larger sorted lists.
391
487
 
392
488
  ```python
393
489
  import invocation_tree as ivt
@@ -395,7 +491,7 @@ import invocation_tree as ivt
395
491
  def quick_sort(values):
396
492
  if len(values) <= 1:
397
493
  return values
398
- pivot = values[0] # choose arbitrarity the first as pivot
494
+ pivot = values[0] # choose arbitrarily the first as pivot
399
495
  smaller = [x for x in values if x < pivot]
400
496
  equal = [x for x in values if x == pivot]
401
497
  larger = [x for x in values if x > pivot]
@@ -411,10 +507,16 @@ print(' sorted values:',values)
411
507
  unsorted values: [7, 4, 10, 11, 2, 6, 9, 1, 5, 3, 8, 12]
412
508
  sorted values: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
413
509
  ```
414
- <!-- ![quick_sort](https://raw.githubusercontent.com/bterwijn/invocation_tree/main/images/quick_sort.gif) -->
510
+
415
511
  See it in the [Invocation Tree Web Debugger](https://www.invocation-tree.com/#codeurl=https://raw.githubusercontent.com/bterwijn/invocation_tree/refs/heads/main/src/quick_sort.py&timestep=0.5&play)
416
512
 
417
- **exercise6:** Rewrite this quick sort program so that we can pass in a list to collect the sorted result and we don't need to use a return value. This would make the program faster as it avoids having to use the `+` list concatenation operator that creates a new list each time we use it, whereas `+=` or `append()` only add to an existing list.
513
+ ### exercise8
514
+ Add a `key` argument so that we can use the `quick_sort(values, key=None)` function to sort each value `x` in `values` as if it was value `key(x)`, in exactly the same way as how the `sorted(iterable, key=None)` function works. For example:
515
+
516
+ - The call `quick_sort([1, 3, 4, 2], key = lambda x : -x)` should return `[4, 3, 2, 1]` because then each value is sorted by its negative value [-4, -3, -2, -1].
517
+ - The call `quick_sort(['aaa', 'bb', 'c'], key = lambda x : len(x))` should return `['c', 'bb', 'aaa']` because then each value is sorted by its length [1, 2, 3].
518
+
519
+ Sort the values as normal when `key` is `None`.
418
520
 
419
521
  # Jugs Puzzle #
420
522
 
@@ -472,14 +574,15 @@ Where:
472
574
 
473
575
  The breadth-first algorithm works and gives us the shortest path to a goal state, but to do that it uses a lot of memory to store each generation and all jugs states it has seen. Now we also want an algorithm that uses much less memory.
474
576
 
475
- **exercise7:** Write a recursive solver for the Jugs Puzzle that uses less memory by searching for the solution in a depth-first manner.
577
+ ### exercise9
578
+ Write a recursive solver for the Jugs Puzzle that uses less memory by searching for the solution in a depth-first manner.
476
579
 
477
580
  - A solution may not have the same jugs state multiple times (this also avoids infinite loops).
478
581
  - It is not necessary to find the shortest path to a goal state (like breadth-first does).
479
582
 
480
- If you want to use the Invocation Tree Web Debugger, you can look at these [configuration examples](https://invocation-tree.com/#codeurl=https://raw.githubusercontent.com/bterwijn/invocation_tree/refs/heads/main/src/config.py) to keep the tree small and readable.
583
+ Use the [decorator interface](#decorator) to visualize the execution on your system (not the Invocation Tree Web Debugger) because that allows you to easily choose which functions show up in the tree.
481
584
 
482
- **solution exercise7:** First try it yourself, we give the [solution](https://www.invocation-tree.com/#codeurl=https://raw.githubusercontent.com/bterwijn/invocation_tree/refs/heads/main/src/jugs_depth_first.py&breakpoints=136&continues=1) here for comparison.
585
+ **solution exercise8:** First try it yourself, we give the [solution](https://www.invocation-tree.com/#codeurl=https://raw.githubusercontent.com/bterwijn/invocation_tree/refs/heads/main/src/jugs_depth_first.py&breakpoints=136&continues=1) here for comparison.
483
586
 
484
587
  A harder more fun instance of this puzzle is with jugs with capacity 3, 5, 34 and 107 liter and the goal of getting to a jug with 51 liters.
485
588
 
@@ -492,7 +595,7 @@ The Invocation Tree Web Debugger gives examples of the [most important configura
492
595
 
493
596
 
494
597
  ## Hidding ##
495
- It can be useful to hide certian variables or functions to avoid unnecessary complexity. This can be done with:
598
+ It can be useful to hide certian variables to avoid unnecessary complexity. This can be done with:
496
599
 
497
600
  ```python
498
601
  tree = ivt.blocking()
@@ -522,26 +625,29 @@ tree = ivt.blocking()
522
625
  tree.ignore_calls.add(r're:namespace\..*')
523
626
  ```
524
627
 
525
- ignores all function of `namespace`.
628
+ ignores all functions starting with `namespace.`.
526
629
 
527
630
  ## Decorator ##
528
631
 
529
- A better way to hide functions is to use the `@ivt.show` decorator on only the functions you want to graph. The decorator uses the global `ivt.decorator_tree`.
632
+ A better way to hide functions is to use the `@ivt.show` decorator on only the functions you want to graph but this can only be used when running the code locally and not in the Invocation Tree Web Debugger. The decorator uses the global `ivt.decorator_tree` tree.
530
633
 
531
634
  ```python
532
635
  import invocation_tree as ivt
636
+ ivt.decorator_tree = ivt.blocking() # set tree used by decorator
637
+ #ivt.decorator_tree = ivt.blocking_each_change() # block at each change, much slower
638
+ #ivt.decorator_tree = ivt.debugger() # for VS Code or PyCharm debugger
533
639
 
534
- ivt.decorator_tree = ivt.blocking() # set tree used by decorator
535
-
536
- @ivt.show # use decorator to select which functions to graph
640
+ @ivt.show # use this decorator to select which functions to graph
537
641
  def permutations(elements, perm, n):
538
642
  if n == 0:
539
643
  print(perm)
540
644
  else:
541
645
  for element in elements:
542
- permutations(elements, perm + '\n' + element, n-1)
543
-
544
- permutations( 'LR', '', 3) # all permutations of L and R of length 3
646
+ perm.append(element) # do action that mutates
647
+ permutations(elements, perm, n-1)
648
+ perm.pop() # undo action
649
+
650
+ permutations('LR', [], 3) # all permutations of L and R of length 3
545
651
  ```
546
652
 
547
653
  ## Blocking ##
@@ -588,12 +694,6 @@ tree = ivt.Invocation_Tree()
588
694
  - if `>=0` the out filename is numbered for animated gif making
589
695
  - **tree.indent** : string
590
696
  - the string used for identing the local variables
591
- - **tree.color_active** : string
592
- - HTML color for active function
593
- - **tree.color_paused*** : string
594
- - HTML color for paused functions
595
- - **tree.color_returned***: string
596
- - HTML color for returned functions
597
697
  - **tree.to_string** : dict[str, fun]
598
698
  - mapping from type/name/id to a to_string() function for custom printing of values
599
699
  - **tree.hide_vars** : set()
@@ -607,6 +707,32 @@ tree = ivt.Invocation_Tree()
607
707
  - **tree.fontsize** : str
608
708
  - the font size used in the graph, default '14'
609
709
 
710
+ ## Functions ##
711
+
712
+ - **tree.dark_mode(b: bool = None)**
713
+ - set dark mode to 'True' or 'False', or 'None' to toggle.
714
+ - **tree.transparent_background(b: bool = None)**
715
+ - set transparent background to 'True' or 'False', or 'None' to toggle.
716
+
717
+ ## Colors ##
718
+
719
+ For light mode the colors are:
720
+
721
+ - ivt.foreground_color_light
722
+ - ivt.background_color_light
723
+ - ivt.color_paused_light
724
+ - ivt.color_active_light
725
+ - ivt.color_returned_light
726
+
727
+ For dark mode the colors are:
728
+
729
+ - ivt.foreground_color_dark
730
+ - ivt.background_color_dark
731
+ - ivt.color_paused_dark
732
+ - ivt.color_active_dark
733
+ - ivt.color_returned_dark
734
+
735
+
610
736
  # Troubleshooting #
611
737
  - Adobe Acrobat Reader [doesn't refresh a PDF file](https://community.adobe.com/t5/acrobat-reader-discussions/reload-refresh-pdfs/td-p/9632292) when it changes on disk and blocks updates which results in an `Could not open 'tree.pdf' for writing : Permission denied` error. One solution is to install a PDF reader that does refresh ([SumatraPDF](https://www.sumatrapdfreader.org/), [Okular](https://okular.kde.org/), ...) and set it as the default PDF reader. Another solution is to `render()` the graph to a different output format.
612
738
 
@@ -10,9 +10,24 @@ import functools
10
10
 
11
11
  import invocation_tree.regex_set as regset
12
12
 
13
- __version__ = "0.0.34"
13
+ __version__ = "0.0.36"
14
14
  __author__ = 'Bas Terwijn'
15
15
 
16
+ # colors dark
17
+ foreground_color_light = '#000000'
18
+ background_color_light = '#ffffff'
19
+ color_paused_light = '#ccffcc'
20
+ color_active_light = '#ffffff'
21
+ color_returned_light = '#ffcccc'
22
+
23
+ # colors light
24
+ foreground_color_dark = '#dddddd'
25
+ background_color_dark = '#1d1d1d'
26
+ color_paused_dark = '#779977'
27
+ color_active_dark = '#1d1d1d'
28
+ color_returned_dark = '#997777'
29
+
30
+
16
31
  def highlight_diff(str1, str2):
17
32
  matcher = difflib.SequenceMatcher(None, str1, str2)
18
33
  result = []
@@ -72,9 +87,6 @@ class Invocation_Tree:
72
87
  gifcount=-1,
73
88
  max_string_len=150,
74
89
  indent=' ',
75
- color_paused = '#ccffcc',
76
- color_active = '#ffffff',
77
- color_returned = '#ffcccc',
78
90
  to_string=None,
79
91
  hide_vars=None,
80
92
  cleanup=True,
@@ -89,9 +101,6 @@ class Invocation_Tree:
89
101
  self.max_string_len = max_string_len
90
102
  self.gifcount = gifcount
91
103
  self.indent = indent
92
- self.color_paused = color_paused
93
- self.color_active = color_active
94
- self.color_returned = color_returned
95
104
  self.each_line = each_line
96
105
  self.to_string = {}
97
106
  if not to_string is None:
@@ -109,6 +118,9 @@ class Invocation_Tree:
109
118
  self.regset_ignore_calls = regset.Regex_Set(self.ignore_calls)
110
119
  self.fontname = 'Times-Roman'
111
120
  self.fontsize = '14'
121
+ self.in_dark_mode = False
122
+ self.in_transparent_background = False
123
+ self.set_colors()
112
124
  # --- core
113
125
  self.stack = []
114
126
  self.returned = []
@@ -122,9 +134,39 @@ class Invocation_Tree:
122
134
  self.graph = None
123
135
  self.prev_global_tracer = None
124
136
 
125
-
126
137
  def __repr__(self):
127
138
  return f'Invocation_Tree(filename={repr(self.filename)}, show={self.show}, block={self.block}, each_line={self.each_line}, gifcount={self.gifcount})'
139
+
140
+ def set_colors(self):
141
+ if self.in_dark_mode:
142
+ self.foreground_color = foreground_color_dark
143
+ self.background_color = background_color_dark
144
+ self.color_paused = color_paused_dark
145
+ self.color_active = color_active_dark
146
+ self.color_returned = color_returned_dark
147
+ else:
148
+ self.foreground_color = foreground_color_light
149
+ self.background_color = background_color_light
150
+ self.color_paused = color_paused_light
151
+ self.color_active = color_active_light
152
+ self.color_returned = color_returned_light
153
+ if self.in_transparent_background:
154
+ self.background_color = 'transparent'
155
+ self.color_active = 'transparent'
156
+
157
+ def dark_mode(self, dark = None):
158
+ if dark is None:
159
+ self.in_dark_mode = not self.in_dark_mode
160
+ else:
161
+ self.in_dark_mode = dark
162
+ self.set_colors()
163
+
164
+ def transparent_background(self, transparent = None):
165
+ if transparent is None:
166
+ self.in_transparent_background = not self.in_transparent_background
167
+ else:
168
+ self.in_transparent_background = transparent
169
+ self.set_colors()
128
170
 
129
171
  def __call__(self, fun, *args, **kwargs):
130
172
  try:
@@ -135,7 +177,7 @@ class Invocation_Tree:
135
177
  sys.settrace(self.prev_global_tracer)
136
178
  return result
137
179
 
138
- def value_to_string(self, key, value):
180
+ def value_to_string(self, key, value, is_value):
139
181
  try:
140
182
  if id(value) in self.to_string:
141
183
  val_str = self.to_string[id(value)](value)
@@ -150,14 +192,18 @@ class Invocation_Tree:
150
192
  if len(val_str) > self.max_string_len:
151
193
  val_str = '...'+val_str[-self.max_string_len:]
152
194
  result = html.escape(val_str)
153
- result = result.replace('\n', '<BR/>') # use HTML line breaks
195
+ if '\n' in result:
196
+ lines = result.split('\n')
197
+ result = '<BR/>' + '<BR/>'.join([line + '&nbsp;' for line in lines]) # use HTML line breaks
198
+ elif is_value and isinstance(value, str):
199
+ result = "'" + result + "'" # add quotes around single line strings
154
200
  return result
155
201
 
156
- def get_hightlighted_content(self, tree_node, key, value, use_old_content=False):
202
+ def get_hightlighted_content(self, tree_node, key, value, use_old_content=False, is_value=False):
157
203
  if use_old_content and key in tree_node.strings:
158
204
  return tree_node.strings[key]
159
205
  is_highlighted = False
160
- content = self.value_to_string(key, value)
206
+ content = self.value_to_string(key, value, is_value)
161
207
  if key in tree_node.strings:
162
208
  use_old_content = tree_node.strings[key]
163
209
  hightlighted_content, is_highlighted = highlight_diff(use_old_content, content)
@@ -184,26 +230,27 @@ class Invocation_Tree:
184
230
  border = 3
185
231
  if is_returned:
186
232
  color = self.color_returned
187
- table = f'<\n<TABLE BORDER="{str(border)}" CELLBORDER="0" CELLSPACING="0" BGCOLOR="{color}">\n <TR>'
233
+ alignment = 'ALIGN="left" BALIGN="LEFT"'
234
+ table = f'<\n<TABLE BORDER="{str(border)}" COLOR="{self.foreground_color}" CELLBORDER="0" CELLSPACING="0" BGCOLOR="{color}">\n <TR>'
188
235
  class_fun_name = get_class_function_name(tree_node.frame)
189
236
  local_vars = tree_node.frame.f_locals
190
237
  hightlighted_content = self.get_hightlighted_content(tree_node, class_fun_name, class_fun_name, use_old_content)
191
- table += '<TD ALIGN="left">'+ '➤'+ hightlighted_content +'</TD>'
238
+ table += '<TD '+alignment+'>'+ '➤'+ hightlighted_content +'</TD>'
192
239
  for var,val in local_vars.items():
193
240
  var_name = class_fun_name+'..'+var
194
241
  val_name = class_fun_name+'.'+var
195
242
  if filter_variables(var,val) and not self.regset_hide_vars.match(val_name, self.hide_vars):
196
243
  table += '</TR>\n <TR>'
197
244
  hightlighted_var = self.get_hightlighted_content(tree_node, var_name, var, use_old_content)
198
- hightlighted_val = self.get_hightlighted_content(tree_node, val_name, val, use_old_content)
245
+ hightlighted_val = self.get_hightlighted_content(tree_node, val_name, val, use_old_content, is_value=True)
199
246
  hightlighted_content = self.indent + hightlighted_var + ': ' + hightlighted_val
200
- table += '<TD ALIGN="left">'+ hightlighted_content +'</TD>'
247
+ table += '<TD '+alignment+'>'+ hightlighted_content +'</TD>'
201
248
  if is_returned:
202
249
  return_name = class_fun_name+'.return'
203
250
  if not self.regset_hide_vars.match(return_name, self.hide_vars):
204
251
  table += '</TR>\n <TR>'
205
- hightlighted_content = self.get_hightlighted_content(tree_node, return_name, return_value, use_old_content)
206
- table += '<TD ALIGN="left">'+ 'return ' + hightlighted_content +'</TD>'
252
+ hightlighted_content = self.get_hightlighted_content(tree_node, return_name, return_value, use_old_content, is_value=True)
253
+ table += '<TD '+alignment+'>'+ 'return ' + hightlighted_content +'</TD>'
207
254
  table += '</TR>\n</TABLE>>'
208
255
  return table
209
256
 
@@ -224,9 +271,24 @@ class Invocation_Tree:
224
271
  return self.filename
225
272
 
226
273
  def create_graph(self):
227
- graphviz_graph_attr = {'fontname': self.fontname, 'fontsize': str(self.fontsize)}
228
- graphviz_node_attr = {'fontname': self.fontname, 'fontsize': str(self.fontsize), 'shape':'plaintext'}
229
- graphviz_edge_attr = {'fontname': self.fontname, 'fontsize': str(self.fontsize)}
274
+ graphviz_graph_attr = {
275
+ 'fontname': self.fontname,
276
+ 'fontsize': str(self.fontsize),
277
+ 'fontcolor': self.foreground_color,
278
+ 'bgcolor': self.background_color,
279
+ }
280
+ graphviz_node_attr = {
281
+ 'fontname': self.fontname,
282
+ 'fontsize': str(self.fontsize),
283
+ 'shape':'plaintext',
284
+ 'fontcolor': self.foreground_color
285
+ }
286
+ graphviz_edge_attr = {
287
+ 'fontname': self.fontname,
288
+ 'fontsize': str(self.fontsize),
289
+ 'fontcolor': self.foreground_color,
290
+ 'color': self.foreground_color
291
+ }
230
292
  graph = Digraph('invocation_tree',
231
293
  graph_attr=graphviz_graph_attr,
232
294
  node_attr=graphviz_node_attr,
@@ -399,6 +461,14 @@ def show(fun):
399
461
 
400
462
  def tracer(frame, event, arg):
401
463
  nonlocal active_depth
464
+
465
+ if event == "exception":
466
+ exc_type, exc, tb = arg
467
+ if exc_type is KeyboardInterrupt:
468
+ # ensure we stop tracing before bailing out
469
+ sys.settrace(prev_tracer)
470
+ raise exc.with_traceback(tb)
471
+
402
472
  # Check if this is our target function
403
473
  if frame.f_code is target_code:
404
474
  if event == "call":
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: invocation_tree
3
- Version: 0.0.34
3
+ Version: 0.0.36
4
4
  Summary: Generates an invocation tree of functions calls.
5
5
  Author-email: Bas Terwijn <bterwijn@gmail.com>
6
6
  License-Expression: BSD-2-Clause
@@ -32,10 +32,14 @@ Run a live demo in the 👉 [**Invocation Tree Web Debugger**](https://invocatio
32
32
  - shows the invocation tree (call tree) of a program **in real time**
33
33
  - helps to **understand recursion** and its depth-first nature
34
34
 
35
+ The new `@ivt.show` [decorator interface](#decorator) now allows for **scaling** to larger code bases.
36
+
35
37
  # Topics #
36
38
 
37
39
  [Iteration and Recursion](#iteration-and-recursion)
38
40
 
41
+ [Divide and Conquer](#divide-and-conquer)
42
+
39
43
  [Permutations](#permutations)
40
44
 
41
45
  [Recursion Benefits](#recursion-benefits)
@@ -46,6 +50,8 @@ Run a live demo in the 👉 [**Invocation Tree Web Debugger**](https://invocatio
46
50
 
47
51
  [Quick Sort](#quick-sort)
48
52
 
53
+ [Mutability](#mutability)
54
+
49
55
  [Jugs Puzzle](#jugs-puzzle)
50
56
 
51
57
  [Configuration](#Configuration)
@@ -147,17 +153,27 @@ Each node in the invocation tree represents a function call, and the node's colo
147
153
 
148
154
  For every function call, the package displays its **local variables** and **return value**. Changes to the values of these variables over time are highlighted using bold text and gray shading to make them easier to track.
149
155
 
156
+ We can also visualize the execution of this program in the [Memory Graph Web Debugger](https://memory-graph.com/#codeurl=https://raw.githubusercontent.com/bterwijn/memory_graph/refs/heads/main/src/factorial.py&timestep=1.0&play):
157
+
158
+ [![factorial_mgwd](https://raw.githubusercontent.com/bterwijn/invocation_tree/main/images/factorial_mgwd.svg)](https://memory-graph.com/#codeurl=https://raw.githubusercontent.com/bterwijn/memory_graph/refs/heads/main/src/factorial.py&timestep=1.0&play)
159
+
160
+ where the **call stack** is explicit. Each function call adds a stack frame to the call stack with a reference to its local variables and when a function returns it's stack frame is removed from the call stack. But when later a function calls itself multiple times the [invocation_tree](https://github.com/bterwijn/invocation_tree?tab=readme-ov-file#installation) will give us a better visualization than [memory_graph](https://github.com/bterwijn/memory_graph?tab=readme-ov-file#installation), so we will use that here mostly.
161
+
162
+ # Divide and Conquer #
163
+
150
164
  With recursion we often use a divide and conquer strategy, splitting the problem in subproblems that are easier to solve. With factorial we split `factorial(4)` in a `4` and `factorial(3)` subproblem.
151
165
 
152
- **exercise1:** Use recursions to compute the sum of all the values in a list (hint: split for example the list `[1, 2, 3, ...]` in head `1` and tail `[2, 3, ...]`).
166
+ ### exercise1
167
+ Use recursions to compute the sum of all the values in a list (hint: split for example the list `[1, 2, 3, ...]` in head `1` and tail `[2, 3, ...]`).
153
168
  ```python
154
- def sum(values):
169
+ def sum_list(values):
155
170
  # <your recursive implementation>
156
171
 
157
- print(sum([3, 7, 4, 9, 2])) # 25
172
+ print(sum_list([3, 7, 4, 9, 2])) # 25
158
173
  ```
159
174
 
160
- **exercise2:** Rewrite this iterative implementation of decimal to binary conversion to a recursive implementation.
175
+ ### exercise2
176
+ Rewrite this iterative implementation of decimal to binary conversion to a recursive implementation.
161
177
 
162
178
  ```python
163
179
  def binary(decimal):
@@ -187,7 +203,7 @@ We can use recursion to compute all permutation of a number of elements with rep
187
203
 
188
204
  ![perms_LR3](https://raw.githubusercontent.com/bterwijn/invocation_tree/main/images/perms_LR3.png)
189
205
 
190
- This can be implemented recursively, using a divide and conquer strategy, like:
206
+ This can be implemented recursively, using a divide and conquer strategy, with a function calling itself multiple times, like:
191
207
 
192
208
  ```python
193
209
  import invocation_tree as ivt
@@ -215,7 +231,7 @@ RRR
215
231
  ![permutations](https://raw.githubusercontent.com/bterwijn/invocation_tree/main/images/permutations.gif)
216
232
  Or see it in the [Invocation Tree Web Debugger](https://invocation-tree.com/#timestep=1.0&play)
217
233
 
218
- The visualization shows the depth-first nature of recursion. In each step the first elements is chosen first, and quickly the bottom of the tree is reached. Then the permutation is printed, the function returns, one step back is made, and the next element is chosen. When each element had it's turn the function returns and another step back is made. This pattern repeats until all permutations are printed.
234
+ The visualization shows the depth-first nature of recursion. In each step the first element is chosen first, and quickly the bottom of the tree is reached. Then the permutation is printed, the function returns, one step back is made, and the next element is chosen. When each element had it's turn the function returns and another step back is made. This pattern repeats until all permutations are printed.
219
235
 
220
236
  We can also iterate over all permutations with replacement using the `product()` function of `iterools` to get the same result:
221
237
 
@@ -264,7 +280,19 @@ Or see it in the [Invocation Tree Web Debugger](https://www.invocation-tree.com/
264
280
 
265
281
  With recursion we can stop neighbors from being equal early, in contrast to iteration, where we would have had to filter out a permutation with equal neighbors after it was fully generated, which could be much slower and would require a more complex program.
266
282
 
267
- **exercise3:** Print all permutations with replacements of elements 'A', 'B', and 'C' of length 5 that are palindrome ('ABABA' is palindrome because if you read it backwards it's the same).
283
+ ### exercise3
284
+ Write function `palindromes(elems, perm, n)` that print all permutations with replacements of elements in `elems` of length `n` that are palindrome ('ABABA' is palindrome because if you read it backwards it's the same). The function call `palindromes('ABC', '', 3)` should result in these lines in any order:
285
+ ```
286
+ AAA
287
+ ABA
288
+ ACA
289
+ BAB
290
+ BBB
291
+ BCB
292
+ CAC
293
+ CBC
294
+ CCC
295
+ ```
268
296
 
269
297
  # Path Planning #
270
298
 
@@ -330,7 +358,8 @@ See it in the [Invocation Tree Web Debugger](https://www.invocation-tree.com/#co
330
358
 
331
359
  Add temporary debug prints wherever behavior isn’t clear. Experiment with what and how you print to maximize clarity.
332
360
 
333
- **exercise4:** In this larger bidirectional graph, print all the paths of length 7 that connect node `a` to node `b` where going over the same node multiple times is allowed (`avjxbxb` is one such path, there are 114 such paths in total).
361
+ ### exercise4
362
+ In this larger bidirectional graph, print all the paths of length 7 that connect node `a` to node `b` where going over the same node multiple times is allowed (`avjxbxb` is one such path, there are 114 such paths in total).
334
363
 
335
364
  ```python
336
365
  edges = [('a', 's'), ('i', 'z'), ('c', 'p'), ('d', 'p'), ('d', 'u'), ('b', 'e'), ('b', 'g'),
@@ -391,7 +420,8 @@ print(results)
391
420
  <!-- ![permutations_collect](https://raw.githubusercontent.com/bterwijn/invocation_tree/main/images/permutations_collect.gif) -->
392
421
  See it in the [Invocation Tree Web Debugger](https://www.invocation-tree.com/#codeurl=https://raw.githubusercontent.com/bterwijn/invocation_tree/refs/heads/main/src/permutations_collect.py&timestep=0.5&play)
393
422
 
394
- **exercise5:** Create a list with all paths of length 10 in the larger bidirectional graph that go from `a` to `b`, and that do go via `d` but do **not** go via `x` (`amajdjaskb` is one such path, there are 145 such paths in total).
423
+ ### exercise5
424
+ Create a list with all paths of length 10 in the larger bidirectional graph that go from `a` to `b`, and that do go via `d` but do **not** go via `x` (`amajdjaskb` is one such path, there are 145 such paths in total).
395
425
 
396
426
  Where is the best place in the code to test for `x` to make the program run fast?
397
427
 
@@ -405,9 +435,75 @@ edges = [('a', 's'), ('i', 'z'), ('c', 'p'), ('d', 'p'), ('d', 'u'), ('b', 'e')
405
435
  ```
406
436
  ![graph_big_d_x.png)](https://raw.githubusercontent.com/bterwijn/invocation_tree/main/images/graph_big_d_x.png)
407
437
 
438
+
439
+ # Mutability #
440
+
441
+ In the permutation problem we could choose to use mutable type `list` to represent a permutation instead of the immutable type `str` we used before. This can be done in two ways. One way is to use the `+` list concatenation operator to add elements to the permutation, but this is slow because this creates a whole new list each time:
442
+
443
+ ```python
444
+ def permutations(elements, perm, n):
445
+ if n == 0:
446
+ print(perm)
447
+ else:
448
+ for element in elements:
449
+ permutations(elements, perm + [element], n-1) # creates new list, SLOW!
450
+
451
+ permutations('LR', [], 3)
452
+ ```
453
+
454
+ The [Memory Graph Web Debugger](https://memory-graph.com/#codeurl=https://raw.githubusercontent.com/bterwijn/invocation_tree/refs/heads/main/src/perm_mutable_copy.py&timestep=1&play) shows that each recursive function call has it's own list copy.
455
+
456
+ The [Invocation Tree Web Debugger](https://invocation-tree.com/#codeurl=https://raw.githubusercontent.com/bterwijn/invocation_tree/refs/heads/main/src/perm_mutable_copy.py&timestep=1&play) shows that all permutation are generated in the same way as when we used immutable type `str` to represent each permutation.
457
+
458
+ A second way is to mutate the `list` value with the `+=` operator or `append()` function and then after the recursive call to undo this action to restore its original value. This way we avoid creating new lists so this is much faster. We now use the same list in each recursive function call. We couldn't do this before with immutable type `str` because a value of immutable type is always automatically copied when we change it. However, now we have to take care to correctly undo each action we take so the code can get it a bit more complex, but this generally is worth it for faster execution. This style of recursion is often called **backtracking with in-place mutation**.
459
+
460
+ ```python
461
+ def permutations(elements, perm, n):
462
+ if n == 0:
463
+ print(perm)
464
+ else:
465
+ for element in elements:
466
+ perm.append(element) # do action that mutates, FAST!
467
+ permutations(elements, perm, n-1)
468
+ perm.pop() # undo action
469
+
470
+ permutations('LR', [], 3)
471
+ ```
472
+
473
+ The [Memory Graph Web Debugger](https://memory-graph.com/#codeurl=https://raw.githubusercontent.com/bterwijn/invocation_tree/refs/heads/main/src/perm_mutable_undo.py&timestep=1&play) now shows that all function calls use the same list.
474
+
475
+ The [Invocation Tree Web Debugger](https://invocation-tree.com/#codeurl=https://raw.githubusercontent.com/bterwijn/invocation_tree/refs/heads/main/src/perm_mutable_undo.py&timestep=1&play) shows that all permutation are generated but with each action being undone so that in the end the list is empty again.
476
+
477
+ ### exercise6
478
+ Rewrite your code of **exercise5** so that it uses a list to represent each path and uses backtracking with in-place mutation so that a single list is used in each recursive function call for faster execution. For example the path `'amajdjaskb'` should now be represented as `['a', 'm', 'a', 'j', 'd', 'j', 'a', 's', 'k', 'b']`.
479
+
480
+ # Lazy Evaluation #
481
+
482
+ We can combine recursion and lazy evaluation using the `yield` and `yield from` keywords. We use `yield` to produce a value, and we use `yield from` when calling each function that (indirectly) produces a value using `yield`. Here we see an example with the `permutations()` function:
483
+
484
+ ```python
485
+ def permutations(elements, perm, n):
486
+ if n == 0:
487
+ yield perm
488
+ else:
489
+ for element in elements:
490
+ yield from permutations(elements, perm + element, n-1)
491
+
492
+ generator_function = permutations('LR', '', 3)
493
+ for perm in generator_function:
494
+ print(perm)
495
+ ```
496
+
497
+ The first call to the `permutations()` function now results in a generator_function where we can iterate over to get all permutations that are yielded.
498
+
499
+ The [Invocation Tree Web Debugger](https://invocation-tree.com/#codeurl=https://raw.githubusercontent.com/bterwijn/invocation_tree/refs/heads/main/src/perm_lazy.py&timestep=1&play) gives an idea about how lazy evaluation is implemented. When we read a value from the generator_function the `permutations()` functions get called recursively until a permutation is yielded and then all `permutations()` calls return. When we read the next value the previous state of the recursion is restored and execution continues until the next permutation is yielded. This patterns repeats until all the recursive calls are completed and the generator is used up. Unfortunately this process makes the tree difficult to read, something we might improve in the future. At least it now gives an idea about the overhead of the lazy evaluation of recursive functions, the price we pay for not having to use memory for the `results` list.
500
+
501
+ ### exercise7
502
+ Rewrite your code of **exercise6** so that `get_all_paths()` recursion is evaluated lazily.
503
+
408
504
  # Quick Sort #
409
505
 
410
- Another nice example of divide-and-conquer is the recursive quicksort algorithm. It works by choosing a pivot element and dividing the list into elements smaller than the pivot and elements larger than the pivot. Each of these sublists is then quicksorted in the same way. When we get to the point a sublist has zero or one element, it is already sorted. When returning, these sorted sublists are then combined with the pivot to produce a larger sorted lists. Here we use the return value to get the sorted result.
506
+ Another nice example of divide-and-conquer is the recursive quicksort algorithm. It works by choosing a pivot element and splitting the list into elements smaller than the pivot, equal to the pivot, and larger than the pivot. The smaller and larger sublists are then quicksorted recursively. When we get to the point a sublist has zero or one element, then it is sorted. When returning, these sorted sublists are then combined with the elements equal to the pivot to produce a larger sorted lists.
411
507
 
412
508
  ```python
413
509
  import invocation_tree as ivt
@@ -415,7 +511,7 @@ import invocation_tree as ivt
415
511
  def quick_sort(values):
416
512
  if len(values) <= 1:
417
513
  return values
418
- pivot = values[0] # choose arbitrarity the first as pivot
514
+ pivot = values[0] # choose arbitrarily the first as pivot
419
515
  smaller = [x for x in values if x < pivot]
420
516
  equal = [x for x in values if x == pivot]
421
517
  larger = [x for x in values if x > pivot]
@@ -431,10 +527,16 @@ print(' sorted values:',values)
431
527
  unsorted values: [7, 4, 10, 11, 2, 6, 9, 1, 5, 3, 8, 12]
432
528
  sorted values: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
433
529
  ```
434
- <!-- ![quick_sort](https://raw.githubusercontent.com/bterwijn/invocation_tree/main/images/quick_sort.gif) -->
530
+
435
531
  See it in the [Invocation Tree Web Debugger](https://www.invocation-tree.com/#codeurl=https://raw.githubusercontent.com/bterwijn/invocation_tree/refs/heads/main/src/quick_sort.py&timestep=0.5&play)
436
532
 
437
- **exercise6:** Rewrite this quick sort program so that we can pass in a list to collect the sorted result and we don't need to use a return value. This would make the program faster as it avoids having to use the `+` list concatenation operator that creates a new list each time we use it, whereas `+=` or `append()` only add to an existing list.
533
+ ### exercise8
534
+ Add a `key` argument so that we can use the `quick_sort(values, key=None)` function to sort each value `x` in `values` as if it was value `key(x)`, in exactly the same way as how the `sorted(iterable, key=None)` function works. For example:
535
+
536
+ - The call `quick_sort([1, 3, 4, 2], key = lambda x : -x)` should return `[4, 3, 2, 1]` because then each value is sorted by its negative value [-4, -3, -2, -1].
537
+ - The call `quick_sort(['aaa', 'bb', 'c'], key = lambda x : len(x))` should return `['c', 'bb', 'aaa']` because then each value is sorted by its length [1, 2, 3].
538
+
539
+ Sort the values as normal when `key` is `None`.
438
540
 
439
541
  # Jugs Puzzle #
440
542
 
@@ -492,14 +594,15 @@ Where:
492
594
 
493
595
  The breadth-first algorithm works and gives us the shortest path to a goal state, but to do that it uses a lot of memory to store each generation and all jugs states it has seen. Now we also want an algorithm that uses much less memory.
494
596
 
495
- **exercise7:** Write a recursive solver for the Jugs Puzzle that uses less memory by searching for the solution in a depth-first manner.
597
+ ### exercise9
598
+ Write a recursive solver for the Jugs Puzzle that uses less memory by searching for the solution in a depth-first manner.
496
599
 
497
600
  - A solution may not have the same jugs state multiple times (this also avoids infinite loops).
498
601
  - It is not necessary to find the shortest path to a goal state (like breadth-first does).
499
602
 
500
- If you want to use the Invocation Tree Web Debugger, you can look at these [configuration examples](https://invocation-tree.com/#codeurl=https://raw.githubusercontent.com/bterwijn/invocation_tree/refs/heads/main/src/config.py) to keep the tree small and readable.
603
+ Use the [decorator interface](#decorator) to visualize the execution on your system (not the Invocation Tree Web Debugger) because that allows you to easily choose which functions show up in the tree.
501
604
 
502
- **solution exercise7:** First try it yourself, we give the [solution](https://www.invocation-tree.com/#codeurl=https://raw.githubusercontent.com/bterwijn/invocation_tree/refs/heads/main/src/jugs_depth_first.py&breakpoints=136&continues=1) here for comparison.
605
+ **solution exercise8:** First try it yourself, we give the [solution](https://www.invocation-tree.com/#codeurl=https://raw.githubusercontent.com/bterwijn/invocation_tree/refs/heads/main/src/jugs_depth_first.py&breakpoints=136&continues=1) here for comparison.
503
606
 
504
607
  A harder more fun instance of this puzzle is with jugs with capacity 3, 5, 34 and 107 liter and the goal of getting to a jug with 51 liters.
505
608
 
@@ -512,7 +615,7 @@ The Invocation Tree Web Debugger gives examples of the [most important configura
512
615
 
513
616
 
514
617
  ## Hidding ##
515
- It can be useful to hide certian variables or functions to avoid unnecessary complexity. This can be done with:
618
+ It can be useful to hide certian variables to avoid unnecessary complexity. This can be done with:
516
619
 
517
620
  ```python
518
621
  tree = ivt.blocking()
@@ -542,26 +645,29 @@ tree = ivt.blocking()
542
645
  tree.ignore_calls.add(r're:namespace\..*')
543
646
  ```
544
647
 
545
- ignores all function of `namespace`.
648
+ ignores all functions starting with `namespace.`.
546
649
 
547
650
  ## Decorator ##
548
651
 
549
- A better way to hide functions is to use the `@ivt.show` decorator on only the functions you want to graph. The decorator uses the global `ivt.decorator_tree`.
652
+ A better way to hide functions is to use the `@ivt.show` decorator on only the functions you want to graph but this can only be used when running the code locally and not in the Invocation Tree Web Debugger. The decorator uses the global `ivt.decorator_tree` tree.
550
653
 
551
654
  ```python
552
655
  import invocation_tree as ivt
656
+ ivt.decorator_tree = ivt.blocking() # set tree used by decorator
657
+ #ivt.decorator_tree = ivt.blocking_each_change() # block at each change, much slower
658
+ #ivt.decorator_tree = ivt.debugger() # for VS Code or PyCharm debugger
553
659
 
554
- ivt.decorator_tree = ivt.blocking() # set tree used by decorator
555
-
556
- @ivt.show # use decorator to select which functions to graph
660
+ @ivt.show # use this decorator to select which functions to graph
557
661
  def permutations(elements, perm, n):
558
662
  if n == 0:
559
663
  print(perm)
560
664
  else:
561
665
  for element in elements:
562
- permutations(elements, perm + '\n' + element, n-1)
563
-
564
- permutations( 'LR', '', 3) # all permutations of L and R of length 3
666
+ perm.append(element) # do action that mutates
667
+ permutations(elements, perm, n-1)
668
+ perm.pop() # undo action
669
+
670
+ permutations('LR', [], 3) # all permutations of L and R of length 3
565
671
  ```
566
672
 
567
673
  ## Blocking ##
@@ -608,12 +714,6 @@ tree = ivt.Invocation_Tree()
608
714
  - if `>=0` the out filename is numbered for animated gif making
609
715
  - **tree.indent** : string
610
716
  - the string used for identing the local variables
611
- - **tree.color_active** : string
612
- - HTML color for active function
613
- - **tree.color_paused*** : string
614
- - HTML color for paused functions
615
- - **tree.color_returned***: string
616
- - HTML color for returned functions
617
717
  - **tree.to_string** : dict[str, fun]
618
718
  - mapping from type/name/id to a to_string() function for custom printing of values
619
719
  - **tree.hide_vars** : set()
@@ -627,6 +727,32 @@ tree = ivt.Invocation_Tree()
627
727
  - **tree.fontsize** : str
628
728
  - the font size used in the graph, default '14'
629
729
 
730
+ ## Functions ##
731
+
732
+ - **tree.dark_mode(b: bool = None)**
733
+ - set dark mode to 'True' or 'False', or 'None' to toggle.
734
+ - **tree.transparent_background(b: bool = None)**
735
+ - set transparent background to 'True' or 'False', or 'None' to toggle.
736
+
737
+ ## Colors ##
738
+
739
+ For light mode the colors are:
740
+
741
+ - ivt.foreground_color_light
742
+ - ivt.background_color_light
743
+ - ivt.color_paused_light
744
+ - ivt.color_active_light
745
+ - ivt.color_returned_light
746
+
747
+ For dark mode the colors are:
748
+
749
+ - ivt.foreground_color_dark
750
+ - ivt.background_color_dark
751
+ - ivt.color_paused_dark
752
+ - ivt.color_active_dark
753
+ - ivt.color_returned_dark
754
+
755
+
630
756
  # Troubleshooting #
631
757
  - Adobe Acrobat Reader [doesn't refresh a PDF file](https://community.adobe.com/t5/acrobat-reader-discussions/reload-refresh-pdfs/td-p/9632292) when it changes on disk and blocks updates which results in an `Could not open 'tree.pdf' for writing : Permission denied` error. One solution is to install a PDF reader that does refresh ([SumatraPDF](https://www.sumatrapdfreader.org/), [Okular](https://okular.kde.org/), ...) and set it as the default PDF reader. Another solution is to `render()` the graph to a different output format.
632
758
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "invocation_tree"
7
- version = "0.0.34"
7
+ version = "0.0.36"
8
8
  description = "Generates an invocation tree of functions calls."
9
9
  authors = [
10
10
  {name = "Bas Terwijn", email = "bterwijn@gmail.com"}