codegrapher 0.3.0__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.
cli/__init__.py ADDED
@@ -0,0 +1 @@
1
+ from .script import cli
cli/script.py ADDED
@@ -0,0 +1,58 @@
1
+ import os
2
+ import re
3
+
4
+ import click
5
+
6
+ from codegrapher.graph import FunctionGrapher
7
+ from codegrapher.parser import FileObject
8
+
9
+
10
+ @click.command()
11
+ @click.argument('code', type=click.Path())
12
+ @click.option('-r', '--recursive', default=False, is_flag=True,
13
+ help='Treat code argument as a directory and parse all files in directory, recursively')
14
+ @click.option('--printed', default=False, is_flag=True, help='Pretty prints the call tree for each class in the file')
15
+ @click.option('--ignore', default=False, is_flag=True, help='Use a .cg_ignore file to ignore functions in call tree')
16
+ @click.option('--remove-builtins', default=False, is_flag=True, help='Removes builtin functions from call trees')
17
+ @click.option('--output', help='Graphviz output file name')
18
+ @click.option('--output-format', default='pdf', help='File type for graphviz output file')
19
+ def cli(code, recursive, printed, ignore, remove_builtins, output, output_format):
20
+ """
21
+ Parses a file.
22
+ codegrapher [file_name]
23
+ """
24
+ file_list = []
25
+ if recursive:
26
+ for dirpath, dirnames, filenames in os.walk(code):
27
+ for filename in filenames:
28
+ if re.search(r'\.py$', filename):
29
+ file_list.append(os.sep.join([dirpath, filename]))
30
+ else:
31
+ file_list.append(code)
32
+
33
+ graph = None
34
+ for file_name in file_list:
35
+ file_object = FileObject(file_name)
36
+ file_object.visit()
37
+ if remove_builtins:
38
+ file_object.remove_builtins()
39
+ if ignore:
40
+ file_object.add_ignore_file()
41
+ file_object.ignore_functions()
42
+ if printed:
43
+ click.echo('Classes in file {}:'.format(file_name))
44
+ for class_object in file_object.classes:
45
+ click.echo('=' * 80)
46
+ click.echo(class_object.name)
47
+ click.echo(class_object.pprint())
48
+ click.echo('')
49
+ if output:
50
+ try:
51
+ graph.add_file_to_graph(file_object)
52
+ except AttributeError:
53
+ graph = FunctionGrapher()
54
+ graph.add_file_to_graph(file_object)
55
+ if output:
56
+ graph.name = output
57
+ graph.format = output_format
58
+ graph.render()
@@ -0,0 +1 @@
1
+ __version__ = '0.3.0'
codegrapher/graph.py ADDED
@@ -0,0 +1,168 @@
1
+ from graphviz import Digraph
2
+
3
+
4
+ class FilenameNotSpecifiedException(Exception):
5
+ """ An exception raised when a file name is not specified in a :class:`FunctionGrapher` instance before calling
6
+ :func:`FunctionGrapher.render` on it.
7
+ """
8
+ pass
9
+
10
+
11
+ class Node(object):
12
+ """ A class to more easily handle manipulations needed to properly display nodes in a graph.
13
+ Optimized to handle nodes that represent functions in a program.
14
+
15
+ Attributes:
16
+ tuple (tuple): Contains the namespace, class, and function name for the current node. If namespace is an empty
17
+ string, this contains just the class and function names. If a string is provided to the constructor this
18
+ is a tuple containing just the function name.
19
+ """
20
+ def __init__(self, input_node):
21
+ if isinstance(input_node, tuple):
22
+ if input_node[0] == '':
23
+ self.tuple = input_node[1:]
24
+ else:
25
+ self.tuple = input_node
26
+ else:
27
+ self.tuple = (input_node,)
28
+
29
+ @property
30
+ def represent(self):
31
+ """ Provides a string representation of the current node
32
+
33
+ Returns:
34
+ (string): Dotted form of current node, as in `namespace.class.function_name`.
35
+ """
36
+ return '.'.join(self.tuple)
37
+
38
+ def __str__(self):
39
+ return '.'.join(self.tuple)
40
+
41
+ def __repr__(self):
42
+ return "<Node: {}>".format(self.tuple)
43
+
44
+ def __hash__(self):
45
+ return hash('.'.join(self.tuple))
46
+
47
+ def __eq__(self, other):
48
+ return self.tuple == other.tuple
49
+
50
+
51
+ class FunctionGrapher(object):
52
+ """ `FunctionGrapher` is a class for producing `graphviz <http://www.graphviz.org/>`_ graphs showing the call
53
+ graph for sets of classes.
54
+
55
+ Attributes:
56
+ name (string): Name to be used when a graph is made.
57
+ nodes (set): Graphviz nodes to be graphed.
58
+ edges (set): Directional edges connecting one node to another.
59
+ format (string): File format for graph. Default is `pdf`.
60
+ """
61
+ def __init__(self):
62
+ self.name = ''
63
+ self.dot_file = Digraph()
64
+ self.nodes = set()
65
+ self.edges = set()
66
+
67
+ @property
68
+ def format(self):
69
+ return self.dot_file.format
70
+
71
+ @format.setter
72
+ def format(self, value):
73
+ self.dot_file.format = value
74
+
75
+ def add_file_to_graph(self, file_object):
76
+ """ When given a :class:`codegrapher.parser.FileObject` object, this adds all classes to the current graph.
77
+
78
+ Arguments:
79
+ file_object (:class:`codegrapher.parser.FileObject`): Visitor objects to have all its classes added to the
80
+ current graph.
81
+ """
82
+ class_namespace = dict((cls.name, file_object.relative_namespace) for cls in file_object.classes)
83
+ for cls in file_object.classes:
84
+ self.add_dict_to_graph(class_namespace, cls.call_tree, file_object.relative_namespace)
85
+ self.add_classes_to_graph(file_object.classes, file_object.relative_namespace)
86
+
87
+ def add_dict_to_graph(self, class_names, dictionary, relative_namespace):
88
+ """ Creates a list of nodes and edges to be rendered. Deduplicates input.
89
+
90
+ Arguments:
91
+ class_names (list): List of class names to be recognized by the graph as `class_name.__init__` nodes.
92
+ dictionary (dict): `ClassObject.call_tree` dict to be added to graph nodes and edges.
93
+ relative_namespace (string): Relative namespace for the current class, i.e. where the current class is
94
+ located relative to the root, in dotted path notation.
95
+ """
96
+ # todo: better handle project hierarchy by looking at imports
97
+ # add nodes
98
+ for origin in dictionary:
99
+ self.nodes.add(Node(origin))
100
+ for destination in dictionary[origin]:
101
+ if destination[0] in class_names:
102
+ destination = Node((relative_namespace, destination[0], '__init__'))
103
+ else:
104
+ destination = Node(destination)
105
+ self.nodes.add(destination)
106
+
107
+ # add edges
108
+ for origin in dictionary:
109
+ for destination in dictionary[origin]:
110
+ # if destination is a class name, it is a constructor
111
+ if destination[0] in class_names:
112
+ destination = (relative_namespace, destination[0], '__init__')
113
+ else:
114
+ destination = destination
115
+ self.edges.add((Node(origin), Node(destination)))
116
+
117
+ def add_classes_to_graph(self, classes, relative_namespace):
118
+ """ Adds classes with constructors to the set.
119
+ This adds edges between a class constructor and the methods called on those items.
120
+
121
+ Arguments:
122
+ classes (list): list of :class:`codegrapher.parser.ClassObject` items.
123
+ relative_namespace (string): namespace of the current class.
124
+ """
125
+ for cls in classes:
126
+ # If a class is here, add it as a node
127
+ self.nodes.add(Node((relative_namespace, cls.name)))
128
+
129
+ if not all((fcn.is_classmethod for fcn in cls.functions)):
130
+ # for case where there is at least one non-classmethod, assume implied (or explicit) __init__
131
+
132
+ # make a node between the class name and __init__
133
+ self.nodes.add(Node((relative_namespace, cls.name, '__init__')))
134
+ self.edges.add((Node((relative_namespace, cls.name)), Node((relative_namespace, cls.name, '__init__'))))
135
+ for fcn in cls.functions:
136
+ # skip classmethods and case where init would refer back to itself
137
+ if not fcn.is_classmethod and not fcn.name == '__init__':
138
+ self.edges.add((Node((relative_namespace, cls.name, '__init__')),
139
+ Node((relative_namespace, cls.name, fcn.name))))
140
+ elif fcn.is_classmethod:
141
+ self.edges.add((Node((relative_namespace, cls.name)),
142
+ Node((relative_namespace, cls.name, fcn.name))))
143
+
144
+ else:
145
+ # for the case where there are only classmethods defined
146
+ for fcn in cls.functions:
147
+ self.edges.add((Node((relative_namespace, cls.name)),
148
+ Node((relative_namespace, cls.name, fcn.name))))
149
+
150
+ def render(self, name=None):
151
+ """ Renders the current graph. `Graphviz <http://www.graphviz.org/>`_ must be installed for the graph to be
152
+ rendered.
153
+
154
+ Arguments:
155
+ name (string): filename to override `self.name`.
156
+
157
+ Raises:
158
+ FilenameNotSpecifiedException: If `FunctionGrapher.name` is not specified.
159
+ """
160
+ for node in self.nodes:
161
+ self.dot_file.node(node.represent)
162
+ for edge in self.edges:
163
+ self.dot_file.edge(edge[0].represent, edge[1].represent)
164
+ if name is None:
165
+ if not self.name:
166
+ raise FilenameNotSpecifiedException
167
+ name = self.name
168
+ self.dot_file.render(name)
codegrapher/parser.py ADDED
@@ -0,0 +1,380 @@
1
+ import ast
2
+ import copy
3
+ import os
4
+ from pprint import pformat
5
+
6
+
7
+ class FileObject:
8
+ """Class for keeping track of files.
9
+
10
+ Attributes:
11
+ modules (dict): dict of current modules with `alias: module_name`, `key:value pairs`.
12
+ aliases (dict): dict of current modules with `alias: original_name`, `key:value pairs`.
13
+ node (:mod:`ast.AST`): AST node for entire file.
14
+ name (string): File name.
15
+ classes (list): :class:`ClassObject` items defined in the current file.
16
+ relative_namespace (string): The namespace for the current file,
17
+ taken from the relative path of the current file
18
+ ignore (set): Functions to be ignored, as defined in a `.cg_ignore` text file.
19
+ """
20
+ def __init__(self, file_name, modules=None, aliases=None):
21
+ self.modules = copy.deepcopy(modules) if modules else {}
22
+ self.aliases = copy.deepcopy(aliases) if aliases else {}
23
+ self.name = file_name
24
+ self.full_path = os.path.abspath(file_name)
25
+ with open(self.full_path, 'r') as input_file:
26
+ self.node = ast.parse(input_file.read(), filename=self.name)
27
+ self.classes = []
28
+ self.relative_namespace = os.path.splitext(self.name)[0].replace(os.path.sep, '.')
29
+ self.ignore = set()
30
+
31
+ def visit(self):
32
+ """Visits all the nodes within the current file AST node.
33
+
34
+ Updates `self.classes` for the current instance.
35
+ """
36
+ file_visitor = FileVisitor(aliases=self.aliases, modules=self.modules)
37
+ file_visitor.visit(self.node)
38
+ self.modules = file_visitor.modules
39
+ self.aliases = file_visitor.aliases
40
+ self.classes = file_visitor.classes
41
+ self.namespace()
42
+
43
+ def remove_builtins(self):
44
+ """Removes builtins from each class in a `FileObject` instance."""
45
+ for class_object in self.classes:
46
+ class_object.remove_builtins()
47
+
48
+ def add_ignore_file(self):
49
+ """Use a file `.cg_ignore` to ignore a list of functions from the call graph
50
+ """
51
+ if os.path.isfile('.cg_ignore'):
52
+ with open('.cg_ignore', 'r') as ignore_file:
53
+ for line in ignore_file:
54
+ if line.strip() and line.strip()[0] != '#':
55
+ self.ignore.add(line.strip())
56
+
57
+ def ignore_functions(self):
58
+ """Ignore all functions in the current class which are present in the instance's `ignore` attribute.
59
+ """
60
+ for class_object in self.classes:
61
+ class_object.ignore_functions(self.ignore)
62
+
63
+ def namespace(self):
64
+ """Programmatically change the name of items in the call tree so they have relative path information
65
+ """
66
+
67
+ for class_object in self.classes:
68
+ class_object.namespace(self.relative_namespace)
69
+
70
+
71
+ class ClassObject:
72
+ """Class for keeping track of classes in code.
73
+
74
+ Attributes:
75
+ modules (dict): dict of current modules with `alias: module_name`, `key:value pairs`.
76
+ aliases (dict): dict of current modules with `alias: original_name`, `key:value pairs`.
77
+ node (:mod:`ast.AST`): AST node for entire class.
78
+ name (string): Class name.
79
+ functions (list): :class:`FunctionObject` items defined in the current class.
80
+ call_tree (dict): dict with `key:value` pairs `(module, FunctionObject.name): (module, identifier)`.
81
+
82
+ """
83
+ def __init__(self, node=None, aliases=None, modules=None):
84
+ self.modules = copy.deepcopy(modules) if modules else {}
85
+ self.aliases = copy.deepcopy(aliases) if aliases else {}
86
+ self.node = node
87
+ self.name = node.name if node else ''
88
+ self.functions = []
89
+ self.call_tree = {}
90
+
91
+ def visit(self):
92
+ """Visits all the nodes within the current class AST node.
93
+
94
+ Updates `self.functions` and `self.call_tree` for the current instance.
95
+ """
96
+ function_visitor = FunctionVisitor(aliases=self.aliases, modules=self.modules)
97
+ function_visitor.visit(self.node)
98
+ self.functions = function_visitor.functions
99
+ self.call_tree = dict(((self.name, k), v) for k, v in function_visitor.calls.items())
100
+
101
+ def remove_builtins(self):
102
+ """For many classes, we may not want to include builtin functions in the graph.
103
+ Remove builtins from the call tree and from called functions list.
104
+ """
105
+ self.call_tree = {caller: [call for call in call_list if not self.is_builtin(call[0])]
106
+ for caller, call_list in self.call_tree.items()}
107
+
108
+ def ignore_functions(self, ignore_set):
109
+ """Ignores all functions matching those specified in a pre-defined ignore set.
110
+
111
+ Args:
112
+ ignore_set (set): Functions whose calls should be removed (ignored) in the class call tree.
113
+ """
114
+ new_call_tree = {}
115
+ for caller, call_list in self.call_tree.items():
116
+ new_call_list = []
117
+ for call in call_list:
118
+ if call[-1] not in ignore_set:
119
+ new_call_list.append(call)
120
+ new_call_tree[caller] = new_call_list
121
+
122
+ self.call_tree = new_call_tree
123
+
124
+ def namespace(self, relative_namespace):
125
+ """Take the relative namespace for the class and prepend it to each item defined in the current class.
126
+
127
+ Args:
128
+ relative_namespace (string): Namespace to be prepended to each item in the call tree.
129
+ """
130
+ new_call_tree = {}
131
+ for caller in self.call_tree:
132
+ new_call_tree[(relative_namespace, caller[0], caller[1])] = self.call_tree[caller]
133
+ self.call_tree = new_call_tree
134
+
135
+ def pprint(self):
136
+ """Pretty print formatter for class object.
137
+
138
+ Returns:
139
+ string
140
+ """
141
+ return pformat(self.call_tree)
142
+
143
+ @staticmethod
144
+ def is_builtin(fn):
145
+ """Checks if a """
146
+ if isinstance(fn, str):
147
+ return fn in __builtins__
148
+ return False
149
+
150
+ def __repr__(self):
151
+ return "ClassObject {}".format(self.name)
152
+
153
+ def __str__(self):
154
+ functions = [fcn.name for fcn in self.functions]
155
+ return "Class {}\nDefined functions: {}".format(self.name, functions)
156
+
157
+
158
+ class FunctionObject:
159
+ """Object that stores information within a single function definition
160
+
161
+ attributes:
162
+ modules: dict of current modules with `alias: module_name`, `key:value pairs`.
163
+ aliases: dict of current modules with `alias: original_name`, `key:value pairs`.
164
+ node (:mod:`ast.AST`): AST node for entire function.
165
+ name (string): function name.
166
+ calls (list): `(module, identifier)` tuples describing items called within current node,
167
+ with identifiers decoded form current alias, and modules expanded to their full import paths.
168
+ decorator_list (list): list of decorators, by name as a string, applied to the current function definition.
169
+ is_classmethod (bool): True if the current function is designated as a classmethod by a decorator.
170
+
171
+ """
172
+ def __init__(self, node=None, aliases=None, modules=None):
173
+ self.modules = copy.deepcopy(modules) if modules else {}
174
+ self.aliases = copy.deepcopy(aliases) if aliases else {}
175
+ self.node = node
176
+ self.name = node.name if node else ''
177
+ self.calls = []
178
+ self.decorator_list = []
179
+ self.is_classmethod = False
180
+
181
+ @classmethod
182
+ def _extract_decorators(cls, node):
183
+ """Pulls out strings for each item in a decorator list on a FunctionDef node
184
+
185
+ Args:
186
+ node (:mod:`ast.AST`): Node from which `decorator_list` will be extracted
187
+ Returns:
188
+ (list): List of decorator strings
189
+ """
190
+ decorator_list = []
191
+ for decorator in node.decorator_list:
192
+ if isinstance(decorator, ast.Name):
193
+ decorator_list.append(decorator.id)
194
+ elif isinstance(decorator, ast.Attribute):
195
+ # this catches things like attr.setter
196
+ decorator_list.append('.'.join([decorator.attr, decorator.value.id]))
197
+ return decorator_list
198
+
199
+ def visit(self):
200
+ """Visits all the nodes within the current function object's AST node.
201
+
202
+ Updates `self.calls`, `self.modules`, and `self.aliases` for the current instance.
203
+ """
204
+ visitor = CallVisitor(aliases=self.aliases, modules=self.modules)
205
+ visitor.visit(self.node)
206
+ self.decorator_list = FunctionObject._extract_decorators(self.node)
207
+ if 'classmethod' in self.decorator_list:
208
+ self.is_classmethod = True
209
+ self.calls = visitor.calls
210
+ self.modules.update(visitor.modules)
211
+ self.aliases.update(visitor.aliases)
212
+
213
+
214
+ class CallInspector(ast.NodeVisitor):
215
+ """Within a call, a Name or Attribute will provide the function name currently in use.
216
+
217
+ Identifies `Name` nodes, which are called as ``name(args)``, and `Attribute` nodes, which are called as
218
+ ``object.attr(args)``
219
+
220
+ Attributes:
221
+ module (string): Current module name on which the current call is made.
222
+ identifier (string): Name of the function called.
223
+ """
224
+ def __init__(self):
225
+ self.module = ''
226
+ self.identifier = ''
227
+
228
+ def visit_Name(self, node):
229
+ self.identifier = node.id
230
+
231
+ def visit_Attribute(self, node):
232
+ # todo: pull out item for the attr to determine whether node defines a classmethod
233
+ # currently does not handle multiple chaining of attr items
234
+ if hasattr(node.value, 'id'):
235
+ self.module = node.value.id
236
+ self.identifier = node.attr
237
+
238
+
239
+ class ImportVisitor(ast.NodeVisitor):
240
+ """For import related calls, store the source modules and aliases used.
241
+ Designed to be inherited by other classes that need to know about imports in their current scope.
242
+
243
+ Attributes:
244
+ modules (dict): dict of current modules with `alias: module_name`, `key:value pairs`.
245
+ aliases (dict): dict of current modules with `alias: original_name`, `key:value pairs`.
246
+ """
247
+ def __init__(self, aliases=None, modules=None):
248
+ self.modules = copy.deepcopy(modules) if modules else {}
249
+ self.aliases = copy.deepcopy(aliases) if aliases else {}
250
+
251
+ def continue_parsing(self, node):
252
+ super(ImportVisitor, self).generic_visit(node)
253
+
254
+ def visit_Import(self, node):
255
+ for item in node.names:
256
+ asname = item.asname if item.asname else item.name
257
+ self.aliases[asname] = item.name
258
+ self.modules[asname] = None
259
+
260
+ def visit_ImportFrom(self, node):
261
+ module = node.module
262
+ for item in node.names:
263
+ asname = item.asname if item.asname else item.name
264
+ self.aliases[asname] = item.name
265
+ self.modules[asname] = module
266
+
267
+
268
+ class CallVisitor(ImportVisitor):
269
+ """Finds all calls present in the current scope and inspect them.
270
+
271
+ Attributes:
272
+ call_names (set): set of :class:`CallInspector.identifier` items within current AST node.
273
+ calls (list): `(module, identifier)` items called within current AST node,
274
+ with identifiers decoded form current alias, and modules expanded to their full import paths.
275
+ """
276
+ def __init__(self, **kwargs):
277
+ super(CallVisitor, self).__init__(**kwargs)
278
+ self.call_names = set()
279
+ self.calls = []
280
+
281
+ def continue_parsing(self, node):
282
+ super(CallVisitor, self).generic_visit(node)
283
+
284
+ def visit_Call(self, node):
285
+ call_visitor = CallInspector()
286
+ call_visitor.visit(node.func)
287
+
288
+ # handles calls within function calls
289
+ for arg in node.args:
290
+ self.continue_parsing(arg)
291
+ if isinstance(arg, ast.Call):
292
+ arg_visitor = CallVisitor(aliases=self.aliases, modules=self.modules)
293
+ arg_visitor.visit(arg)
294
+ self.call_names.update(arg_visitor.call_names)
295
+ self.calls.extend(arg_visitor.calls)
296
+
297
+ self.call_names.add(call_visitor.identifier)
298
+
299
+ # if names are aliased, pull out aliased name
300
+ if call_visitor.identifier in self.aliases:
301
+ identifier = self.aliases[call_visitor.identifier]
302
+ else:
303
+ identifier = call_visitor.identifier
304
+
305
+ if call_visitor.module in self.modules:
306
+ # module is imported and called by attr
307
+ if self.modules[call_visitor.module]:
308
+ module = '.'.join([self.modules[call_visitor.module], self.aliases[call_visitor.module]])
309
+ else:
310
+ module = self.aliases[call_visitor.module]
311
+ elif call_visitor.identifier in self.modules:
312
+ # module is imported, but not called by attr
313
+ module = self.modules[call_visitor.identifier]
314
+ else:
315
+ # no module specified
316
+ module = None
317
+
318
+ if module:
319
+ call = (module, identifier)
320
+ else:
321
+ call = (identifier,)
322
+
323
+ self.calls.append(call)
324
+
325
+
326
+ class FunctionVisitor(ImportVisitor):
327
+ """Function definitions are where the function is defined, and the call is where the ast for that function exists.
328
+
329
+ This only looks for items that are called within the scope of a function, and associates those items
330
+ with the function.
331
+
332
+ Attributes:
333
+ defined_functions (set): names of functions found by function visitor instance.
334
+ functions (list): :class:`FunctionObject` instances found by function visitor instance.
335
+ calls (dict): mapping from function names defined to calls within that function definition.
336
+ """
337
+ def __init__(self, **kwargs):
338
+ super(FunctionVisitor, self).__init__(**kwargs)
339
+ self.defined_functions = set()
340
+ self.functions = []
341
+ self.calls = {}
342
+
343
+ def continue_parsing(self, node):
344
+ super(FunctionVisitor, self).generic_visit(node)
345
+
346
+ def visit_FunctionDef(self, node):
347
+ self.defined_functions.add(node.name)
348
+ function_def = FunctionObject(node=node, aliases=self.aliases, modules=self.modules)
349
+ function_def.visit()
350
+ self.calls[function_def.name] = function_def.calls
351
+ self.functions.append(function_def)
352
+
353
+
354
+ class FileVisitor(ImportVisitor):
355
+ """First visitor that should be called on the file level.
356
+
357
+ Attributes:
358
+ classes (list): list of :class:`ClassObject` instances defined in the current file.
359
+ """
360
+ def __init__(self, **kwargs):
361
+ super(FileVisitor, self).__init__(**kwargs)
362
+ self.classes = []
363
+
364
+ def continue_parsing(self, node):
365
+ super(FileVisitor, self).generic_visit(node)
366
+
367
+ def visit_Module(self, node):
368
+ self.continue_parsing(node)
369
+
370
+ def visit_ClassDef(self, node):
371
+ # once a class is found, create a class object for it and traverse the ast with its visitor
372
+ new_class = ClassObject(node=node, aliases=self.aliases, modules=self.modules)
373
+ new_class.visit()
374
+ self.classes.append(new_class)
375
+
376
+ def remove_builtins(self):
377
+ """Removes builtins from each class in a `FileVisitor` instance.
378
+ """
379
+ for class_object in self.classes:
380
+ class_object.remove_builtins()
@@ -0,0 +1,150 @@
1
+ Metadata-Version: 2.4
2
+ Name: codegrapher
3
+ Version: 0.3.0
4
+ Summary: Code that graphs code
5
+ Project-URL: Homepage, https://github.com/LaurEars/codegrapher
6
+ Project-URL: Repository, https://github.com/LaurEars/codegrapher
7
+ Project-URL: Issues, https://github.com/LaurEars/codegrapher/issues
8
+ Author: Laura Rupprecht
9
+ License: The MIT License (MIT)
10
+
11
+ Copyright (c) 2026 Laura Rupprecht
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
14
+ documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
15
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
16
+ persons to whom the Software is furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
19
+ Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
22
+ WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
23
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
24
+ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25
+ License-File: LICENSE
26
+ Keywords: ast,call-graph,code,documentation,graph,graphviz
27
+ Classifier: Development Status :: 4 - Beta
28
+ Classifier: Intended Audience :: Developers
29
+ Classifier: License :: OSI Approved :: MIT License
30
+ Classifier: Programming Language :: Python :: 3
31
+ Classifier: Programming Language :: Python :: 3.8
32
+ Classifier: Programming Language :: Python :: 3.9
33
+ Classifier: Programming Language :: Python :: 3.10
34
+ Classifier: Programming Language :: Python :: 3.11
35
+ Classifier: Programming Language :: Python :: 3.12
36
+ Classifier: Programming Language :: Python :: 3.13
37
+ Classifier: Topic :: Software Development :: Documentation
38
+ Requires-Python: >=3.8
39
+ Requires-Dist: click>=7.0
40
+ Requires-Dist: graphviz>=0.4.2
41
+ Provides-Extra: dev
42
+ Requires-Dist: coverage>=5.0; extra == 'dev'
43
+ Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
44
+ Requires-Dist: pytest>=7.0.0; extra == 'dev'
45
+ Description-Content-Type: text/x-rst
46
+
47
+ codegrapher
48
+ ===========
49
+
50
+ .. image:: https://github.com/LaurEars/codegrapher/actions/workflows/main.yaml/badge.svg
51
+ :target: https://github.com/LaurEars/codegrapher/actions/workflows/main.yaml
52
+
53
+
54
+ Code that graphs code
55
+ ---------------------
56
+ Uses the python `AST <https://docs.python.org/3/library/ast.html>`_ to parse Python source code and build a call graph.
57
+
58
+
59
+ Output
60
+ ------
61
+ An example of the current output of the parser parsing itself.
62
+
63
+ .. image:: docs/codegrapher.png
64
+ :target: docs/codegrapher.png
65
+ :align: center
66
+ :width: 100 %
67
+ :alt: parser.py
68
+
69
+
70
+ Installation
71
+ ------------
72
+
73
+ .. code:: bash
74
+
75
+ pip install codegrapher
76
+
77
+
78
+ To generate graphs, `graphviz <http://www.graphviz.org/Download.php>`_ must be installed.
79
+
80
+
81
+ Usage
82
+ -----
83
+
84
+ At the command line
85
+ ~~~~~~~~~~~~~~~~~~~
86
+ To parse a file and output results to the console:
87
+
88
+ .. code:: bash
89
+
90
+ codegrapher path/to/file.py --printed
91
+
92
+
93
+ To parse a file and output results to a file:
94
+
95
+ .. code:: bash
96
+
97
+ codegrapher path/to/file.py --output output_file_name --output-type png
98
+
99
+ To analyze a directory of files, along with all files it contains:
100
+
101
+ .. code:: bash
102
+
103
+ codegrapher -r path/to/directory --output multiple_file_analysis
104
+
105
+ And if you have a list of functions that aren't useful in your graph, add it to a `.cg_ignore` file:
106
+
107
+ ::
108
+
109
+ # cg_ignore file
110
+ # all lines beginning with '#' are ignored
111
+
112
+ # every function calls this, so it's not helpful in my graph:
113
+ log_error
114
+
115
+ # I don't want to see this in my graph:
116
+ parse
117
+ lower
118
+
119
+ Then add the `--ignore` flag to your command. Using the flag `--remove-builtins` provides the same functionality
120
+ for ignoring items found in `__builtins__`.
121
+
122
+ As a Python module
123
+ ~~~~~~~~~~~~~~~~~~
124
+
125
+ To easily parse code in Python :
126
+
127
+ .. code:: python
128
+
129
+ from codegrapher.parser import FileObject
130
+
131
+ file_object = FileObject('path/to/file.py')
132
+ file_object.visit()
133
+
134
+ And then to add that code to a graph and render it (using graphviz):
135
+
136
+ .. code:: python
137
+
138
+ from codegrapher.graph import FunctionGrapher
139
+
140
+ graph = FunctionGrapher()
141
+ graph.add_file_to_graph(file_object)
142
+ graph.name = 'name.gv'
143
+ graph.format = 'png'
144
+ graph.render()
145
+
146
+ Which will produce your code as a png file, `name.gv.png`, along with a
147
+ `dot file <http://en.wikipedia.org/wiki/DOT_%28graph_description_language%29>`_ `name.gv`
148
+
149
+ More documentation for the Python module can be found at
150
+ `Read the Docs <http://codegrapher.readthedocs.org/en/latest/>`_.
@@ -0,0 +1,10 @@
1
+ cli/__init__.py,sha256=T4hveAC4W-KBh3GWiedPiINl-iHT1ZhcSxqSE1MEwsw,24
2
+ cli/script.py,sha256=a3SRMouuae1ae6OqZoeNgjmo0xIpH8dhIxNXP6Syzqw,2216
3
+ codegrapher/__init__.py,sha256=3wVEs2QD_7OcTlD97cZdCeizd2hUbJJ0GeIO8wQIjrk,22
4
+ codegrapher/graph.py,sha256=Teacm7Hd6w43nM9wiIMaJUJcL_0mZuXHHsoN6aXYoTg,7095
5
+ codegrapher/parser.py,sha256=sjqfXp2H216mP-3W4liIANyvRFDyUe_mfUv8deyEy1E,15052
6
+ codegrapher-0.3.0.dist-info/METADATA,sha256=JPi5u8DMmxMzL-jdW60FWWQ5J4uwMvfETZcIIohYp9k,4872
7
+ codegrapher-0.3.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
8
+ codegrapher-0.3.0.dist-info/entry_points.txt,sha256=6SlRVjRRDdv0haGsEyWaY9XbORuQzNim3VE0_pVlJE0,40
9
+ codegrapher-0.3.0.dist-info/licenses/LICENSE,sha256=6bfha4F6flqIqyfRSvW-0aXHFDPaMhERmkLBVLTLQiM,1081
10
+ codegrapher-0.3.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ codegrapher = cli:cli
@@ -0,0 +1,16 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Laura Rupprecht
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
6
+ documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
7
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
8
+ persons to whom the Software is furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
11
+ Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
14
+ WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
15
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16
+ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.