├── LICENSE.txt
├── MANIFEST.in
├── README.md
├── images
├── add_one.png
├── add_one.py
├── avltree.py
├── avltree_dir.png
├── avltree_fail.png
├── avltree_key_value.png
├── avltree_leaf.png
├── avltree_linear.png
├── avltree_table.png
├── bin_tree.png
├── bin_tree.py
├── colab_example.png
├── copies.png
├── copies.py
├── copy_method.png
├── copy_method.py
├── create_gif.sh
├── create_images.sh
├── debug_vscode.png
├── debugging.gif
├── debugging.py
├── extension_numpy.png
├── extension_numpy.py
├── extension_pandas.png
├── extension_pandas.py
├── factorial.gif
├── factorial.py
├── hash_set.png
├── hash_set.py
├── hidden_edges.png
├── highlight.png
├── highlight.py
├── immutable.py
├── immutable1.png
├── immutable2.png
├── introspect_depth.png
├── introspect_depth.py
├── ipython.png
├── jupyter_example.png
├── linked_list.png
├── linked_list.py
├── many_types.png
├── many_types.py
├── mutable.py
├── mutable1.png
├── mutable2.png
├── name_rebinding.py
├── not_node_types.py
├── not_node_types1.png
├── not_node_types2.png
├── power_set.gif
├── power_set.py
├── pyodide.png
├── rebinding1.png
├── rebinding2.png
├── uva.png
└── vscode_copying.gif
├── install.txt
├── memory_graph
├── __init__.py
├── call_stack.py
├── config.py
├── config_default.py
├── config_helpers.py
├── extension_numpy.py
├── extension_pandas.py
├── html_table.py
├── list_view.py
├── memory_to_nodes.py
├── node_base.py
├── node_key_value.py
├── node_leaf.py
├── node_linear.py
├── node_table.py
├── sequence.py
├── slicer.py
├── slices.py
├── slices_iterator.py
├── slices_table_iterator.py
├── test.py
├── test_max_graph_depth.py
├── test_memory_graph.py
├── test_memory_to_nodes.py
├── test_sequence.py
├── test_slicer.py
├── test_slices.py
├── test_slices_iterator.py
└── utils.py
├── pyproject.toml
├── src
├── auto_memory_graph.py
├── colab_example.ipynb
├── jupyter_example.ipynb
└── pyodide.html
└── uml
└── memory_graph.uxf
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | BSD 2-Clause License
2 |
3 | Copyright (c) 2017, pyexample
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions are met:
8 |
9 | * Redistributions of source code must retain the above copyright notice, this
10 | list of conditions and the following disclaimer.
11 |
12 | * Redistributions in binary form must reproduce the above copyright notice,
13 | this list of conditions and the following disclaimer in the documentation
14 | and/or other materials provided with the distribution.
15 |
16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | recursive-include images/ *
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Installation #
2 | Install (or upgrade) `memory_graph` using pip:
3 | ```
4 | pip install --upgrade memory_graph
5 | ```
6 | Additionally [Graphviz](https://graphviz.org/download/) needs to be installed.
7 |
8 | # Highlight #
9 | 
10 |
11 | # Videos #
12 | | [](https://www.youtube.com/watch?v=23_bHcr7hqo) | [](https://www.youtube.com/watch?v=pvIJgHCaXhU) |
13 | |:--:|:--:|
14 | | [Quick Intro (3:49)](https://www.youtube.com/watch?v=23_bHcr7hqo) | [Mutability (17:29)](https://www.youtube.com/watch?v=pvIJgHCaXhU) |
15 |
16 | # Memory Graph #
17 | For program understanding and debugging, the [memory_graph](https://pypi.org/project/memory-graph/) package can visualize your data, supporting many different data types, including but not limited to:
18 |
19 | ```python
20 | import memory_graph as mg
21 |
22 | class My_Class:
23 |
24 | def __init__(self, x, y):
25 | self.x = x
26 | self.y = y
27 |
28 | data = [ range(1, 2), (3, 4), {5, 6}, {7:'seven', 8:'eight'}, My_Class(9, 10) ]
29 | mg.show(data)
30 | ```
31 | 
32 |
33 | Instead of showing the graph on screen you can also render it to an output file (see [Graphviz Output Formats](https://graphviz.org/docs/outputs/)) using for example:
34 |
35 | ```python
36 | mg.render(data, "my_graph.pdf")
37 | mg.render(data, "my_graph.svg")
38 | mg.render(data, "my_graph.png")
39 | mg.render(data, "my_graph.gv") # Graphviz DOT file
40 | mg.render(data) # renders to 'mg.render_filename' with default value: 'memory_graph.pdf'
41 | ```
42 |
43 | # Sharing Values #
44 | In Python, assigning the list from variable `a` to variable `b` causes both variables to reference the same list value and thus share it. Consequently, any change applied through one variable will impact the other. This behavior can lead to elusive bugs if a programmer incorrectly assumes that list `a` and `b` are independent.
45 |
46 |
47 |
48 | ```python
49 | import memory_graph as mg
50 |
51 | # create the lists 'a' and 'b'
52 | a = [4, 3, 2]
53 | b = a
54 | b.append(1) # changing 'b' changes 'a'
55 |
56 | # print the 'a' and 'b' list
57 | print('a:', a)
58 | print('b:', b)
59 |
60 | # check if 'a' and 'b' share data
61 | print('ids:', id(a), id(b))
62 | print('identical?:', a is b)
63 |
64 | # show all local variables in a graph
65 | mg.show( locals() )
66 | ```
67 |
68 |
69 |
70 | 
71 |
72 | a graph showing `a` and `b` share data
73 |
74 |
75 |
76 | The fact that `a` and `b` share data can not be verified by printing the lists. It can be verified by comparing the identity of both variables using the `id()` function or by using the `is` comparison operator as shown in the program output below, but this quickly becomes impractical for larger programs.
77 | ```{verbatim}
78 | a: 4, 3, 2, 1
79 | b: 4, 3, 2, 1
80 | ids: 126432214913216 126432214913216
81 | identical?: True
82 | ```
83 | A better way to understand what data is shared is to draw a graph of the data using the [memory_graph](https://pypi.org/project/memory-graph/) package.
84 |
85 | # Chapters #
86 |
87 | [Python Data Model](#python-data-model)
88 |
89 | [Call Stack](#call-stack)
90 |
91 | [Global Import Trick](#global-import-trick)
92 |
93 | [Debugging](#debugging)
94 |
95 | [Data Structure Examples](#data-structure-examples)
96 |
97 | [Configuration](#configuration)
98 |
99 | [Extensions](#extensions)
100 |
101 | [Introspection](#introspection)
102 |
103 | [Graph Depth](#graph-depth)
104 |
105 | [Jupyter Notebook](#jupyter-notebook)
106 |
107 | [ipython](#ipython)
108 |
109 | [Google Colab](#google-colab)
110 |
111 | [In the Browser](#in-the-browser)
112 |
113 | [Animated GIF](#animated-gif)
114 |
115 | [Troubleshooting](#troubleshooting)
116 |
117 |
118 | ## Author ##
119 | Bas Terwijn
120 |
121 | ## Inspiration ##
122 | Inspired by [Python Tutor](https://pythontutor.com/).
123 |
124 | ## Supported by ##
125 |
126 |
127 | ___
128 | ___
129 |
130 | # Python Data Model #
131 | The [Python Data Model](https://docs.python.org/3/reference/datamodel.html) makes a distiction between immutable and mutable types:
132 |
133 | * **immutable**: bool, int, float, complex, str, tuple, bytes, frozenset
134 | * **mutable**: list, set, dict, classes, ... (most other types)
135 |
136 |
137 | ## Immutable Type ##
138 | In the code below variable `a` and `b` both reference the same tuple value (4, 3, 2). A tuple is an immutable type and therefore when we change variable `b` its value **cannot** be mutated in place, and thus an automatic copy is made and `a` and `b` reference a different value afterwards.
139 |
140 | ```python
141 | import memory_graph as mg
142 |
143 | a = (4, 3, 2)
144 | b = a
145 | mg.render(locals(), 'immutable1.png')
146 |
147 | b += (1,)
148 | mg.render(locals(), 'immutable2.png')
149 | ```
150 | |  |  |
151 | |:-----------------------------------------------------------:|:-------------------------------------------------------------:|
152 | | immutable1.png | immutable2.png |
153 |
154 |
155 | ## Mutable Type ##
156 | With mutable types the result is different. In the code below variable `a` and `b` both reference the same `list` value [4, 3, 2]. A `list` is a mutable type and therefore when we change variable `b` its value **can** be mutated in place and thus `a` and `b` both reference the same new value afterwards. Thus changing `b` also changes `a` and vice versa. Sometimes we want this but other times we don't and then we will have to make a copy ourselfs so that `a` and `b` are independent.
157 |
158 | ```python
159 | import memory_graph as mg
160 |
161 | a = [4, 3, 2]
162 | b = a
163 | mg.render(locals(), 'mutable1.png')
164 |
165 | b += [1] # equivalent to: b.append(1)
166 | mg.render(locals(), 'mutable2.png')
167 | ```
168 | |  |  |
169 | |:-----------------------------------------------------------:|:-------------------------------------------------------------:|
170 | | mutable1.png | mutable2.png |
171 |
172 | One practical reason why Python makes the distinction between mutable and immutable types is that a value of a mutable type can be large, making it inefficient to copy each time we change it. Immutable values generally don't need to change as much, or are small making copying less of a concern.
173 |
174 | ## Copying ##
175 | Python offers three different "copy" options that we will demonstrate using a nested list:
176 |
177 | ```python
178 | import memory_graph as mg
179 | import copy
180 |
181 | a = [ [1, 2], ['x', 'y'] ] # a nested list (a list containing lists)
182 |
183 | # three different ways to make a "copy" of 'a':
184 | c1 = a
185 | c2 = copy.copy(a) # equivalent to: a.copy() a[:] list(a)
186 | c3 = copy.deepcopy(a)
187 |
188 | mg.show(locals())
189 | ```
190 |
191 | * `c1` is an **assignment**, nothing is copied, all the values are shared
192 | * `c2` is a **shallow copy**, only the value referenced by the first reference is copied, all the underlying values are shared
193 | * `c3` is a **deep copy**, all the values are copied, nothing is shared
194 |
195 | 
196 |
197 |
198 | ## Custom Copy ##
199 | We can write our own custom copy function or method in case the three standard "copy" options don't do what we want. For example, in the code below the copy() method of My_Class copies the `digits` but shares the `letters` between two objects.
200 |
201 | ```python
202 | import memory_graph as mg
203 | import copy
204 |
205 | class My_Class:
206 |
207 | def __init__(self):
208 | self.digits = [1, 2]
209 | self.letters = ['x', 'y']
210 |
211 | def custom_copy(self):
212 | """ Copies 'digits' but shares 'letters'. """
213 | c = copy.copy(self)
214 | c.digits = copy.copy(self.digits)
215 | return c
216 |
217 | a = My_Class()
218 | b = a.custom_copy()
219 |
220 | mg.show(locals())
221 | ```
222 | 
223 |
224 | ## Name Rebinding ##
225 | When `a` and `b` share a mutable value, then changing the value of `b` changes the value of `a` and vice versa. However, reassigning `b` does not change `a`. When you reassign `b`, you only rebind the name `b` to a new value without effecting any other variables.
226 |
227 | ```python
228 | import memory_graph as mg
229 |
230 | a = [4, 3, 2]
231 | b = a
232 | mg.render(locals(), 'rebinding1.png')
233 |
234 | b += [1] # changes the value of 'b' and 'a'
235 | b = [100, 200] # rebinds 'b' to a new value, 'a' is uneffected
236 | mg.render(locals(), 'rebinding2.png')
237 | ```
238 | |  |  |
239 | |:-----------------------------------------------------------:|:-------------------------------------------------------------:|
240 | | rebinding1.png | rebinding2.png |
241 |
242 | # Call Stack #
243 | The `mg.stack()` function retrieves the entire call stack, including the local variables for each function on the stack. This enables us to visualize the local variables across all active functions simultaneously. By examining the graph, we can determine whether any local variables from different functions share data. For instance, consider the function `add_one()` which adds the value `1` to each of its parameters `a`, `b`, and `c`.
244 |
245 | ```python
246 | import memory_graph as mg
247 |
248 | def add_one(a, b, c):
249 | a += [1]
250 | b += (1,)
251 | c += [1]
252 | mg.show(mg.stack())
253 |
254 | a = [4, 3, 2]
255 | b = (4, 3, 2)
256 | c = [4, 3, 2]
257 |
258 | add_one(a, b, c.copy())
259 | print(f"a:{a} b:{b} c:{c}")
260 | ```
261 | 
262 |
263 | In the printed output only `a` is changed as a result of the function call:
264 | ```
265 | a:[4, 3, 2, 1] b:(4, 3, 2) c:[4, 3, 2]
266 | ```
267 |
268 | This is because `b` is of immutable type 'tuple' so its value gets copied automatically when it is changed. And because the function is called with a copy of `c`, its original value is not changed by the function. The value of variable `a` is the only value of mutable type that is shared between the root stack frame **'0: \'** and the **'1: add_one'** stack frame of the function so only that variable is affected as a result of the function call. The other changes remain confined to the local variables of the ```add_one()``` function.
269 |
270 | ## Block ##
271 | It is often helpful to temporarily block program execution to inspect the graph. For this we can use the `mg.block()` function:
272 |
273 | ```python
274 | mg.block(fun, arg1, arg2, ...)
275 | ```
276 |
277 | This function:
278 | * first executes `fun(arg1, arg2, ...)`
279 | * then prints the current source location in the program
280 | * then blocks execution until the <Enter> key is pressed
281 | * finally returns the value of the `fun()` call
282 |
283 | To change its behavior:
284 | * Set `mg.block_prints_location = False` to skip printing the source location.
285 | * Set `mg.press_enter_message = None` to skip printing "Press <Enter> to continue...".
286 |
287 | ## Recursion ##
288 | The call stack is also helpful to visualize how recursion works. Here we use `mg.block()` to show each step of how recursively ```factorial(3)``` is computed:
289 |
290 | ```python
291 | import memory_graph as mg
292 |
293 | def factorial(n):
294 | if n==0:
295 | return 1
296 | mg.block(mg.show, mg.stack())
297 | result = n * factorial(n-1)
298 | mg.block(mg.show, mg.stack())
299 | return result
300 |
301 | print(factorial(3))
302 | ```
303 |
304 | 
305 |
306 | and the result is: 1 x 2 x 3 = 6
307 |
308 | ## Power Set ##
309 | A more interesting recursive example that shows sharing of data is power_set(). A power set is the set of all subsets of a collection of values.
310 |
311 | ```python
312 | import memory_graph as mg
313 |
314 | def get_subsets(subsets, data, i, subset):
315 | mg.block(mg.show, mg.stack())
316 | if i == len(data):
317 | subsets.append(subset.copy())
318 | return
319 | subset.append(data[i])
320 | get_subsets(subsets, data, i+1, subset) # do include data[i]
321 | subset.pop()
322 | get_subsets(subsets, data, i+1, subset) # don't include data[i]
323 | mg.block(mg.show, mg.stack())
324 |
325 | def power_set(data):
326 | subsets = []
327 | get_subsets(subsets, data, 0, [])
328 | return subsets
329 |
330 | print( power_set(['a', 'b', 'c']) )
331 | ```
332 |
333 | 
334 | ```
335 | [['a', 'b', 'c'], ['a', 'b'], ['a', 'c'], ['a'], ['b', 'c'], ['b'], ['c'], []]
336 | ```
337 |
338 | # Debugging #
339 |
340 | For the best debugging experience with memory_graph set for example expression:
341 | ```
342 | mg.render(locals(), "my_graph.pdf")
343 | ```
344 | as a *watch* in a debugger tool such as the integrated debugger in Visual Studio Code. Then open the "my_graph.pdf" output file to continuously see all the local variables while debugging. This avoids having to add any memory_graph `show()` or `render()` calls to your code.
345 |
346 | ## Call Stack in Watch Context ##
347 | The ```mg.stack()``` doesn't work well in *watch* context in most debuggers because debuggers introduce additional stack frames that cause problems. Use these alternative functions for various debuggers to filter out these problematic stack frames:
348 |
349 | | debugger | function to get the call stack |
350 | |:---|:---|
351 | | [pdb](https://docs.python.org/3/library/pdb.html), [pudb](https://pypi.org/project/pudb/) | `mg.stack_pdb()` |
352 | | [Visual Studio Code](https://code.visualstudio.com/docs/languages/python) | `mg.stack_vscode()` |
353 | | [Cursor AI](https://www.cursor.com/) | `mg.stack_cursor()` |
354 | | [PyCharm](https://www.jetbrains.com/pycharm/) | `mg.stack_pycharm()` |
355 | | [Wing](https://wingware.com/) | `mg.stack_wing()` |
356 |
357 | 
358 |
359 | ## Other Debuggers ##
360 | For other debuggers, invoke this function within the *watch* context. Then, in the "call_stack.txt" file, identify the slice of functions you wish to include as stack frames in the call stack.
361 | ```
362 | mg.save_call_stack("call_stack.txt")
363 | ```
364 | Choose the list of `after_functions` after any of which the slice start. Then choose the `through_function` at which the slice ends. The optional `drop` argument can be used to drop a number of stack frames at the start:
365 | ```
366 | mg.stack_after_through(after_functions : list[str],
367 | through_function : str = "",
368 | drop : int = 0)
369 | ```
370 |
371 | ## Debugging without Debugger Tool ##
372 |
373 | To simplify debugging without a debugger tool, we offer these alias functions that you can insert into your code at the point where you want to visualize a graph:
374 |
375 | | alias | purpose | function call |
376 | |:---|:---|:---|
377 | | `mg.sl()` | **s**how **l**ocal variables | `mg.show(locals())` |
378 | | `mg.ss()` | **s**how the call **s**tack | `mg.show(mg.stack())` |
379 | | `mg.bsl()` | **b**lock after **s**howing **l**ocal variables | `mg.block(mg.show, locals())` |
380 | | `mg.bss()` | **b**lock after **s**howing the call **s**tack | `mg.block(mg.show, mg.stack())` |
381 | | `mg.rl()` | **r**ender **l**ocal variables | `mg.render(locals())` |
382 | | `mg.rs()` | **r**ender the call **s**tack | `mg.render(mg.stack())` |
383 | | `mg.brl()` | **b**lock after **r**endering **l**ocal variables | `mg.block(mg.render, locals())` |
384 | | `mg.brs()` | **b**lock after **r**endering the call **s**tack | `mg.block(mg.render, mg.stack())` |
385 | | `mg.l()` | same as `mg.bsl()` | |
386 | | `mg.s()` | same as `mg.bss()` | |
387 |
388 | For example, executing this program:
389 |
390 | ```python
391 | from memory_graph as mg
392 |
393 | squares = []
394 | squares_collector = []
395 | for i in range(1, 6):
396 | squares.append(i**2)
397 | squares_collector.append(squares.copy())
398 | mg.l() # block after showing local variables
399 | ```
400 | and pressing <Enter> a number of times, results in:
401 |
402 | 
403 |
404 | # Data Structure Examples #
405 | Package memory_graph can be very useful in a data structures course, some examples:
406 |
407 | ## Circular Doubly Linked List ##
408 | ```python
409 | import memory_graph as mg
410 | import random
411 | random.seed(0) # use same random numbers each run
412 |
413 | class Linked_List:
414 | """ Circular doubly linked list """
415 |
416 | def __init__(self, value=None,
417 | prev=None, next=None):
418 | self.prev = prev if prev else self
419 | self.value = value
420 | self.next = next if next else self
421 |
422 | def add_back(self, value):
423 | if self.value == None:
424 | self.value = value
425 | else:
426 | new_node = Linked_List(value,
427 | prev=self.prev,
428 | next=self)
429 | self.prev.next = new_node
430 | self.prev = new_node
431 |
432 | linked_list = Linked_List()
433 | n = 100
434 | for i in range(n):
435 | value = random.randrange(n)
436 | linked_list.add_back(value)
437 | mg.block(mg.show, locals()) # <--- draw locals
438 | ```
439 | 
440 |
441 | ## Binary Tree ##
442 | ```python
443 | import memory_graph as mg
444 | import random
445 | random.seed(0) # use same random numbers each run
446 |
447 | class BinTree:
448 |
449 | def __init__(self, value=None, smaller=None, larger=None):
450 | self.smaller = smaller
451 | self.value = value
452 | self.larger = larger
453 |
454 | def add(self, value):
455 | if self.value is None:
456 | self.value = value
457 | elif value < self.value:
458 | if self.smaller is None:
459 | self.smaller = BinTree(value)
460 | else:
461 | self.smaller.add(value)
462 | else:
463 | if self.larger is None:
464 | self.larger = BinTree(value)
465 | else:
466 | self.larger.add(value)
467 | mg.block(mg.show, mg.stack()) # <--- draw stack
468 |
469 | tree = BinTree()
470 | n = 100
471 | for i in range(n):
472 | value = random.randrange(n)
473 | tree.add(value)
474 | ```
475 | 
476 |
477 | ## Hash Set ##
478 | ```python
479 | import memory_graph as mg
480 | import random
481 | random.seed(0) # use same random numbers each run
482 |
483 | class HashSet:
484 |
485 | def __init__(self, capacity=15):
486 | self.buckets = [None] * capacity
487 |
488 | def add(self, value):
489 | index = hash(value) % len(self.buckets)
490 | if self.buckets[index] is None:
491 | self.buckets[index] = []
492 | bucket = self.buckets[index]
493 | bucket.append(value)
494 | mg.block(mg.show, locals()) # <--- draw locals
495 |
496 | def contains(self, value):
497 | index = hash(value) % len(self.buckets)
498 | if self.buckets[index] is None:
499 | return False
500 | return value in self.buckets[index]
501 |
502 | def remove(self, value):
503 | index = hash(value) % len(self.buckets)
504 | if self.buckets[index] is not None:
505 | self.buckets[index].remove(value)
506 |
507 | hash_set = HashSet()
508 | n = 100
509 | for i in range(n):
510 | new_value = random.randrange(n)
511 | hash_set.add(new_value)
512 | ```
513 | 
514 |
515 |
516 | # Configuration #
517 | Different aspects of memory_graph can be configured. The default configuration is reset by importing 'memory_graph.config_default'.
518 |
519 | - ***mg.config.max_string_length*** : int
520 | - The maximum length of strings shown in the graph. Longer strings will be truncated.
521 |
522 | - ***mg.config.not_node_types*** : set[type]
523 | - Holds all types for which no seperate node is drawn but that instead are shown as elements in their parent Node.
524 |
525 | - ***mg.config.no_child_references_types*** : set[type]
526 | - The set of key_value types that don't draw references to their direct childeren but have their children shown as elements of their node.
527 |
528 | - ***mg.config.type_to_node*** : dict[type, fun(data) -> Node]
529 | - Determines how a data types is converted to a Node (sub)class for visualization in the graph.
530 |
531 | - ***mg.config.type_to_color*** : dict[type, color]
532 | - Maps a type to the [graphviz color](https://graphviz.org/doc/info/colors.html) it gets in the graph.
533 |
534 | - ***mg.config.type_to_vertical_orientation*** : dict[type, bool]
535 | - Maps a type to its orientation. Use 'True' for vertical and 'False' for horizontal. If not specified Node_Linear and Node_Key_Value are vertical unless they have references to children.
536 |
537 | - ***mg.config.type_to_slicer*** : dict[type, int]
538 | - Maps a type to a Slicer. A slicer determines how many elements of a data type are shown in the graph to prevent the graph from getting too big. 'Slicer()' does no slicing, 'Slicer(1,2,3)' shows just 1 element at the beginning, 2 in the middle, and 3 at the end.
539 |
540 | - ***mg.config.max_graph_depth*** : int
541 | - The maxium depth of the graph with default value 12.
542 |
543 | - ***config.graph_cut_symbol*** : str
544 | - The symbol indicating where the graph is cut short with default `✂`.
545 |
546 | - ***mg.config.type_to_depth*** : dict[type, int]
547 | - Maps a type to graph depth to limit the graph size.
548 |
549 | - ***max_missing_edges*** : int
550 | - Maximum number of missing edges that are shown with default value 2. Dashed references are used to indicate that there are more references to a node than are shown.
551 |
552 |
553 | ## Simplified Graph ##
554 | Memory_graph simplifies the visualization (and the viewer's mental model) by **not** showing separate nodes for immutable types like `bool`, `int`, `float`, `complex`, and `str` by default. This simplification can sometimes be slightly misleading. As in the example below, after a shallow copy, lists `a` and `b` technically share their `int` values, but the graph makes it appear as though `a` and `b` each have their own copies. However, since `int` is immutable, this simplification will never lead to unexpected changes (changing `a` won’t affect `b`) so will never result in bugs.
555 |
556 | The simplification strikes a balance: it is slightly misleading but keeps the graph clean and easy to understand and focuses on the mutable types where unexpected changes can occur. This is why it is the default behavior. If you do want to show separate nodes for `int` values, such as for educational purposes, you can simply remove `int` from the `mg.config.not_node_types` set:
557 | ```python
558 | import memory_graph as mg
559 |
560 | a = [100, 200, 300]
561 | b = a.copy()
562 | mg.render(locals(), 'not_node_types1.png')
563 |
564 | mg.config.not_node_types.remove(int) # now show separate nodes for int values
565 |
566 | mg.render(locals(), 'not_node_types2.png')
567 | ```
568 | |  |  |
569 | |:-----------------------------------------------------------:|:-------------------------------------------------------------:|
570 | | not_node_types1.png — simplified | not_node_types2.png — technically correct |
571 |
572 | Additionally, the simplification hides away the [reuse of small int values \[-5, 256\]](https://docs.python.org/3/c-api/long.html#c.PyLong_FromLong) in the current CPython implementation, an optimization that might otherwise confuse beginner Python programmers. For instance, after executing `a[1]+=1; b[1]+=1` the `201` value is, maybe surprisingly, still shared between `a` and `b`, whereas executing `a[2]+=1; b[2]+=1` does not result in sharing the `301` value.
573 |
574 | ## Temporary Configuration ##
575 | In addition to the global configuration, a temporary configuration can be set for a single `show()` or `render()` call to change the colors, orientation, and slicer. This example highlights a particular list element in red, gives it a horizontal orientation, and overwrites the default slicer for lists:
576 |
577 | ```python
578 | import memory_graph as mg
579 | from memory_graph.slicer import Slicer
580 |
581 | data = [ list(range(20)) for i in range(1,5)]
582 | highlight = data[2]
583 |
584 | mg.show( locals(),
585 | colors = {id(highlight): "red" }, # set color to red
586 | vertical_orientations = {id(highlight): False }, # set horizontal orientation
587 | slicers = {id(highlight): Slicer()} # set no slicing
588 | )
589 | ```
590 | 
591 |
592 | # Extensions #
593 | Different extensions are available for types from other Python packages.
594 |
595 | ## Numpy ##
596 | Numpy types `array` and `matrix` and `ndarray` can be graphed with "memory_graph.extension_numpy":
597 |
598 | ```python
599 | import memory_graph as mg
600 | import numpy as np
601 | import memory_graph.extension_numpy
602 | np.random.seed(0) # use same random numbers each run
603 |
604 | array = np.array([1.1, 2, 3, 4, 5])
605 | matrix = np.matrix([[i*20+j for j in range(20)] for i in range(20)])
606 | ndarray = np.random.rand(20,20)
607 | mg.show(locals())
608 | ```
609 | 
610 |
611 | ## Pandas ##
612 | Pandas types `Series` and `DataFrame` can be graphed with "memory_graph.extension_pandas":
613 |
614 | ```python
615 | import memory_graph as mg
616 | import pandas as pd
617 | import memory_graph.extension_pandas
618 |
619 | series = pd.Series( [i for i in range(20)] )
620 | dataframe1 = pd.DataFrame({ "calories": [420, 380, 390],
621 | "duration": [50, 40, 45] })
622 | dataframe2 = pd.DataFrame({ 'Name' : [ 'Tom', 'Anna', 'Steve', 'Lisa'],
623 | 'Age' : [ 28, 34, 29, 42],
624 | 'Length' : [ 1.70, 1.66, 1.82, 1.73] },
625 | index=['one', 'two', 'three', 'four']) # with row names
626 | mg.show(locals())
627 | ```
628 | 
629 |
630 | # Introspection #
631 | This section is likely to change. Sometimes the introspection fails or is not as desired. For example the `bintrees.avltree.Node` object doesn't show any attributes in the graph below.
632 |
633 | ```python
634 | import memory_graph as mg
635 | import bintrees
636 |
637 | # Create an AVL tree
638 | tree = bintrees.AVLTree()
639 | tree.insert(10, "ten")
640 | tree.insert(5, "five")
641 | tree.insert(20, "twenty")
642 | tree.insert(15, "fifteen")
643 |
644 | mg.show(locals())
645 | ```
646 | 
647 |
648 |
649 | ## All attributes using dir() ##
650 | A useful start is to give it some color, show the list of all its attributes using `dir()`, and setting an empty Slicer to see the attribute list in full.
651 |
652 | ```python
653 | import memory_graph as mg
654 | import bintrees
655 |
656 | # Create an AVL tree
657 | tree = bintrees.AVLTree()
658 | tree.insert(10, "ten")
659 | tree.insert(5, "five")
660 | tree.insert(20, "twenty")
661 | tree.insert(15, "fifteen")
662 |
663 | mg.config.type_to_color[bintrees.avltree.Node] = "sandybrown"
664 | mg.config.type_to_node[bintrees.avltree.Node] = lambda data: mg.node_linear.Node_Linear(data,
665 | dir(data))
666 | mg.config.type_to_slicer[bintrees.avltree.Node] = mg.slicer.Slicer()
667 |
668 | mg.show(locals())
669 | ```
670 | 
671 |
672 | Next figure out what are the attributes you want to graph and choose a Node type, there are four options:
673 |
674 | ## 1) Node_Leaf ##
675 | Node_Leaf is a node with no children and shows just a single value.
676 | ```python
677 | import memory_graph as mg
678 | import bintrees
679 |
680 | # Create an AVL tree
681 | tree = bintrees.AVLTree()
682 | tree.insert(10, "ten")
683 | tree.insert(5, "five")
684 | tree.insert(20, "twenty")
685 | tree.insert(15, "fifteen")
686 |
687 | mg.config.type_to_color[bintrees.avltree.Node] = "sandybrown"
688 | mg.config.type_to_node[bintrees.avltree.Node] = lambda data: mg.node_leaf.Node_Leaf(data,
689 | f"key:{data.key} value:{data.value}")
690 |
691 | mg.show(locals())
692 | ```
693 | 
694 |
695 | ## 2) Node_Linear ##
696 | Node_Linear shows multiple values in a line like a list.
697 | ```python
698 | import memory_graph as mg
699 | import bintrees
700 |
701 | # Create an AVL tree
702 | tree = bintrees.AVLTree()
703 | tree.insert(10, "ten")
704 | tree.insert(5, "five")
705 | tree.insert(20, "twenty")
706 | tree.insert(15, "fifteen")
707 |
708 | mg.config.type_to_color[bintrees.avltree.Node] = "sandybrown"
709 | mg.config.type_to_node[bintrees.avltree.Node] = lambda data: mg.node_linear.Node_Linear(data,
710 | ['left:', data.left,
711 | 'key:', data.key,
712 | 'value:', data.value,
713 | 'right:', data.right] )
714 |
715 | mg.show(locals())
716 | ```
717 | 
718 |
719 | ## 3) Node_Key_Value ##
720 | Node_Key_Value shows key-value pairs like a dictionary. Note the required `items()` call at the end.
721 | ```python
722 | import memory_graph as mg
723 | import bintrees
724 |
725 | # Create an AVL tree
726 | tree = bintrees.AVLTree()
727 | tree.insert(10, "ten")
728 | tree.insert(5, "five")
729 | tree.insert(20, "twenty")
730 | tree.insert(15, "fifteen")
731 |
732 | mg.config.type_to_color[bintrees.avltree.Node] = "sandybrown"
733 | mg.config.type_to_node[bintrees.avltree.Node] = lambda data: mg.node_key_value.Node_Key_Value(data,
734 | {'left': data.left,
735 | 'key': data.key,
736 | 'value': data.value,
737 | 'right': data.right}.items() )
738 |
739 | mg.show(locals())
740 | ```
741 | 
742 |
743 | ## 4) Node_Table ##
744 | Node_Table shows all the values as a table.
745 | ```python
746 | import memory_graph as mg
747 | import bintrees
748 |
749 | # Create an AVL tree
750 | tree = bintrees.AVLTree()
751 | tree.insert(10, "ten")
752 | tree.insert(5, "five")
753 | tree.insert(20, "twenty")
754 | tree.insert(15, "fifteen")
755 |
756 | mg.config.type_to_color[bintrees.avltree.Node] = "sandybrown"
757 | mg.config.type_to_node[bintrees.avltree.Node] = lambda data: mg.node_table.Node_Table(data,
758 | [[data.key, data.value],
759 | [data.left, data.right]] )
760 |
761 |
762 | mg.show(locals())
763 | ```
764 | 
765 |
766 |
767 | # Graph Depth #
768 | To limit the size of the graph the maximum depth of the graph is set by `mg.config.max_graph_depth`. Additionally for each type a depth can be set to further limit the graph, as is done for type `B` in the example below. Scissors indicate where the graph is cut short. Alternatively the `id()` of a data elements can be used to limit the graph for that specific element, as is done for the value referenced by variable `c`.
769 |
770 | The value of variable `x` is shown as it is at depth 1 from the root of the graph, but as it can also be reached via `b2`, that path need to be shown as well to avoid confusion, so this overwrites the depth limit set for type `B`.
771 |
772 | ```python
773 | import memory_graph as mg
774 |
775 | class Base:
776 |
777 | def __init__(self, n):
778 | self.elements = [1]
779 | iter = self.elements
780 | for i in range(2,n):
781 | iter.append([i])
782 | iter = iter[-1]
783 |
784 | def get_last(self):
785 | iter = self.elements
786 | while len(iter)>1:
787 | iter = iter[-1]
788 | return iter
789 |
790 | class A(Base):
791 |
792 | def __init__(self, n):
793 | super().__init__(n)
794 |
795 | class B(Base):
796 |
797 | def __init__(self, n):
798 | super().__init__(n)
799 |
800 | class C(Base):
801 |
802 | def __init__(self, n):
803 | super().__init__(n)
804 |
805 | a = A(6)
806 | b1 = B(6)
807 | b2 = B(6)
808 | c = C(6)
809 |
810 | x = ['x']
811 | b2.get_last().append(x)
812 |
813 | mg.config.type_to_depth[B] = 3
814 | mg.config.type_to_depth[id(c)] = 2
815 | mg.show(locals())
816 | ```
817 | 
818 |
819 | ## Hidden Edges ##
820 |
821 | As the value of `x` is shown in the graph, we would want to show all the references to it, but the default list Slicer hides references by slicing the list to keep the graph small. The `max_missing_edges` variable then determines how many additional hidden references to `x` we show. If there are more references then we show, then theses hidden references are shown with dashed lines to indicate some references are left out.
822 |
823 | ```python
824 | import memory_graph as mg
825 |
826 | data = []
827 | x = ['x']
828 | for i in range(20):
829 | data.append(x)
830 |
831 | mg.show(locals())
832 | ```
833 | 
834 |
835 | # Jupyter Notebook #
836 | In Jupyter Notebook `locals()` has additional variables that cause problems in the graph, use `mg.locals_jupyter()` to get the local variables with these problematic variables filtered out. Use `mg.stack_jupyter()` to get the whole call stack with these variables filtered out.
837 |
838 | We can use `mg.show()` and `mg.render()` in a Jupyter Notebook, but alternatively we can also use `mg.create_graph()` to create a graph and the `display()` function to render it inline with for example:
839 |
840 | ```python
841 | display( mg.create_graph(mg.locals_jupyter()) ) # display the local variables inline
842 | mg.block(display, mg.create_graph(mg.locals_jupyter()) ) # the same but blocked
843 | ```
844 |
845 | See for example [jupyter_example.ipynb](https://raw.githubusercontent.com/bterwijn/memory_graph/main/src/jupyter_example.ipynb).
846 | 
847 |
848 | # ipython #
849 | In ipython `locals()` has additional variables that cause problems in the graph, use `mg.locals_ipython()` to get the local variables with these problematic variables filtered out. Use `mg.stack_ipython()` to get the whole call stack with these variables filtered out.
850 |
851 | Additionally install file [auto_memory_graph.py](https://raw.githubusercontent.com/bterwijn/memory_graph/main/src/auto_memory_graph.py) in the ipython startup directory:
852 | * Linux/Mac: `~/.ipython/profile_default/startup/`
853 | * Windows: `%USERPROFILE%\.ipython\profile_default\startup\`
854 |
855 | Then after starting 'ipython' call function `mg_switch()` to turn on/off the automatic visualization of local variables after each command.
856 | 
857 |
858 | # Google Colab #
859 | In Google Colab `locals()` has additional variables that cause problems in the graph, use `mg.locals_colab()` to get the local variables with these problematic variables filtered out. Use `mg.stack_colab()` to get the whole call stack with these variables filtered out.
860 |
861 | See for example [colab_example.ipynb](https://raw.githubusercontent.com/bterwijn/memory_graph/main/src/colab_example.ipynb).
862 | 
863 |
864 | # In the Browser #
865 | We can also run memory_graph in the browser: Pyodide Example
866 | 
867 |
868 |
869 | # Animated GIF #
870 | To make an animated GIF use for example `mg.show` or `mg.render` like this:
871 |
872 | * mg.show(locals(), 'animated.png', numbered=True)
873 | * mg.render(locals(), 'animated.png', numbered=True)
874 |
875 | in your source or better as a *watch* in a debugger so that stepping through the code generates images:
876 |
877 | animated0.png, animated1.png, animated2.png, ...
878 |
879 | Then use these images to make an animated GIF, for example using this Bash script [create_gif.sh](https://raw.githubusercontent.com/bterwijn/memory_graph/main/images/create_gif.sh):
880 |
881 | ```bash
882 | $ bash create_gif.sh animated
883 | ```
884 |
885 | # Troubleshooting #
886 | - 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 'somefile.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 and to open it manually.
887 |
888 | - When graph edges overlap it can be hard to distinguish them. Using an interactive graphviz viewer, such as [xdot](https://github.com/jrfonseca/xdot.py), on a '*.gv' DOT output file will help.
889 |
890 | ## Invocation_Tree Package ##
891 | The [memory_graph](https://pypi.org/project/memory-graph/) package visualizes your data. If instead you want to visualize function calls, check out the [invocation_tree](https://pypi.org/project/invocation-tree/) package.
892 |
--------------------------------------------------------------------------------
/images/add_one.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bterwijn/memory_graph/14ecccb7607180070a44b83e9d857cccb2a4bb2d/images/add_one.png
--------------------------------------------------------------------------------
/images/add_one.py:
--------------------------------------------------------------------------------
1 | # This file is part of memory_graph.
2 | # Copyright (c) 2023, Bas Terwijn.
3 | # SPDX-License-Identifier: BSD-2-Clause
4 |
5 | import memory_graph as mg
6 |
7 | def add_one(a, b, c):
8 | a += [1]
9 | b += (1,)
10 | c += [1]
11 | mg.render( mg.stack(), "add_one.png")
12 |
13 | a = [4, 3, 2]
14 | b = (4, 3, 2)
15 | c = [4, 3, 2]
16 |
17 | add_one(a, b, c.copy())
18 | print(f"a:{a} b:{b} c:{c}")
19 |
--------------------------------------------------------------------------------
/images/avltree.py:
--------------------------------------------------------------------------------
1 | # This file is part of memory_graph.
2 | # Copyright (c) 2023, Bas Terwijn.
3 | # SPDX-License-Identifier: BSD-2-Clause
4 |
5 | import memory_graph as mg
6 | import bintrees
7 |
8 | # Create an AVL tree
9 | tree = bintrees.AVLTree()
10 | tree.insert(10, "ten")
11 | tree.insert(5, "five")
12 | tree.insert(20, "twenty")
13 | tree.insert(15, "fifteen")
14 |
15 | # mg.render(locals(), 'avltree_fail.png') # id keeps changing
16 |
17 | mg.config.type_to_color[bintrees.avltree.Node] = "sandybrown"
18 | mg.config.type_to_node[bintrees.avltree.Node] = lambda data: mg.node_linear.Node_Linear(data, dir(data))
19 | mg.config.type_to_slicer[bintrees.avltree.Node] = mg.slicer.Slicer()
20 | # mg.render(locals(), 'avltree_dir.png') # stuff changes
21 |
22 | mg.config.type_to_node[bintrees.avltree.Node] = lambda data: mg.node_leaf.Node_Leaf(data, f"key:{data.key} value:{data.value}")
23 | mg.render(locals(), 'avltree_leaf.png')
24 |
25 | mg.config.type_to_node[bintrees.avltree.Node] = lambda data: mg.node_linear.Node_Linear(data,
26 | ['left:', data.left,
27 | 'key:', data.key,
28 | 'value:', data.value,
29 | 'right:', data.right])
30 | mg.render(locals(), 'avltree_linear.png')
31 |
32 | mg.config.type_to_node[bintrees.avltree.Node] = lambda data: mg.node_key_value.Node_Key_Value(data,
33 | {'left': data.left,
34 | 'key': data.key,
35 | 'value': data.value,
36 | 'right': data.right}.items())
37 | mg.render(locals(), 'avltree_key_value.png')
38 |
39 | mg.config.type_to_node[bintrees.avltree.Node] = lambda data: mg.node_table.Node_Table(data,
40 | [[data.key, data.value],
41 | [data.left, data.right]]
42 | )
43 | mg.render(locals(), 'avltree_table.png')
44 |
--------------------------------------------------------------------------------
/images/avltree_dir.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bterwijn/memory_graph/14ecccb7607180070a44b83e9d857cccb2a4bb2d/images/avltree_dir.png
--------------------------------------------------------------------------------
/images/avltree_fail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bterwijn/memory_graph/14ecccb7607180070a44b83e9d857cccb2a4bb2d/images/avltree_fail.png
--------------------------------------------------------------------------------
/images/avltree_key_value.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bterwijn/memory_graph/14ecccb7607180070a44b83e9d857cccb2a4bb2d/images/avltree_key_value.png
--------------------------------------------------------------------------------
/images/avltree_leaf.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bterwijn/memory_graph/14ecccb7607180070a44b83e9d857cccb2a4bb2d/images/avltree_leaf.png
--------------------------------------------------------------------------------
/images/avltree_linear.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bterwijn/memory_graph/14ecccb7607180070a44b83e9d857cccb2a4bb2d/images/avltree_linear.png
--------------------------------------------------------------------------------
/images/avltree_table.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bterwijn/memory_graph/14ecccb7607180070a44b83e9d857cccb2a4bb2d/images/avltree_table.png
--------------------------------------------------------------------------------
/images/bin_tree.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bterwijn/memory_graph/14ecccb7607180070a44b83e9d857cccb2a4bb2d/images/bin_tree.png
--------------------------------------------------------------------------------
/images/bin_tree.py:
--------------------------------------------------------------------------------
1 | # This file is part of memory_graph.
2 | # Copyright (c) 2023, Bas Terwijn.
3 | # SPDX-License-Identifier: BSD-2-Clause
4 |
5 | import memory_graph as mg
6 | import random
7 | random.seed(0) # use same random numbers each run
8 |
9 | class BinTree:
10 |
11 | def __init__(self, value=None, smaller=None, larger=None):
12 | self.smaller = smaller
13 | self.value = value
14 | self.larger = larger
15 |
16 | def add(self, value):
17 | if self.value is None:
18 | self.value = value
19 | elif value < self.value:
20 | if self.smaller is None:
21 | self.smaller = BinTree(value)
22 | else:
23 | self.smaller.add(value)
24 | else:
25 | if self.larger is None:
26 | self.larger = BinTree(value)
27 | else:
28 | self.larger.add(value)
29 | if value == 51:
30 | mg.render(mg.stack(), f"bin_tree.png")
31 | exit(0)
32 |
33 | tree = BinTree()
34 | n = 100
35 | for i in range(n):
36 | value = random.randrange(n)
37 | tree.add(value)
38 |
39 |
--------------------------------------------------------------------------------
/images/colab_example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bterwijn/memory_graph/14ecccb7607180070a44b83e9d857cccb2a4bb2d/images/colab_example.png
--------------------------------------------------------------------------------
/images/copies.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bterwijn/memory_graph/14ecccb7607180070a44b83e9d857cccb2a4bb2d/images/copies.png
--------------------------------------------------------------------------------
/images/copies.py:
--------------------------------------------------------------------------------
1 | # This file is part of memory_graph.
2 | # Copyright (c) 2023, Bas Terwijn.
3 | # SPDX-License-Identifier: BSD-2-Clause
4 |
5 | import memory_graph as mg
6 | import copy
7 |
8 | a = [ [1, 2], ['x', 'y'] ] # a nested list (a list containing lists)
9 |
10 | # three different ways to make a "copy" of 'a':
11 | c1 = a
12 | c2 = copy.copy(a) # equivalent to: a.copy() a[:] list(a)
13 | c3 = copy.deepcopy(a)
14 |
15 | mg.render(locals(), 'copies.png')
16 |
--------------------------------------------------------------------------------
/images/copy_method.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bterwijn/memory_graph/14ecccb7607180070a44b83e9d857cccb2a4bb2d/images/copy_method.png
--------------------------------------------------------------------------------
/images/copy_method.py:
--------------------------------------------------------------------------------
1 | # This file is part of memory_graph.
2 | # Copyright (c) 2023, Bas Terwijn.
3 | # SPDX-License-Identifier: BSD-2-Clause
4 |
5 | import memory_graph as mg
6 | import copy
7 |
8 | class My_Class:
9 |
10 | def __init__(self):
11 | self.digits = [1, 2]
12 | self.letters = ['x', 'y']
13 |
14 | def custom_copy(self):
15 | """ Copies 'digits' but shares 'letters'. """
16 | c = copy.copy(self)
17 | c.digits = copy.copy(self.digits)
18 | return c
19 |
20 | a = My_Class()
21 | b = a.custom_copy()
22 |
23 | mg.render(locals(), 'copy_method.png')
24 |
--------------------------------------------------------------------------------
/images/create_gif.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #
3 | # install:
4 | #
5 | # sudo apt install imagemagick
6 |
7 | name="$1"
8 | files=$(ls -v $name*.png)
9 | echo "creating gif with:"
10 | echo "$files"
11 |
12 | largest_size=$(identify -format "%H %Wx%H %f\n" $name*.png | sort -nr | head -n1| awk '{print $2}')
13 | echo "largest_size: $largest_size"
14 |
15 | echo "resizing images"
16 | mogrify -resize $largest_size -background white -gravity center -extent $largest_size $files
17 |
18 | echo "creating file: $name.gif"
19 | convert -delay 150 -loop 0 $files $name.gif
20 | echo "done"
21 |
--------------------------------------------------------------------------------
/images/create_images.sh:
--------------------------------------------------------------------------------
1 | # intro
2 | python many_types.py
3 |
4 | # debugging
5 | python debugging.py
6 | bash create_gif.sh debugging
7 |
8 | # data model
9 | python immutable.py
10 | python mutable.py
11 | python copies.py
12 | python copy_method.py
13 | python name_rebinding.py
14 |
15 | # call stack
16 | python add_one.py
17 | python factorial.py
18 | bash create_gif.sh factorial
19 | python power_set.py
20 | bash create_gif.sh power_set
21 |
22 | # datastructures
23 | python linked_list.py
24 | python bin_tree.py
25 | python hash_set.py
26 |
27 | # configuration
28 | python not_node_types.py
29 | python highlight.py
30 |
31 | # extensions
32 | python extension_numpy.py
33 | python extension_pandas.py
34 |
35 | # introspection
36 | python avltree.py
37 | python introspect_depth.py
38 |
--------------------------------------------------------------------------------
/images/debug_vscode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bterwijn/memory_graph/14ecccb7607180070a44b83e9d857cccb2a4bb2d/images/debug_vscode.png
--------------------------------------------------------------------------------
/images/debugging.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bterwijn/memory_graph/14ecccb7607180070a44b83e9d857cccb2a4bb2d/images/debugging.gif
--------------------------------------------------------------------------------
/images/debugging.py:
--------------------------------------------------------------------------------
1 | # This file is part of memory_graph.
2 | # Copyright (c) 2023, Bas Terwijn.
3 | # SPDX-License-Identifier: BSD-2-Clause
4 |
5 | import memory_graph as mg
6 |
7 | image=0
8 | def get_fac_name():
9 | global image
10 | image+=1
11 | return f"debugging{image:02d}.png"
12 |
13 | squares = []
14 | squares_collector = []
15 | for i in range(1,6):
16 | squares.append(i**2)
17 | squares_collector.append(squares.copy())
18 | mg.render(locals(), get_fac_name())
19 | mg.render(locals(), get_fac_name())
20 |
--------------------------------------------------------------------------------
/images/extension_numpy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bterwijn/memory_graph/14ecccb7607180070a44b83e9d857cccb2a4bb2d/images/extension_numpy.png
--------------------------------------------------------------------------------
/images/extension_numpy.py:
--------------------------------------------------------------------------------
1 | # This file is part of memory_graph.
2 | # Copyright (c) 2023, Bas Terwijn.
3 | # SPDX-License-Identifier: BSD-2-Clause
4 |
5 | import memory_graph as mg
6 | import numpy as np
7 | import memory_graph.extension_numpy
8 | np.random.seed(0) # use same random numbers each run
9 |
10 | array = np.array([1.1, 2, 3, 4, 5])
11 | matrix = np.matrix([[i*20+j for j in range(20)] for i in range(20)])
12 | ndarray = np.random.rand(20,20)
13 |
14 | mg.render( locals(), "extension_numpy.png")
15 |
--------------------------------------------------------------------------------
/images/extension_pandas.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bterwijn/memory_graph/14ecccb7607180070a44b83e9d857cccb2a4bb2d/images/extension_pandas.png
--------------------------------------------------------------------------------
/images/extension_pandas.py:
--------------------------------------------------------------------------------
1 | # This file is part of memory_graph.
2 | # Copyright (c) 2023, Bas Terwijn.
3 | # SPDX-License-Identifier: BSD-2-Clause
4 |
5 | import memory_graph as mg
6 | import pandas as pd
7 | import memory_graph.extension_pandas
8 |
9 | series = pd.Series( [i for i in range(20)] )
10 | dataframe1 = pd.DataFrame({ "calories": [420, 380, 390],
11 | "duration": [50, 40, 45] })
12 | dataframe2 = pd.DataFrame({ 'Name' : [ 'Tom', 'Anna', 'Steve', 'Lisa'],
13 | 'Age' : [ 28, 34, 29, 42],
14 | 'Length' : [ 1.70, 1.66, 1.82, 1.73] },
15 | index=['one', 'two', 'three', 'four']) # with row names
16 |
17 | mg.render( locals(), "extension_pandas.png")
18 |
--------------------------------------------------------------------------------
/images/factorial.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bterwijn/memory_graph/14ecccb7607180070a44b83e9d857cccb2a4bb2d/images/factorial.gif
--------------------------------------------------------------------------------
/images/factorial.py:
--------------------------------------------------------------------------------
1 | # This file is part of memory_graph.
2 | # Copyright (c) 2023, Bas Terwijn.
3 | # SPDX-License-Identifier: BSD-2-Clause
4 |
5 | import memory_graph as mg
6 |
7 | def factorial(n):
8 | if n==0:
9 | return 1
10 | mg.render( mg.stack(), 'factorial.png', numbered=True)
11 | result = n*factorial(n-1)
12 | mg.render( mg.stack(), 'factorial.png', numbered=True)
13 | return result
14 |
15 | mg.render( mg.stack(), 'factorial.png', numbered=True)
16 | factorial(3)
17 |
--------------------------------------------------------------------------------
/images/hash_set.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bterwijn/memory_graph/14ecccb7607180070a44b83e9d857cccb2a4bb2d/images/hash_set.png
--------------------------------------------------------------------------------
/images/hash_set.py:
--------------------------------------------------------------------------------
1 | # This file is part of memory_graph.
2 | # Copyright (c) 2023, Bas Terwijn.
3 | # SPDX-License-Identifier: BSD-2-Clause
4 |
5 | import memory_graph as mg
6 | import random
7 | random.seed(0) # use same random numbers each run
8 |
9 | class HashSet:
10 |
11 | def __init__(self, capacity=15):
12 | self.buckets = [None] * capacity
13 |
14 | def add(self, value):
15 | index = hash(value) % len(self.buckets)
16 | if self.buckets[index] is None:
17 | self.buckets[index] = []
18 | bucket = self.buckets[index]
19 | bucket.append(value)
20 | if value == 36:
21 | mg.render(locals(), "hash_set.png")
22 | exit()
23 |
24 | def contains(self, value):
25 | index = hash(value) % len(self.buckets)
26 | if self.buckets[index] is None:
27 | return False
28 | return value in self.buckets[index]
29 |
30 | def remove(self, value):
31 | index = hash(value) % len(self.buckets)
32 | if self.buckets[index] is not None:
33 | self.buckets[index].remove(value)
34 |
35 | hash_set = HashSet()
36 | n = 100
37 | for i in range(n):
38 | new_value = random.randrange(n)
39 | hash_set.add(new_value)
40 |
--------------------------------------------------------------------------------
/images/hidden_edges.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bterwijn/memory_graph/14ecccb7607180070a44b83e9d857cccb2a4bb2d/images/hidden_edges.png
--------------------------------------------------------------------------------
/images/highlight.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bterwijn/memory_graph/14ecccb7607180070a44b83e9d857cccb2a4bb2d/images/highlight.png
--------------------------------------------------------------------------------
/images/highlight.py:
--------------------------------------------------------------------------------
1 | # This file is part of memory_graph.
2 | # Copyright (c) 2023, Bas Terwijn.
3 | # SPDX-License-Identifier: BSD-2-Clause
4 |
5 | import memory_graph as mg
6 | from memory_graph.slicer import Slicer
7 |
8 | data = [ list(range(20)) for i in range(1,5)]
9 | highlight = data[2]
10 |
11 | mg.render( locals(), "highlight.png",
12 | colors = {id(highlight): "red" }, # set color to "red"
13 | vertical_orientations = {id(highlight): False }, # set horizontal orientation
14 | slicers = {id(highlight): Slicer()} # set no slicing
15 | )
16 |
--------------------------------------------------------------------------------
/images/immutable.py:
--------------------------------------------------------------------------------
1 | # This file is part of memory_graph.
2 | # Copyright (c) 2023, Bas Terwijn.
3 | # SPDX-License-Identifier: BSD-2-Clause
4 |
5 | import memory_graph as mg
6 |
7 | a = (4, 3, 2)
8 | b = a
9 | mg.render(locals(), 'immutable1.png')
10 | b += (1,)
11 | mg.render(locals(), 'immutable2.png')
12 |
--------------------------------------------------------------------------------
/images/immutable1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bterwijn/memory_graph/14ecccb7607180070a44b83e9d857cccb2a4bb2d/images/immutable1.png
--------------------------------------------------------------------------------
/images/immutable2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bterwijn/memory_graph/14ecccb7607180070a44b83e9d857cccb2a4bb2d/images/immutable2.png
--------------------------------------------------------------------------------
/images/introspect_depth.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bterwijn/memory_graph/14ecccb7607180070a44b83e9d857cccb2a4bb2d/images/introspect_depth.png
--------------------------------------------------------------------------------
/images/introspect_depth.py:
--------------------------------------------------------------------------------
1 | import memory_graph as mg
2 |
3 | class Base:
4 |
5 | def __init__(self, n):
6 | self.elements = [1]
7 | iter = self.elements
8 | for i in range(2,n):
9 | iter.append([i])
10 | iter = iter[-1]
11 |
12 | def get_last(self):
13 | iter = self.elements
14 | while len(iter)>1:
15 | iter = iter[-1]
16 | return iter
17 |
18 | class A(Base):
19 |
20 | def __init__(self, n):
21 | super().__init__(n)
22 |
23 | class B(Base):
24 |
25 | def __init__(self, n):
26 | super().__init__(n)
27 |
28 | class C(Base):
29 |
30 | def __init__(self, n):
31 | super().__init__(n)
32 |
33 | a = A(6)
34 | b1 = B(6)
35 | b2 = B(6)
36 | c = C(6)
37 |
38 | x = ['x']
39 | b2.get_last().append(x)
40 |
41 | mg.config.type_to_depth[B] = 3
42 | mg.config.type_to_depth[id(c)] = 2
43 | mg.render(locals(), 'introspect_depth.png')
44 |
--------------------------------------------------------------------------------
/images/ipython.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bterwijn/memory_graph/14ecccb7607180070a44b83e9d857cccb2a4bb2d/images/ipython.png
--------------------------------------------------------------------------------
/images/jupyter_example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bterwijn/memory_graph/14ecccb7607180070a44b83e9d857cccb2a4bb2d/images/jupyter_example.png
--------------------------------------------------------------------------------
/images/linked_list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bterwijn/memory_graph/14ecccb7607180070a44b83e9d857cccb2a4bb2d/images/linked_list.png
--------------------------------------------------------------------------------
/images/linked_list.py:
--------------------------------------------------------------------------------
1 | # This file is part of memory_graph.
2 | # Copyright (c) 2023, Bas Terwijn.
3 | # SPDX-License-Identifier: BSD-2-Clause
4 |
5 | import memory_graph as mg
6 | import random
7 | random.seed(0) # use same random numbers each run
8 |
9 | class Linked_List:
10 | """ Circular doubly linked list """
11 |
12 | def __init__(self, value=None,
13 | prev=None, next=None):
14 | self.prev = prev if prev else self
15 | self.value = value
16 | self.next = next if next else self
17 |
18 | def add_back(self, value):
19 | if self.value == None:
20 | self.value = value
21 | else:
22 | new_node = Linked_List(value,
23 | prev=self.prev,
24 | next=self)
25 | self.prev.next = new_node
26 | self.prev = new_node
27 |
28 | linked_list = Linked_List()
29 | n = 100
30 | for i in range(n):
31 | value = random.randrange(n)
32 | linked_list.add_back(value)
33 | if value == 33:
34 | mg.render(locals(), "linked_list.png")
35 | exit()
36 |
--------------------------------------------------------------------------------
/images/many_types.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bterwijn/memory_graph/14ecccb7607180070a44b83e9d857cccb2a4bb2d/images/many_types.png
--------------------------------------------------------------------------------
/images/many_types.py:
--------------------------------------------------------------------------------
1 | # This file is part of memory_graph.
2 | # Copyright (c) 2023, Bas Terwijn.
3 | # SPDX-License-Identifier: BSD-2-Clause
4 |
5 | import memory_graph as mg
6 |
7 | class My_Class:
8 | def __init__(self, x, y):
9 | self.x = x
10 | self.y = y
11 |
12 | data = [ range(1, 2), (3, 4), {5, 6}, {7:'seven', 8:'eight'}, My_Class(9, 10) ]
13 | mg.render(data, 'many_types.png')
14 |
--------------------------------------------------------------------------------
/images/mutable.py:
--------------------------------------------------------------------------------
1 | # This file is part of memory_graph.
2 | # Copyright (c) 2023, Bas Terwijn.
3 | # SPDX-License-Identifier: BSD-2-Clause
4 |
5 | import memory_graph as mg
6 |
7 | a = [4, 3, 2]
8 | b = a
9 | mg.render(locals(), 'mutable1.png')
10 | b += [1] # equivalent to: b.append(1)
11 | mg.render(locals(), 'mutable2.png')
12 |
--------------------------------------------------------------------------------
/images/mutable1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bterwijn/memory_graph/14ecccb7607180070a44b83e9d857cccb2a4bb2d/images/mutable1.png
--------------------------------------------------------------------------------
/images/mutable2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bterwijn/memory_graph/14ecccb7607180070a44b83e9d857cccb2a4bb2d/images/mutable2.png
--------------------------------------------------------------------------------
/images/name_rebinding.py:
--------------------------------------------------------------------------------
1 | import memory_graph as mg
2 |
3 | a = [4, 3, 2]
4 | b = a
5 | mg.render(locals(), 'rebinding1.png')
6 |
7 | b += [1] # changes value of 'b' and 'a'
8 | b = [100, 200] # rebinds 'b' to a new value, 'a' is uneffected
9 | mg.render(locals(), 'rebinding2.png')
10 |
--------------------------------------------------------------------------------
/images/not_node_types.py:
--------------------------------------------------------------------------------
1 | import memory_graph as mg
2 |
3 | a = [100, 200, 300]
4 | b = a.copy()
5 | mg.render(locals(), 'not_node_types1.png')
6 |
7 | mg.config.not_node_types.remove(int) # create a node for int values
8 |
9 | mg.render(locals(), 'not_node_types2.png')
10 |
--------------------------------------------------------------------------------
/images/not_node_types1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bterwijn/memory_graph/14ecccb7607180070a44b83e9d857cccb2a4bb2d/images/not_node_types1.png
--------------------------------------------------------------------------------
/images/not_node_types2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bterwijn/memory_graph/14ecccb7607180070a44b83e9d857cccb2a4bb2d/images/not_node_types2.png
--------------------------------------------------------------------------------
/images/power_set.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bterwijn/memory_graph/14ecccb7607180070a44b83e9d857cccb2a4bb2d/images/power_set.gif
--------------------------------------------------------------------------------
/images/power_set.py:
--------------------------------------------------------------------------------
1 | # This file is part of memory_graph.
2 | # Copyright (c) 2023, Bas Terwijn.
3 | # SPDX-License-Identifier: BSD-2-Clause
4 | import memory_graph as mg
5 |
6 |
7 | def get_subsets(subsets, data, i, subset):
8 | global image
9 | mg.render(mg.stack(), 'power_set.png', numbered=True)
10 | if i == len(data):
11 | subsets.append(subset.copy())
12 | return
13 | subset.append(data[i])
14 | get_subsets(subsets, data, i+1, subset) # do include data[i]
15 | subset.pop()
16 | get_subsets(subsets, data, i+1, subset) # don't include data[i]
17 | mg.render(mg.stack(), 'power_set.png', numbered=True)
18 |
19 | def power_set(data):
20 | subsets = []
21 | mg.render(mg.stack(), 'power_set.png', numbered=True)
22 | get_subsets(subsets, data, 0, [])
23 | mg.render(mg.stack(), 'power_set.png', numbered=True)
24 | return subsets
25 |
26 | print( power_set(['a', 'b', 'c']) )
27 |
--------------------------------------------------------------------------------
/images/pyodide.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bterwijn/memory_graph/14ecccb7607180070a44b83e9d857cccb2a4bb2d/images/pyodide.png
--------------------------------------------------------------------------------
/images/rebinding1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bterwijn/memory_graph/14ecccb7607180070a44b83e9d857cccb2a4bb2d/images/rebinding1.png
--------------------------------------------------------------------------------
/images/rebinding2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bterwijn/memory_graph/14ecccb7607180070a44b83e9d857cccb2a4bb2d/images/rebinding2.png
--------------------------------------------------------------------------------
/images/uva.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bterwijn/memory_graph/14ecccb7607180070a44b83e9d857cccb2a4bb2d/images/uva.png
--------------------------------------------------------------------------------
/images/vscode_copying.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bterwijn/memory_graph/14ecccb7607180070a44b83e9d857cccb2a4bb2d/images/vscode_copying.gif
--------------------------------------------------------------------------------
/install.txt:
--------------------------------------------------------------------------------
1 | # just some notes about how to deal with Python modules and pip packages
2 |
3 |
4 | # ===== pip install
5 | pip install --upgrade build twine setuptools wheel
6 |
7 | # ===== (un)install current module
8 | pip uninstall memory_graph
9 | pip install .
10 | pip install --upgrade .
11 |
12 |
13 | # ===== prepare packages for upload
14 | # - increase version number in: pyproject.toml memory_graph/__init__.py
15 | # - update images:
16 | cd images; bash create_images.sh; cd ..
17 | # - git commit -am "version X.X.X" && git push
18 |
19 | rm -rf ./dist/ ./build/ ./*.egg-info && python -m build -n -s -w
20 |
21 |
22 | # ===== upload packages to pypi for 'pip install' purposes
23 | # - upload to test url:
24 | twine upload --repository-url https://test.pypi.org/legacy/ dist/*
25 | # - upload to pypi for real:
26 | twine upload dist/*
27 |
--------------------------------------------------------------------------------
/memory_graph/__init__.py:
--------------------------------------------------------------------------------
1 | # This file is part of memory_graph.
2 | # Copyright (c) 2023, Bas Terwijn.
3 | # SPDX-License-Identifier: BSD-2-Clause
4 |
5 | import memory_graph.memory_to_nodes as memory_to_nodes
6 | import memory_graph.config as config
7 | import memory_graph.config_default
8 | import memory_graph.config_helpers as config_helper
9 | import memory_graph.utils as utils
10 |
11 | import inspect
12 | import sys
13 | import itertools as it
14 | from memory_graph.call_stack import call_stack
15 |
16 | import graphviz
17 |
18 | # Add 'mg' to builtins so it is available in all subsequent imports
19 | import memory_graph as mg
20 | import builtins
21 | if not hasattr(builtins, "mg"):
22 | builtins.mg = mg
23 |
24 | __version__ = "0.3.34"
25 | __author__ = 'Bas Terwijn'
26 | render_filename = 'memory_graph.pdf'
27 | render_filename_count = 0
28 | last_show_filename = None
29 | block_prints_location = True
30 | press_enter_message = "Press to continue..."
31 |
32 | def get_source_location(stack_index=0):
33 | """ Helper function to get the source location of the stack with 'stack_index' of the call stack. """
34 | frameInfo = inspect.stack()[1+stack_index] # get frameInfo of calling frame
35 | filename= frameInfo.filename
36 | line_nr= frameInfo.lineno
37 | function = frameInfo.function
38 | return f'{filename}:{line_nr} in "{function}"'
39 |
40 | def block(fun=None, *args, **kwargs):
41 | """
42 | Calls the given function `fun` with specified arguments and keyword arguments,
43 | waits for the user to press Enter, and returns the result of `fun`.
44 | """
45 | stack_index = 0
46 | if 'stack_index' in kwargs:
47 | stack_index = kwargs['stack_index']
48 | del kwargs['stack_index']
49 | result = None
50 | if callable(fun):
51 | result = fun(*args, **kwargs)
52 | if memory_graph.block_prints_location:
53 | print('blocked at ' + get_source_location(1+stack_index), end=', ')
54 | if memory_graph.press_enter_message:
55 | print(memory_graph.press_enter_message)
56 | input()
57 | return result
58 |
59 | def create_graph(data,
60 | colors = None,
61 | vertical_orientations = None,
62 | slicers = None):
63 | """ Creates and returns a memory graph from 'data'. """
64 | config_helper.set_config(colors, vertical_orientations, slicers)
65 | graphviz_graph = memory_to_nodes.memory_to_nodes(data)
66 | return graphviz_graph
67 |
68 | def number_filename(outfile):
69 | """ Returns the 'outfile' with 'render_filename_count'. """
70 | global render_filename_count
71 | splits = outfile.split('.')
72 | if len(splits)>1:
73 | splits[-2]+=str(render_filename_count)
74 | render_filename_count += 1
75 | return '.'.join(splits)
76 | return self.filename
77 |
78 | def render(data=None, outfile=None, view=False,
79 | colors = None,
80 | vertical_orientations = None,
81 | slicers = None,
82 | numbered = False):
83 | """ Renders the graph of 'data' to 'outfile' or `memory_graph.render_filename` when not specified. """
84 | if data is None:
85 | data = locals()
86 | if outfile is None:
87 | outfile = memory_graph.render_filename
88 | graph = create_graph(data, colors, vertical_orientations, slicers)
89 | if numbered:
90 | outfile = number_filename(outfile)
91 | if outfile.endswith('.gv') or outfile.endswith('.dot'):
92 | graph.save(filename=outfile)
93 | else:
94 | graph.render(outfile=outfile, view=view, cleanup=False, quiet=False, quiet_view=False)
95 |
96 |
97 | def show(data=None, outfile=None, view=False,
98 | colors = None,
99 | vertical_orientations = None,
100 | slicers = None,
101 | numbered = False):
102 | """ Shows the graph of 'data' by first rendering and then opening the default viewer
103 | application by file extension at first call, when the outfile changes, or
104 | when view is True. """
105 | if data is None:
106 | data = locals()
107 | if outfile is None:
108 | outfile = memory_graph.render_filename
109 | open_view = (outfile != memory_graph.last_show_filename) or view
110 | render(data=data, outfile=outfile, view=open_view,
111 | colors=colors,
112 | vertical_orientations=vertical_orientations,
113 | slicers=slicers, numbered=numbered)
114 | memory_graph.last_show_filename = outfile
115 |
116 |
117 | # ------------ aliases
118 |
119 | def sl(stack_index=0, colors = None, vertical_orientations = None, slicers = None):
120 | """
121 | Shows the graph of locals() and blocks.
122 | """
123 | data = get_locals_from_call_stack(stack_index=1+stack_index)
124 | memory_graph.show(data, colors=colors, vertical_orientations=vertical_orientations, slicers=slicers)
125 |
126 | def ss(stack_index=0, colors = None, vertical_orientations = None, slicers = None):
127 | """
128 | Shows the graph of mg.stack() and blocks.
129 | """
130 | data = stack(stack_index=1+stack_index)
131 | memory_graph.show(data, colors=colors, vertical_orientations=vertical_orientations, slicers=slicers)
132 |
133 | def bsl(stack_index=0, colors = None, vertical_orientations = None, slicers = None):
134 | """
135 | Shows the graph of locals() and blocks.
136 | """
137 | data = get_locals_from_call_stack(stack_index=1+stack_index)
138 | memory_graph.block(memory_graph.show, data, stack_index=1+stack_index, block=False,
139 | colors=colors, vertical_orientations=vertical_orientations, slicers=slicers)
140 |
141 | def bss(stack_index=0, colors = None, vertical_orientations = None, slicers = None):
142 | """
143 | Shows the graph of mg.stack() and blocks.
144 | """
145 | data = stack(stack_index=1+stack_index)
146 | memory_graph.block(memory_graph.show, data, stack_index=1+stack_index, block=False,
147 | colors=colors, vertical_orientations=vertical_orientations, slicers=slicers)
148 |
149 | def rl(stack_index=0, colors = None, vertical_orientations = None, slicers = None):
150 | """
151 | Shows the graph of locals() and blocks.
152 | """
153 | data = get_locals_from_call_stack(stack_index=1+stack_index)
154 | memory_graph.render(data, block=False, colors=colors, vertical_orientations=vertical_orientations, slicers=slicers)
155 |
156 | def rs(stack_index=0, colors = None, vertical_orientations = None, slicers = None):
157 | """
158 | Shows the graph of mg.stack() and blocks.
159 | """
160 | data = stack(stack_index=1+stack_index)
161 | memory_graph.render(data, block=False, colors=colors, vertical_orientations=vertical_orientations, slicers=slicers)
162 |
163 | def brl(stack_index=0, colors = None, vertical_orientations = None, slicers = None):
164 | """
165 | Shows the graph of locals() and blocks.
166 | """
167 | data = get_locals_from_call_stack(stack_index=1+stack_index)
168 | memory_graph.block(memory_graph.render, data, stack_index=1+stack_index, block=False,
169 | colors=colors, vertical_orientations=vertical_orientations, slicers=slicers)
170 |
171 | def brs(stack_index=0, colors = None, vertical_orientations = None, slicers = None):
172 | """
173 | Shows the graph of mg.stack() and blocks.
174 | """
175 | data = stack(stack_index=1+stack_index)
176 | memory_graph.block(memory_graph.render, data, stack_index=1+stack_index, block=False,
177 | colors=colors, vertical_orientations=vertical_orientations, slicers=slicers)
178 |
179 | def l(stack_index=0, colors = None, vertical_orientations = None, slicers = None):
180 | """
181 | Shows the graph of locals() and blocks.
182 | """
183 | bsl(stack_index=1+stack_index, colors=colors, vertical_orientations=vertical_orientations, slicers=slicers)
184 |
185 | def s(stack_index=0, colors = None, vertical_orientations = None, slicers = None):
186 | """
187 | Shows the graph of mg.stack() and blocks.
188 | """
189 | bss(stack_index=1+stack_index, colors=colors, vertical_orientations=vertical_orientations, slicers=slicers)
190 |
191 |
192 | # ------------ call stack
193 |
194 | def get_locals_from_call_stack(stack_index=0):
195 | """ Helper function to get locals of the stack with 'stack_inex' of the call stack. """
196 | frameInfo = inspect.stack()[1+stack_index] # get frameInfo of calling frame
197 | return frameInfo.frame.f_locals
198 |
199 | def get_function_name(frameInfo):
200 | frame = frameInfo.frame
201 | func_name = frame.f_code.co_name
202 | if 'self' in frame.f_locals: # instance method
203 | return f"{frame.f_locals['self'].__class__.__name__}.{func_name}"
204 | elif 'cls' in frame.f_locals: # class method
205 | return f"{frame.f_locals['cls'].__name__}.{func_name}"
206 | else: # forget about static method, too complex
207 | return func_name # just the function
208 |
209 | def stack_frames_to_dict(frames):
210 | """ Returns a dictionary representing the data on the call stack.
211 | Each key is the stack level and function name, each value is the locals of the frame at that level.
212 | """
213 | def to_dict(value): # fix by TerenceTux for Python 3.13
214 | return {k: v for k, v in value.items()}
215 | return call_stack({f"{level}: {get_function_name(frameInfo)}" : to_dict(frameInfo.frame.f_locals)
216 | for level, frameInfo in enumerate(frames)})
217 |
218 | def locals():
219 | """ Returns local variables. """
220 | return locals()
221 |
222 | def stack(through_function="",stack_index=0):
223 | """ Gets the call stack up to and including the function 'through_function'. """
224 | frames = reversed(list(
225 | utils.take_through(lambda i: i.function==through_function, inspect.stack()[1+stack_index:])
226 | ))
227 | return stack_frames_to_dict(frames)
228 |
229 | def stack_after_through(after_functions : list[str],
230 | through_function : str = "",
231 | drop : int = 0):
232 | """ Gets the call stack after any of the 'after_functions' function up to
233 | and including the function 'through_function' and drops the first 'drop'
234 | stack frames. """
235 | frames = reversed(list(it.islice(
236 | utils.take_through(lambda i: i.function == through_function,
237 | utils.take_after(lambda i: i.function in after_functions, inspect.stack()))
238 | , drop, None)))
239 | return stack_frames_to_dict(frames)
240 |
241 | def stack_pdb(after_functions=["trace_dispatch"],through_function=""):
242 | """ Get the call stack in a 'pdb' debugger session, filtering out the 'pdb' functions that polute the graph. """
243 | return stack_after_through(after_functions,through_function)
244 |
245 | def stack_vscode(after_functions=["do_wait_suspend"],through_function=""):
246 | """ Get the call stack in a 'vscode' debugger session, filtering out the 'vscode' functions that polute the graph. """
247 | return stack_after_through(after_functions,through_function)
248 |
249 | def stack_cursor(after_functions=["do_wait_suspend"],through_function=""):
250 | """ Get the call stack in a 'cursor' debugger session, filtering out the 'cursor' functions that polute the graph. """
251 | return stack_after_through(after_functions,through_function)
252 |
253 | def stack_pycharm(after_functions=["do_wait_suspend"],through_function=""):
254 | """ Get the call stack in a 'pycharm' debugger session, filtering out the 'pycharm' functions that polute the graph. """
255 | return stack_after_through(after_functions,through_function, 1)
256 |
257 | def stack_wing(after_functions=["_py_line_event","_py_return_event"],through_function=""):
258 | """ Get the call stack in a 'wing' debugger session, filtering out the 'wing' functions that polute the graph. """
259 | return stack_after_through(after_functions,through_function, 0)
260 |
261 |
262 | def save_call_stack(filename):
263 | """ Saves the call stack to 'filename' for inspection to see what functions need to be
264 | filtered out to create the desired graph. """
265 | with open(filename,'w') as file:
266 | for frame in inspect.stack():
267 | file.write(f"function:{frame.function} filename:{frame.filename}\n")
268 |
269 | def print_call_stack_vars(stack_index=0):
270 | """ Prints all variables on the call stack. """
271 | for level, frameInfo in enumerate(reversed(inspect.stack())):
272 | print('=====',level,frameInfo.function)
273 | print(tuple(frameInfo.frame.f_locals.keys()))
274 |
275 |
276 | # ------------ jupyter filtering
277 |
278 | jupyter_filter_keys = {'exit','quit','v','In','Out','jupyter_filter_keys'}
279 | def jupyter_locals_filter(jupyter_locals):
280 | """ Filter out the jupyter specific keys that polute the graph. """
281 | return {k:v for k,v in utils.filter_dict(jupyter_locals)
282 | if k not in jupyter_filter_keys and k[0] != '_'}
283 |
284 | def locals_jupyter(stack_index=0):
285 | """ Get the locals of the calling frame in a jupyter notebook, filtering out the jupyter specific keys. """
286 | return jupyter_locals_filter(get_locals_from_call_stack(1+stack_index))
287 |
288 | def stack_jupyter(through_function="",stack_index=0):
289 | """ Get the call stack in a jupyter notebook, filtering out the jupyter specific keys. """
290 | call_stack = stack(through_function,1+stack_index)
291 | globals_frame = next(iter(call_stack))
292 | call_stack[globals_frame] = jupyter_locals_filter(call_stack[globals_frame])
293 | return call_stack
294 |
295 |
296 | # ------------ ipython filtering
297 |
298 | ipython_filter_keys = {'mg_visualization_status', 'sys', 'ipython', 'In', 'Out', 'get_ipython', 'exit', 'quit', 'open'}
299 | def ipython_locals_filter(ipython_locals):
300 | """ Filter out the ipython specific keys that polute the graph. """
301 | return {k:v for k,v in utils.filter_dict(ipython_locals)
302 | if k not in ipython_filter_keys and k[0] != '_'}
303 |
304 | def locals_ipython(stack_index=0):
305 | """ Get the locals of the calling frame in a ipython, filtering out the ipython specific keys. """
306 | return ipython_locals_filter(get_locals_from_call_stack(1+stack_index))
307 |
308 | def stack_ipython(through_function="", stack_index=0):
309 | """ Get the call stack in a ipython, filtering out the ipython specific keys. """
310 | call_stack = stack(through_function,1+stack_index)
311 | globals_frame = next(iter(call_stack))
312 | call_stack[globals_frame] = ipython_locals_filter(call_stack[globals_frame])
313 | return call_stack
314 |
315 | # ------------ google colab filtering
316 |
317 | colab_filter_keys = {'In', 'Out', 'exit', 'quit'}
318 | def colab_locals_filter(colab_locals):
319 | """ Filter out the colab specific keys that polute the graph. """
320 | return {k:v for k,v in utils.filter_dict(colab_locals)
321 | if k not in colab_filter_keys and k[0] != '_'}
322 |
323 | def locals_colab(stack_index=0):
324 | """ Get the locals of the calling frame in a colab, filtering out the colab specific keys. """
325 | return colab_locals_filter(get_locals_from_call_stack(1+stack_index))
326 |
327 | def stack_colab(through_function="", stack_index=0):
328 | """ Get the call stack in a colab, filtering out the colab specific keys. """
329 | call_stack = stack(through_function,1+stack_index)
330 | globals_frame = next(iter(call_stack))
331 | call_stack[globals_frame] = colab_locals_filter(call_stack[globals_frame])
332 | return call_stack
333 |
--------------------------------------------------------------------------------
/memory_graph/call_stack.py:
--------------------------------------------------------------------------------
1 | # This file is part of memory_graph.
2 | # Copyright (c) 2023, Bas Terwijn.
3 | # SPDX-License-Identifier: BSD-2-Clause
4 |
5 | class call_stack(dict):
6 | """Inherits from dict to give the call stack it own name and color. """
7 |
8 | def __init__(self, *args, **kwargs):
9 | super().__init__(*args, **kwargs)
10 |
--------------------------------------------------------------------------------
/memory_graph/config.py:
--------------------------------------------------------------------------------
1 | # This file is part of memory_graph.
2 | # Copyright (c) 2023, Bas Terwijn.
3 | # SPDX-License-Identifier: BSD-2-Clause
4 |
5 | """ Configuration file for the graph visualizer. The configuration values are set later by the 'config_default.py' file. """
6 |
7 |
8 | max_string_length = None
9 | graph_stability = None
10 |
11 | not_node_types = {}
12 | no_child_references_types = set()
13 |
14 | type_to_string = { }
15 |
16 | def to_string(data):
17 | """ Convert data to string. """
18 | data_type = type(data)
19 | if data_type in type_to_string:
20 | return type_to_string[data_type](data)
21 | return str(data)
22 |
23 | type_to_node = { }
24 |
25 | type_to_color = { }
26 |
27 | type_to_vertical_orientation = { }
28 |
29 | type_to_slicer = { }
30 |
31 | max_graph_depth = None
32 | graph_cut_symbol = None
33 | max_missing_edges = None
34 |
35 | type_to_depth = { }
36 |
--------------------------------------------------------------------------------
/memory_graph/config_default.py:
--------------------------------------------------------------------------------
1 | # This file is part of memory_graph.
2 | # Copyright (c) 2023, Bas Terwijn.
3 | # SPDX-License-Identifier: BSD-2-Clause
4 |
5 | """ Sets the default configuration values for the memory graph. """
6 | from memory_graph.node_leaf import Node_Leaf
7 | from memory_graph.node_linear import Node_Linear
8 | from memory_graph.node_key_value import Node_Key_Value
9 | from memory_graph.node_table import Node_Table
10 |
11 | from memory_graph.call_stack import call_stack
12 | from memory_graph.slicer import Slicer
13 |
14 | import memory_graph.config as config
15 | import memory_graph.utils as utils
16 |
17 | import types
18 |
19 |
20 | """ The maximum length of strings shown in the graph. Longer strings will be truncated. """
21 | config.max_string_length = 42
22 |
23 | """ The number of references keeping child nodes in order versus other references pulling them out. """
24 | config.graph_stability = 10
25 |
26 | """ Types that by default will not have references pointing to them in the graph but instead will be visualized in the node of their parent. """
27 | config.not_node_types = {
28 | type(None), bool, int, float, complex, str,
29 | types.FunctionType,
30 | types.MethodType,
31 | classmethod,
32 | staticmethod,
33 | type(len),
34 | }
35 |
36 | """ Types that will not have references pointing to their children in the graph but instead will have their children visualized in their node. """
37 | config.no_child_references_types = {dict, types.MappingProxyType}
38 |
39 | """ Types that need an special conversion """
40 | config.type_to_string = {
41 | types.FunctionType: lambda data: data.__qualname__,
42 | types.MethodType: lambda data: data.__qualname__,
43 | classmethod: lambda data: data.__qualname__,
44 | staticmethod: lambda data: data.__qualname__,
45 | type(len): lambda data: data.__qualname__,
46 | }
47 |
48 | """ Conversion from type to Node objects. """
49 | config.type_to_node = {
50 | str: lambda data: Node_Leaf(data, data), # visit as whole string, don't iterate over characters
51 | call_stack: lambda data: Node_Key_Value(data, data.items()),
52 | type: lambda data: Node_Key_Value(data, utils.filter_type_attributes(vars(data).items())),
53 | range: lambda data: Node_Key_Value(data, {'start':data.start, 'stop':data.stop, 'step':data.step}.items()),
54 | dict: lambda data: (
55 | Node_Key_Value(data, utils.filter_dict(data) )
56 | if dict in config.no_child_references_types else
57 | Node_Linear(data, utils.filter_dict(data) )
58 | ),
59 | }
60 |
61 | """ Colors of different types in the graph. """
62 | config.type_to_color = {
63 | # ================= singular
64 | type(None) : "gray",
65 | bool : "pink",
66 | int : "green",
67 | float : "violetred1",
68 | complex : "yellow",
69 | str : "cyan",
70 | # ================= linear
71 | tuple : "orange",
72 | list : "lightcoral",
73 | set : "orchid1",
74 | frozenset : "orchid2",
75 | bytes : "khaki1",
76 | bytearray : "khaki2",
77 | # ================= key_value
78 | Node_Key_Value : "seagreen1", # for classes
79 | call_stack : 'khaki',
80 | type: "seagreen3", # where class variables are stored
81 | dict : "#60a5ff",
82 | types.MappingProxyType : "dodgerblue2", # not used
83 | range : "cornsilk2",
84 | }
85 |
86 | """ Types that will be visualized in vertical orientation if 'True', or horizontal orientation
87 | if 'False'. Otherwise the Node decides based on it having references."""
88 | config.type_to_vertical_orientation = {
89 | }
90 |
91 | """ Slicer objects for different types. """
92 | config.type_to_slicer = {
93 | Node_Linear: Slicer(5,3,5),
94 | Node_Key_Value: Slicer(5,3,5),
95 | Node_Table: (Slicer(3,2,3), Slicer(3,2,3)),
96 | }
97 |
98 | """ The maximum depth of nodes in the graph. When the graph gets too big set this to a small positive number. A `✂` symbol indictes where the graph is cut short. """
99 | config.max_graph_depth = 12
100 | config.graph_cut_symbol = '✂'
101 |
102 |
103 | """ Maximum introspection depth for different types. """
104 | config.type_to_depth = {
105 | }
106 |
107 | """ Maximum number of missing edges that are shown. """
108 | config.max_missing_edges = 2
109 |
--------------------------------------------------------------------------------
/memory_graph/config_helpers.py:
--------------------------------------------------------------------------------
1 | # This file is part of memory_graph.
2 | # Copyright (c) 2023, Bas Terwijn.
3 | # SPDX-License-Identifier: BSD-2-Clause
4 |
5 | """ This module provides helper functions to access the configuration of the memory graph. """
6 | from memory_graph.slicer import Slicer
7 |
8 | import memory_graph.config as config
9 |
10 | type_to_color = None
11 | type_to_vertical_orientation = None
12 | type_to_slicer = None
13 |
14 | def set_config(colors=None, vertical_orientations=None, slicers=None):
15 | global type_to_color
16 | global type_to_vertical_orientation
17 | global type_to_slicer
18 | type_to_color = config.type_to_color.copy()
19 | type_to_vertical_orientation = config.type_to_vertical_orientation.copy()
20 | type_to_slicer = config.type_to_slicer.copy()
21 | if colors:
22 | type_to_color.update(colors)
23 | if vertical_orientations:
24 | type_to_vertical_orientation.update(vertical_orientations)
25 | if slicers:
26 | type_to_slicer.update(slicers)
27 |
28 | def get_property(data_id, data_type, node_type, dictionary, default):
29 | if data_id in dictionary:
30 | return dictionary[data_id]
31 | if data_type in dictionary:
32 | return dictionary[data_type]
33 | if node_type in dictionary:
34 | return dictionary[node_type]
35 | return default
36 |
37 | def get_color(node, default='white'):
38 | return get_property(node.get_id(),
39 | node.get_type(),
40 | type(node),
41 | type_to_color,
42 | default)
43 |
44 | def get_vertical_orientation(node, default):
45 | return get_property(node.get_id(),
46 | node.get_type(),
47 | type(node),
48 | type_to_vertical_orientation,
49 | default)
50 |
51 | def get_slicer(node, data, default=Slicer(3,2,3)):
52 | return get_property(id(data),
53 | type(data),
54 | type(node),
55 | type_to_slicer,
56 | default)
57 |
--------------------------------------------------------------------------------
/memory_graph/extension_numpy.py:
--------------------------------------------------------------------------------
1 | # This file is part of memory_graph.
2 | # Copyright (c) 2023, Bas Terwijn.
3 | # SPDX-License-Identifier: BSD-2-Clause
4 |
5 | """ Extension to add the memory graph configuration for Numpy types. """
6 | from memory_graph.node_linear import Node_Linear
7 | from memory_graph.node_table import Node_Table
8 |
9 | import memory_graph.config as config
10 |
11 | import numpy as np
12 |
13 | config.not_node_types |= {
14 | np.int8, np.int16, np.int32, np.int64, np.uint8, np.uint16, np.uint32, np.uint64,
15 | np.float16, np.float32, np.float64,
16 | np.complex64, np.complex128,
17 | np.bool_, np.bytes_, np.str_, np.datetime64, np.timedelta64
18 | }
19 |
20 | def ndarrayy_to_node(ndarrayy_data):
21 | if len(ndarrayy_data.shape) == 2:
22 | return Node_Table(ndarrayy_data, ndarrayy_data)
23 | else:
24 | return Node_Linear(ndarrayy_data, ndarrayy_data)
25 |
26 | config.type_to_node[np.matrix] = lambda data : Node_Table(data, np.asarray(data)) # convert to ndarray to avoid infinite recursion due to index issue
27 | config.type_to_node[np.ndarray] = lambda data : ndarrayy_to_node(data)
28 |
29 | config.type_to_color[np.ndarray] = "hotpink1"
30 | config.type_to_color[np.matrix] = "hotpink2"
31 |
--------------------------------------------------------------------------------
/memory_graph/extension_pandas.py:
--------------------------------------------------------------------------------
1 | # This file is part of memory_graph.
2 | # Copyright (c) 2023, Bas Terwijn.
3 | # SPDX-License-Identifier: BSD-2-Clause
4 |
5 | """ Extension to add the memory graph configuration for Pandas type. """
6 | from memory_graph.node_linear import Node_Linear
7 | from memory_graph.node_table import Node_Table
8 |
9 | import memory_graph.config as config
10 |
11 | import pandas as pd
12 |
13 | config.type_to_node[pd.DataFrame] = lambda data : (
14 | Node_Table(data,
15 | data.values.tolist(),
16 | col_names = data.columns.tolist(),
17 | row_names = [ str(i) for i in data.index.tolist()]
18 | )
19 | )
20 |
21 | config.type_to_node[pd.Series] = lambda data : (
22 | Node_Linear(data, data.tolist())
23 | )
24 |
25 | config.type_to_color[pd.DataFrame] = "olivedrab1"
26 | config.type_to_color[pd.Series] = "olivedrab2"
27 |
--------------------------------------------------------------------------------
/memory_graph/html_table.py:
--------------------------------------------------------------------------------
1 | # This file is part of memory_graph.
2 | # Copyright (c) 2023, Bas Terwijn.
3 | # SPDX-License-Identifier: BSD-2-Clause
4 |
5 | from memory_graph.node_base import Node_Base
6 | import memory_graph.node_base
7 | import memory_graph.config as config
8 | import html
9 |
10 | def html_table_frame(s, border, color, spacing=5):
11 | """ Helper function to add the HTML table frame to the string s setting the 'border' and 'color'. """
12 | return (f'<\n
\n
' +
13 | s + '
\n
\n>')
14 |
15 | def format_string(s):
16 | """ Helper function to format the string s to be shown in the graph. Setting the max_string_length and escaping html characters. """
17 | s = config.to_string(s)
18 | s = (s[:config.max_string_length] + '...') if len(s) > config.max_string_length else s
19 | return html.escape(s)
20 |
21 | class HTML_Table:
22 | """
23 | The HTML_Table class is used to create a table of data that can be visualized in the graph.
24 | """
25 |
26 | def __init__(self):
27 | """
28 | Create an HTML_Table object.
29 | """
30 | self.html = ''
31 | self.add_new_line_flag = False
32 | self.is_empty = True
33 | self.col_count = 0
34 | self.row_count = 0
35 | self.ref_count = 0
36 | self.max_col_count = 0
37 | self.edges = []
38 |
39 | def __repr__(self):
40 | """ Get the string representation of the HTML_Table object. """
41 | return self.html
42 |
43 | def add_new_line(self):
44 | """ Set the 'add_new_line_flag' to add a new line to the table when adding the next table element. """
45 | self.add_new_line_flag = True
46 | self.row_count += 1
47 | if self.col_count > self.max_col_count:
48 | self.max_col_count = self.col_count
49 | self.col_count = 0
50 |
51 | def check_add_new_line(self):
52 | """ Check if a new line should be added to the table, and if so add it and sets the 'add_new_line_flag' to False."""
53 | if self.add_new_line_flag:
54 | self.html += '\n
'
55 | self.add_new_line_flag = False
56 |
57 | def add_string(self, s, border=0):
58 | """ Add a string s to the table. """
59 | self.html += f'
'+format_string(s)+'
'
60 | self.is_empty = False
61 |
62 | def add_index(self, s):
63 | """ Add an index s to the table. """
64 | self.check_add_new_line()
65 | self.html += f'
{str(s)}
'
66 | self.col_count += 1
67 |
68 | def add_entry(self, node, nodes, child, id_to_slices, rounded=False, border=1, dashed=False):
69 | """ Add child to the table either as reference if it is a Node_Base or as a value otherwise. """
70 | #print('child:', child)
71 | child_id = id(child)
72 | if child_id in nodes:
73 | child = nodes[child_id]
74 | if child_id in id_to_slices:
75 | self.add_reference(node, child, rounded, border, dashed)
76 | else:
77 | self.add_value(config.graph_cut_symbol, rounded, border)
78 | else:
79 | self.add_value(child, rounded, border)
80 |
81 | def add_value(self, s, rounded=False, border=1):
82 | """ Helper function to add a value s to the table. """
83 | self.check_add_new_line()
84 | r = ' STYLE="ROUNDED"' if rounded else ''
85 | self.html += f'
{format_string(s)}
'
86 | self.col_count += 1
87 |
88 | def add_reference(self, node, child, rounded=False, border=1, dashed=False):
89 | """ Helper function to add a reference to the table. """
90 | self.check_add_new_line()
91 | r = ' STYLE="ROUNDED"' if rounded else ''
92 | self.html += f'
'
93 | self.edges.append( (f'{node.get_name()}:ref{self.ref_count}',
94 | child.get_name(), dashed) )
95 | self.ref_count += 1
96 | self.col_count += 1
97 |
98 | def add_dots(self, rounded=False, border=1):
99 | """ Helper function to add dots to the table. """
100 | self.check_add_new_line()
101 | r = 'STYLE="ROUNDED"' if rounded else ''
102 | self.html += f'
...
'
103 | self.col_count += 1
104 |
105 | def to_string(self, border=1, color='white'):
106 | """ Construct the HTML table string with the 'border' and 'color' settings. """
107 | if self.col_count == 0 and self.row_count == 0:
108 | if self.is_empty:
109 | self.add_string(' ')
110 | return html_table_frame(self.html, border, color, spacing=0)
111 | return html_table_frame(self.html, border, color)
112 |
113 | def get_column(self):
114 | """ Get the number of columns in the table. """
115 | return self.col_count
116 |
117 | def get_max_column(self):
118 | """ Get the maximum value of the number of columns of rows in the table. """
119 | return self.max_col_count
120 |
121 | def get_row(self):
122 | """ Get the number of rows in the table. """
123 | return self.row_count
124 |
125 | def get_edges(self):
126 | """ Get the edges that need to be added to connect the table to other tables in the graph. """
127 | return self.edges
128 |
129 | if __name__ == '__main__':
130 | table = HTML_Table()
131 | rows = 4
132 | columns = 5
133 | for r in range(rows):
134 | for c in range(columns):
135 | table.add_value(f'{c},{r}')
136 | table.add_new_line()
137 | print(table.to_string())
138 |
--------------------------------------------------------------------------------
/memory_graph/list_view.py:
--------------------------------------------------------------------------------
1 | # This file is part of memory_graph.
2 | # Copyright (c) 2023, Bas Terwijn.
3 | # SPDX-License-Identifier: BSD-2-Clause
4 |
5 |
6 | class List_View:
7 | def __init__(self, base_list, begin, end):
8 | self.base_list = base_list
9 | self.begin = max(0, begin)
10 | self.end = min(end, len(base_list))
11 |
12 | def __getitem__(self, index):
13 | if isinstance(index, slice):
14 | # Calculate new begin and end indices within the bounds of the current view
15 | start, stop, step = index.indices(self.end - self.begin)
16 | if step != 1:
17 | raise ValueError("List_View does not support slices with steps other than 1")
18 | # Adjust the indices relative to the base list
19 | new_start = self.begin + start
20 | new_end = self.begin + stop
21 | return List_View(self.base_list, new_start, new_end)
22 | elif isinstance(index, int):
23 | if index < 0 or index >= (self.end - self.begin):
24 | raise IndexError("list index out of range")
25 | return self.base_list[self.begin + index]
26 | else:
27 | raise TypeError("Invalid index type")
28 |
29 | def __setitem__(self, index, value):
30 | if index < 0 or index >= (self.end - self.begin):
31 | raise IndexError("list index out of range")
32 | self.base_list[self.begin + index] = value
33 |
34 | def __len__(self):
35 | return self.end - self.begin
36 |
37 | def __iter__(self):
38 | for i in range(self.begin, self.end):
39 | yield self.base_list[i]
40 |
41 | def __repr__(self):
42 | return f"List_View({self.base_list[self.begin:self.end]})"
43 |
44 | def test_list_vew():
45 | original_list = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
46 | list_view = List_View(original_list, 3, 8)
47 | print(list_view) # Output: List_View([3, 4, 5, 6, 7])
48 | print(list_view[1:4]) # Output: List_View([4, 5, 6])
49 | # 2D array
50 | n = 4
51 | data = [i for i in range(n*n)]
52 | list_views = [List_View(data, i, i+n) for i in range(0,len(data),n)]
53 | for row in list_views:
54 | print(row)
55 |
56 | if __name__ == "__main__":
57 | test_list_vew()
58 |
--------------------------------------------------------------------------------
/memory_graph/memory_to_nodes.py:
--------------------------------------------------------------------------------
1 | # This file is part of memory_graph.
2 | # Copyright (c) 2023, Bas Terwijn.
3 | # SPDX-License-Identifier: BSD-2-Clause
4 |
5 | from memory_graph.node_leaf import Node_Leaf
6 | from memory_graph.node_linear import Node_Linear
7 | from memory_graph.node_key_value import Node_Key_Value
8 |
9 | import memory_graph.utils as utils
10 | import memory_graph.config as config
11 |
12 | import graphviz
13 |
14 | def read_nodes(data):
15 |
16 | def data_to_node(data_type, data):
17 | if data_type in config.type_to_node: # for predefined types
18 | return config.type_to_node[data_type](data)
19 | elif utils.has_dict_attributes(data): # for user defined classes
20 | return Node_Key_Value(data, utils.filter_dict(utils.get_dict_attributes(data)) )
21 | elif utils.is_finite_iterable(data): # for lists, tuples, sets, ...
22 | return Node_Linear(data, data)
23 | else:
24 | return Node_Leaf(data, data)
25 |
26 | def memory_to_nodes_recursive(nodes, data, parent, parent_index):
27 | data_type = type(data)
28 | if not data_type in config.not_node_types or parent is None:
29 | data_id = id(data)
30 | if data_id in nodes:
31 | node = nodes[data_id]
32 | else:
33 | node = data_to_node(data_type, data)
34 | nodes[data_id] = node
35 | for index in node.get_children().indices_all():
36 | child = node.get_children()[index]
37 | memory_to_nodes_recursive(nodes, child, node, index)
38 | if not parent is None:
39 | node.add_parent_index(parent, parent_index)
40 |
41 | nodes = {}
42 | memory_to_nodes_recursive(nodes, data, None, None)
43 | root_id = id(data)
44 | return nodes, root_id
45 |
46 | # --------------------------------------------------------------------------------------------
47 |
48 | def get_max_type_depth(node_id, node):
49 | if node_id in config.type_to_depth:
50 | return config.type_to_depth[node_id]
51 | elif node.get_type() in config.type_to_depth:
52 | return config.type_to_depth[node.get_type()]
53 | return None
54 |
55 | def slice_nodes(nodes, root_id, max_graph_depth):
56 |
57 | def slice_nodes_recursive(nodes, node_id, id_to_slices, max_graph_depth):
58 | if max_graph_depth == 0 or node_id in id_to_slices:
59 | return
60 | if node_id in nodes:
61 | node = nodes[node_id]
62 | children = node.get_children()
63 | if children.is_empty():
64 | id_to_slices[node_id] = None
65 | else:
66 | slicer = node.get_slicer()
67 | slices = children.slice(slicer)
68 | id_to_slices[node_id] = slices
69 | if not node.is_hidden_node():
70 | max_graph_depth -= 1
71 | max_type_depth = get_max_type_depth(node_id, node)
72 | if max_type_depth:
73 | max_graph_depth = min(max_type_depth, max_graph_depth)
74 | for index in slices:
75 | slice_nodes_recursive(nodes, id(children[index]), id_to_slices, max_graph_depth)
76 | id_to_slices = {}
77 | slice_nodes_recursive(nodes, root_id, id_to_slices, max_graph_depth)
78 | return id_to_slices
79 |
80 | # --------------------------------------------------------------------------------------------
81 |
82 | def add_parent_indices(nodes, type_to_parent_indices, id_to_slices, max_missing_edges):
83 | #print('add_parent_indices type_to_parent_indices:',type_to_parent_indices)
84 | for _, parent_indices in type_to_parent_indices.items():
85 | dashed = len(parent_indices) > max_missing_edges
86 | for parent, index in parent_indices[0:max_missing_edges]:
87 | new_parent = False
88 | parent_id = parent.get_id()
89 | if not parent_id in id_to_slices:
90 | new_parent = True
91 | id_to_slices[parent_id] = parent.get_children().empty_slices()
92 | slices = id_to_slices[parent_id]
93 | slices.add_index(index, dashed=dashed)
94 | if new_parent:
95 | add_indices_to_parents(nodes, parent_id, id_to_slices, max_missing_edges)
96 |
97 | def add_indices_to_parents(nodes, node_id, id_to_slices, max_missing_edges):
98 | #print('add_indices_to_parents node_id:',node_id)
99 | type_to_parent_indices = {}
100 | parent_indices = nodes[node_id].get_parent_indices()
101 | for parent, indices in parent_indices.items():
102 | if parent is None:
103 | continue
104 | parent_type = parent.get_type()
105 | parent_id = parent.get_id()
106 | if (parent_type in type_to_parent_indices and
107 | len(type_to_parent_indices[parent_type]) > max_missing_edges): # early stop
108 | continue
109 | parent_slices = None
110 | if parent_id in id_to_slices:
111 | parent_slices = id_to_slices[parent_id]
112 | for index in indices:
113 | if parent_slices is None or not parent_slices.has_index(index):
114 | if not parent_type in type_to_parent_indices:
115 | type_to_parent_indices[parent_type] = []
116 | parent_indices = type_to_parent_indices[parent_type]
117 | if len(parent_indices) > max_missing_edges:
118 | break
119 | else:
120 | parent_indices.append((parent, index))
121 | add_parent_indices(nodes, type_to_parent_indices, id_to_slices, max_missing_edges)
122 |
123 | def add_missing_edges(nodes, id_to_slices, max_missing_edges=3):
124 | old_id_to_slices_keys = set(id_to_slices.keys())
125 | for node_id in old_id_to_slices_keys:
126 | add_indices_to_parents(nodes, node_id, id_to_slices, max_missing_edges)
127 | return id_to_slices
128 |
129 | # --------------------------------------------------------------------------------------------
130 |
131 | import memory_graph.config_helpers as config_helpers
132 |
133 | def create_depth_of_nodes(nodes, nodes_at_depth):
134 | depth_of_nodes = {}
135 | for node_id, depth in nodes_at_depth.items():
136 | node = nodes[node_id]
137 | if node_id in nodes and not node.is_hidden_node():
138 | if not depth in depth_of_nodes:
139 | depth_of_nodes[depth] = []
140 | depth_of_nodes[depth].append(node)
141 | return depth_of_nodes
142 |
143 | def add_subgraph(graphviz_graph, nodes_to_subgraph):
144 | new_node_names = [node.get_name() for node in nodes_to_subgraph]
145 | if len(new_node_names) > 1:
146 | graphviz_graph.body.append('subgraph { rank=same; '+ ' -> '.join(new_node_names) + '[weight='+str(config.graph_stability)+', style=invis]; }\n')
147 |
148 | def add_to_graphviz_graph(graphviz_graph, nodes, node, slices, id_to_slices, subgraphed_nodes, depth):
149 | html_table = node.get_html_table(nodes, slices, id_to_slices)
150 | edges = html_table.get_edges()
151 | color = config_helpers.get_color(node)
152 | border = 3 if node.is_root() else 1
153 | graphviz_graph.node(node.get_name(),
154 | html_table.to_string(border, color),
155 | xlabel=node.get_label(slices))
156 | # ------------ edges
157 | for parent,child,dashed in edges:
158 | graphviz_graph.edge(parent, child+':table', style='dashed' if dashed else 'solid')
159 |
160 | def build_graph_depth_first(graphviz_graph, nodes, node_id, id_to_slices, nodes_at_depth, subgraphed_nodes, depth):
161 | if node_id in id_to_slices:
162 | if node_id in nodes_at_depth:
163 | return
164 | nodes_at_depth[node_id] = depth
165 | node = nodes[node_id]
166 | children = node.get_children()
167 | slices = None
168 | if node_id in id_to_slices:
169 | slices = id_to_slices[node_id]
170 | if not slices is None:
171 | for index in slices:
172 | child_id = id(children[index])
173 | build_graph_depth_first(graphviz_graph, nodes, child_id, id_to_slices, nodes_at_depth, subgraphed_nodes, depth+1)
174 | if not node.is_hidden_node():
175 | add_to_graphviz_graph(graphviz_graph, nodes, node, slices, id_to_slices, subgraphed_nodes, depth)
176 |
177 | def build_graph(graphviz_graph, nodes, root_id, id_to_slices):
178 | nodes_at_depth = {}
179 | build_graph_depth_first(graphviz_graph, nodes, root_id, id_to_slices, nodes_at_depth, set(), 0)
180 | depth_of_nodes = create_depth_of_nodes(nodes, nodes_at_depth)
181 | #print('nodes_at_depth:',nodes_at_depth,'depth_of_nodes:', depth_of_nodes)
182 | for depth, depth_nodes in depth_of_nodes.items():
183 | add_subgraph(graphviz_graph, depth_nodes)
184 |
185 | def memory_to_nodes(data):
186 | nodes, root_id = read_nodes(data)
187 | #print('nodes:',nodes,'root_id:',root_id)
188 | id_to_slices = slice_nodes(nodes, root_id, config.max_graph_depth)
189 | #print('id_to_slices:',id_to_slices)
190 | id_to_slices = add_missing_edges(nodes, id_to_slices, config.max_missing_edges)
191 | #print('id_to_slices:',id_to_slices)
192 | graphviz_graph_attr = {}
193 | graphviz_node_attr = {'shape':'plaintext'}
194 | graphviz_edge_attr = {}
195 | graphviz_graph=graphviz.Digraph('memory_graph',
196 | graph_attr=graphviz_graph_attr,
197 | node_attr=graphviz_node_attr,
198 | edge_attr=graphviz_edge_attr)
199 | build_graph(graphviz_graph, nodes, root_id, id_to_slices)
200 | return graphviz_graph
201 |
--------------------------------------------------------------------------------
/memory_graph/node_base.py:
--------------------------------------------------------------------------------
1 | # This file is part of memory_graph.
2 | # Copyright (c) 2023, Bas Terwijn.
3 | # SPDX-License-Identifier: BSD-2-Clause
4 |
5 | import memory_graph.utils as utils
6 | import memory_graph.config as config
7 | import memory_graph.config_helpers as config_helpers
8 | from memory_graph.sequence import Sequence1D
9 |
10 | from abc import ABC, abstractmethod
11 |
12 | class Node_Base(ABC):
13 | """
14 | Node_Base represents a node in the memory graph. This base class has different subclasses for different types of nodes.
15 | """
16 |
17 | def __init__(self, data, children=None):
18 | """
19 | Create a Node_Base object.
20 | """
21 | self.data = data
22 | self.children = Sequence1D([]) if children is None else children
23 | self.parent_indices = {}
24 |
25 | def __repr__(self):
26 | """
27 | Return a string representation of the node showing the original data represented by the node.
28 | """
29 | return f'{self.get_type_name()}'
30 |
31 | def add_parent_index(self, parent, parent_index):
32 | """
33 | Add a parent to the node.
34 | """
35 | if not parent in self.parent_indices:
36 | self.parent_indices[parent] = []
37 | self.parent_indices[parent].append(parent_index)
38 |
39 | def is_root(self):
40 | """
41 | Return if the node is the root node.
42 | """
43 | return len(self.parent_indices) == 0
44 |
45 | def get_parent_indices(self):
46 | return self.parent_indices
47 |
48 | def get_id(self):
49 | """
50 | Return the id of the node.
51 | """
52 | return id(self.data)
53 |
54 | def __eq__(self, other):
55 | """
56 | Return if the node is equal to another node.
57 | """
58 | return self.get_id() == other.get_id()
59 |
60 | def __hash__(self):
61 | """
62 | Return the hash of the node.
63 | """
64 | return self.get_id()
65 |
66 | def get_data(self):
67 | """
68 | Return the original data represented by the node.
69 | """
70 | return self.data
71 |
72 | def get_type(self):
73 | """
74 | Return the type of the data represented by the node.
75 | """
76 | return type(self.data)
77 |
78 | def get_type_name(self):
79 | """
80 | Return the name of the type of the data represented by the node.
81 | """
82 | return utils.get_type_name(self.data)
83 |
84 | def get_children(self):
85 | """
86 | Return the children of the node. Initially the children are raw data, but
87 | later they too are converted to Node_Base by the Memory_visitor using the Node_Base 'transform' method.
88 | """
89 | return self.children
90 |
91 | def get_name(self):
92 | """
93 | Return a unique name for the node.
94 | """
95 | return f'node{self.get_id()}'
96 |
97 | def get_html_table(self, nodes, slices, id_to_slices):
98 | """
99 | Return the HTML_Table object that determines how the node is visualized in the graph.
100 | """
101 | from memory_graph.html_table import HTML_Table
102 | import memory_graph.node_base
103 | html_table = HTML_Table()
104 | self.fill_html_table(nodes, html_table, slices, id_to_slices)
105 | return html_table
106 |
107 | def get_slicer(self):
108 | return config_helpers.get_slicer(self, self.get_data())
109 |
110 | def is_hidden_node(self):
111 | """
112 | Return if the node is a hidden node in the graph.
113 | """
114 | from memory_graph.node_key_value import Node_Key_Value
115 | if self.get_type() is tuple:
116 | parent_indices = self.get_parent_indices()
117 | if len(parent_indices) == 1:
118 | first_parent = next(iter(parent_indices))
119 | if type(first_parent) is Node_Key_Value:
120 | return True
121 | return False
122 |
123 | # -------------------- Node_Base interface, overriden by subclasses --------------------
124 |
125 | @abstractmethod
126 | def fill_html_table(self, html_table, slices, id_to_slices):
127 | """
128 | Fill the HTML_Table object with each child of the node.
129 | """
130 | pass
131 |
132 | def get_label(self, slices):
133 | """
134 | Return a label for the node to be shown in the graph next to the HTML table.
135 | """
136 | return self.get_type_name()
137 |
--------------------------------------------------------------------------------
/memory_graph/node_key_value.py:
--------------------------------------------------------------------------------
1 | # This file is part of memory_graph.
2 | # Copyright (c) 2023, Bas Terwijn.
3 | # SPDX-License-Identifier: BSD-2-Clause
4 |
5 | from memory_graph.node_base import Node_Base
6 | from memory_graph.sequence import Sequence1D
7 |
8 | import memory_graph.config_helpers as config_helpers
9 |
10 | class Node_Key_Value(Node_Base):
11 | """
12 | Node_Key_Value (subclass of Node_Base) is a node that represents a node with key-value
13 | pairs (tuples) as children. This node type mainly used for dictionaries and classes.
14 | Each child is made a Hidden_Node_Base so that each tuple is not shown as a separate node
15 | but instead as a key,value pair in the current node.
16 | """
17 |
18 | def __init__(self, data, children):
19 | """
20 | Create a Node_Key_Value object. Use a Slicer to slice the children so the
21 | Node_Base will not get too big or have too many childeren in the graph.
22 | """
23 | super().__init__(data, Sequence1D(children))
24 |
25 | def has_references(self, nodes, slices, id_to_slices):
26 | """
27 | Return if the node has references to other nodes.
28 | """
29 | for index in slices:
30 | child_id = id(self.children[index])
31 | child = nodes[child_id]
32 | key_id = id(child.get_children()[0])
33 | if key_id in nodes:
34 | key = nodes[key_id]
35 | if not key.is_hidden_node():
36 | return True
37 | value_id = id(child.get_children()[1])
38 | if value_id in nodes:
39 | value = nodes[value_id]
40 | if not value.is_hidden_node():
41 | return True
42 | return False
43 |
44 | def is_vertical(self, nodes, slices, id_to_slices):
45 | """
46 | Return if the node is vertical or horizontal based on the orientation of the children.
47 | """
48 | vertical = config_helpers.get_vertical_orientation(self, None)
49 | if vertical is None:
50 | vertical = not self.has_references(nodes, slices, id_to_slices)
51 | return vertical
52 |
53 | def fill_html_table(self, nodes, html_table, slices, id_to_slices):
54 | """
55 | Fill the html_table with the children of the Node_Base.
56 | """
57 | if slices is None:
58 | return
59 | vertical = self.is_vertical(nodes, slices, id_to_slices)
60 | if vertical:
61 | self.fill_html_table_vertical(html_table, nodes, slices, id_to_slices)
62 | else:
63 | self.fill_html_table_horizontal(html_table, nodes, slices, id_to_slices)
64 |
65 | @staticmethod
66 | def get_value_dashed(nodes, child, index, id_to_slices):
67 | grandchild = child[index]
68 | child_id = id(child)
69 | is_dashed = False
70 | if child_id in id_to_slices:
71 | slices = id_to_slices[child_id]
72 | if not slices is None:
73 | is_dashed = slices.is_dashed(index)
74 | return grandchild, is_dashed
75 |
76 | def fill_html_table_vertical(self, html_table, nodes, slices, id_to_slices):
77 | """
78 | Helper function to fill the html_table with the children of the Node_Base in vertical orientation.
79 | """
80 | for index in slices.table_iter(self.children.size()):
81 | if index>=0:
82 | child = self.children[index]
83 | key, is_dashed = self.get_value_dashed(nodes, child,0,id_to_slices)
84 | html_table.add_entry(self, nodes, key, id_to_slices, rounded=True, dashed=is_dashed)
85 | value, is_dashed = self.get_value_dashed(nodes, child,1,id_to_slices)
86 | html_table.add_entry(self, nodes, value, id_to_slices, dashed=is_dashed)
87 | else:
88 | html_table.add_dots(rounded=True)
89 | html_table.add_dots()
90 | html_table.add_new_line()
91 |
92 | def fill_html_table_horizontal(self, html_table, nodes, slices, id_to_slices):
93 | """
94 | Helper function to fill the html_table with the children of the Node_Base in horizontal orientation.
95 | """
96 | for index in slices.table_iter(self.children.size()):
97 | if index>=0:
98 | child = self.children[index]
99 | key, is_dashed = self.get_value_dashed(nodes, child,0,id_to_slices)
100 | html_table.add_entry(self, nodes, key, id_to_slices, rounded=True, dashed=is_dashed)
101 | else:
102 | html_table.add_dots(rounded=True)
103 | html_table.add_new_line()
104 | for index in slices.table_iter(self.children.size()):
105 | if index>=0:
106 | child = self.children[index]
107 | value, is_dashed = self.get_value_dashed(nodes, child,1,id_to_slices)
108 | html_table.add_entry(self, nodes, value, id_to_slices, dashed=is_dashed)
109 | else:
110 | html_table.add_dots()
111 |
112 | def get_label(self, slices):
113 | """
114 | Return a label for the node to be shown in the graph next to the HTML table.
115 | """
116 | type_name = self.get_type_name()
117 | if slices is None:
118 | return f'{type_name}'
119 | size = self.get_children().size()
120 | s = slices.get_slices()
121 | if len(s) == 1:
122 | if s[0][1] - s[0][0] == size:
123 | return f'{type_name}'
124 | return f'{type_name} {size}'
125 |
--------------------------------------------------------------------------------
/memory_graph/node_leaf.py:
--------------------------------------------------------------------------------
1 | # This file is part of memory_graph.
2 | # Copyright (c) 2023, Bas Terwijn.
3 | # SPDX-License-Identifier: BSD-2-Clause
4 |
5 | from memory_graph.node_base import Node_Base
6 |
7 | class Node_Leaf(Node_Base):
8 | """
9 | Node_Leaf (subclass of Node_Base) is a leaf node with no children but a value.
10 | """
11 |
12 | def __init__(self, data, value):
13 | """
14 | Create a Node_Leaf object.
15 | """
16 | super().__init__(data)
17 | self.value = value
18 |
19 | def fill_html_table(self, nodes, html_table, slices, id_to_slices):
20 | """
21 | Fill the html_table with the children of the Node_Base.
22 | """
23 | html_table.add_value(self.value)
24 |
--------------------------------------------------------------------------------
/memory_graph/node_linear.py:
--------------------------------------------------------------------------------
1 | # This file is part of memory_graph.
2 | # Copyright (c) 2023, Bas Terwijn.
3 | # SPDX-License-Identifier: BSD-2-Clause
4 |
5 | from memory_graph.node_base import Node_Base
6 | from memory_graph.sequence import Sequence1D
7 |
8 | import memory_graph.config_helpers as config_helpers
9 |
10 | class Node_Linear(Node_Base):
11 | """
12 | Node_Linear (subclass of Node_Base) is a node that represents a linear sequence
13 | of data used for most iterable type like list, tuple, set, etc.
14 | """
15 |
16 | def __init__(self, data, children=None):
17 | """
18 | Create a Node_Linear object. Use a Slicer to slice the children so the
19 | Node_Base will not get to big or have too many childeren in the graph.
20 | """
21 | super().__init__(data, Sequence1D(children))
22 |
23 | def has_references(self, nodes, slices):
24 | """
25 | Return if the node has references to other nodes.
26 | """
27 | for index in slices:
28 | child_id = id(self.children[index])
29 | if child_id in nodes:
30 | child = nodes[child_id]
31 | if not child.is_hidden_node():
32 | return True
33 | return False
34 |
35 | def is_vertical(self, nodes, slices, id_to_slices):
36 | """
37 | Return if the node is vertical or horizontal based on the orientation of the children.
38 | """
39 | vertical = config_helpers.get_vertical_orientation(self, None)
40 | if vertical is None:
41 | vertical = not self.has_references(nodes, slices)
42 | return vertical
43 |
44 | def fill_html_table(self, nodes, html_table, slices, id_to_slices):
45 | """
46 | Fill the html_table with the children of the Node_Base.
47 | """
48 | if slices is None:
49 | return
50 | if self.is_vertical(nodes, slices, id_to_slices):
51 | self.fill_html_table_vertical(html_table, nodes, slices, id_to_slices)
52 | else:
53 | self.fill_html_table_horizontal(html_table, nodes, slices, id_to_slices)
54 |
55 | def fill_html_table_vertical(self, html_table, nodes, slices, id_to_slices):
56 | """
57 | Helper function to fill the html_table with the children of the Node_Base in vertical orientation.
58 | """
59 | for index in slices.table_iter(self.children.size()):
60 | if index>=0:
61 | html_table.add_index(index)
62 | child = self.children[index]
63 | html_table.add_entry(self, nodes, child, id_to_slices, dashed=slices.is_dashed(index))
64 | html_table.add_new_line()
65 | else:
66 | html_table.add_value('', border=0)
67 | html_table.add_dots()
68 | html_table.add_new_line()
69 |
70 | def fill_html_table_horizontal(self, html_table, nodes, slices, id_to_slices):
71 | """
72 | Helper function to fill the html_table with the children of the Node_Base in horizontal orientation.
73 | """
74 | for index in slices.table_iter(self.children.size()):
75 | if index>=0:
76 | html_table.add_index(index)
77 | else:
78 | html_table.add_value('', border=0)
79 | html_table.add_new_line()
80 | for index in slices.table_iter(self.children.size()):
81 | if index>=0:
82 | child = self.children[index]
83 | html_table.add_entry(self, nodes, child, id_to_slices, dashed=slices.is_dashed(index))
84 | else:
85 | html_table.add_dots()
86 |
87 | def get_label(self, slices):
88 | """
89 | Return a label for the node to be shown in the graph next to the HTML table.
90 | """
91 | type_name = self.get_type_name()
92 | if slices is None:
93 | return f'{type_name}'
94 | size = self.get_children().size()
95 | s = slices.get_slices()
96 | if len(s) == 1:
97 | if s[0][1] - s[0][0] == size:
98 | return f'{type_name}'
99 | return f'{type_name} {size}'
100 |
101 |
--------------------------------------------------------------------------------
/memory_graph/node_table.py:
--------------------------------------------------------------------------------
1 | # This file is part of memory_graph.
2 | # Copyright (c) 2023, Bas Terwijn.
3 | # SPDX-License-Identifier: BSD-2-Clause
4 |
5 | from memory_graph.node_base import Node_Base
6 | from memory_graph.sequence import Sequence2D
7 | from memory_graph.list_view import List_View
8 |
9 | class Node_Table(Node_Base):
10 | """
11 | Node_Table (subclass of Node_Base) is a node that represents a 2D table of data used for
12 | example for Numpy arrays and Pandas DataFrames.
13 | """
14 |
15 | def __init__(self, data, children, data_width=None, row_names=None, col_names=None):
16 | """
17 | Create a Node_Table object. Use a Slicer to slice the children so the Node_Base
18 | will not get to big or have too many childeren in the graph.
19 | """
20 | self.row_names = row_names
21 | self.col_names = col_names
22 | if data_width is None:
23 | super().__init__(data, Sequence2D(children))
24 | else:
25 | list_views = [List_View(children, i, i+data_width) for i in range(0,len(children),data_width)]
26 | super().__init__(data, Sequence2D(list_views))
27 |
28 | def add_index_or_name(self, html_table, index, names):
29 | if not names is None and index < len(names):
30 | html_table.add_value(names[index], rounded=1, border=1)
31 | else:
32 | html_table.add_index(index)
33 |
34 | def fill_html_table(self, nodes, html_table, slices, id_to_slices):
35 | """
36 | Fill the html_table with the children of the Node_Base.
37 | """
38 | if slices is None or slices.is_empty():
39 | return
40 | children = self.children
41 | children_size = children.size()
42 | children_width = children_size[1]
43 | col_slices = slices.get_col_slices()
44 |
45 | # use column indices for header row
46 | html_table.add_value('', border=0)
47 | for coli in col_slices.table_iter(children_width):
48 | if coli == -1:
49 | html_table.add_value('', border=0)
50 | else:
51 | self.add_index_or_name(html_table, coli, self.col_names)
52 | html_table.add_new_line()
53 |
54 | # add remaing rows
55 | first_col = True
56 | for index in slices.table_iter(children_size):
57 | rowi, coli = index
58 | if first_col and not coli==-3:
59 | first_col = False
60 | self.add_index_or_name(html_table, rowi, self.row_names)
61 | if coli == -1:
62 | html_table.add_dots()
63 | elif coli == -2:
64 | html_table.add_new_line()
65 | first_col = True
66 | elif coli == -3:
67 | html_table.add_new_line()
68 | html_table.add_value('', border=0)
69 | for _ in range (html_table.get_max_column()-1):
70 | html_table.add_dots()
71 | html_table.add_new_line()
72 | first_col = True
73 | else:
74 | child = children[index]
75 | html_table.add_entry(self, nodes, child, id_to_slices, dashed=slices.is_dashed(index))
76 |
77 | def get_label(self, slices):
78 | """
79 | Return a label for the node to be shown in the graph next to the HTML table.
80 | """
81 | size = self.get_children().size()
82 | return f'{self.get_type_name()} {size[0]}⨯{size[1]}'
83 |
84 |
--------------------------------------------------------------------------------
/memory_graph/sequence.py:
--------------------------------------------------------------------------------
1 | # This file is part of memory_graph.
2 | # Copyright (c) 2023, Bas Terwijn.
3 | # SPDX-License-Identifier: BSD-2-Clause
4 |
5 | from abc import ABC, abstractmethod
6 |
7 | from memory_graph.slices import Slices1D, Slices2D
8 | import memory_graph.utils as utils
9 |
10 | class Sequence(ABC):
11 |
12 | @abstractmethod
13 | def is_empty(self):
14 | pass
15 |
16 | @abstractmethod
17 | def size(self):
18 | pass
19 |
20 | @abstractmethod
21 | def empty_slices(self):
22 | pass
23 |
24 | @abstractmethod
25 | def slice(self, slicer):
26 | pass
27 |
28 | @abstractmethod
29 | def indices_all(self):
30 | pass
31 |
32 | @abstractmethod
33 | def __getitem__(self, index):
34 | pass
35 |
36 | @abstractmethod
37 | def __setitem__(self, index, value):
38 | pass
39 |
40 | class Sequence1D(Sequence):
41 |
42 | def __init__(self, data):
43 | self.data = utils.make_sliceable(data)
44 |
45 | def __repr__(self):
46 | return f'Sequence1D: {self.data}'
47 |
48 | def is_empty(self):
49 | return len(self.data) == 0
50 |
51 | def size(self):
52 | return len(self.data)
53 |
54 | def empty_slices(self):
55 | return Slices1D()
56 |
57 | def slice(self, slicer):
58 | return slicer.get_slices( len(self.data) )
59 |
60 | def indices_all(self):
61 | for i in range(len(self.data)):
62 | yield i
63 |
64 | def __getitem__(self, index):
65 | return self.data[index]
66 |
67 | def __setitem__(self, index, value):
68 | self.data[index] = value
69 |
70 | class Sequence2D(Sequence):
71 |
72 | def __init__(self, data):
73 | self.data = utils.make_sliceable(data)
74 |
75 | def __repr__(self):
76 | return f'Sequence2D: {self.data}'
77 |
78 | def is_empty(self):
79 | return len(self.data) == 0
80 |
81 | def size(self):
82 | l1, l2 = len(self.data), 0
83 | if l1 > 0:
84 | l2 = len(self.data[0])
85 | return l1, l2
86 |
87 | def empty_slices(self):
88 | return Slices2D()
89 |
90 | def slice(self, slicer0):
91 | if type(slicer0) is tuple:
92 | slicer0, slicer1 = slicer0
93 | else:
94 | slicer1 = slicer0
95 | slices0 = slicer0.get_slices( len(self.data) )
96 | slices1 = slicer1.get_slices( len(self.data[0]) )
97 | return Slices2D(slices0, slices1)
98 |
99 | def indices_all(self):
100 | len0 = len(self.data)
101 | if len0 > 0:
102 | len1 = len(self.data[0])
103 | for y in range(len0):
104 | for x in range(len1):
105 | yield (y,x)
106 |
107 | def __getitem__(self, index):
108 | return self.data[index[0]][index[1]]
109 |
110 | def __setitem__(self, index, value):
111 | self.data[index[0]][index[1]] = value
112 |
--------------------------------------------------------------------------------
/memory_graph/slicer.py:
--------------------------------------------------------------------------------
1 | # This file is part of memory_graph.
2 | # Copyright (c) 2023, Bas Terwijn.
3 | # SPDX-License-Identifier: BSD-2-Clause
4 |
5 | from memory_graph.slices import Slices1D
6 | import memory_graph.utils as utils
7 |
8 | class Slicer:
9 |
10 | def __init__(self, begin=None, end=None, middle=None) -> None:
11 | self.begin = begin
12 | self.end = end
13 | self.middle = middle
14 | if not self.middle is None:
15 | self.end, self.middle = self.middle, self.end
16 |
17 | def __repr__(self) -> str:
18 | return f"Slicer({self.begin},{self.middle},{self.end})"
19 |
20 | def get_slices(self, length):
21 | slices1d = Slices1D()
22 | if self.begin is None:
23 | slices1d.add_slice([0, length])
24 | else:
25 | if isinstance(self.begin, float):
26 | slices1d.add_slice([0,
27 | min(length,utils.my_round(length*self.begin))])
28 | else:
29 | slices1d.add_slice([0,
30 | min(length,self.begin)])
31 | if not self.middle is None:
32 | mid = length/2
33 | if isinstance(self.middle, float):
34 | half = length*self.middle/2
35 | else:
36 | half = self.middle/2
37 | slices1d.add_slice([max(0,utils.my_round(mid-half)),
38 | min(length,utils.my_round(mid+half))])
39 | if not self.end is None:
40 | if isinstance(self.end, float):
41 | slices1d.add_slice([max(0,utils.my_round(length-length*self.end)),
42 | length])
43 | else:
44 | slices1d.add_slice([max(0,length-self.end),
45 | length])
46 | return slices1d
47 |
--------------------------------------------------------------------------------
/memory_graph/slices.py:
--------------------------------------------------------------------------------
1 | # This file is part of memory_graph.
2 | # Copyright (c) 2023, Bas Terwijn.
3 | # SPDX-License-Identifier: BSD-2-Clause
4 |
5 | from abc import ABC, abstractmethod
6 |
7 | import bisect
8 | import copy
9 |
10 | from memory_graph.slices_iterator import Slices_Iterator, Slices_Iterator1D, Slices_Iterator2D
11 | from memory_graph.slices_table_iterator import Slices_Table_Iterator1D, Slices_Table_Iterator2D
12 |
13 |
14 | class Slices(ABC):
15 |
16 | def __init__(self):
17 | self.dashed = set()
18 |
19 | def __repr__(self) -> str:
20 | return f"dashed: {self.dashed}"
21 |
22 | def is_dashed(self, index):
23 | return index in self.dashed
24 |
25 | @abstractmethod
26 | def __iter__(self):
27 | pass
28 |
29 | @abstractmethod
30 | def has_index(self, index):
31 | pass
32 |
33 | @abstractmethod
34 | def add_index(self, index):
35 | pass
36 |
37 | @abstractmethod
38 | def table_iter(self, size):
39 | pass
40 |
41 | @abstractmethod
42 | def is_empty(self):
43 | pass
44 |
45 |
46 | class Slices1D(Slices):
47 |
48 | def __init__(self, slices=None) -> None:
49 | super().__init__()
50 | self.slices = []
51 | if not slices is None:
52 | for i in slices:
53 | self.add_slice(i)
54 |
55 | def __repr__(self) -> str:
56 | return f"Slices1D({self.slices}) "+super().__repr__()
57 |
58 | def get_iter(self, length):
59 | return Slices_Iterator(self.slices, length)
60 |
61 | def copy(self):
62 | s = Slices1D()
63 | s.slices = copy.deepcopy(self.slices)
64 | s.dashed = copy.deepcopy(self.dashed)
65 | return s
66 |
67 | def get_slices(self):
68 | return self.slices
69 |
70 | def has_index(self, index):
71 | for i in self.slices:
72 | if i[0] <= index and index < i[1]:
73 | return True
74 | return False
75 |
76 | def add_slice(self, begin_end, remove_interposed_dots=1):
77 | i0, i1 = begin_end
78 | if i1 <= i0:
79 | return False
80 | insert0 = bisect.bisect_right([s[0] for s in self.slices], i0)
81 | insert1 = bisect.bisect_left([s[1] for s in self.slices], i1)
82 | merge_begin, merge_end = False, False
83 | if insert0 > 0:
84 | if self.slices[insert0-1][1] >= (i0 - remove_interposed_dots):
85 | merge_begin = True
86 | if insert1 < len(self.slices):
87 | if self.slices[insert1][0] <= (i1 + remove_interposed_dots):
88 | merge_end = True
89 | if merge_begin and merge_end:
90 | if insert0 - insert1 == 1: # no slices changed
91 | return False
92 | self.slices[insert0-1][1] = self.slices[insert1][1]
93 | del self.slices[insert0:insert1+1]
94 | elif merge_begin:
95 | self.slices[insert0 - 1][1] = max(self.slices[insert1-1][1],
96 | begin_end[1])
97 | del self.slices[insert0:insert1]
98 | elif merge_end:
99 | self.slices[insert1][0] = min(self.slices[insert0][0],
100 | begin_end[0])
101 | del self.slices[insert0:insert1]
102 | else:
103 | del self.slices[insert0:insert1]
104 | self.slices.insert(insert0, begin_end)
105 | return True
106 |
107 | def __iter__(self):
108 | return Slices_Iterator1D(self)
109 |
110 | def has_index(self, index):
111 | insert = bisect.bisect_right([s[0] for s in self.slices], index)
112 | # print('insert:',insert,'index:',index,'slices:',self.slices)
113 | if insert == 0:
114 | return False
115 | i0, i1 = self.slices[insert-1]
116 | return i0 <= index and index < i1
117 |
118 | def add_index(self, index, dashed=False):
119 | self.add_slice([index, index+1], 0)
120 | if dashed:
121 | self.dashed.add(index)
122 |
123 | def table_iter(self, size):
124 | return Slices_Table_Iterator1D(self, size)
125 |
126 | def is_empty(self):
127 | return len(self.slices) == 0
128 |
129 |
130 | class Slices2D(Slices):
131 |
132 | def __init__(self, row_slices=None, col_slices=None) -> None:
133 | super().__init__()
134 | self.row_slices = Slices1D() if row_slices is None else row_slices
135 | self.col_slices = Slices1D() if col_slices is None else col_slices
136 |
137 | def __repr__(self):
138 | s = 'Sices2D:\n'
139 | s += 'row_slices:' + str(self.row_slices) + '\n'
140 | s += 'col_slices:' + str(self.col_slices) + '\n'
141 | s += super().__repr__() + '\n'
142 | return s
143 |
144 | def get_row_slices(self):
145 | return self.row_slices
146 |
147 | def get_col_slices(self):
148 | return self.col_slices
149 |
150 | def __iter__(self):
151 | return Slices_Iterator2D(self)
152 |
153 | def has_index(self, index):
154 | i0, i1 = index
155 | return self.row_slices.has_index(i0) and self.col_slices.has_index(i1)
156 |
157 | def add_index(self, index, dashed=False):
158 | i0, i1 = index
159 | self.row_slices.add_slice([i0, i0+1], 0)
160 | self.col_slices.add_slice([i1, i1+1], 0)
161 | if dashed:
162 | self.dashed.add((i0, i1))
163 |
164 | def table_iter(self, size):
165 | return Slices_Table_Iterator2D(self, size)
166 |
167 | def is_empty(self):
168 | return len(self.row_slices.slices) == 0
169 |
--------------------------------------------------------------------------------
/memory_graph/slices_iterator.py:
--------------------------------------------------------------------------------
1 | # This file is part of memory_graph.
2 | # Copyright (c) 2023, Bas Terwijn.
3 | # SPDX-License-Identifier: BSD-2-Clause
4 |
5 |
6 | from abc import ABC, abstractmethod
7 |
8 | class Slices_Iterator(ABC):
9 |
10 | @abstractmethod
11 | def __iter__(self):
12 | pass
13 |
14 | @abstractmethod
15 | def __next__(self):
16 | pass
17 |
18 | class Slices_Iterator1D(Slices_Iterator):
19 |
20 | def __init__(self, slices1d):
21 | self.slices = slices1d
22 | self.gen = self.generate()
23 |
24 | def __iter__(self):
25 | return self
26 |
27 | def generate(self):
28 | slices = self.slices.get_slices()
29 | for si in range(len(slices)):
30 | for i in range(slices[si][0], slices[si][1]):
31 | yield i
32 |
33 | def __next__(self):
34 | return next(self.gen)
35 |
36 | class Slices_Iterator2D(Slices_Iterator):
37 |
38 | def __init__(self, slices2d):
39 | self.slices = slices2d
40 | self.gen = self.generate()
41 |
42 | def __iter__(self):
43 | return self
44 |
45 | def generate(self):
46 | row_slices = self.slices.get_row_slices().get_slices()
47 | col_slices = self.slices.get_col_slices().get_slices()
48 | for row_slice in row_slices:
49 | for row_i in range(row_slice[0], row_slice[1]):
50 | for col_slice in col_slices:
51 | for col_i in range(col_slice[0], col_slice[1]):
52 | yield (row_i, col_i)
53 |
54 | def __next__(self):
55 | return next(self.gen)
56 |
--------------------------------------------------------------------------------
/memory_graph/slices_table_iterator.py:
--------------------------------------------------------------------------------
1 | # This file is part of memory_graph.
2 | # Copyright (c) 2023, Bas Terwijn.
3 | # SPDX-License-Identifier: BSD-2-Clause
4 |
5 | from abc import ABC, abstractmethod
6 |
7 | class Slices_Table_Iterator(ABC):
8 |
9 | @abstractmethod
10 | def __iter__(self):
11 | pass
12 |
13 | @abstractmethod
14 | def __next__(self):
15 | pass
16 |
17 | class Slices_Table_Iterator1D(Slices_Table_Iterator):
18 |
19 | def __init__(self, slices1d, size):
20 | self.slices = slices1d
21 | self.size = size
22 | self.gen = self.generate()
23 |
24 | def __iter__(self):
25 | return self
26 |
27 | def generate(self):
28 | slices = self.slices.get_slices()
29 | if len(slices) > 0 and slices[0][0] > 0:
30 | yield -1
31 | for slice in slices:
32 | for i in range(slice[0], slice[1]):
33 | yield i
34 | if i < self.size-1:
35 | yield -1
36 |
37 | def __next__(self):
38 | return next(self.gen)
39 |
40 | class Slices_Table_Iterator2D(Slices_Table_Iterator):
41 |
42 | def __init__(self, slices2d, size):
43 | self.slices = slices2d
44 | self.size = size
45 | self.gen = self.generate()
46 |
47 | def __iter__(self):
48 | return self
49 |
50 | def generate(self):
51 | row_slices = self.slices.get_row_slices().get_slices()
52 | col_slices = self.slices.get_col_slices().get_slices()
53 | first_row_slice = True
54 | for row_slice in row_slices:
55 | if first_row_slice:
56 | if len(row_slices) > 0 and row_slice[0] > 0:
57 | yield (-3, -3)
58 | first_col_slice = False
59 | else:
60 | yield (-3, -3)
61 | for row_i in range(row_slice[0], row_slice[1]):
62 | first_col_slice = True
63 | for col_slice in col_slices:
64 | if first_col_slice:
65 | if len(col_slices) > 0 and col_slice[0] > 0:
66 | yield (row_i, -1)
67 | first_col_slice = False
68 | else:
69 | yield (row_i, -1)
70 | for col_i in range(col_slice[0], col_slice[1]):
71 | yield (row_i, col_i)
72 | if col_i < self.size[1]-1:
73 | yield (row_i, -1)
74 | yield (row_i, -2)
75 | if len(row_slices)>0 and row_i < self.size[0]-1:
76 | yield (row_i, -3)
77 |
78 | def __next__(self):
79 | return next(self.gen)
80 |
--------------------------------------------------------------------------------
/memory_graph/test.py:
--------------------------------------------------------------------------------
1 | # This file is part of memory_graph.
2 | # Copyright (c) 2023, Bas Terwijn.
3 | # SPDX-License-Identifier: BSD-2-Clause
4 |
5 | """ Some test data to make memory graph of for test purposes. """
6 | import memory_graph.utils as utils
7 | import memory_graph.config as config
8 |
9 | import numpy as np
10 | import pandas as pd
11 | import memory_graph.extension_numpy
12 | import memory_graph.extension_pandas
13 |
14 | from memory_graph.node_table import Node_Table
15 |
16 | import random
17 |
18 | def test_singular(fun):
19 | data = 100
20 | fun(data)
21 |
22 | def test_linear(fun):
23 | data = [None, True, 1, 2.2, complex(3,4), 'hello this is a very long string that should be cut off at some point.']
24 | fun(data)
25 |
26 | def test_linears(fun):
27 | data = [(1,2), [3,4], {5,6}, frozenset((7,8)), {9:'nine', 10:'ten'} , bytes('11', 'utf-8'), bytearray('12', 'utf-8')]
28 | data.append( [i for i in range(20)] )
29 | fun(data)
30 |
31 | def test_colors(fun):
32 | data1 = [None, True, 1, 2.2, complex(3,4), 'hello']
33 | class My_Class:
34 | class_var = 1
35 | def __init__(self):
36 | self.var=2
37 | data2 = [(1,2), [3,4], {5,6}, frozenset((7,8)), {9:'9', 10:'10'} , bytes('11', 'utf-8'), bytearray('12', 'utf-8'), My_Class(), My_Class]
38 | restore = config.not_node_types.copy()
39 | config.not_node_types.clear()
40 | fun([data1, data2])
41 | config.not_node_types = restore
42 |
43 | def test_empty_linear(fun):
44 | data = [tuple(), list(), set(), frozenset(), dict() , bytes(), bytearray()]
45 | fun(data)
46 |
47 | def test_nested_list(fun):
48 | data = utils.nested_list([2,2,2,2,2,2,2])
49 | fun(data)
50 |
51 | def test_key_value(fun):
52 | data1 = {1:'a', 2:'b', 3:'c', 4:'d'}
53 | data2 = {10:100, 20:200, 30:300, 40:400}
54 | data2[50] = ('c','c')
55 | data2[60] = data1
56 | data = {'first':data1, 'second':data2}
57 | fun(data)
58 |
59 | def test_class(fun):
60 | class My_Class1:
61 | def __init__(self):
62 | self.foo1=10
63 | self.bar1=20
64 | class My_Class2:
65 | def __init__(self):
66 | self.foo2=10
67 | self.bar2=20
68 | data = [My_Class1(), My_Class2()]
69 | fun(data)
70 |
71 | def test_class_vars(fun):
72 | class My_Class1:
73 | class_var1 = 'a'
74 | class_var2 = 'b'
75 | def __init__(self):
76 | self.var1=10
77 | self.var2=20
78 | def my_method(self):
79 | return 100
80 | m = My_Class1()
81 | data = locals()
82 | fun(data)
83 |
84 | def test_share_tuple(fun):
85 | class My_Class:
86 | def __init__(self):
87 | self.a=1
88 | data = [('a',1), ('a',1), {'a':1}, {'a':1}, My_Class(),My_Class()]
89 | fun(data)
90 |
91 | def test_share_children(fun):
92 | a=['a']
93 | b=['b']
94 | c=['c']
95 | d=['d']
96 | data = [ [a,b,c,], [a,c,d] ]
97 | fun(data)
98 |
99 | def test_list_split(fun):
100 | data = [ list(range(i)) for i in range(50) ]
101 | fun(data)
102 |
103 | def test_key_value_split(fun):
104 | data = { i:i*10 for i in range(1,20)}
105 | fun(data)
106 |
107 | def test_table(fun):
108 | class My_Table:
109 | def __init__(self,size):
110 | self.size=size
111 | self.data = [i for i in range(size[0]*size[1])]
112 | data = My_Table((15,15))
113 | config.type_to_color[My_Table] = 'plum1'
114 | config.type_to_node[My_Table] = lambda data: (
115 | Node_Table(data, data.data , data.size[0],
116 | #column_names = [f'col{i}' for i in range(data.size[1])],
117 | #row_names = [f'row{i}' for i in range(data.size[0])]
118 | )
119 | )
120 | fun(data)
121 |
122 | def test_numpy(fun):
123 | a = np.array([1, 2, 3])
124 | print(a, type(a))
125 | data = [
126 | np.array([1, 2, 3]),
127 | np.matrix([[i*3+j for j in range(20)] for i in range(3)]),
128 | np.random.rand(20,20)
129 | ]
130 | #data = np.matrix('1 2; 3 4')
131 | fun(data)
132 |
133 | def test_pandas(fun):
134 | data = [
135 | pd.DataFrame({ "calories": [420, 380, 390],
136 | "duration": [50, 40, 45]
137 | }),
138 |
139 | pd.DataFrame({ 'Name' : [ 'Tom', 'Anna', 'Steve', 'Lisa'],
140 | 'Age' : [ 28, 34, 29, 42],
141 | 'Length' : [ 1.70, 1.66, 1.82, 1.73] },
142 | index=['one', 'two', 'three', 'four']),
143 |
144 | pd.Series( [i for i in range(20)] )
145 | ]
146 | fun(data)
147 |
148 | def example_function(a):
149 | return a*10
150 |
151 | class Example_Class:
152 | class_var1 = 100
153 | class_var2 = 200
154 | def __init__(self):
155 | self.a=1
156 | self.b=2
157 | def example_method(self):
158 | return self.a+self.b
159 |
160 | def test_function():
161 | return 10
162 |
163 | def test_different_types(fun):
164 | object = Example_Class()
165 | object_type = type(object)
166 | func = test_function
167 | func_type = type(func)
168 | method = Example_Class.example_method
169 | method_type = type(method)
170 | lambda_fun = lambda x: x*10
171 | lambda_fun_type = type(lambda_fun)
172 | data = memory_graph.stack()
173 | fun(data)
174 |
175 |
176 | class Node:
177 | shared_data = [1,2,3]
178 |
179 | def __init__(self, value):
180 | self.smaller = None
181 | self.shared = Node.shared_data
182 | self.value = value
183 | self.larger = None
184 |
185 | class BinTree:
186 |
187 | def __init__(self):
188 | self.root = None
189 |
190 | def insert_recursive(self, node, value):
191 | nn = None
192 | if value < node.value:
193 | if node.smaller is None:
194 | nn = Node(value)
195 | node.smaller = nn
196 | else:
197 | nn = self.insert_recursive(node.smaller, value)
198 | else:
199 | if node.larger is None:
200 | nn = Node(value)
201 | node.larger = nn
202 | else:
203 | nn = self.insert_recursive(node.larger, value)
204 | return nn
205 |
206 | def insert(self, value):
207 | nn = None
208 | if self.root is None:
209 | nn = Node(value)
210 | self.root = nn
211 | else:
212 | nn = self.insert_recursive(self.root, value)
213 | return nn
214 |
215 | def test_missing_edges(fun):
216 | random.seed(0)
217 | config.max_graph_depth = 7
218 | config.max_missing_edges = 5
219 | tree = BinTree()
220 | last_node = None
221 | n = 200
222 | for i in range(n):
223 | last_node = tree.insert(random.randint(0,n*10))
224 | fun( memory_graph.stack() )
225 |
226 | def test_all(fun):
227 | pass
228 | test_singular(fun)
229 | test_linear(fun)
230 | test_linears(fun)
231 | test_colors(fun)
232 | test_empty_linear(fun)
233 | test_nested_list(fun)
234 | test_key_value(fun)
235 | test_class(fun)
236 | test_class_vars(fun)
237 | test_share_tuple(fun)
238 | test_share_children(fun)
239 | test_list_split(fun)
240 | test_key_value_split(fun)
241 | test_table(fun)
242 | test_numpy(fun)
243 | test_pandas(fun)
244 | test_different_types(fun)
245 | test_missing_edges(fun)
246 |
--------------------------------------------------------------------------------
/memory_graph/test_max_graph_depth.py:
--------------------------------------------------------------------------------
1 | # This file is part of memory_graph.
2 | # Copyright (c) 2023, Bas Terwijn.
3 | # SPDX-License-Identifier: BSD-2-Clause
4 |
5 | import memory_graph as mg
6 |
7 | def build_nested_list(depth = 15):
8 | first = [1,2]
9 | last = first
10 | if depth>0:
11 | first2, last = build_nested_list(depth-1)
12 | first.append(first2)
13 | return first, last
14 |
15 | first,last = build_nested_list(15)
16 | for i in range(20):
17 | last.append('X')
18 |
19 | child = ('who', 'are', 'my', 'parents?')
20 | last[4] = child
21 | last[5] = child
22 | last[6] = child
23 | last[7] = child
24 | last[8] = child
25 |
26 | mg.show([first,child])
27 | #mg.show([first])
--------------------------------------------------------------------------------
/memory_graph/test_memory_graph.py:
--------------------------------------------------------------------------------
1 | # This file is part of memory_graph.
2 | # Copyright (c) 2023, Bas Terwijn.
3 | # SPDX-License-Identifier: BSD-2-Clause
4 |
5 | import memory_graph
6 |
7 | import memory_graph.test as test
8 |
9 | if __name__ == '__main__':
10 | test_fun_count = 0
11 | def test_fun(data):
12 | global test_fun_count
13 | memory_graph.render(data, f'test_graph{test_fun_count}.png')
14 | test_fun_count += 1
15 | test.test_all(test_fun)
16 |
--------------------------------------------------------------------------------
/memory_graph/test_memory_to_nodes.py:
--------------------------------------------------------------------------------
1 | # This file is part of memory_graph.
2 | # Copyright (c) 2023, Bas Terwijn.
3 | # SPDX-License-Identifier: BSD-2-Clause
4 |
5 |
6 | import memory_graph.memory_to_nodes as memory_to_nodes
7 | import memory_graph.config_helpers as config_helper
8 |
9 | config_helper.set_config()
10 |
11 | l1 = [1,2]
12 | l2 = [3,4]
13 | data = [l1,l2,l1,[5,l2]]
14 | nodes = memory_to_nodes.memory_to_nodes(data)
15 | #print(nodes)
--------------------------------------------------------------------------------
/memory_graph/test_sequence.py:
--------------------------------------------------------------------------------
1 | # This file is part of memory_graph.
2 | # Copyright (c) 2023, Bas Terwijn.
3 | # SPDX-License-Identifier: BSD-2-Clause
4 |
5 | from memory_graph.sequence import Sequence1D, Sequence2D
6 | from memory_graph.slicer import Slicer
7 |
8 | def status(index):
9 | if type(index) == tuple:
10 | return index[0]
11 | return index
12 |
13 | def test_slicing(sequence, slicer):
14 | print(sequence)
15 | print(slicer)
16 | for i in sequence.indices_all():
17 | print(i, sequence[i])
18 | slices = sequence.slice(slicer)
19 | print('slices:',slices)
20 | for index in slices:
21 | print(index, ':', sequence[index])
22 | for index in slices.table_iter(sequence.size()):
23 | print(f'{index}: {sequence[index] if status(index)>=0 else None}')
24 |
25 | def test_sequence():
26 | sequence = Sequence1D([i for i in range(8)])
27 | slicer = Slicer(2,3)
28 | test_slicing(sequence, slicer)
29 |
30 | width = 5
31 | height = 6
32 | sequence = Sequence2D([[x+y*width for x in range(width)] for y in range(height)])
33 | slicer = (Slicer(1,2), Slicer(2,1))
34 | test_slicing(sequence, slicer)
35 |
36 | if __name__ == '__main__':
37 | test_sequence()
38 |
--------------------------------------------------------------------------------
/memory_graph/test_slicer.py:
--------------------------------------------------------------------------------
1 | # This file is part of memory_graph.
2 | # Copyright (c) 2023, Bas Terwijn.
3 | # SPDX-License-Identifier: BSD-2-Clause
4 |
5 | from memory_graph.slicer import Slicer
6 |
7 | def test_slicer():
8 | slicer = Slicer(0.1, 0.2, 0.3)
9 | slices = slicer.get_slices(100)
10 | assert slices.get_slices() == [[0, 10], [40, 60], [70, 100]], "Slicer error"
11 |
12 | slicer = Slicer(10, 20, 30)
13 | slices = slicer.get_slices(100)
14 | assert slices.get_slices() == [[0, 10], [40, 60], [70, 100]], "Slicer error"
15 |
16 | slicer = Slicer(0.1, 0.3)
17 | slices = slicer.get_slices(100)
18 | assert slices.get_slices() == [[0, 10], [70, 100]], "Slicer error"
19 |
20 | slicer = Slicer(10, 30)
21 | slices = slicer.get_slices(100)
22 | assert slices.get_slices() == [[0, 10], [70, 100]], "Slicer error"
23 |
24 | slicer = Slicer(0.1)
25 | slices = slicer.get_slices(100)
26 | assert slices.get_slices() == [[0, 10]], "Slicer error"
27 |
28 | slicer = Slicer(10)
29 | slices = slicer.get_slices(100)
30 | assert slices.get_slices() == [[0, 10]], "Slicer error"
31 |
32 | slicer = Slicer()
33 | slices = slicer.get_slices(100)
34 | assert slices.get_slices() == [[0, 100]], "Slicer error"
35 |
36 | slicer = Slicer(2,2)
37 | slices = slicer.get_slices(0)
38 | assert slices.get_slices() == [], "Slicer error"
39 |
40 | slicer = Slicer(2,2)
41 | slices = slicer.get_slices(5)
42 | assert slices.get_slices() == [[0,5]], "Slicer error"
43 |
44 | slicer = Slicer(2,2)
45 | slices = slicer.get_slices(6)
46 | assert slices.get_slices() == [[0,2],[4,6]], "Slicer error"
47 |
48 | if __name__ == '__main__':
49 | test_slicer()
50 | print('Slicer test passed')
51 |
--------------------------------------------------------------------------------
/memory_graph/test_slices.py:
--------------------------------------------------------------------------------
1 | # This file is part of memory_graph.
2 | # Copyright (c) 2023, Bas Terwijn.
3 | # SPDX-License-Identifier: BSD-2-Clause
4 |
5 |
6 | from memory_graph.slices import Slices1D, Slices2D
7 |
8 | def test_slices1d():
9 | test = Slices1D( [[10,20], [30,40], [60,70], [80,90]] )
10 | slices = test.copy()
11 | slices.add_slice([21,79])
12 | assert slices.get_slices() == [[10,90]], "Slice error: merging begin and end"
13 |
14 | slices = test.copy()
15 | slices.add_slice([31,39])
16 | assert slices.get_slices() == [[10,20], [30,40], [60,70], [80,90]], "Slice error: merging begin and end"
17 |
18 | slices = test.copy()
19 | slices.add_slice([15,50])
20 | assert slices.get_slices() == [[10,50], [60,70], [80,90]], "Slice error: merging begin"
21 |
22 | slices = test.copy()
23 | slices.add_slice([15,65])
24 | assert slices.get_slices() == [[10,70], [80,90]], "Slice error: merging begin"
25 |
26 | slices = test.copy()
27 | slices.add_slice([35,45])
28 | assert slices.get_slices() == [[10,20], [30,45], [60,70], [80,90]], "Slice error: merging begin"
29 |
30 | slices = test.copy()
31 | slices.add_slice([25,65])
32 | assert slices.get_slices() == [[10,20], [25,70], [80,90]], "Slice error: merging end"
33 |
34 | slices = test.copy()
35 | slices.add_slice([15,65])
36 | assert slices.get_slices() == [[10,70], [80,90]], "Slice error: merging end"
37 |
38 | slices = test.copy()
39 | slices.add_slice([55,65])
40 | assert slices.get_slices() == [[10,20], [30,40], [55,70], [80,90]], "Slice error: merging end"
41 |
42 | slices = test.copy()
43 | slices.add_slice([25,75])
44 | assert slices.get_slices() == [[10,20], [25,75], [80,90]], "Slice error: merging none"
45 |
46 | slices = test.copy()
47 | slices.add_slice([5,6])
48 | assert slices.get_slices() == [[5,6], [10,20], [30,40], [60,70], [80,90]], "Slice error: merging none"
49 |
50 | slices = test.copy()
51 | slices.add_slice([95,96])
52 | assert slices.get_slices() == [[10,20], [30,40], [60,70], [80,90], [95,96]], "Slice error: merging none"
53 |
54 | slices = test.copy()
55 | assert not slices.add_slice([10,11])
56 | assert not slices.add_slice([19,20])
57 | assert not slices.add_slice([30,31])
58 | assert not slices.add_slice([39,40])
59 | assert not slices.add_slice([65,66])
60 | assert slices.add_slice([9,10])
61 | assert slices.add_slice([20,21])
62 | assert slices.add_slice([28,29])
63 | assert slices.add_slice([41,42])
64 | assert slices.add_slice([75,76])
65 | assert slices.add_slice([100,200])
66 |
67 | test = Slices1D( [ [i,i+2] for i in range(0,30,4)] )
68 | #print('test:',test)
69 | for index in range(30):
70 | #print('index:',index, 'has_index:',test.has_index(index))
71 | assert test.has_index(index) == ((index//2) % 2 == 0), f"Error: {index}"
72 |
73 | def test_slices2d():
74 | from memory_graph.slices import Slices
75 | slices2d = Slices2D( Slices1D([[20,30]]),
76 | Slices1D([[20,30]])
77 | )
78 |
79 | slices2d.add_index((19,19))
80 | slices2d.add_index((31,31))
81 | print(slices2d)
82 | slices2d.add_index((18,19))
83 | print(slices2d)
84 | slices2d.add_index((19,18))
85 | slices2d.add_index((30,30))
86 | print(slices2d)
87 |
88 | if __name__ == "__main__":
89 | test_slices1d()
90 | test_slices2d()
91 | print("Slices: All tests pass")
92 |
--------------------------------------------------------------------------------
/memory_graph/test_slices_iterator.py:
--------------------------------------------------------------------------------
1 | # This file is part of memory_graph.
2 | # Copyright (c) 2023, Bas Terwijn.
3 | # SPDX-License-Identifier: BSD-2-Clause
4 |
5 |
6 | from memory_graph.slices_iterator import Slices_Iterator1D, Slices_Iterator2D
7 | from memory_graph.slices import Slices1D, Slices2D
8 |
9 | def test_slices_iterator1d():
10 | slices1d = Slices1D( [[10,20], [30,40], [60,70], [80,90]] )
11 | iter = Slices_Iterator1D(slices1d)
12 | for i in iter:
13 | print(i)
14 | assert list(Slices_Iterator1D(slices1d)) == [10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89], "Slices_Iterator: Error in iteration"
15 |
16 | def test_slices_iterator2d():
17 | slices2d = Slices2D( Slices1D( [[10,12], [20,22]] ),
18 | Slices1D( [[10,12], [30,32]] ) )
19 | iter = Slices_Iterator2D(slices2d)
20 | for i in iter:
21 | print(i)
22 | assert list(Slices_Iterator2D(slices2d)) == [(10, 10), (10, 11), (10, 30), (10, 31), (11, 10), (11, 11), (11, 30), (11, 31), (20, 10), (20, 11), (20, 30), (20, 31), (21, 10), (21, 11), (21, 30), (21, 31)]
23 |
24 |
25 | if __name__ == '__main__':
26 | test_slices_iterator1d()
27 | test_slices_iterator2d()
28 | print("Slices_Iterator: All tests pass")
29 |
--------------------------------------------------------------------------------
/memory_graph/utils.py:
--------------------------------------------------------------------------------
1 | # This file is part of memory_graph.
2 | # Copyright (c) 2023, Bas Terwijn.
3 | # SPDX-License-Identifier: BSD-2-Clause
4 |
5 | import math
6 | import types
7 |
8 | def has_dict_attributes(value):
9 | """ Returns 'True' if 'value' has a '__dict__' attribute. """
10 | return hasattr(value,"__dict__")
11 |
12 | def get_dict_attributes(value):
13 | """ Returns the items of the '__dict__' attribute of 'value'."""
14 | return getattr(value,"__dict__")
15 |
16 | def is_function(obj):
17 | if isinstance(obj, types.FunctionType) or isinstance(obj, types.MethodType):
18 | return True
19 | return type(obj).__name__ in {'method_descriptor', 'builtin_function_or_method', 'getset_descriptor', 'classmethod_descriptor'}
20 |
21 | def filter_dict(dictionary):
22 | """ Filters out the unwanted dict attributes. """
23 | if '__name__' in dictionary: # only filter stack frames, for example locals()
24 | return [
25 | (k,v) for k, v in dictionary.items() if
26 | not (type(k) is str and k.startswith('__')) and
27 | not isinstance(v,types.ModuleType) and
28 | not is_function(v)
29 | ]
30 | return [
31 | (k,v) for k, v in dictionary.items() if
32 | not (type(k) is str and k.startswith('__'))
33 | ]
34 |
35 | def filter_type_attributes(tuples):
36 | """ Filters out the unwanted type attributes (class/static methods). """
37 | return [
38 | (k,v) for k, v in tuples if
39 | not (type(k) is str and k.startswith('__')) and
40 | not type(v) in {classmethod, staticmethod} and
41 | not callable(v)
42 | ]
43 |
44 | def make_sliceable(data):
45 | """ Returns a sliceble version of data, convert to list if not yet sliceble. """
46 | try:
47 | data[0:0]
48 | return data
49 | except TypeError:
50 | return list(data)
51 |
52 | def is_finite_iterable(data):
53 | """ Returns 'True' if 'data' is finite iterable. """
54 | try:
55 | iter(data) # iterable
56 | len(data) # and not infinite (not a strong test, but what else?)
57 | return True
58 | except TypeError:
59 | return False
60 |
61 | def get_type_name(data):
62 | """ Returns the name of the type of 'data'. """
63 | return type(data).__name__
64 |
65 | def nested_list(sizes, i=0, value=[0]):
66 | """ Returns a nested list with the given 'sizes' for test purposes. """
67 | if i == len(sizes)-1:
68 | data = []
69 | for _ in range(sizes[i]):
70 | data.append( value[0] )
71 | value[0]+=1
72 | else:
73 | data = []
74 | for size in range(sizes[i]):
75 | data.append( nested_list(sizes,i+1) )
76 | return data
77 |
78 | def my_round(value):
79 | """ Rounds the value to the nearest integer rounding '.5' up consistantly. """
80 | return math.floor(value + 0.5)
81 |
82 | def generator_has_data(generator):
83 | """ Returns 'True' if the generator has data. """
84 | try:
85 | next(generator)
86 | return True
87 | except StopIteration:
88 | return False
89 |
90 | def take_through(condition, iterable):
91 | for i in iterable:
92 | yield i
93 | if condition(i):
94 | return
95 |
96 | def take_after(condition, iterable):
97 | taking = False
98 | for i in iterable:
99 | if taking:
100 | yield i
101 | elif condition(i):
102 | taking = True
103 |
104 | if __name__ == '__main__':
105 | print( nested_list([4,3,2]) )
106 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools>=61.0", "wheel"]
3 | build-backend = "setuptools.build_meta"
4 |
5 | [project]
6 | name = "memory_graph"
7 | version = "0.3.34"
8 | description = "Teaching tool and debugging aid in context of references, mutable data types, and shallow and deep copy."
9 | authors = [
10 | {name = "Bas Terwijn", email = "bterwijn@gmail.com"}
11 | ]
12 | #license = { text = "BSD-2-Clause" } # needed for python <=3.8
13 | license = "BSD-2-Clause"
14 | readme = "README.md"
15 | requires-python = ">=3.7"
16 | classifiers = [
17 | "Development Status :: 4 - Beta",
18 | "Intended Audience :: Education",
19 | "Intended Audience :: Developers",
20 | "Programming Language :: Python :: 3",
21 | "Topic :: Education",
22 | "Topic :: Software Development :: Debuggers",
23 | ]
24 | dependencies = [
25 | "graphviz",
26 | ]
27 |
28 | [project.urls]
29 | Homepage = "https://github.com/bterwijn/memory_graph"
30 | Repository = "https://github.com/bterwijn/memory_graph.git"
31 |
32 | [tool.setuptools]
33 | packages = ["memory_graph"]
34 |
--------------------------------------------------------------------------------
/src/auto_memory_graph.py:
--------------------------------------------------------------------------------
1 | import memory_graph as mg
2 |
3 | mg_visualization_status = False
4 | print('running:', __file__)
5 | print('Call mg_switch() to turn on/off auto memory_graph visualization.')
6 |
7 | def mg_visualization(execution_result):
8 | ipython_locals = get_ipython().user_ns
9 | mg.show(mg.ipython_locals_filter(ipython_locals))
10 |
11 | def mg_switch(status = None):
12 | global mg_visualization_status
13 | if isinstance(status, bool):
14 | mg_visualization_status = status
15 | else:
16 | mg_visualization_status = not mg_visualization_status
17 | if mg_visualization_status:
18 | get_ipython().events.register("post_run_cell", mg_visualization)
19 | else:
20 | get_ipython().events.unregister("post_run_cell", mg_visualization)
21 | return mg_visualization_status
--------------------------------------------------------------------------------
/src/colab_example.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "nbformat": 4,
3 | "nbformat_minor": 0,
4 | "metadata": {
5 | "colab": {
6 | "provenance": []
7 | },
8 | "kernelspec": {
9 | "name": "python3",
10 | "display_name": "Python 3"
11 | },
12 | "language_info": {
13 | "name": "python"
14 | }
15 | },
16 | "cells": [
17 | {
18 | "cell_type": "code",
19 | "execution_count": null,
20 | "metadata": {
21 | "id": "bjVWFC4L3xwA"
22 | },
23 | "outputs": [],
24 | "source": [
25 | "# install/upgrade memory_graph\n",
26 | "!pip install --upgrade memory_graph"
27 | ]
28 | },
29 | {
30 | "cell_type": "code",
31 | "source": [
32 | "import memory_graph as mg\n",
33 | "from IPython.display import SVG\n",
34 | "import copy\n",
35 | "\n",
36 | "def custom_copy(a):\n",
37 | " c = a.copy()\n",
38 | " c[1] = a[1].copy()\n",
39 | " mg.render(mg.stack_colab(), 'graph_stack.svg') # call stack\n",
40 | " return c\n",
41 | "\n",
42 | "a = [[1, 2], ['x', 'y']]\n",
43 | "c1 = a\n",
44 | "c2 = copy.copy(a)\n",
45 | "c3 = custom_copy(a)\n",
46 | "SVG(filename='graph_stack.svg')"
47 | ],
48 | "metadata": {
49 | "id": "7sqUe1Ut3036"
50 | },
51 | "execution_count": null,
52 | "outputs": []
53 | },
54 | {
55 | "cell_type": "code",
56 | "source": [
57 | "c4 = copy.deepcopy(a)\n",
58 | "mg.render(mg.locals_colab(), 'graph_locals.svg') # local variables\n",
59 | "SVG(filename='graph_locals.svg')"
60 | ],
61 | "metadata": {
62 | "id": "99SXvl0IKBtO"
63 | },
64 | "execution_count": null,
65 | "outputs": []
66 | },
67 | {
68 | "cell_type": "code",
69 | "source": [],
70 | "metadata": {
71 | "id": "O74cC1usQn2P"
72 | },
73 | "execution_count": null,
74 | "outputs": []
75 | }
76 | ]
77 | }
--------------------------------------------------------------------------------
/src/jupyter_example.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "id": "23f6d43f-dd17-4020-971e-5bb8a5b1e30b",
6 | "metadata": {},
7 | "source": [
8 | "# test: locals_jupyter()\n",
9 | "Show a graph build with the filtered Jupyter locals using function `mg.locals_jupyter()`. Just adding integers to a list:"
10 | ]
11 | },
12 | {
13 | "cell_type": "code",
14 | "execution_count": null,
15 | "id": "e8913787-bbef-4adb-b027-ac0f28500233",
16 | "metadata": {},
17 | "outputs": [],
18 | "source": [
19 | "import memory_graph as mg\n",
20 | "\n",
21 | "data = []\n",
22 | "for i in range(5):\n",
23 | " data.append(i)\n",
24 | " display(mg.create_graph(mg.locals_jupyter())) # display in jupyter notebook\n",
25 | " mg.block(mg.show, mg.locals_jupyter()) # display in PDF reader\n",
26 | " "
27 | ]
28 | },
29 | {
30 | "cell_type": "markdown",
31 | "id": "f66d9b8d-0937-4ad0-97b4-a7459e84c4f2",
32 | "metadata": {},
33 | "source": [
34 | "# test: stack_jupyter()\n",
35 | "Show a graph build the filterd Jupyter call stack from function `mg.stack_jupyter()`. Recursively filling a list with all permutation of elements with resampling:"
36 | ]
37 | },
38 | {
39 | "cell_type": "code",
40 | "execution_count": null,
41 | "id": "15d0c443-7cc6-4b4f-a9db-598aaf261364",
42 | "metadata": {},
43 | "outputs": [],
44 | "source": [
45 | "import memory_graph as mg\n",
46 | "\n",
47 | "def get_all_permutations(permutations, elements, data, max_length):\n",
48 | " if len(data) == max_length: # recursive stop condition\n",
49 | " permutations.append(data.copy())\n",
50 | " else:\n",
51 | " for i in elements:\n",
52 | " data.append(i)\n",
53 | " mg.block(mg.show, mg.stack_jupyter())\n",
54 | " get_all_permutations(permutations, elements, data, max_length)\n",
55 | " data.pop()\n",
56 | " mg.block(mg.show, mg.stack_jupyter())\n",
57 | "\n",
58 | "permutations = []\n",
59 | "get_all_permutations(permutations, ['L','R'], [], 3)\n",
60 | "print(permutations)"
61 | ]
62 | }
63 | ],
64 | "metadata": {
65 | "kernelspec": {
66 | "display_name": "Python 3 (ipykernel)",
67 | "language": "python",
68 | "name": "python3"
69 | },
70 | "language_info": {
71 | "codemirror_mode": {
72 | "name": "ipython",
73 | "version": 3
74 | },
75 | "file_extension": ".py",
76 | "mimetype": "text/x-python",
77 | "name": "python",
78 | "nbconvert_exporter": "python",
79 | "pygments_lexer": "ipython3",
80 | "version": "3.12.3"
81 | }
82 | },
83 | "nbformat": 4,
84 | "nbformat_minor": 5
85 | }
86 |
--------------------------------------------------------------------------------
/src/pyodide.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Pyodide Python Runner
7 |
8 |
9 |
10 |
54 |
55 |
56 |