invocation-tree 0.0.15__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,283 @@
1
+ # This file is part of invocation_tree.
2
+ # Copyright (c) 2023, Bas Terwijn.
3
+ # SPDX-License-Identifier: BSD-2-Clause
4
+
5
+ from graphviz import Digraph
6
+ import html
7
+ import sys
8
+ import difflib
9
+
10
+ __version__ = "0.0.15"
11
+ __author__ = 'Bas Terwijn'
12
+
13
+ def highlight_diff(str1, str2):
14
+ matcher = difflib.SequenceMatcher(None, str1, str2)
15
+ result = []
16
+ is_highlighted = False
17
+ for tag, i1, i2, j1, j2 in matcher.get_opcodes():
18
+ if tag == 'replace':
19
+ result.append(f'<B>{str2[j1:j2]}&#8203;</B>&#8203;')
20
+ is_highlighted = True
21
+ elif tag == 'delete':
22
+ result.append(f'<FONT COLOR="#aaaaaa"><I>{str1[i1:i2]}&#8203;</I></FONT>&#8203;')
23
+ is_highlighted = True
24
+ elif tag == 'insert':
25
+ result.append(f'<B>{str2[j1:j2]}&#8203;</B>&#8203;')
26
+ is_highlighted = True
27
+ elif tag == 'equal':
28
+ result.append(str2[j1:j2])
29
+ diff = ''.join(result)
30
+ return diff, is_highlighted
31
+
32
+ def get_class_function_name(frame):
33
+ class_name = ''
34
+ if 'self' in frame.f_locals:
35
+ class_name = frame.f_locals['self'].__class__.__name__ + '.'
36
+ function_name = class_name+frame.f_code.co_name
37
+ return function_name
38
+
39
+ def filter_variables(var, val):
40
+ if callable(val):
41
+ return False
42
+ if isinstance(val, (type, type(object), type(__import__('os')))):
43
+ return False
44
+ if var.startswith('__'):
45
+ return False
46
+ return True
47
+
48
+ class Tree_Node:
49
+
50
+ def __init__(self, node_id, frame, return_value):
51
+ self.node_id = node_id
52
+ self.frame = frame
53
+ self.return_value = return_value
54
+ self.is_returned = False
55
+ self.strings = {}
56
+
57
+ def __repr__(self):
58
+ return f'node_id:{self.node_id} frame:{self.frame} return_value:{self.return_value}'
59
+
60
+ class Invocation_Tree:
61
+
62
+ def __init__(self,
63
+ filename='tree.pdf',
64
+ show=True,
65
+ block=True,
66
+ src_loc=True,
67
+ each_line=False,
68
+ gifcount=-1,
69
+ max_string_len=150,
70
+ indent=' ',
71
+ color_paused = '#ccffcc',
72
+ color_active = '#ffffff',
73
+ color_returned = '#ffcccc',
74
+ to_string=None,
75
+ hide=None,
76
+ cleanup=True,
77
+ quiet=True):
78
+ # --- config
79
+ self.filename = filename
80
+ self.prev_filename = None
81
+ self.show = show
82
+ self.block = block
83
+ self.src_loc = src_loc
84
+ self.max_string_len = max_string_len
85
+ self.gifcount = gifcount
86
+ self.indent = indent
87
+ self.color_paused = color_paused
88
+ self.color_active = color_active
89
+ self.color_returned = color_returned
90
+ self.each_line = each_line
91
+ self.to_string = {}
92
+ if not to_string is None:
93
+ self.to_string = to_string
94
+ self.hide = set()
95
+ if not hide is None:
96
+ self.hide = hide
97
+ self.cleanup = cleanup
98
+ self.quiet = quiet
99
+ # --- core
100
+ self.stack = []
101
+ self.returned = []
102
+ self.prev_returned = []
103
+ self.node_id = 0
104
+ self.node_id_to_table = {}
105
+ self.edges = []
106
+ self.is_highlighted = False
107
+ self.ignore_calls = {'Invocation_Tree.__exit__', 'Invocation_Tree.stop_trace'}
108
+
109
+ def __repr__(self):
110
+ return f'Invocation_Tree(filename={repr(self.filename)}, show={self.show}, block={self.block}, each_line={self.each_line}, gifcount={self.gifcount})'
111
+
112
+ def __call__(self, fun, *args, **kwargs):
113
+ try:
114
+ sys.settrace(self.trace_calls)
115
+ result = fun(*args, **kwargs)
116
+ finally:
117
+ sys.settrace(None)
118
+ return result
119
+
120
+ def value_to_string(self, key, value, use_repr=False):
121
+ try:
122
+ if id(value) in self.to_string:
123
+ val_str = self.to_string[id(value)](value)
124
+ elif key in self.to_string:
125
+ val_str = self.to_string[key](value)
126
+ elif type(value) in self.to_string:
127
+ val_str = self.to_string[type(value)](value)
128
+ else:
129
+ val_str = repr(value) if use_repr else str(value)
130
+ except Exception as e:
131
+ val_str = '<not-string-convertable>'
132
+ if len(val_str) > self.max_string_len:
133
+ val_str = '...'+val_str[-self.max_string_len:]
134
+ return html.escape(val_str)
135
+
136
+ def get_hightlighted_content(self, tree_node, key, value, use_old_content=False, use_repr=False):
137
+ if use_old_content:
138
+ return tree_node.strings[key]
139
+ is_highlighted = False
140
+ content = self.value_to_string(key, value, use_repr=use_repr)
141
+ if key in tree_node.strings:
142
+ use_old_content = tree_node.strings[key]
143
+ hightlighted_content, is_highlighted = highlight_diff(use_old_content, content)
144
+ else:
145
+ if len(content.strip())>0: # fixes graphviz error on empty <B></B> tag
146
+ hightlighted_content = '<B>'+content+'</B>'
147
+ is_highlighted = True
148
+ else:
149
+ hightlighted_content = content
150
+ tree_node.strings[key] = content
151
+ self.is_highlighted |= is_highlighted
152
+ return hightlighted_content
153
+
154
+ def build_html_table(self, tree_node, active=False, is_returned=None, use_old_content=False):
155
+ if is_returned is None:
156
+ is_returned = tree_node.is_returned
157
+ else:
158
+ tree_node.is_returned = is_returned
159
+ return_value = tree_node.return_value
160
+ border = 1
161
+ color = self.color_paused
162
+ if active:
163
+ color = self.color_active
164
+ border = 3
165
+ if is_returned:
166
+ color = self.color_returned
167
+ table = f'<\n<TABLE BORDER="{str(border)}" CELLBORDER="0" CELLSPACING="0" BGCOLOR="{color}">\n <TR>'
168
+ class_fun_name = get_class_function_name(tree_node.frame)
169
+ local_vars = tree_node.frame.f_locals
170
+ hightlighted_content = self.get_hightlighted_content(tree_node, class_fun_name, class_fun_name, use_old_content)
171
+ table += '<TD ALIGN="left">'+ '➤'+ hightlighted_content +'</TD>'
172
+ for var,val in local_vars.items():
173
+ var_name = class_fun_name+'..'+var
174
+ val_name = class_fun_name+'.'+var
175
+ if filter_variables(var,val) and not val_name in self.hide:
176
+ table += '</TR>\n <TR>'
177
+ hightlighted_var = self.get_hightlighted_content(tree_node, var_name, var, use_old_content)
178
+ hightlighted_val = self.get_hightlighted_content(tree_node, val_name, val, use_old_content, use_repr=True)
179
+ hightlighted_content = self.indent + hightlighted_var + ': ' + hightlighted_val
180
+ table += '<TD ALIGN="left">'+ hightlighted_content +'</TD>'
181
+ if is_returned:
182
+ return_name = class_fun_name+'.return'
183
+ if not return_name in self.hide:
184
+ table += '</TR>\n <TR>'
185
+ hightlighted_content = self.get_hightlighted_content(tree_node, return_name, return_value, use_old_content, use_repr=True)
186
+ table += '<TD ALIGN="left">'+ 'return ' + hightlighted_content +'</TD>'
187
+ table += '</TR>\n</TABLE>>'
188
+ return table
189
+
190
+ def update_node(self, tree_node, active=False, returned=None, use_old_content=False):
191
+ table = self.build_html_table(tree_node, active, returned, use_old_content=use_old_content)
192
+ self.node_id_to_table[str(tree_node.node_id)] = table
193
+
194
+ def add_edge(self, tree_node1, tree_node2):
195
+ self.edges.append((str(tree_node1.node_id), str(tree_node2.node_id)))
196
+
197
+ def get_output_filename(self):
198
+ if self.gifcount >= 0:
199
+ splits = self.filename.split('.')
200
+ if len(splits)>1:
201
+ splits[-2]+=str(self.gifcount)
202
+ self.gifcount += 1
203
+ return '.'.join(splits)
204
+ return self.filename
205
+
206
+ def create_graph(self):
207
+ graphviz_graph_attr = {}
208
+ graphviz_node_attr = {'shape':'plaintext'}
209
+ graphviz_edge_attr = {}
210
+ graph = Digraph('invocation_tree',
211
+ graph_attr=graphviz_graph_attr,
212
+ node_attr=graphviz_node_attr,
213
+ edge_attr=graphviz_edge_attr)
214
+ for node in self.prev_returned:
215
+ self.update_node(node, use_old_content=True)
216
+ self.prev_returned = []
217
+ for node in self.returned:
218
+ self.update_node(node, returned=True)
219
+ self.prev_returned.append(node)
220
+ self.returned = []
221
+ for node in self.stack:
222
+ self.update_node(node, active=(node is self.stack[-1]))
223
+ for nid, table in self.node_id_to_table.items():
224
+ graph.node(nid, label=table)
225
+ for nid1, nid2 in self.edges:
226
+ graph.edge(nid1, nid2)
227
+ return graph
228
+
229
+ def render_graph(self, graph):
230
+ view = (self.filename!=self.prev_filename) and self.show
231
+ graph.render(outfile=self.get_output_filename(), view=view, cleanup=self.cleanup, quiet=self.quiet)
232
+ self.prev_filename = self.filename
233
+
234
+ def output_graph(self, frame, event):
235
+ if self.block or self.gifcount >= 0:
236
+ self.is_highlighted = False
237
+ graph = self.create_graph()
238
+ if self.is_highlighted:
239
+ self.render_graph(graph)
240
+ if self.block:
241
+ if self.src_loc:
242
+ filename = frame.f_code.co_filename
243
+ line_nr = frame.f_lineno
244
+ print(f'{event.capitalize()} at {filename}:{line_nr}', end='. ')
245
+ input('Press <Enter> to continue...')
246
+ else:
247
+ graph = self.create_graph()
248
+ self.render_graph(graph)
249
+
250
+ def trace_calls(self, frame, event, arg):
251
+ class_fun_name = get_class_function_name(frame)
252
+ if not class_fun_name in self.ignore_calls:
253
+ if event == 'call':
254
+ self.stack.append(Tree_Node(self.node_id, frame, None))
255
+ self.node_id += 1
256
+ if len(self.stack)>1:
257
+ self.add_edge(self.stack[-2], self.stack[-1])
258
+ self.output_graph(frame, event)
259
+ elif event == 'return':
260
+ self.stack[-1].return_value = arg
261
+ self.returned.append(self.stack.pop())
262
+ self.output_graph(frame, event)
263
+ elif event == 'line' and self.each_line:
264
+ self.output_graph(frame, event)
265
+ return self.trace_calls
266
+
267
+ def blocking(filename='tree.pdf'):
268
+ return Invocation_Tree(filename=filename)
269
+
270
+ def blocking_each_change(filename='tree.pdf'):
271
+ return Invocation_Tree(filename=filename, each_line=True)
272
+
273
+ def debugger(filename='tree.pdf'):
274
+ return Invocation_Tree(filename=filename, show=False, block=False, each_line=True)
275
+
276
+ def gif(filename='tree.png'):
277
+ return Invocation_Tree(filename=filename, show=False, block=False, gifcount=0)
278
+
279
+ def gif_each_change(filename='tree.png'):
280
+ return Invocation_Tree(filename=filename, show=False, block=False, gifcount=0, each_line=True)
281
+
282
+ def non_blocking(filename='tree.pdf'):
283
+ return Invocation_Tree(filename=filename, block=False)
@@ -0,0 +1,252 @@
1
+ Metadata-Version: 2.4
2
+ Name: invocation_tree
3
+ Version: 0.0.15
4
+ Summary: Generates an invocation tree of functions calls.
5
+ Author-email: Bas Terwijn <bterwijn@gmail.com>
6
+ License-Expression: BSD-2-Clause
7
+ Project-URL: Homepage, https://github.com/bterwijn/invocation-tree
8
+ Project-URL: Repository, https://github.com/bterwijn/invocation-tree.git
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Education
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Education
14
+ Classifier: Topic :: Software Development :: Debuggers
15
+ Requires-Python: >=3.7
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE.txt
18
+ Requires-Dist: graphviz
19
+ Dynamic: license-file
20
+
21
+ # Installation #
22
+ Install (or upgrade) `invocation_tree` using pip:
23
+ ```
24
+ pip install --upgrade invocation_tree
25
+ ```
26
+ Additionally [Graphviz](https://graphviz.org/download/) needs to be installed.
27
+
28
+ # Invocation Tree #
29
+ The [invocation_tree](https://pypi.org/project/invocation-tree/) package is designed to help with **program understanding and debugging** by visualizing the **tree of function invocations** that occur during program execution. Here’s a simple example of how it works, we start with `a = 1` and compute:
30
+
31
+ ```
32
+ (a - 3 + 9) * 6
33
+ ```
34
+
35
+ ```python
36
+ import invocation_tree as ivt
37
+
38
+ def main():
39
+ a = 1
40
+ a = expression(a)
41
+ return multiply(a, 6)
42
+
43
+ def expression(a):
44
+ a = subtract(a, 3)
45
+ return add(a, 9)
46
+
47
+ def subtract(a, b):
48
+ return a - b
49
+
50
+ def add(a, b):
51
+ return a + b
52
+
53
+ def multiply(a, b):
54
+ return a * b
55
+
56
+ tree = ivt.blocking()
57
+ print( tree(main) ) # show invocation tree starting at main
58
+ ```
59
+ Running the program and pressing &lt;Enter&gt; a number of times results in:
60
+ ![compute](https://raw.githubusercontent.com/bterwijn/invocation_tree/main/images/compute.gif)
61
+ ```
62
+ 42
63
+ ```
64
+ Each node in the tree represents a function call, and the node's color indicates its state:
65
+
66
+ - White: The function is currently being executed (it is at the top of the call stack).
67
+ - Green: The function is paused and will resume execution later (it is lower down on the call stack).
68
+ - Red: The function has completed execution and returned (it has been removed from the call stack).
69
+
70
+ For every function, the package displays its **local variables** and **return value**. Changes to these values over time are highlighted using bold text and gray shading to make them easy to track.
71
+
72
+ # Chapters #
73
+
74
+ [Comprehensions](#Comprehensions)
75
+
76
+ [Debugger](#Debugger)
77
+
78
+ [Recursion](#Recursion)
79
+
80
+ [Configuration](#Configuration)
81
+
82
+ [Troubleshooting](#Troubleshooting)
83
+
84
+ # Author #
85
+ Bas Terwijn
86
+
87
+ # Inspiration #
88
+ Inspired by [rcviz](https://github.com/carlsborg/rcviz).
89
+
90
+ # Supported by #
91
+ <img src="https://raw.githubusercontent.com/bterwijn/memory_graph/main/images/uva.png" alt="University of Amsterdam" width="600">
92
+
93
+ ___
94
+ ___
95
+
96
+ # Comprehensions #
97
+ In this more interesting example we compute which students pass a course by using list and dictionary comprehensions.
98
+
99
+ ```python
100
+ import invocation_tree as ivt
101
+ from decimal import Decimal, ROUND_HALF_UP
102
+
103
+ def main():
104
+ students = {'Ann':[7.5, 8.0],
105
+ 'Bob':[4.5, 6.0],
106
+ 'Coy':[7.5, 6.0]}
107
+ averages = {student:compute_average(grades)
108
+ for student, grades in students.items()}
109
+ passing = passing_students(averages)
110
+ print(passing)
111
+
112
+ def compute_average(grades):
113
+ average = sum(grades)/len(grades)
114
+ return half_up_round(average, 1)
115
+
116
+ def half_up_round(value, digits=0):
117
+ """ High-precision half-up rounding of 'value' to a specified number of 'digits'. """
118
+ return float(Decimal(str(value)).quantize(Decimal(f"1e-{digits}"),
119
+ rounding=ROUND_HALF_UP))
120
+
121
+ def passing_students(averages):
122
+ return [student
123
+ for student, average in averages.items()
124
+ if average >= 5.5]
125
+
126
+ if __name__ == '__main__':
127
+ tree = ivt.blocking()
128
+ tree(main)
129
+ ```
130
+ ![students](https://raw.githubusercontent.com/bterwijn/invocation_tree/main/images/students.gif)
131
+ ```
132
+ ['Ann', 'Coy']
133
+ ```
134
+
135
+ ## Blocking ##
136
+ The program blocks execution at every function call and return statement, printing the current location in the source code. Press the &lt;Enter&gt; key to continue execution. To block at every line of the program (like in a debugger tool) and only where a change of value occured, use instead:
137
+
138
+ ```python
139
+ tree = ivt.blocking_each_change()
140
+ ```
141
+
142
+ # Debugger #
143
+ To visualize the invocation tree in a debugger tool, such as the integrated debugger in Visual Studio Code, use instead:
144
+
145
+ ```python
146
+ tree = ivt.debugger()
147
+ ```
148
+
149
+ and open the 'tree.pdf' file manually.
150
+ ![Visual Studio Code debugger](https://raw.githubusercontent.com/bterwijn/invocation_tree/main/images/vscode.png)
151
+
152
+ # Recursion #
153
+ An invocation tree is particularly helpful to better understand recursion. A simple `factorial()` example:
154
+
155
+ ```python
156
+ import invocation_tree as ivt
157
+
158
+ def factorial(n):
159
+ if n <= 1:
160
+ return 1
161
+ prev_result = factorial(n - 1)
162
+ return n * prev_result
163
+
164
+ tree = ivt.blocking()
165
+ print( tree(factorial, 4) ) # show invocation tree of calling factorial(4)
166
+ ```
167
+ ![factorial](https://raw.githubusercontent.com/bterwijn/invocation_tree/main/images/factorial.gif)
168
+ ```
169
+ 24
170
+ ```
171
+
172
+ ## Permutations ##
173
+ This `permutations()` example shows the depth-first nature of recursive execution:
174
+
175
+ ```python
176
+ import invocation_tree as ivt
177
+
178
+ def permutations(elements, perm, n):
179
+ if n==0:
180
+ return [perm]
181
+ all_perms = []
182
+ for element in elements:
183
+ all_perms.extend(permutations(elements, perm + element, n-1))
184
+ return all_perms
185
+
186
+ tree = ivt.blocking()
187
+ result = tree(permutations, ['L','R'], '', 2)
188
+ print(result) # all permutations of going Left or Right of length 2
189
+ ```
190
+ ![permutations](https://raw.githubusercontent.com/bterwijn/invocation_tree/main/images/permutations.gif)
191
+ ```
192
+ ['LL', 'LR', 'RL', 'RR']
193
+ ```
194
+
195
+ ## Hide Variables ##
196
+ In an educational context it can be useful to hide certian variables to avoid unnecessary complexity. This can for example be done with:
197
+
198
+ ```python
199
+ tree = ivt.blocking()
200
+ tree.hide.add('permutations.elements')
201
+ tree.hide.add('permutations.element')
202
+ tree.hide.add('permutations.all_perms')
203
+ ```
204
+
205
+ # Configuration #
206
+ These invocation_tree configurations are available for an `Invocation_Tree` objects:
207
+
208
+ ```python
209
+ tree = ivt.Invocation_Tree()
210
+ ```
211
+
212
+ - **tree.filename** : str
213
+ - filename to save the tree to, defaults to 'tree.pdf'
214
+ - **tree.show** : bool
215
+ - if `True` the default application is open to view 'tree.filename'
216
+ - **tree.block** : bool
217
+ - if `True` program execution is blocked after the tree is saved
218
+ - **tree.src_loc** : bool
219
+ - if `True` the source location is printed when blocking
220
+ - **tree.each_line** : bool
221
+ - if `True` each line of the program is stepped through
222
+ - **tree.max_string_len** : int
223
+ - the maximum string length, only the end is shown of longer strings
224
+ - **tree.gifcount** : int
225
+ - if `>=0` the out filename is numbered for animated gif making
226
+ - **tree.indent** : string
227
+ - the string used for identing the local variables
228
+ - **tree.color_active** : string
229
+ - HTML color for active function
230
+ - **tree.color_paused*** : string
231
+ - HTML color for paused functions
232
+ - **tree.color_returned***: string
233
+ - HTML color for returned functions
234
+ - **tree.hide** : set()
235
+ - set of all variables names that are not shown in the tree
236
+ - **tree.to_string** : dict[str, fun]
237
+ - mapping from type/name to a to_string() function for custom printing of values
238
+
239
+ For convenience we provide these functions to set common configurations:
240
+
241
+ - **ivt.blocking(filename)**, blocks on function call and return
242
+ - **ivt.blocking_each_change(filename)**, blocks on each change of value
243
+ - **ivt.debugger(filename)**, non-blocking for use in debugger tool (open &lt;filename&gt; manually)
244
+ - **ivt.gif(filename)**, generates many output files on function call and return for gif creation
245
+ - **ivt.gif_each_change(filename)**, generates many output files on each change of value for gif creation
246
+ - **ivt.non_blocking(filename)**, non-blocking on each function call and return
247
+
248
+ # Troubleshooting #
249
+ - 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.
250
+
251
+ ## Memory_Graph Package ##
252
+ The [invocation_tree](https://pypi.org/project/invocation-tree/) package visualizes function calls at different moments in time. If instead you want a detailed visualization of your data at the current time, check out the [memory_graph](https://pypi.org/project/memory-graph/) package.
@@ -0,0 +1,6 @@
1
+ invocation_tree/__init__.py,sha256=mOh8VMlN_2QtFXPO3Sf0teB-3gzG5TDzIHPhKZOrR6E,11222
2
+ invocation_tree-0.0.15.dist-info/licenses/LICENSE.txt,sha256=lhBfhX4yJut_-ahPAsH87Xhk-01Df17Std-X1Xy6rhU,1314
3
+ invocation_tree-0.0.15.dist-info/METADATA,sha256=or2WV3il5CDUPBmXGJzs8wusp1YQUv4Bphl8gBru6pw,8971
4
+ invocation_tree-0.0.15.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
5
+ invocation_tree-0.0.15.dist-info/top_level.txt,sha256=-UiQipEd5_8mqbNW12sUtrMfxlTqV0HV0T4WFCeeD_I,16
6
+ invocation_tree-0.0.15.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,25 @@
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.
@@ -0,0 +1 @@
1
+ invocation_tree