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