From c0af4501a22b86d2db307cdd5518fcc106783702 Mon Sep 17 00:00:00 2001
From: MTlls <matheustellestab8@gmail.com>
Date: Tue, 18 Mar 2025 02:12:13 -0300
Subject: [PATCH] add: pycfg adicionado

---
 src/pycfg-0.1/PKG-INFO                        |  37 ++
 src/pycfg-0.1/pycfg.egg-info/PKG-INFO         |  37 ++
 src/pycfg-0.1/pycfg.egg-info/SOURCES.txt      |   8 +
 .../pycfg.egg-info/dependency_links.txt       |   1 +
 src/pycfg-0.1/pycfg.egg-info/not-zip-safe     |   1 +
 src/pycfg-0.1/pycfg.egg-info/top_level.txt    |   1 +
 src/pycfg-0.1/pycfg/branchcov.py              |  65 +++
 src/pycfg-0.1/pycfg/pycfg.py                  | 474 ++++++++++++++++++
 src/pycfg-0.1/setup.cfg                       |   4 +
 src/pycfg-0.1/setup.py                        |  40 ++
 10 files changed, 668 insertions(+)
 create mode 100644 src/pycfg-0.1/PKG-INFO
 create mode 100644 src/pycfg-0.1/pycfg.egg-info/PKG-INFO
 create mode 100644 src/pycfg-0.1/pycfg.egg-info/SOURCES.txt
 create mode 100644 src/pycfg-0.1/pycfg.egg-info/dependency_links.txt
 create mode 100644 src/pycfg-0.1/pycfg.egg-info/not-zip-safe
 create mode 100644 src/pycfg-0.1/pycfg.egg-info/top_level.txt
 create mode 100644 src/pycfg-0.1/pycfg/branchcov.py
 create mode 100755 src/pycfg-0.1/pycfg/pycfg.py
 create mode 100644 src/pycfg-0.1/setup.cfg
 create mode 100644 src/pycfg-0.1/setup.py

diff --git a/src/pycfg-0.1/PKG-INFO b/src/pycfg-0.1/PKG-INFO
new file mode 100644
index 0000000..783862d
--- /dev/null
+++ b/src/pycfg-0.1/PKG-INFO
@@ -0,0 +1,37 @@
+Metadata-Version: 1.1
+Name: pycfg
+Version: 0.1
+Summary: The python3 ast based control flow graph
+Home-page: http://github.com/vrthra/pycfg
+Author: Rahul Gopinath
+Author-email: rahul@gopinath.org
+License: GPLv3
+Description-Content-Type: UNKNOWN
+Description: 
+        This package generates a control flow graph of the passed python file based on the AST generated (rather than the bytecode). It supports only a few python statements at this point. Notably absent are exceptions and generators. Essentially, only those constructs found in example.py are supported.
+        
+                Compatibility
+                -------------
+                It was tested on Python 3.6
+                
+                
+                To run
+                ------
+                
+                  python pycfg/pycfg.py <program to be analyzed> -d
+                  or
+                  python pycfg/pycfg.py <program to be analyzed> -d -y <branch coverage file>
+        
+                  Inspect Makefile for a better idea
+               
+Keywords: control-flow-graph abstract-syntax-tree
+Platform: UNKNOWN
+Classifier: Development Status :: 3 - Alpha
+Classifier: Environment :: Console
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python :: 3
+Classifier: Topic :: Software Development
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
+Classifier: Topic :: Software Development :: Compilers
diff --git a/src/pycfg-0.1/pycfg.egg-info/PKG-INFO b/src/pycfg-0.1/pycfg.egg-info/PKG-INFO
new file mode 100644
index 0000000..783862d
--- /dev/null
+++ b/src/pycfg-0.1/pycfg.egg-info/PKG-INFO
@@ -0,0 +1,37 @@
+Metadata-Version: 1.1
+Name: pycfg
+Version: 0.1
+Summary: The python3 ast based control flow graph
+Home-page: http://github.com/vrthra/pycfg
+Author: Rahul Gopinath
+Author-email: rahul@gopinath.org
+License: GPLv3
+Description-Content-Type: UNKNOWN
+Description: 
+        This package generates a control flow graph of the passed python file based on the AST generated (rather than the bytecode). It supports only a few python statements at this point. Notably absent are exceptions and generators. Essentially, only those constructs found in example.py are supported.
+        
+                Compatibility
+                -------------
+                It was tested on Python 3.6
+                
+                
+                To run
+                ------
+                
+                  python pycfg/pycfg.py <program to be analyzed> -d
+                  or
+                  python pycfg/pycfg.py <program to be analyzed> -d -y <branch coverage file>
+        
+                  Inspect Makefile for a better idea
+               
+Keywords: control-flow-graph abstract-syntax-tree
+Platform: UNKNOWN
+Classifier: Development Status :: 3 - Alpha
+Classifier: Environment :: Console
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python :: 3
+Classifier: Topic :: Software Development
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
+Classifier: Topic :: Software Development :: Compilers
diff --git a/src/pycfg-0.1/pycfg.egg-info/SOURCES.txt b/src/pycfg-0.1/pycfg.egg-info/SOURCES.txt
new file mode 100644
index 0000000..ca010ae
--- /dev/null
+++ b/src/pycfg-0.1/pycfg.egg-info/SOURCES.txt
@@ -0,0 +1,8 @@
+setup.py
+pycfg/branchcov.py
+pycfg/pycfg.py
+pycfg.egg-info/PKG-INFO
+pycfg.egg-info/SOURCES.txt
+pycfg.egg-info/dependency_links.txt
+pycfg.egg-info/not-zip-safe
+pycfg.egg-info/top_level.txt
\ No newline at end of file
diff --git a/src/pycfg-0.1/pycfg.egg-info/dependency_links.txt b/src/pycfg-0.1/pycfg.egg-info/dependency_links.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/src/pycfg-0.1/pycfg.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/src/pycfg-0.1/pycfg.egg-info/not-zip-safe b/src/pycfg-0.1/pycfg.egg-info/not-zip-safe
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/src/pycfg-0.1/pycfg.egg-info/not-zip-safe
@@ -0,0 +1 @@
+
diff --git a/src/pycfg-0.1/pycfg.egg-info/top_level.txt b/src/pycfg-0.1/pycfg.egg-info/top_level.txt
new file mode 100644
index 0000000..e51ba93
--- /dev/null
+++ b/src/pycfg-0.1/pycfg.egg-info/top_level.txt
@@ -0,0 +1 @@
+pycfg
diff --git a/src/pycfg-0.1/pycfg/branchcov.py b/src/pycfg-0.1/pycfg/branchcov.py
new file mode 100644
index 0000000..1dae81f
--- /dev/null
+++ b/src/pycfg-0.1/pycfg/branchcov.py
@@ -0,0 +1,65 @@
+import sys
+import inspect
+import re
+
+# these control flow contstructs have conditionals we can evaluate
+Control_Flow = ['if', 'elif', 'while', 'for']
+Control_Flow_Re = [re.compile(r'^ *%s +(.+) *: *' % i) for i in Control_Flow]
+# else, continue, break, and pass do not have conditionals
+# that can be evaluated.
+
+def traceit(frame, event, arg):
+    if event in ['call', 'return', 'line']:
+        fname, line = frame.f_code.co_filename, frame.f_lineno
+        myvars = {**frame.f_globals, **frame.f_locals} # should we do deep copy?
+        finfo = inspect.getframeinfo(frame)
+        src = finfo.code_context[finfo.index]
+        matches = (ctrl.match(src) for ctrl in Control_Flow_Re)
+        conditional = next((m.group(1) for m in matches if m), None)
+
+        traceit.cov_arcs.append((traceit.pfname, fname, traceit.prevline, line, conditional, myvars))
+        traceit.prevline = line
+        traceit.pfname = fname
+    else: pass # 'exception'
+    return traceit
+
+def capture_coverage(fn, fsrc):
+    traceit.cov_arcs = []
+    traceit.prevline = 0
+    traceit.pfname = None
+    oldtrace = sys.gettrace()
+    sys.settrace(traceit)
+    fn()
+    sys.settrace(oldtrace)
+    branch_cov = {}
+    source_code = {}
+    cov_arcs = []
+    for pf, f,i,j,conditional,l in traceit.cov_arcs:
+        # if the current file is not asked file, skip
+        if fsrc not in f: continue
+        # if the previous file was not asked file, set prevline to 0
+        if fsrc not in pf: i = 0
+        # dont count branch_cov of 0th line
+        if i != 0:
+            branch_cov.setdefault(i, set()).add(j)
+        source_code[j] = (f, conditional, l)
+        cov_arcs.append((f, i, j, conditional, l))
+
+    # return format:
+    #     of cov_arcs:
+    #      (<filename>, <parent>, <child>, <condstr> if line is a coditional else <None>, <local variables>)
+    #     of branch_cov:
+    #      {<parent>: <set of children>}
+    #     of source_code:
+    #       {<lineno>: (<filename>, <condstr> if conditional, <locals>)}
+    return (cov_arcs, source_code, branch_cov)
+
+if __name__ == '__main__':
+    import json
+    from importlib.machinery import SourceFileLoader
+    v = SourceFileLoader('', sys.argv[1]).load_module()
+    method = sys.argv[2] if len(sys.argv) > 2 else 'main'
+    arg = sys.argv[3] if len(sys.argv) > 3 else '%20abc'
+    arcs, source, bcov = capture_coverage(lambda: getattr(v, method)(arg), sys.argv[1])
+    cov = [ (i,j) for f,i,j,src,l in arcs]
+    print(json.dumps(cov), file=sys.stderr)
diff --git a/src/pycfg-0.1/pycfg/pycfg.py b/src/pycfg-0.1/pycfg/pycfg.py
new file mode 100755
index 0000000..99537c2
--- /dev/null
+++ b/src/pycfg-0.1/pycfg/pycfg.py
@@ -0,0 +1,474 @@
+#!/usr/bin/env python3
+# Author: Rahul Gopinath <rahul.gopinath@cispa.saarland>
+# License: GPLv3
+"""
+PyCFG for Python MCI
+Use http://viz-js.com/ to view digraph output
+"""
+
+import ast
+import re
+import astunparse
+import pygraphviz
+
+class CFGNode(dict):
+    registry = 0
+    cache = {}
+    stack = []
+    def __init__(self, parents=[], ast=None):
+        assert type(parents) is list
+        self.parents = parents
+        self.calls = []
+        self.children = []
+        self.ast_node = ast
+        self.rid  = CFGNode.registry
+        CFGNode.cache[self.rid] = self
+        CFGNode.registry += 1
+
+    def lineno(self):
+        return self.ast_node.lineno if hasattr(self.ast_node, 'lineno') else 0
+
+    def __str__(self):
+        return "id:%d line[%d] parents: %s : %s" % (self.rid, self.lineno(), str([p.rid for p in self.parents]), self.source())
+
+    def __repr__(self):
+        return str(self)
+
+    def add_child(self, c):
+        if c not in self.children:
+            self.children.append(c)
+
+    def __eq__(self, other):
+        return self.rid == other.rid
+
+    def __neq__(self, other):
+        return self.rid != other.rid
+
+    def set_parents(self, p):
+        self.parents = p
+
+    def add_parent(self, p):
+        if p not in self.parents:
+            self.parents.append(p)
+
+    def add_parents(self, ps):
+        for p in ps:
+            self.add_parent(p)
+
+    def add_calls(self, func):
+        self.calls.append(func)
+
+    def source(self):
+        return astunparse.unparse(self.ast_node).strip()
+
+    def to_json(self):
+        return {'id':self.rid, 'parents': [p.rid for p in self.parents], 'children': [c.rid for c in self.children], 'calls': self.calls, 'at':self.lineno() ,'ast':self.source()}
+
+    @classmethod
+    def to_graph(cls, arcs=[]):
+        def unhack(v):
+            for i in ['if', 'while', 'for', 'elif']:
+                v = re.sub(r'^_%s:' % i, '%s:' % i, v)
+            return v
+        G = pygraphviz.AGraph(directed=True)
+        cov_lines = set(i for i,j in arcs)
+        for nid, cnode in CFGNode.cache.items():
+            G.add_node(cnode.rid)
+            n = G.get_node(cnode.rid)
+            lineno = cnode.lineno()
+            n.attr['label'] = "%d: %s" % (lineno, unhack(cnode.source()))
+            for pn in cnode.parents:
+                plineno = pn.lineno()
+                if hasattr(pn, 'calllink') and pn.calllink > 0 and not hasattr(cnode, 'calleelink'):
+                    G.add_edge(pn.rid, cnode.rid, style='dotted', weight=100)
+                    continue
+
+                if arcs:
+                    if  (plineno, lineno) in arcs:
+                        G.add_edge(pn.rid, cnode.rid, color='blue')
+                    elif plineno == lineno and lineno in cov_lines:
+                        G.add_edge(pn.rid, cnode.rid, color='blue')
+                    elif hasattr(cnode, 'fn_exit_node') and plineno in cov_lines:  # child is exit and parent is covered
+                        G.add_edge(pn.rid, cnode.rid, color='blue')
+                    elif hasattr(pn, 'fn_exit_node') and len(set(n.lineno() for n in pn.parents) | cov_lines) > 0: # parent is exit and one of its parents is covered.
+                        G.add_edge(pn.rid, cnode.rid, color='blue')
+                    elif plineno in cov_lines and hasattr(cnode, 'calleelink'): # child is a callee (has calleelink) and one of the parents is covered.
+                        G.add_edge(pn.rid, cnode.rid, color='blue')
+                    else:
+                        G.add_edge(pn.rid, cnode.rid, color='red')
+                else:
+                    G.add_edge(pn.rid, cnode.rid)
+        return G
+
+class PyCFG:
+    """
+    The python CFG
+    """
+    def __init__(self):
+        self.founder = CFGNode(parents=[], ast=ast.parse('start').body[0]) # sentinel
+        self.founder.ast_node.lineno = 0
+        self.functions = {}
+        self.functions_node = {}
+
+    def parse(self, src):
+        return ast.parse(src)
+
+    def walk(self, node, myparents):
+        if node is None: return
+        fname = "on_%s" % node.__class__.__name__.lower()
+        if hasattr(self, fname):
+            fn = getattr(self, fname)
+            v = fn(node, myparents)
+            return v
+        else:
+            return myparents
+
+    def on_module(self, node, myparents):
+        """
+        Module(stmt* body)
+        """
+        # each time a statement is executed unconditionally, make a link from
+        # the result to next statement
+        p = myparents
+        for n in node.body:
+            p = self.walk(n, p)
+        return p
+
+    def on_assign(self, node, myparents):
+        """
+        Assign(expr* targets, expr value)
+        TODO: AugAssign(expr target, operator op, expr value)
+        -- 'simple' indicates that we annotate simple name without parens
+        TODO: AnnAssign(expr target, expr annotation, expr? value, int simple)
+        """
+        if len(node.targets) > 1: raise NotImplemented('Parallel assignments')
+
+        p = [CFGNode(parents=myparents, ast=node)]
+        p = self.walk(node.value, p)
+
+        return p
+
+    def on_pass(self, node, myparents):
+        return [CFGNode(parents=myparents, ast=node)]
+
+    def on_break(self, node, myparents):
+        parent = myparents[0]
+        while not hasattr(parent, 'exit_nodes'):
+            # we have ordered parents
+            parent = parent.parents[0]
+
+        assert hasattr(parent, 'exit_nodes')
+        p = CFGNode(parents=myparents, ast=node)
+
+        # make the break one of the parents of label node.
+        parent.exit_nodes.append(p)
+
+        # break doesnt have immediate children
+        return []
+
+    def on_continue(self, node, myparents):
+        parent = myparents[0]
+        while not hasattr(parent, 'exit_nodes'):
+            # we have ordered parents
+            parent = parent.parents[0]
+        assert hasattr(parent, 'exit_nodes')
+        p = CFGNode(parents=myparents, ast=node)
+
+        # make continue one of the parents of the original test node.
+        parent.add_parent(p)
+
+        # return the parent because a continue is not the parent
+        # for the just next node
+        return []
+
+    def on_for(self, node, myparents):
+        #node.target in node.iter: node.body
+        _test_node = CFGNode(parents=myparents, ast=ast.parse('_for: True if %s else False' % astunparse.unparse(node.iter).strip()).body[0])
+        ast.copy_location(_test_node.ast_node, node)
+
+        # we attach the label node here so that break can find it.
+        _test_node.exit_nodes = []
+        test_node = self.walk(node.iter, [_test_node])
+
+        extract_node = CFGNode(parents=[_test_node], ast=ast.parse('%s = %s.shift()' % (astunparse.unparse(node.target).strip(), astunparse.unparse(node.iter).strip())).body[0])
+        ast.copy_location(extract_node.ast_node, _test_node.ast_node)
+
+        # now we evaluate the body, one at a time.
+        p1 = [extract_node]
+        for n in node.body:
+            p1 = self.walk(n, p1)
+
+        # the test node is looped back at the end of processing.
+        _test_node.add_parents(p1)
+
+        return _test_node.exit_nodes + test_node
+
+
+    def on_while(self, node, myparents):
+        # For a while, the earliest parent is the node.test
+        _test_node = CFGNode(parents=myparents, ast=ast.parse('_while: %s' % astunparse.unparse(node.test).strip()).body[0])
+        ast.copy_location(_test_node.ast_node, node.test)
+        _test_node.exit_nodes = []
+        test_node = self.walk(node.test, [_test_node])
+
+        # we attach the label node here so that break can find it.
+
+        # now we evaluate the body, one at a time.
+        p1 = test_node
+        for n in node.body:
+            p1 = self.walk(n, p1)
+
+        # the test node is looped back at the end of processing.
+        _test_node.add_parents(p1)
+
+        # link label node back to the condition.
+        return _test_node.exit_nodes + test_node
+
+    def on_if(self, node, myparents):
+        _test_node = CFGNode(parents=myparents, ast=ast.parse('_if: %s' % astunparse.unparse(node.test).strip()).body[0])
+        ast.copy_location(_test_node.ast_node, node.test)
+        test_node = self.walk(node.test, [_test_node])
+        g1 = test_node
+        for n in node.body:
+            g1 = self.walk(n, g1)
+        g2 = test_node
+        for n in node.orelse:
+            g2 = self.walk(n, g2)
+
+        return g1 + g2
+
+    def on_binop(self, node, myparents):
+        left = self.walk(node.left, myparents)
+        right = self.walk(node.right, left)
+        return right
+
+    def on_compare(self, node, myparents):
+        left = self.walk(node.left, myparents)
+        right = self.walk(node.comparators[0], left)
+        return right
+
+    def on_unaryop(self, node, myparents):
+        return self.walk(node.operand, myparents)
+
+    def on_call(self, node, myparents):
+        def get_func(node):
+            if type(node.func) is ast.Name:
+                mid = node.func.id
+            elif type(node.func) is ast.Attribute:
+                mid = node.func.attr
+            elif type(node.func) is ast.Call:
+                mid = get_func(node.func)
+            else:
+                raise Exception(str(type(node.func)))
+            return mid
+                #mid = node.func.value.id
+
+        p = myparents
+        for a in node.args:
+            p = self.walk(a, p)
+        mid = get_func(node)
+        myparents[0].add_calls(mid)
+
+        # these need to be unlinked later if our module actually defines these
+        # functions. Otherwsise we may leave them around.
+        # during a call, the direct child is not the next
+        # statement in text.
+        for c in p:
+            c.calllink = 0
+        return p
+
+    def on_expr(self, node, myparents):
+        p = [CFGNode(parents=myparents, ast=node)]
+        return self.walk(node.value, p)
+
+    def on_return(self, node, myparents):
+        parent = myparents[0]
+
+        val_node = self.walk(node.value, myparents)
+        # on return look back to the function definition.
+        while not hasattr(parent, 'return_nodes'):
+            parent = parent.parents[0]
+        assert hasattr(parent, 'return_nodes')
+
+        p = CFGNode(parents=val_node, ast=node)
+
+        # make the break one of the parents of label node.
+        parent.return_nodes.append(p)
+
+        # return doesnt have immediate children
+        return []
+
+    def on_functiondef(self, node, myparents):
+        # a function definition does not actually continue the thread of
+        # control flow
+        # name, args, body, decorator_list, returns
+        fname = node.name
+        args = node.args
+        returns = node.returns
+
+        enter_node = CFGNode(parents=[], ast=ast.parse('enter: %s(%s)' % (node.name, ', '.join([a.arg for a in node.args.args])) ).body[0]) # sentinel
+        enter_node.calleelink = True
+        ast.copy_location(enter_node.ast_node, node)
+        exit_node = CFGNode(parents=[], ast=ast.parse('exit: %s(%s)' % (node.name, ', '.join([a.arg for a in node.args.args])) ).body[0]) # sentinel
+        exit_node.fn_exit_node = True
+        ast.copy_location(exit_node.ast_node, node)
+        enter_node.return_nodes = [] # sentinel
+
+        p = [enter_node]
+        for n in node.body:
+            p = self.walk(n, p)
+
+        for n in p:
+            if n not in enter_node.return_nodes:
+                enter_node.return_nodes.append(n)
+
+        for n in enter_node.return_nodes:
+            exit_node.add_parent(n)
+
+        self.functions[fname] = [enter_node, exit_node]
+        self.functions_node[enter_node.lineno()] = fname
+
+        return myparents
+
+    def get_defining_function(self, node):
+        if node.lineno() in self.functions_node: return self.functions_node[node.lineno()]
+        if not node.parents:
+            self.functions_node[node.lineno()] = ''
+            return ''
+        val = self.get_defining_function(node.parents[0])
+        self.functions_node[node.lineno()] = val
+        return val
+
+    def link_functions(self):
+        for nid,node in CFGNode.cache.items():
+            if node.calls:
+                for calls in node.calls:
+                    if calls in self.functions:
+                        enter, exit = self.functions[calls]
+                        enter.add_parent(node)
+                        if node.children:
+                            # # until we link the functions up, the node
+                            # # should only have succeeding node in text as
+                            # # children.
+                            # assert(len(node.children) == 1)
+                            # passn = node.children[0]
+                            # # We require a single pass statement after every
+                            # # call (which means no complex expressions)
+                            # assert(type(passn.ast_node) == ast.Pass)
+
+                            # # unlink the call statement
+                            assert node.calllink > -1
+                            node.calllink += 1
+                            for i in node.children:
+                                i.add_parent(exit)
+                            # passn.set_parents([exit])
+                            # ast.copy_location(exit.ast_node, passn.ast_node)
+
+
+                            # #for c in passn.children: c.add_parent(exit)
+                            # #passn.ast_node = exit.ast_node
+
+    def update_functions(self):
+        for nid,node in CFGNode.cache.items():
+            _n = self.get_defining_function(node)
+
+    def update_children(self):
+        for nid,node in CFGNode.cache.items():
+            for p in node.parents:
+                p.add_child(node)
+
+    def gen_cfg(self, src):
+        """
+        >>> i = PyCFG()
+        >>> i.walk("100")
+        5
+        """
+        node = self.parse(src)
+        nodes = self.walk(node, [self.founder])
+        self.last_node = CFGNode(parents=nodes, ast=ast.parse('stop').body[0])
+        ast.copy_location(self.last_node.ast_node, self.founder.ast_node)
+        self.update_children()
+        self.update_functions()
+        self.link_functions()
+
+def compute_dominator(cfg, start = 0, key='parents'):
+    dominator = {}
+    dominator[start] = {start}
+    all_nodes = set(cfg.keys())
+    rem_nodes = all_nodes - {start}
+    for n in rem_nodes:
+        dominator[n] = all_nodes
+
+    c = True
+    while c:
+        c = False
+        for n in rem_nodes:
+            pred_n = cfg[n][key]
+            doms = [dominator[p] for p in pred_n]
+            i = set.intersection(*doms) if doms else set()
+            v = {n} | i
+            if dominator[n] != v:
+                c = True
+            dominator[n] = v
+    return dominator
+
+def slurp(f):
+    with open(f, 'r') as f: return f.read()
+
+
+def get_cfg(pythonfile):
+    cfg = PyCFG()
+    cfg.gen_cfg(slurp(pythonfile).strip())
+    cache = CFGNode.cache
+    g = {}
+    for k,v in cache.items():
+        j = v.to_json()
+        at = j['at']
+        parents_at = [cache[p].to_json()['at'] for p in j['parents']]
+        children_at = [cache[c].to_json()['at'] for c in j['children']]
+        if at not in g:
+            g[at] = {'parents':set(), 'children':set()}
+        # remove dummy nodes
+        ps = set([p for p in parents_at if p != at])
+        cs = set([c for c in children_at if c != at])
+        g[at]['parents'] |= ps
+        g[at]['children'] |= cs
+        if v.calls:
+            g[at]['calls'] = v.calls
+        g[at]['function'] = cfg.functions_node[v.lineno()]
+    return (g, cfg.founder.ast_node.lineno, cfg.last_node.ast_node.lineno)
+
+def compute_flow(pythonfile):
+    cfg,first,last = get_cfg(pythonfile)
+    return cfg, compute_dominator(cfg, start=first), compute_dominator(cfg, start=last, key='children')
+
+if __name__ == '__main__':
+    import json
+    import sys
+    import argparse
+    parser = argparse.ArgumentParser()
+    parser.add_argument('pythonfile', help='The python file to be analyzed')
+    parser.add_argument('-d','--dots', action='store_true', help='generate a dot file')
+    parser.add_argument('-c','--cfg', action='store_true', help='print cfg')
+    parser.add_argument('-x','--coverage', action='store', dest='coverage', type=str, help='branch coverage file')
+    parser.add_argument('-y','--ccoverage', action='store', dest='ccoverage', type=str, help='custom coverage file')
+    args = parser.parse_args()
+    if args.dots:
+        arcs = None
+        if args.coverage:
+            cdata = coverage.CoverageData()
+            cdata.read_file(filename=args.coverage)
+            arcs = [(abs(i),abs(j)) for i,j in cdata.arcs(cdata.measured_files()[0])]
+        elif args.ccoverage:
+            arcs = [(i,j) for i,j in json.loads(open(args.ccoverage).read())]
+        else:
+            arcs = []
+        cfg = PyCFG()
+        cfg.gen_cfg(slurp(args.pythonfile).strip())
+        g = CFGNode.to_graph(arcs)
+        g.draw(args.pythonfile + '.png', prog='dot')
+        print(g.string(), file=sys.stderr)
+    elif args.cfg:
+        cfg,first,last = get_cfg(args.pythonfile)
+        for i in sorted(cfg.keys()):
+            print(i,'parents:', cfg[i]['parents'], 'children:', cfg[i]['children'])
diff --git a/src/pycfg-0.1/setup.cfg b/src/pycfg-0.1/setup.cfg
new file mode 100644
index 0000000..8bfd5a1
--- /dev/null
+++ b/src/pycfg-0.1/setup.cfg
@@ -0,0 +1,4 @@
+[egg_info]
+tag_build = 
+tag_date = 0
+
diff --git a/src/pycfg-0.1/setup.py b/src/pycfg-0.1/setup.py
new file mode 100644
index 0000000..2364acd
--- /dev/null
+++ b/src/pycfg-0.1/setup.py
@@ -0,0 +1,40 @@
+from setuptools import setup
+
+setup(name='pycfg',
+      version='0.1',
+      description='The python3 ast based control flow graph',
+      long_description="""
+This package generates a control flow graph of the passed python file based on the AST generated (rather than the bytecode). It supports only a few python statements at this point. Notably absent are exceptions and generators. Essentially, only those constructs found in example.py are supported.
+
+        Compatibility
+        -------------
+        It was tested on Python 3.6
+        
+        
+        To run
+        ------
+        
+          python pycfg/pycfg.py <program to be analyzed> -d
+          or
+          python pycfg/pycfg.py <program to be analyzed> -d -y <branch coverage file>
+
+          Inspect Makefile for a better idea
+       """,
+      classifiers=[
+        'Development Status :: 3 - Alpha',
+        'Environment :: Console',
+        'Intended Audience :: Developers',
+        'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
+        'Operating System :: OS Independent',
+        'Programming Language :: Python :: 3',
+        'Topic :: Software Development',
+        'Topic :: Software Development :: Libraries :: Python Modules',
+        'Topic :: Software Development :: Compilers'
+      ],
+      keywords='control-flow-graph abstract-syntax-tree',
+      url='http://github.com/vrthra/pycfg',
+      author='Rahul Gopinath',
+      author_email='rahul@gopinath.org',
+      license='GPLv3',
+      packages=['pycfg'],
+      zip_safe=False)
-- 
GitLab