├── CONTRIBUTING.md ├── requirements.txt ├── visualiser ├── __init__.py └── visualiser.py ├── examples ├── subset.gif ├── subset.png ├── factorial.gif ├── factorial.png ├── fibonacci.gif ├── fibonacci.png ├── make_sum.gif ├── make_sum.png ├── coin_change.gif ├── coin_change.png ├── binary_string.gif ├── binary_string.png ├── combinations.gif ├── combinations.png ├── missionaries.gif ├── missionaries.png ├── combinations.py ├── binary_string.py ├── factorial.py ├── fibonacci.py ├── coin_change.py ├── subset.py ├── make_sum.py └── missionaries.py ├── packaging.txt ├── Dockerfile ├── Examples.md ├── docker-compose.yml ├── setup.py ├── .github └── workflows │ └── python-publish.yml ├── LICENSE.md ├── README.md └── .gitignore /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | imageio==2.6.1 2 | pydot==1.4.1 3 | -------------------------------------------------------------------------------- /visualiser/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Version of the recursion-visualiser 3 | __version__ = "1.0.1" -------------------------------------------------------------------------------- /examples/subset.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bishalsarang/Recursion-Tree-Visualizer/HEAD/examples/subset.gif -------------------------------------------------------------------------------- /examples/subset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bishalsarang/Recursion-Tree-Visualizer/HEAD/examples/subset.png -------------------------------------------------------------------------------- /examples/factorial.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bishalsarang/Recursion-Tree-Visualizer/HEAD/examples/factorial.gif -------------------------------------------------------------------------------- /examples/factorial.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bishalsarang/Recursion-Tree-Visualizer/HEAD/examples/factorial.png -------------------------------------------------------------------------------- /examples/fibonacci.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bishalsarang/Recursion-Tree-Visualizer/HEAD/examples/fibonacci.gif -------------------------------------------------------------------------------- /examples/fibonacci.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bishalsarang/Recursion-Tree-Visualizer/HEAD/examples/fibonacci.png -------------------------------------------------------------------------------- /examples/make_sum.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bishalsarang/Recursion-Tree-Visualizer/HEAD/examples/make_sum.gif -------------------------------------------------------------------------------- /examples/make_sum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bishalsarang/Recursion-Tree-Visualizer/HEAD/examples/make_sum.png -------------------------------------------------------------------------------- /examples/coin_change.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bishalsarang/Recursion-Tree-Visualizer/HEAD/examples/coin_change.gif -------------------------------------------------------------------------------- /examples/coin_change.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bishalsarang/Recursion-Tree-Visualizer/HEAD/examples/coin_change.png -------------------------------------------------------------------------------- /examples/binary_string.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bishalsarang/Recursion-Tree-Visualizer/HEAD/examples/binary_string.gif -------------------------------------------------------------------------------- /examples/binary_string.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bishalsarang/Recursion-Tree-Visualizer/HEAD/examples/binary_string.png -------------------------------------------------------------------------------- /examples/combinations.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bishalsarang/Recursion-Tree-Visualizer/HEAD/examples/combinations.gif -------------------------------------------------------------------------------- /examples/combinations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bishalsarang/Recursion-Tree-Visualizer/HEAD/examples/combinations.png -------------------------------------------------------------------------------- /examples/missionaries.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bishalsarang/Recursion-Tree-Visualizer/HEAD/examples/missionaries.gif -------------------------------------------------------------------------------- /examples/missionaries.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bishalsarang/Recursion-Tree-Visualizer/HEAD/examples/missionaries.png -------------------------------------------------------------------------------- /packaging.txt: -------------------------------------------------------------------------------- 1 | python3 setup.py sdist bdist_wheel 2 | 3 | twine upload --repository-url https://test.pypi.org/legacy/ dist/* 4 | 5 | twine upload dist/* 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.6-slim 2 | RUN apt-get update 3 | RUN apt-get -y install graphviz 4 | ADD . /vs 5 | WORKDIR /vs 6 | RUN pip install recursion-visualiser -------------------------------------------------------------------------------- /Examples.md: -------------------------------------------------------------------------------- 1 | # Recursion Visualiser Examples: 2 | In this example series, I'm going to start with making minimal recursion tree to making some complex recursion tree. 3 | 4 | Let's start with first example: 5 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.6" 2 | services: 3 | vs: 4 | build: . 5 | container_name: recursion-visualiser-env 6 | stdin_open: true 7 | tty: true 8 | user: ${CURRENT_UID} 9 | volumes: 10 | - .:/vs -------------------------------------------------------------------------------- /examples/combinations.py: -------------------------------------------------------------------------------- 1 | from visualiser.visualiser import Visualiser as vs 2 | 3 | st= [] 4 | @vs(show_argument_name=False, node_properties_kwargs={"shape":"record", "color":"#f57542", "style":"filled", "fillcolor":"grey"}) 5 | def combi(prefix, s): 6 | if len(s) == 0: 7 | return " " 8 | else: 9 | st.append(prefix + s[0]) 10 | combi(prefix=prefix + s[0], s=s[1:]) 11 | combi(prefix=prefix, s=s[1:]) 12 | return st 13 | 14 | print(combi(prefix="",s='abc')) 15 | vs.make_animation("combinations.gif", delay=3) -------------------------------------------------------------------------------- /examples/binary_string.py: -------------------------------------------------------------------------------- 1 | from visualiser.visualiser import Visualiser as vs 2 | 3 | """ 4 | Problem Link: https://stackoverflow.com/questions/33808653/recursion-tree-with-fibonacci-python/60126306#60126306 5 | """ 6 | 7 | @vs(node_properties_kwargs={"shape":"record", "color":"#f57542", "style":"filled", "fillcolor":"grey"}) 8 | def binary(length, outstr=""): 9 | if len(outstr) == length: 10 | print(outstr) 11 | else: 12 | for i in ["0", "1"]: 13 | binary(length=length, outstr=outstr + i) 14 | 15 | binary(length=3,outstr="") 16 | vs.make_animation("binary_string.gif", delay=2) -------------------------------------------------------------------------------- /examples/factorial.py: -------------------------------------------------------------------------------- 1 | # Author: Bishal Sarang 2 | 3 | # Import Visualiser class from module visualiser 4 | from visualiser.visualiser import Visualiser as vs 5 | 6 | # Add decorator 7 | # Decorator accepts optional arguments: ignore_args , show_argument_name, show_return_value and node_properties_kwargs 8 | @vs() 9 | def fact(n): 10 | if n <= 1: 11 | return n 12 | return n * fact(n=n-1) 13 | 14 | 15 | def main(): 16 | # Call function 17 | print(fact(n=6)) 18 | # Save recursion tree to a file 19 | vs.make_animation("factorial.gif", delay=2) 20 | 21 | 22 | if __name__ == "__main__": 23 | main() -------------------------------------------------------------------------------- /examples/fibonacci.py: -------------------------------------------------------------------------------- 1 | # Author: Bishal Sarang 2 | 3 | # Import Visualiser class from module visualiser 4 | from visualiser.visualiser import Visualiser as vs 5 | 6 | # Add decorator 7 | # Decorator accepts optional arguments: ignore_args , show_argument_name, show_return_value and node_properties_kwargs 8 | @vs(node_properties_kwargs={"shape":"record", "color":"#f57542", "style":"filled", "fillcolor":"grey"}) 9 | def fib(n): 10 | if n <= 1: 11 | return n 12 | return fib(n=n - 1) + fib(n=n - 2) 13 | 14 | 15 | def main(): 16 | # Call function 17 | print(fib(n=6)) 18 | # Save recursion tree to a file 19 | vs.make_animation("fibonacci.gif", delay=2) 20 | 21 | 22 | if __name__ == "__main__": 23 | main() -------------------------------------------------------------------------------- /examples/coin_change.py: -------------------------------------------------------------------------------- 1 | # Author: Bishal Sarang 2 | from visualiser.visualiser import Visualiser as vs 3 | 4 | """ 5 | Number of ways to make change: 6 | """ 7 | @vs(ignore_args=["coins"], show_argument_name=False) 8 | def f(coins, amount, n): 9 | if amount == 0: 10 | return 1 11 | 12 | if amount < 0: 13 | return 0 14 | 15 | if n <= 0 and amount >= 1: 16 | return 0 17 | 18 | include = f(coins=coins, amount=amount - coins[n - 1], n=n) 19 | exclude = f(coins=coins, amount=amount, n=n-1) 20 | 21 | return include + exclude 22 | 23 | def main(): 24 | amount = 5 25 | coins = [1, 2, 5] 26 | print(f(coins=coins, amount=amount, n=len(coins))) 27 | vs.make_animation("coin_change.gif", delay=3) 28 | 29 | if __name__ == "__main__": 30 | main() -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="recursion-visualiser", 8 | version="1.0.3", 9 | author="Bishal Sarangkoti", 10 | author_email="sarangbishal@gmail.com", 11 | description="A small python package to visualise recursive function on Python. It draws recursion tree", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/Bishalsarang/Recursion-Tree-Visualizer", 15 | packages=setuptools.find_packages(), 16 | classifiers=[ 17 | "Programming Language :: Python :: 3", 18 | "License :: OSI Approved :: MIT License", 19 | "Operating System :: OS Independent", 20 | ], 21 | install_requires=["pydot", "imageio"], 22 | python_requires='>=3.6', 23 | ) -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflows will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | deploy: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Python 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: '3.x' 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install setuptools wheel twine 25 | - name: Build and publish 26 | env: 27 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 28 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 29 | run: | 30 | python setup.py sdist bdist_wheel 31 | twine upload dist/* 32 | -------------------------------------------------------------------------------- /examples/subset.py: -------------------------------------------------------------------------------- 1 | # Author: Bishal Sarang 2 | 3 | # Import Visualiser class from module visualiser 4 | from visualiser.visualiser import Visualiser as vs 5 | 6 | """ 7 | Given an array of numbers, find all the subsets: 8 | eg: nums = [1, 2, 3] 9 | Output: 10 | [[], [1], [2], [2, 1], [3], [3, 1], [3, 2], [3, 2 , 1]] 11 | You can find my explanation here: https://qr.ae/TWHmsi 12 | """ 13 | 14 | subsets = [] 15 | @vs(ignore_args=["nums"], show_return_value=False, show_argument_name=False) 16 | def f(nums, i, current_subset): 17 | # If no more elements left 18 | if i == 0: 19 | subsets.append(current_subset) 20 | return 21 | # Exclude Current element 22 | f(nums=nums, i=i - 1, current_subset=current_subset) 23 | 24 | # Include current element 25 | f(nums=nums, i=i - 1, current_subset=current_subset + [nums[i - 1]]) 26 | 27 | if __name__ == "__main__": 28 | nums = [1, 2, 3] 29 | f(nums=nums, i = len(nums), current_subset=[]) 30 | # Save recursion tree to a file 31 | vs.make_animation("subset.gif", delay=3) -------------------------------------------------------------------------------- /examples/make_sum.py: -------------------------------------------------------------------------------- 1 | # Author: Bishal Sarang 2 | from visualiser.visualiser import Visualiser as vs 3 | 4 | """ 5 | Problemm Link: https://qr.ae/TltTCV 6 | Find all permutations of 2, 3, and 7 that can add up to make 10. (Ex: 2,2,2,2,2; or 3,7) 7 | Output: 8 | [2, 2, 2, 2, 2] 9 | [2, 2, 3, 3] 10 | [2, 3, 2, 3] 11 | [2, 3, 3, 2] 12 | [3, 2, 2, 3] 13 | [3, 2, 3, 2] 14 | [3, 3, 2, 2] 15 | [3, 7] 16 | [7, 3] 17 | """ 18 | 19 | 20 | @vs(ignore_args=['node_num'], show_argument_name=False, show_return_value=False) 21 | def f(sum, ans): 22 | # If sum becoms 0 we have found the required list 23 | if sum == 0: 24 | print(ans) 25 | 26 | # Include every other element to make the sum 27 | # Number that is included also can be included 28 | for elem in nums: 29 | if sum - elem >= 0: 30 | f(sum=sum - elem, ans=ans + [elem]) 31 | 32 | 33 | # We want to make the sum from list nums 34 | nums = [2, 3, 7] 35 | sum = 10 36 | 37 | # Call solve with sum and an empty list 38 | f(sum=sum, ans=[]) 39 | vs.write_image("make_sum.png") -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Bishal Sarangkoti 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /examples/missionaries.py: -------------------------------------------------------------------------------- 1 | from visualiser.visualiser import Visualiser as vs 2 | 3 | start_state = (3, 3, 1) 4 | goal_state = (0, 0, 0) 5 | 6 | options = [(2, 0), (1, 1), (0, 2), (1, 0), (0, 1)] 7 | visited = dict() 8 | 9 | def is_valid(m, c): 10 | return m >= 0 and m <= 3 and c >= 0 and c <= 3 11 | 12 | 13 | @vs(ignore_args=["node_num", "level"]) 14 | def dfs(m, c, s, level): 15 | if (m, c, s) == goal_state: 16 | return True 17 | 18 | if m > 0 and c > m: 19 | return False 20 | 21 | right_side_m = 3 - m 22 | right_side_c = 3 - c 23 | if right_side_m > 0 and right_side_c > right_side_m: 24 | return False 25 | 26 | visited[(m, c, s)] = True 27 | 28 | if s == 1: 29 | op = -1 30 | else: 31 | op = 1 32 | 33 | solved = False 34 | for i in range(5): 35 | next_m, next_c, next_side = m + op * options[i][0], c + op * options[i][1], int(not s) 36 | 37 | if is_valid(next_m, next_c): 38 | 39 | if (next_m, next_c, next_side) not in visited: 40 | solved = (solved or dfs(m=next_m, c=next_c, s=next_side, level=level + 1)) 41 | 42 | if solved: 43 | return True 44 | return solved 45 | 46 | 47 | if (dfs(m=3, c=3, s=1, level=0)): 48 | print("SOlution Found") 49 | # Save recursion tree to a file 50 | vs.make_animation("missionaries.gif", delay=2) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Recursion Visualiser 2 | 3 | ![](https://forthebadge.com/images/badges/made-with-python.svg) 4 | 5 | ![PyPI downloads](https://img.shields.io/pypi/dm/recursion-visualiser) 6 | ![Stars](https://img.shields.io/github/stars/Bishalsarang/Recursion-Tree-Visualizer) 7 | ![Forks](https://img.shields.io/github/forks/Bishalsarang/Recursion-Tree-Visualizer) 8 | 9 | ![](https://img.shields.io/pypi/v/recursion-visualiser) 10 | ![](https://img.shields.io/pypi/pyversions/recursion-visualiser) 11 | ![](https://img.shields.io/github/license/Bishalsarang/Recursion-Tree-Visualizer?logo=MIT) 12 | 13 | Recursion visualiser is a python tool that visualizes recursion tree with animation and draws recursion tree for recursive function. 14 | It works with almost any type of recursive function. 15 | Just add the recursion-visualiser decorator to your function and let it do the rest of the work. 16 | 17 | 18 | ## Installation 19 | ### 1. Installing graphviz 20 | #### Windows 21 | The only dependency for recursion visualiser is Graphviz 22 | - Download [graphviz binary](https://www2.graphviz.org/Packages/stable/windows/10/msbuild/Release/Win32/) 23 | - Add graphviz bin to path manually or by adding the following line on your script. Change the installation directory according to your installation path 24 | ``` 25 | # Set it to bin folder of graphviz 26 | os.environ["PATH"] += os.pathsep + 'C:/Program Files (x86)/Graphviz2.38/bin/' 27 | ``` 28 | 29 | #### Ubuntu 30 | - Install graphviz 31 | ``` 32 | sudo apt install graphviz 33 | ``` 34 | 35 | > The instructions to install graphviz for other operating system is available [here](https://www.graphviz.org/download/#linux) 36 | 37 | ### 2. Installing recursion-visualiser 38 | 39 | The easiest way to install ```recursion-visualiser``` package is from [pypi](https://pypi.org/project/recursion-visualiser/) 40 | ``` 41 | pip install recursion-visualiser 42 | ``` 43 | 44 | 45 | An alternative way is to clone the repository and install all the requirements. 46 | ``` 47 | pip install -r requirements.txt 48 | ``` 49 | 50 | ## Alternative Installation using Docker 51 | If you have `docker` and `docker-compose` installed then you can install `recursion-tree-visualiser` using `Docker` and `docker-compose.yml` file 52 | 1. Download `Docker` file from repo 53 | ```bash 54 | curl https://raw.githubusercontent.com/Bishalsarang/Recursion-Tree-Visualizer/master/Dockerfile --output Dockerfile 55 | ``` 56 | 57 | 3. Download `docker-compose.yml` 58 | ```bash 59 | curl https://raw.githubusercontent.com/Bishalsarang/Recursion-Tree-Visualizer/master/docker-compose.yml --output docker-compose.yml 60 | ``` 61 | 5. Start docker container 62 | ```bash 63 | CURRENT_UID=$(id -u):$(id -g) docker-compose up 64 | ``` 65 | 66 | 7. Run any python scripts and run using 67 | ``` 68 | docker-compose exec vs python fibonacci.py 69 | ``` 70 | ## Usage 71 | The preferred way to import the decorator class from the package is as: 72 | ```python 73 | from visualiser.visualiser import Visualiser as vs 74 | ``` 75 | ### 1. Fibonacci 76 | Let's draw the recursion tree for fibonacci number. 77 | Here is how the simple code looks like 78 | ```python 79 | def fib(n): 80 | if n <= 1: 81 | return n 82 | return fib(n - 1) + fib(n - 2) 83 | 84 | print(fib(6)) 85 | ``` 86 | 87 | Now we want to draw the recursion tree for this function. It is as simple as adding a decorator 88 | ```python 89 | # Author: Bishal Sarang 90 | 91 | # Import Visualiser class from module visualiser 92 | from visualiser.visualiser import Visualiser as vs 93 | 94 | # Add decorator 95 | # Decorator accepts optional arguments: ignore_args , show_argument_name, show_return_value and node_properties_kwargs 96 | @vs(node_properties_kwargs={"shape":"record", "color":"#f57542", "style":"filled", "fillcolor":"grey"}) 97 | def fib(n): 98 | if n <= 1: 99 | return n 100 | return fib(n=n - 1) + fib(n=n - 2) 101 | 102 | 103 | def main(): 104 | # Call function 105 | print(fib(n=6)) 106 | # Save recursion tree to a file 107 | vs.make_animation("fibonacci.gif", delay=2) 108 | 109 | 110 | if __name__ == "__main__": 111 | main() 112 | ``` 113 | Here are the changes required: 114 | 115 | - Add decorator Visualiser which accepts optional arguments `ignore_args`, `show_argument_name` and 'show_return_value' 116 | - Change every function calls to pass as keyword arguments. 117 | - Make_animation 118 | 119 | The output image are saved as "fibonacci.gif" and "fibonacci.png" 120 | 121 | Here is how the recursion tree looks like: 122 | Animation: 123 | ![enter image description here](https://raw.githubusercontent.com/Bishalsarang/Recursion-Tree-Visualizer/master/examples/fibonacci.gif) 124 | 125 | ![enter image description here](https://raw.githubusercontent.com/Bishalsarang/Recursion-Tree-Visualizer/master/examples/fibonacci.png) 126 | 127 | 128 | ## Support 129 | If you like this project and want to support it, consider buying me a coffee! 130 | 131 | [![Buy Me A Coffee](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/bishalsarang) 132 | 133 | Thank you for your support! 134 | 135 | 136 | ## TODO: 137 | - [x] Minimal working version 138 | - [x] Upload package to pypi 139 | - [x] Support animation 140 | - [x] Add node styles 141 | - [ ] Support aliasing for function name 142 | - [ ] Show repeated states 143 | - [x] Support node_color, backgroundcolor etc 144 | - [ ] Refactor 145 | - [ ] Handle base cases 146 | - [ ] Make more beautiful trees 147 | -------------------------------------------------------------------------------- /visualiser/visualiser.py: -------------------------------------------------------------------------------- 1 | # Author: Bishal Sarang 2 | import sys 3 | from functools import wraps 4 | from collections import OrderedDict 5 | import pydot 6 | import imageio 7 | import glob 8 | import os 9 | import shutil 10 | 11 | # Dot Language for graph 12 | dot_str_start = "digraph G {\n" 13 | dot_str_body = "" 14 | dot_str_end = "}" 15 | 16 | 17 | class Visualiser(object): 18 | def __init__(self, ignore_args=None, show_argument_name=True, 19 | show_return_value=True, node_properties_kwargs={}): 20 | self.init_graph() 21 | # If enabled shows keyword arguments ordered by keys 22 | self.show_argument_name = show_argument_name 23 | # If enables shows the return value at every nodes 24 | self.show_return_value = show_return_value 25 | 26 | self.node_properties_kwargs = node_properties_kwargs 27 | 28 | # Argument string that are to be ignored in diagram 29 | if ignore_args is None: 30 | self.ignore_args = ['node_num'] 31 | else: 32 | self.ignore_args = ['node_num'] + ignore_args 33 | 34 | @classmethod 35 | def write_image(cls, filename="out.png"): 36 | try: 37 | cls.graph.write_png(f"{filename}") 38 | print(f"File {filename} successfully written") 39 | except Exception: 40 | print(f"Writing {filename} failed") 41 | 42 | @classmethod 43 | def make_frames(cls): 44 | """ 45 | Make frame for each steps 46 | """ 47 | # If frame directory doesn't exist 48 | if not os.path.exists("frames"): 49 | os.makedirs("frames") 50 | 51 | Edges = cls.edges[::] 52 | Nodes = cls.nodes[::] 53 | print("Writing frames....") 54 | for i in range(len(Edges)): 55 | nodes = Nodes[::] 56 | edges = Edges[::] 57 | 58 | for j in range(0, i + 1): 59 | nodes[j] += '];' 60 | 61 | for j in range(i + 1, len(Edges)): 62 | nodes[j] += ' , style=invis];' 63 | edges[j] += ' [style=invis];' 64 | 65 | dot_str_body = "\n".join(nodes) + "\n" 66 | dot_str_body += "\n".join(edges) 67 | dot_str = dot_str_start + dot_str_body + dot_str_end 68 | g = pydot.graph_from_dot_data(dot_str) 69 | g[0].write_png(f"frames/temp_{i}.png") 70 | 71 | @classmethod 72 | def write_gif(cls, name="out.gif", delay=3): 73 | images = [] 74 | 75 | # sort frames images in ascending order to number in image filename 76 | # image filename: frames/temp_1.png 77 | sorted_images = sorted( 78 | glob.glob("frames/*.png"), 79 | key=lambda fn: int(fn.split("_")[1].split(".")[0]) 80 | ) 81 | 82 | for filename in sorted_images: 83 | images.append(imageio.imread(filename)) 84 | print("Writing gif...") 85 | imageio.mimsave(name, images, fps=delay) 86 | print(f"Saved gif {name} successfully") 87 | # Delete temporary directory 88 | shutil.rmtree("frames") 89 | 90 | @classmethod 91 | def make_animation(cls, filename="out.gif", delay=3): 92 | print("Starting to make animation") 93 | # Save final tree image as png 94 | try: 95 | cls.write_image(f"{filename.split('.')[0]}.png") 96 | except: 97 | print("Error saving image.") 98 | 99 | # Make animation as gif 100 | try: 101 | cls.make_frames() 102 | except: 103 | print("Error writing frames") 104 | 105 | try: 106 | cls.write_gif(filename, delay=delay) 107 | except: 108 | print("Error saving gif.") 109 | 110 | cls.init_graph() 111 | 112 | def extract_arg_strings(self, *args, **kwargs): 113 | """ 114 | Returns function signature arguments function label arguments as 115 | string. 116 | label_args_string contains only the arguments that are not in 117 | ignore_args. 118 | signature_args_string contains all the arguments available for the 119 | function. 120 | """ 121 | 122 | def get_kwargs_strings(ignore_args=[]): 123 | """Returns list of kwargs in string format from given kwargs items 124 | 125 | Args: 126 | ignore_args (list, optional) : list of ignored arguments. 127 | Default to []. 128 | 129 | Returns: 130 | strings_list: list of kwargs in string format 131 | """ 132 | 133 | strings_list = [] 134 | 135 | for key, value in kwargs.items(): 136 | if key not in ignore_args: 137 | if not self.show_argument_name: 138 | strings_list.append(f"{repr(value)}") 139 | else: 140 | strings_list.append(f"{key}={repr(value)}") 141 | 142 | return strings_list 143 | 144 | args_string = [repr(a) for a in args] 145 | signature_kwargs_string = [f"{repr(kwargs.get('node_num'))}"] 146 | label_kwargs_string = get_kwargs_strings(ignore_args=self.ignore_args) 147 | 148 | signature_args_string = ', '.join(signature_kwargs_string) 149 | label_args_string = ', '.join(args_string + label_kwargs_string) 150 | 151 | return signature_args_string, label_args_string 152 | 153 | def __call__(self, fn): 154 | @ wraps(fn) 155 | def wrapper(*args, **kwargs): 156 | global dot_str_body 157 | # Increment total number of nodes when a call is made 158 | self.node_count += 1 159 | 160 | # Update kwargs by adding dummy keyword node_num which helps to 161 | # uniquely identify each node 162 | kwargs.update({'node_num': self.node_count}) 163 | # Order all the keyword arguments 164 | kwargs = OrderedDict(sorted(kwargs.items())) 165 | 166 | """Details about current Function""" 167 | # Get signature and label arguments strings for current function 168 | (signature_args_string, 169 | label_args_string) = self.extract_arg_strings( 170 | *args, **kwargs) 171 | 172 | # Details about current function 173 | function_name = fn.__name__ 174 | 175 | # Current function signature looks as follows: 176 | # foo(1, 31, 0) or foo(a=1, b=31, c=0) 177 | function_signature = f"{function_name}({signature_args_string})" 178 | function_label = f"{function_name}({label_args_string})" 179 | """""" 180 | 181 | """Details about caller function""" 182 | caller_func_frame = sys._getframe(1) 183 | # All the argument names in caller/parent function 184 | caller_func_arg_names = caller_func_frame.f_code.co_varnames[ 185 | : fn.__code__.co_argcount] 186 | caller_func_locals = caller_func_frame.f_locals 187 | # Sort all the locals of caller function 188 | caller_func_locals = OrderedDict( 189 | sorted(caller_func_locals.items())) 190 | 191 | caller_func_kwargs = dict() 192 | 193 | # Extract only those locals that are in arguments 194 | for key, value in caller_func_locals.items(): 195 | if key in caller_func_arg_names: 196 | caller_func_kwargs[key] = value 197 | 198 | # If the nodes has parent node get node_num from parent node 199 | if self.stack: 200 | caller_func_kwargs.update({'node_num': self.stack[-1]}) 201 | 202 | caller_func_kwargs = OrderedDict( 203 | sorted(caller_func_kwargs.items())) 204 | 205 | (caller_func_args_string, 206 | caller_func_label_args_string) = self.extract_arg_strings( 207 | **caller_func_kwargs) 208 | 209 | # Caller Function 210 | caller_func_name = caller_func_frame.f_code.co_name 211 | 212 | # Extract the names of arguments only 213 | caller_func_signature = "{}({})".format( 214 | caller_func_name, caller_func_args_string) 215 | caller_func_label = "{}({})".format( 216 | caller_func_name, caller_func_label_args_string) 217 | """""" 218 | 219 | if caller_func_name == '': 220 | print(f"Drawing for {function_signature}") 221 | 222 | # Push node_count to stack 223 | self.stack.append(self.node_count) 224 | # Before actual function call delete keyword 'node_num' from kwargs 225 | del kwargs['node_num'] 226 | 227 | self.edges.append( 228 | f'"{caller_func_signature}" -> "{function_signature}"') 229 | 230 | # Construct node string to be rendered in graphviz 231 | node_string = f'"{function_signature}" [label="{function_label}"' 232 | 233 | if self.node_properties_kwargs: 234 | node_string += ", " + \ 235 | ", ".join([f'{key}="{value}"' for key, 236 | value in self.node_properties_kwargs.items()]) 237 | 238 | self.nodes.append(node_string) 239 | 240 | # Return after function call 241 | result = fn(*args, **kwargs) 242 | 243 | # Pop from tha stack after returning 244 | self.stack.pop() 245 | 246 | # If show_return_value flag is set, display the result 247 | if self.show_return_value: 248 | # If shape is set to record 249 | # Then separate function label and return value by a row 250 | if "record" in self.node_properties_kwargs.values(): 251 | function_label = "{" + \ 252 | function_label + f"|{result} }}" 253 | else: 254 | function_label += f"\n => {result}" 255 | 256 | child_node = pydot.Node(name=function_signature, 257 | label=function_label, 258 | **self.node_properties_kwargs) 259 | self.graph.add_node(child_node) 260 | 261 | # If the function is called by another function 262 | if caller_func_name not in ['', 'main']: 263 | parent_node = pydot.Node(name=caller_func_signature, 264 | label=caller_func_label, 265 | **self.node_properties_kwargs) 266 | self.graph.add_node(parent_node) 267 | edge = pydot.Edge(parent_node, child_node) 268 | self.graph.add_edge(edge) 269 | 270 | return result 271 | return wrapper 272 | 273 | @classmethod 274 | def init_graph(cls): 275 | # Total number of nodes 276 | cls.node_count = 0 277 | cls.graph = pydot.Dot(graph_type="digraph", bgcolor="#fff3af") 278 | # To track function call numbers 279 | cls.stack = [] 280 | cls.edges = [] 281 | cls.nodes = [] 282 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | test* 2 | .idea 3 | .idea 4 | img 5 | out* 6 | *frames* 7 | 8 | packaging.txt 9 | 10 | # Byte-compiled / optimized / DLL files 11 | __pycache__/ 12 | *.py[cod] 13 | *$py.class 14 | 15 | # C extensions 16 | *.so 17 | 18 | # Distribution / packaging 19 | .Python 20 | build/ 21 | develop-eggs/ 22 | dist/ 23 | downloads/ 24 | eggs/ 25 | .eggs/ 26 | lib/ 27 | lib64/ 28 | parts/ 29 | sdist/ 30 | var/ 31 | wheels/ 32 | pip-wheel-metadata/ 33 | share/python-wheels/ 34 | *.egg-info/ 35 | .installed.cfg 36 | *.egg 37 | MANIFEST 38 | 39 | # PyInstaller 40 | # Usually these files are written by a python script from a template 41 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 42 | *.manifest 43 | *.spec 44 | 45 | # Installer logs 46 | pip-log.txt 47 | pip-delete-this-directory.txt 48 | 49 | # Unit test / coverage reports 50 | htmlcov/ 51 | .tox/ 52 | .nox/ 53 | .coverage 54 | .coverage.* 55 | .cache 56 | nosetests.xml 57 | coverage.xml 58 | *.cover 59 | *.py,cover 60 | .hypothesis/ 61 | .pytest_cache/ 62 | 63 | # Translations 64 | *.mo 65 | *.pot 66 | 67 | # Django stuff: 68 | *.log 69 | local_settings.py 70 | db.sqlite3 71 | db.sqlite3-journal 72 | 73 | # Flask stuff: 74 | instance/ 75 | .webassets-cache 76 | 77 | # Scrapy stuff: 78 | .scrapy 79 | 80 | # Sphinx documentation 81 | docs/_build/ 82 | 83 | # PyBuilder 84 | target/ 85 | 86 | # Jupyter Notebook 87 | .ipynb_checkpoints 88 | 89 | # IPython 90 | profile_default/ 91 | ipython_config.py 92 | 93 | # pyenv 94 | .python-version 95 | 96 | # pipenv 97 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 98 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 99 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 100 | # install all needed dependencies. 101 | #Pipfile.lock 102 | 103 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 104 | __pypackages__/ 105 | 106 | # Celery stuff 107 | celerybeat-schedule 108 | celerybeat.pid 109 | 110 | # SageMath parsed files 111 | *.sage.py 112 | 113 | # Environments 114 | .env 115 | .venv 116 | env/ 117 | venv/ 118 | ENV/ 119 | env.bak/ 120 | venv.bak/ 121 | 122 | # Spyder project settings 123 | .spyderproject 124 | .spyproject 125 | 126 | # Rope project settings 127 | .ropeproject 128 | 129 | # mkdocs documentation 130 | /site 131 | 132 | # mypy 133 | .mypy_cache/ 134 | .dmypy.json 135 | dmypy.json 136 | 137 | # Pyre type checker 138 | .pyre/ 139 | ### JupyterNotebooks template 140 | # gitignore template for Jupyter Notebooks 141 | # website: http://jupyter.org/ 142 | 143 | .ipynb_checkpoints 144 | */.ipynb_checkpoints/* 145 | 146 | # IPython 147 | profile_default/ 148 | ipython_config.py 149 | 150 | # Remove previous ipynb_checkpoints 151 | # git rm -r .ipynb_checkpoints/ 152 | 153 | ### Python template 154 | # Byte-compiled / optimized / DLL files 155 | __pycache__/ 156 | *.py[cod] 157 | *$py.class 158 | 159 | # C extensions 160 | *.so 161 | 162 | # Distribution / packaging 163 | .Python 164 | build/ 165 | develop-eggs/ 166 | dist/ 167 | downloads/ 168 | eggs/ 169 | .eggs/ 170 | lib/ 171 | lib64/ 172 | parts/ 173 | sdist/ 174 | var/ 175 | wheels/ 176 | pip-wheel-metadata/ 177 | share/python-wheels/ 178 | *.egg-info/ 179 | .installed.cfg 180 | *.egg 181 | MANIFEST 182 | 183 | # PyInstaller 184 | # Usually these files are written by a python script from a template 185 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 186 | *.manifest 187 | *.spec 188 | 189 | # Installer logs 190 | pip-log.txt 191 | pip-delete-this-directory.txt 192 | 193 | # Unit test / coverage reports 194 | htmlcov/ 195 | .tox/ 196 | .nox/ 197 | .coverage 198 | .coverage.* 199 | .cache 200 | nosetests.xml 201 | coverage.xml 202 | *.cover 203 | *.py,cover 204 | .hypothesis/ 205 | .pytest_cache/ 206 | 207 | # Translations 208 | *.mo 209 | *.pot 210 | 211 | # Django stuff: 212 | *.log 213 | local_settings.py 214 | db.sqlite3 215 | db.sqlite3-journal 216 | 217 | # Flask stuff: 218 | instance/ 219 | .webassets-cache 220 | 221 | # Scrapy stuff: 222 | .scrapy 223 | 224 | # Sphinx documentation 225 | docs/_build/ 226 | 227 | # PyBuilder 228 | target/ 229 | 230 | # Jupyter Notebook 231 | .ipynb_checkpoints 232 | 233 | # IPython 234 | profile_default/ 235 | ipython_config.py 236 | 237 | # pyenv 238 | .python-version 239 | 240 | # pipenv 241 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 242 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 243 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 244 | # install all needed dependencies. 245 | #Pipfile.lock 246 | 247 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 248 | __pypackages__/ 249 | 250 | # Celery stuff 251 | celerybeat-schedule 252 | celerybeat.pid 253 | 254 | # SageMath parsed files 255 | *.sage.py 256 | 257 | # Environments 258 | .env 259 | .venv 260 | env/ 261 | venv/ 262 | ENV/ 263 | env.bak/ 264 | venv.bak/ 265 | 266 | # Spyder project settings 267 | .spyderproject 268 | .spyproject 269 | 270 | # Rope project settings 271 | .ropeproject 272 | 273 | # mkdocs documentation 274 | /site 275 | 276 | # mypy 277 | .mypy_cache/ 278 | .dmypy.json 279 | dmypy.json 280 | 281 | # Pyre type checker 282 | .pyre/ 283 | 284 | ### VisualStudio template 285 | ## Ignore Visual Studio temporary files, build results, and 286 | ## files generated by popular Visual Studio add-ons. 287 | ## 288 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 289 | 290 | # User-specific files 291 | *.rsuser 292 | *.suo 293 | *.user 294 | *.userosscache 295 | *.sln.docstates 296 | 297 | # User-specific files (MonoDevelop/Xamarin Studio) 298 | *.userprefs 299 | 300 | # Mono auto generated files 301 | mono_crash.* 302 | 303 | # Build results 304 | [Dd]ebug/ 305 | [Dd]ebugPublic/ 306 | [Rr]elease/ 307 | [Rr]eleases/ 308 | x64/ 309 | x86/ 310 | [Aa][Rr][Mm]/ 311 | [Aa][Rr][Mm]64/ 312 | bld/ 313 | [Bb]in/ 314 | [Oo]bj/ 315 | [Ll]og/ 316 | [Ll]ogs/ 317 | 318 | # Visual Studio 2015/2017 cache/options directory 319 | .vs/ 320 | # Uncomment if you have tasks that create the project's static files in wwwroot 321 | #wwwroot/ 322 | 323 | # Visual Studio 2017 auto generated files 324 | Generated\ Files/ 325 | 326 | # MSTest test Results 327 | [Tt]est[Rr]esult*/ 328 | [Bb]uild[Ll]og.* 329 | 330 | # NUnit 331 | *.VisualState.xml 332 | TestResult.xml 333 | nunit-*.xml 334 | 335 | # Build Results of an ATL Project 336 | [Dd]ebugPS/ 337 | [Rr]eleasePS/ 338 | dlldata.c 339 | 340 | # Benchmark Results 341 | BenchmarkDotNet.Artifacts/ 342 | 343 | # .NET Core 344 | project.lock.json 345 | project.fragment.lock.json 346 | artifacts/ 347 | 348 | # StyleCop 349 | StyleCopReport.xml 350 | 351 | # Files built by Visual Studio 352 | *_i.c 353 | *_p.c 354 | *_h.h 355 | *.ilk 356 | *.meta 357 | *.obj 358 | *.iobj 359 | *.pch 360 | *.pdb 361 | *.ipdb 362 | *.pgc 363 | *.pgd 364 | *.rsp 365 | *.sbr 366 | *.tlb 367 | *.tli 368 | *.tlh 369 | *.tmp 370 | *.tmp_proj 371 | *_wpftmp.csproj 372 | *.log 373 | *.vspscc 374 | *.vssscc 375 | .builds 376 | *.pidb 377 | *.svclog 378 | *.scc 379 | 380 | # Chutzpah Test files 381 | _Chutzpah* 382 | 383 | # Visual C++ cache files 384 | ipch/ 385 | *.aps 386 | *.ncb 387 | *.opendb 388 | *.opensdf 389 | *.sdf 390 | *.cachefile 391 | *.VC.db 392 | *.VC.VC.opendb 393 | 394 | # Visual Studio profiler 395 | *.psess 396 | *.vsp 397 | *.vspx 398 | *.sap 399 | 400 | # Visual Studio Trace Files 401 | *.e2e 402 | 403 | # TFS 2012 Local Workspace 404 | $tf/ 405 | 406 | # Guidance Automation Toolkit 407 | *.gpState 408 | 409 | # ReSharper is a .NET coding add-in 410 | _ReSharper*/ 411 | *.[Rr]e[Ss]harper 412 | *.DotSettings.user 413 | 414 | # TeamCity is a build add-in 415 | _TeamCity* 416 | 417 | # DotCover is a Code Coverage Tool 418 | *.dotCover 419 | 420 | # AxoCover is a Code Coverage Tool 421 | .axoCover/* 422 | !.axoCover/settings.json 423 | 424 | # Visual Studio code coverage results 425 | *.coverage 426 | *.coveragexml 427 | 428 | # NCrunch 429 | _NCrunch_* 430 | .*crunch*.local.xml 431 | nCrunchTemp_* 432 | 433 | # MightyMoose 434 | *.mm.* 435 | AutoTest.Net/ 436 | 437 | # Web workbench (sass) 438 | .sass-cache/ 439 | 440 | # Installshield output folder 441 | [Ee]xpress/ 442 | 443 | # DocProject is a documentation generator add-in 444 | DocProject/buildhelp/ 445 | DocProject/Help/*.HxT 446 | DocProject/Help/*.HxC 447 | DocProject/Help/*.hhc 448 | DocProject/Help/*.hhk 449 | DocProject/Help/*.hhp 450 | DocProject/Help/Html2 451 | DocProject/Help/html 452 | 453 | # Click-Once directory 454 | publish/ 455 | 456 | # Publish Web Output 457 | *.[Pp]ublish.xml 458 | *.azurePubxml 459 | # Note: Comment the next line if you want to checkin your web deploy settings, 460 | # but database connection strings (with potential passwords) will be unencrypted 461 | *.pubxml 462 | *.publishproj 463 | 464 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 465 | # checkin your Azure Web App publish settings, but sensitive information contained 466 | # in these scripts will be unencrypted 467 | PublishScripts/ 468 | 469 | # NuGet Packages 470 | *.nupkg 471 | # NuGet Symbol Packages 472 | *.snupkg 473 | # The packages folder can be ignored because of Package Restore 474 | **/[Pp]ackages/* 475 | # except build/, which is used as an MSBuild target. 476 | !**/[Pp]ackages/build/ 477 | # Uncomment if necessary however generally it will be regenerated when needed 478 | #!**/[Pp]ackages/repositories.config 479 | # NuGet v3's project.json files produces more ignorable files 480 | *.nuget.props 481 | *.nuget.targets 482 | 483 | # Microsoft Azure Build Output 484 | csx/ 485 | *.build.csdef 486 | 487 | # Microsoft Azure Emulator 488 | ecf/ 489 | rcf/ 490 | 491 | # Windows Store app package directories and files 492 | AppPackages/ 493 | BundleArtifacts/ 494 | Package.StoreAssociation.xml 495 | _pkginfo.txt 496 | *.appx 497 | *.appxbundle 498 | *.appxupload 499 | 500 | # Visual Studio cache files 501 | # files ending in .cache can be ignored 502 | *.[Cc]ache 503 | # but keep track of directories ending in .cache 504 | !?*.[Cc]ache/ 505 | 506 | # Others 507 | ClientBin/ 508 | ~$* 509 | *~ 510 | *.dbmdl 511 | *.dbproj.schemaview 512 | *.jfm 513 | *.pfx 514 | *.publishsettings 515 | orleans.codegen.cs 516 | 517 | # Including strong name files can present a security risk 518 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 519 | #*.snk 520 | 521 | # Since there are multiple workflows, uncomment next line to ignore bower_components 522 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 523 | #bower_components/ 524 | 525 | # RIA/Silverlight projects 526 | Generated_Code/ 527 | 528 | # Backup & report files from converting an old project file 529 | # to a newer Visual Studio version. Backup files are not needed, 530 | # because we have git ;-) 531 | _UpgradeReport_Files/ 532 | Backup*/ 533 | UpgradeLog*.XML 534 | UpgradeLog*.htm 535 | ServiceFabricBackup/ 536 | *.rptproj.bak 537 | 538 | # SQL Server files 539 | *.mdf 540 | *.ldf 541 | *.ndf 542 | 543 | # Business Intelligence projects 544 | *.rdl.data 545 | *.bim.layout 546 | *.bim_*.settings 547 | *.rptproj.rsuser 548 | *- [Bb]ackup.rdl 549 | *- [Bb]ackup ([0-9]).rdl 550 | *- [Bb]ackup ([0-9][0-9]).rdl 551 | 552 | # Microsoft Fakes 553 | FakesAssemblies/ 554 | 555 | # GhostDoc plugin setting file 556 | *.GhostDoc.xml 557 | 558 | # Node.js Tools for Visual Studio 559 | .ntvs_analysis.dat 560 | node_modules/ 561 | 562 | # Visual Studio 6 build log 563 | *.plg 564 | 565 | # Visual Studio 6 workspace options file 566 | *.opt 567 | 568 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 569 | *.vbw 570 | 571 | # Visual Studio LightSwitch build output 572 | **/*.HTMLClient/GeneratedArtifacts 573 | **/*.DesktopClient/GeneratedArtifacts 574 | **/*.DesktopClient/ModelManifest.xml 575 | **/*.Server/GeneratedArtifacts 576 | **/*.Server/ModelManifest.xml 577 | _Pvt_Extensions 578 | 579 | # Paket dependency manager 580 | .paket/paket.exe 581 | paket-files/ 582 | 583 | # FAKE - F# Make 584 | .fake/ 585 | 586 | # CodeRush personal settings 587 | .cr/personal 588 | 589 | # Python Tools for Visual Studio (PTVS) 590 | __pycache__/ 591 | *.pyc 592 | 593 | # Cake - Uncomment if you are using it 594 | # tools/** 595 | # !tools/packages.config 596 | 597 | # Tabs Studio 598 | *.tss 599 | 600 | # Telerik's JustMock configuration file 601 | *.jmconfig 602 | 603 | # BizTalk build output 604 | *.btp.cs 605 | *.btm.cs 606 | *.odx.cs 607 | *.xsd.cs 608 | 609 | # OpenCover UI analysis results 610 | OpenCover/ 611 | 612 | # Azure Stream Analytics local run output 613 | ASALocalRun/ 614 | 615 | # MSBuild Binary and Structured Log 616 | *.binlog 617 | 618 | # NVidia Nsight GPU debugger configuration file 619 | *.nvuser 620 | 621 | # MFractors (Xamarin productivity tool) working folder 622 | .mfractor/ 623 | 624 | # Local History for Visual Studio 625 | .localhistory/ 626 | 627 | # BeatPulse healthcheck temp database 628 | healthchecksdb 629 | 630 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 631 | MigrationBackup/ 632 | 633 | # Ionide (cross platform F# VS Code tools) working folder 634 | .ionide/ 635 | 636 | !/packaging.txt 637 | !/packaging.txt 638 | --------------------------------------------------------------------------------