invocation-tree 0.0.35__tar.gz → 0.0.37__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.35
3
+ Version: 0.0.37
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,12 +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 to scale to application in **production code**.
35
+ The new `@ivt.show` [decorator interface](#decorator) now allows for **scaling** to larger code bases.
36
36
 
37
37
  # Topics #
38
38
 
39
39
  [Iteration and Recursion](#iteration-and-recursion)
40
40
 
41
+ [Divide and Conquer](#divide-and-conquer)
42
+
41
43
  [Permutations](#permutations)
42
44
 
43
45
  [Recursion Benefits](#recursion-benefits)
@@ -48,6 +50,8 @@ The new `@ivt.show` [decorator interface](#decorator) now allows to scale to app
48
50
 
49
51
  [Quick Sort](#quick-sort)
50
52
 
53
+ [Mutability](#mutability)
54
+
51
55
  [Jugs Puzzle](#jugs-puzzle)
52
56
 
53
57
  [Configuration](#Configuration)
@@ -149,17 +153,27 @@ Each node in the invocation tree represents a function call, and the node's colo
149
153
 
150
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.
151
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
+
152
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.
153
165
 
154
- **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, ...]`).
155
168
  ```python
156
- def sum(values):
169
+ def sum_list(values):
157
170
  # <your recursive implementation>
158
171
 
159
- print(sum([3, 7, 4, 9, 2])) # 25
172
+ print(sum_list([3, 7, 4, 9, 2])) # 25
160
173
  ```
161
174
 
162
- **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.
163
177
 
164
178
  ```python
165
179
  def binary(decimal):
@@ -189,7 +203,7 @@ We can use recursion to compute all permutation of a number of elements with rep
189
203
 
190
204
  ![perms_LR3](https://raw.githubusercontent.com/bterwijn/invocation_tree/main/images/perms_LR3.png)
191
205
 
192
- 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:
193
207
 
194
208
  ```python
195
209
  import invocation_tree as ivt
@@ -217,7 +231,7 @@ RRR
217
231
  ![permutations](https://raw.githubusercontent.com/bterwijn/invocation_tree/main/images/permutations.gif)
218
232
  Or see it in the [Invocation Tree Web Debugger](https://invocation-tree.com/#timestep=1.0&play)
219
233
 
220
- 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.
221
235
 
222
236
  We can also iterate over all permutations with replacement using the `product()` function of `iterools` to get the same result:
223
237
 
@@ -266,7 +280,19 @@ Or see it in the [Invocation Tree Web Debugger](https://www.invocation-tree.com/
266
280
 
267
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.
268
282
 
269
- **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
+ ```
270
296
 
271
297
  # Path Planning #
272
298
 
@@ -332,7 +358,8 @@ See it in the [Invocation Tree Web Debugger](https://www.invocation-tree.com/#co
332
358
 
333
359
  Add temporary debug prints wherever behavior isn’t clear. Experiment with what and how you print to maximize clarity.
334
360
 
335
- **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).
336
363
 
337
364
  ```python
338
365
  edges = [('a', 's'), ('i', 'z'), ('c', 'p'), ('d', 'p'), ('d', 'u'), ('b', 'e'), ('b', 'g'),
@@ -393,7 +420,8 @@ print(results)
393
420
  <!-- ![permutations_collect](https://raw.githubusercontent.com/bterwijn/invocation_tree/main/images/permutations_collect.gif) -->
394
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)
395
422
 
396
- **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).
397
425
 
398
426
  Where is the best place in the code to test for `x` to make the program run fast?
399
427
 
@@ -407,9 +435,75 @@ edges = [('a', 's'), ('i', 'z'), ('c', 'p'), ('d', 'p'), ('d', 'u'), ('b', 'e')
407
435
  ```
408
436
  ![graph_big_d_x.png)](https://raw.githubusercontent.com/bterwijn/invocation_tree/main/images/graph_big_d_x.png)
409
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
+
410
504
  # Quick Sort #
411
505
 
412
- 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.
413
507
 
414
508
  ```python
415
509
  import invocation_tree as ivt
@@ -417,7 +511,7 @@ import invocation_tree as ivt
417
511
  def quick_sort(values):
418
512
  if len(values) <= 1:
419
513
  return values
420
- pivot = values[0] # choose arbitrarity the first as pivot
514
+ pivot = values[0] # choose arbitrarily the first as pivot
421
515
  smaller = [x for x in values if x < pivot]
422
516
  equal = [x for x in values if x == pivot]
423
517
  larger = [x for x in values if x > pivot]
@@ -433,10 +527,16 @@ print(' sorted values:',values)
433
527
  unsorted values: [7, 4, 10, 11, 2, 6, 9, 1, 5, 3, 8, 12]
434
528
  sorted values: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
435
529
  ```
436
- <!-- ![quick_sort](https://raw.githubusercontent.com/bterwijn/invocation_tree/main/images/quick_sort.gif) -->
530
+
437
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)
438
532
 
439
- **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`.
440
540
 
441
541
  # Jugs Puzzle #
442
542
 
@@ -494,14 +594,15 @@ Where:
494
594
 
495
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.
496
596
 
497
- **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.
498
599
 
499
600
  - A solution may not have the same jugs state multiple times (this also avoids infinite loops).
500
601
  - It is not necessary to find the shortest path to a goal state (like breadth-first does).
501
602
 
502
- 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.
503
604
 
504
- **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.
505
606
 
506
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.
507
608
 
@@ -514,7 +615,7 @@ The Invocation Tree Web Debugger gives examples of the [most important configura
514
615
 
515
616
 
516
617
  ## Hidding ##
517
- 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:
518
619
 
519
620
  ```python
520
621
  tree = ivt.blocking()
@@ -544,28 +645,29 @@ tree = ivt.blocking()
544
645
  tree.ignore_calls.add(r're:namespace\..*')
545
646
  ```
546
647
 
547
- ignores all function of `namespace`.
648
+ ignores all functions starting with `namespace.`.
548
649
 
549
650
  ## Decorator ##
550
651
 
551
- 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` 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.
552
653
 
553
654
  ```python
554
655
  import invocation_tree as ivt
555
656
  ivt.decorator_tree = ivt.blocking() # set tree used by decorator
556
- #ivt.decorator_tree = ivt.blocking_each_change() # block at each change, but much slower
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
557
659
 
558
- @ivt.show # use decorator to select which functions to graph
660
+ @ivt.show # use this decorator to select which functions to graph
559
661
  def permutations(elements, perm, n):
560
662
  if n == 0:
561
663
  print(perm)
562
664
  else:
563
665
  for element in elements:
564
- perm.append(element)
666
+ perm.append(element) # do action that mutates
565
667
  permutations(elements, perm, n-1)
566
- perm.pop()
668
+ perm.pop() # undo action
567
669
 
568
- permutations( 'LR', [], 3) # all permutations of L and R of length 3
670
+ permutations('LR', [], 3) # all permutations of L and R of length 3
569
671
  ```
570
672
 
571
673
  ## Blocking ##
@@ -612,12 +714,6 @@ tree = ivt.Invocation_Tree()
612
714
  - if `>=0` the out filename is numbered for animated gif making
613
715
  - **tree.indent** : string
614
716
  - the string used for identing the local variables
615
- - **tree.color_active** : string
616
- - HTML color for active function
617
- - **tree.color_paused*** : string
618
- - HTML color for paused functions
619
- - **tree.color_returned***: string
620
- - HTML color for returned functions
621
717
  - **tree.to_string** : dict[str, fun]
622
718
  - mapping from type/name/id to a to_string() function for custom printing of values
623
719
  - **tree.hide_vars** : set()
@@ -631,6 +727,32 @@ tree = ivt.Invocation_Tree()
631
727
  - **tree.fontsize** : str
632
728
  - the font size used in the graph, default '14'
633
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
+
634
756
  # Troubleshooting #
635
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.
636
758