├── 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 | ![vscode_copying.gif](https://raw.githubusercontent.com/bterwijn/memory_graph/main/images/vscode_copying.gif) 10 | 11 | # Videos # 12 | | [![Quick Intro](https://img.youtube.com/vi/23_bHcr7hqo/0.jpg)](https://www.youtube.com/watch?v=23_bHcr7hqo) | [![Mutability](https://img.youtube.com/vi/pvIJgHCaXhU/0.jpg)](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 | ![many_types.png](https://raw.githubusercontent.com/bterwijn/memory_graph/main/images/many_types.png) 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 | ![mutable2.png](https://raw.githubusercontent.com/bterwijn/memory_graph/main/images/mutable2.png) 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 | University of Amsterdam 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 | | ![mutable1.png](https://raw.githubusercontent.com/bterwijn/memory_graph/main/images/immutable1.png) | ![mutable2.png](https://raw.githubusercontent.com/bterwijn/memory_graph/main/images/immutable2.png) | 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 | | ![mutable1.png](https://raw.githubusercontent.com/bterwijn/memory_graph/main/images/mutable1.png) | ![mutable2.png](https://raw.githubusercontent.com/bterwijn/memory_graph/main/images/mutable2.png) | 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 | ![copies.png](https://raw.githubusercontent.com/bterwijn/memory_graph/main/images/copies.png) 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 | ![copy_method.png](https://raw.githubusercontent.com/bterwijn/memory_graph/main/images/copy_method.png) 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 | | ![rebinding1.png](https://raw.githubusercontent.com/bterwijn/memory_graph/main/images/rebinding1.png) | ![rebinding2.png](https://raw.githubusercontent.com/bterwijn/memory_graph/main/images/rebinding2.png) | 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 | ![add_one.png](https://raw.githubusercontent.com/bterwijn/memory_graph/main/images/add_one.png) 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 | ![factorial.gif](https://raw.githubusercontent.com/bterwijn/memory_graph/main/images/factorial.gif) 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 | ![power_set.gif](https://raw.githubusercontent.com/bterwijn/memory_graph/main/images/power_set.gif) 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 | ![vscode_copying.gif](https://raw.githubusercontent.com/bterwijn/memory_graph/main/images/vscode_copying.gif) 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 | ![debugging.png](https://raw.githubusercontent.com/bterwijn/memory_graph/main/images/debugging.gif) 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 | ![linked_list.png](https://raw.githubusercontent.com/bterwijn/memory_graph/main/images/linked_list.png) 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 | ![bin_tree.png](https://raw.githubusercontent.com/bterwijn/memory_graph/main/images/bin_tree.png) 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 | ![hash_set.png](https://raw.githubusercontent.com/bterwijn/memory_graph/main/images/hash_set.png) 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 | | ![not_node_types1](https://raw.githubusercontent.com/bterwijn/memory_graph/main/images/not_node_types1.png) | ![not_node_types2](https://raw.githubusercontent.com/bterwijn/memory_graph/main/images/not_node_types2.png) | 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 | ![highlight.png](https://raw.githubusercontent.com/bterwijn/memory_graph/main/images/highlight.png) 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 | ![extension_numpy.png](https://raw.githubusercontent.com/bterwijn/memory_graph/main/images/extension_numpy.png) 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 | ![extension_pandas.png](https://raw.githubusercontent.com/bterwijn/memory_graph/main/images/extension_pandas.png) 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 | ![extension_numpy.png](https://raw.githubusercontent.com/bterwijn/memory_graph/main/images/avltree_fail.png) 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 | ![extension_numpy.png](https://raw.githubusercontent.com/bterwijn/memory_graph/main/images/avltree_dir.png) 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 | ![extension_numpy.png](https://raw.githubusercontent.com/bterwijn/memory_graph/main/images/avltree_leaf.png) 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 | ![extension_numpy.png](https://raw.githubusercontent.com/bterwijn/memory_graph/main/images/avltree_linear.png) 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 | ![extension_numpy.png](https://raw.githubusercontent.com/bterwijn/memory_graph/main/images/avltree_key_value.png) 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 | ![extension_numpy.png](https://raw.githubusercontent.com/bterwijn/memory_graph/main/images/avltree_table.png) 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 | ![extension_numpy.png](https://raw.githubusercontent.com/bterwijn/memory_graph/main/images/introspect_depth.png) 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 | ![extension_numpy.png](https://raw.githubusercontent.com/bterwijn/memory_graph/main/images/hidden_edges.png) 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 | ![jupyter_example.png](https://raw.githubusercontent.com/bterwijn/memory_graph/main/images/jupyter_example.png) 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 | ![ipyton.png](https://raw.githubusercontent.com/bterwijn/memory_graph/main/images/ipython.png) 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 | ![colab_example.png](https://raw.githubusercontent.com/bterwijn/memory_graph/main/images/colab_example.png) 863 | 864 | # In the Browser # 865 | We can also run memory_graph in the browser: Pyodide Example 866 | ![pyodide.png](https://raw.githubusercontent.com/bterwijn/memory_graph/main/images/pyodide.png) 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 |
57 |
58 |

See the memory_graph python package.
59 | Log:

60 | 61 |
62 | 63 |

Python Code:

64 | 86 | 87 |
88 | 89 |

Graph:

90 |
91 |
92 | 93 | 94 |

Output:

95 | 96 |
97 |
98 | 99 | 100 | 181 | 182 | 183 | -------------------------------------------------------------------------------- /uml/memory_graph.uxf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 4 | 5 | UMLClass 6 | 7 | 510 8 | 30 9 | 320 10 | 120 11 | 12 | Memory_Visitor 13 | - 14 | node_ids 15 | - 16 | Memory_Visitor(data) 17 | visit_recursive(data, parent) 18 | data_to_node(data) 19 | 20 | 21 | 22 | UMLClass 23 | 24 | 130 25 | 30 26 | 210 27 | 130 28 | 29 | Graph 30 | - 31 | memory_visitor 32 | - 33 | Graph(data) 34 | backtrack_callback(node) 35 | add_node(node) 36 | 37 | 38 | 39 | UMLClass 40 | 41 | 90 42 | 200 43 | 430 44 | 230 45 | 46 | Node 47 | - 48 | data 49 | parent 50 | childeren 51 | - 52 | Node(data, children) 53 | set_parent(parent) 54 | get_name() 55 | get_parent() 56 | get_type() 57 | get_label() 58 | get_html_table() 59 | get_edges() 60 | 61 | 62 | 63 | UMLClass 64 | 65 | 700 66 | 730 67 | 340 68 | 150 69 | 70 | Children_Table 71 | - 72 | column_slicer 73 | row_slicer 74 | - 75 | Children_Table(children, children_width, 76 | column_names, row_names) 77 | add_to_graph(graph, node) 78 | 79 | 80 | 81 | UMLClass 82 | 83 | 40 84 | 730 85 | 280 86 | 90 87 | 88 | Children_Linear 89 | - 90 | slicer 91 | - 92 | Children_Linear(children) 93 | add_to_graph(graph, node) 94 | 95 | 96 | 97 | UMLClass 98 | 99 | 370 100 | 730 101 | 280 102 | 90 103 | 104 | Children_Key_Value 105 | - 106 | slicer 107 | - 108 | Children_Key_Value(children) 109 | add_to_graph(graph, node) 110 | 111 | 112 | 113 | UMLClass 114 | 115 | 300 116 | 490 117 | 430 118 | 160 119 | 120 | Children 121 | - 122 | children 123 | original_size 124 | - 125 | Children(children) 126 | transform(fun) 127 | visit(fun) 128 | visit_with_depth(fun) 129 | 130 | 131 | front_back_split_table(data, front_back, data_width) 132 | 133 | 134 | 135 | UMLClass 136 | 137 | 890 138 | 490 139 | 150 140 | 130 141 | 142 | Slicer 143 | - 144 | 145 | - 146 | Slicer(slices) 147 | slice(data) 148 | 149 | 150 | 151 | 152 | UMLClass 153 | 154 | 990 155 | 200 156 | 210 157 | 100 158 | 159 | utils 160 | - 161 | 162 | - 163 | 164 | 165 | 166 | 167 | UMLClass 168 | 169 | 620 170 | 190 171 | 230 172 | 240 173 | 174 | HTML_Table 175 | - 176 | 177 | - 178 | add_new_line() 179 | add_inner_table() 180 | add_string(s) 181 | add_index(s) 182 | add_entry(node, child) 183 | add_dots() 184 | get_edges() 185 | to_string() 186 | get_column() 187 | get_row() 188 | get_edges() 189 | 190 | 191 | 192 | UMLClass 193 | 194 | 990 195 | 330 196 | 210 197 | 100 198 | 199 | test 200 | - 201 | 202 | - 203 | 204 | 205 | 206 | 207 | Relation 208 | 209 | 330 210 | 100 211 | 200 212 | 30 213 | 214 | lt=<<<<- 215 | 10.0;10.0;180.0;10.0 216 | 217 | 218 | Relation 219 | 220 | 170 221 | 640 222 | 170 223 | 110 224 | 225 | lt=<<- 226 | 150.0;10.0;10.0;90.0 227 | 228 | 229 | Relation 230 | 231 | 490 232 | 640 233 | 30 234 | 110 235 | 236 | lt=<<- 237 | 10.0;10.0;10.0;90.0 238 | 239 | 240 | Relation 241 | 242 | 680 243 | 640 244 | 120 245 | 110 246 | 247 | lt=<<- 248 | 10.0;10.0;100.0;90.0 249 | 250 | 251 | Relation 252 | 253 | 420 254 | 420 255 | 30 256 | 90 257 | 258 | lt=<<<<- 259 | 10.0;10.0;10.0;70.0 260 | 261 | 262 | UMLClass 263 | 264 | 990 265 | 80 266 | 210 267 | 100 268 | 269 | config 270 | - 271 | 272 | - 273 | 274 | 275 | 276 | 277 | UMLClass 278 | 279 | 1090 280 | 490 281 | 130 282 | 130 283 | 284 | Sliced 285 | - 286 | slices 287 | - 288 | add_slice(slice) 289 | has_data() 290 | transform(fun) 291 | 292 | 293 | 294 | 295 | UMLClass 296 | 297 | 1100 298 | 670 299 | 130 300 | 130 301 | 302 | Slice 303 | - 304 | index 305 | data 306 | - 307 | 308 | 309 | 310 | 311 | 312 | Relation 313 | 314 | 1160 315 | 610 316 | 30 317 | 80 318 | 319 | lt=<<<<- 320 | 10.0;10.0;10.0;60.0 321 | 322 | 323 | --------------------------------------------------------------------------------