Skip to content

Commit d20686a

Browse files
committed
init work version 0.0.4
0 parents  commit d20686a

15 files changed

Lines changed: 807 additions & 0 deletions

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.idea
2+
dist/
3+
codegraph.egg-info/
4+
codegraph/__pycache__/

README.rst

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
CodeGraph
2+
=========
3+
4+
Tool that create a graph of code to show dependencies between code entities (methods, classes and etc).
5+
CodeGraph does not execute code, it is based only on lex and syntax parse, so it not need to install
6+
all your code dependencies.
7+
8+
Usage:
9+
10+
pip install codegraph
11+
12+
cg /path/to/your_python_code
13+
# path must be absolute
14+
15+
your_python_code - module with your python code
16+
17+
For example, if I put codegraph in my user home directory path will be:
18+
19+
cg /Users/myuser/codegraph/codegraph
20+
21+
Pass '-o' flag if you want only print dependencies in console and don't want graph visualisation
22+
23+
cg /path/to/your_python_code -o
24+
25+
26+
27+
![Code Graph - Code with not used module](/docs/img/code_with_trash_module.png?raw=true "Code with not used module")
28+
![Code Graph - Code there all modules linked together](/docs/img/normal_code.png?raw=true "Code with modules that linked together")
29+
30+
TODO:
31+
1. Create normal readme
32+
2. Add tests
33+
3. Add possibility to work with any code based (not depend on Python language only)
34+
4. Work on visual part of Graph (now it is not very user friendly)
35+
5. Add support to variables (names) as entities
36+
37+
Contributing:
38+
Open PR with improvements that you want to add
39+
40+
If you have any questions - write me xnuinside@gmail.com

codegraph/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__version__ = '0.0.4'

codegraph/conf/cli.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
parser:
2+
prog: 'CodeGraph'
3+
description: 'Tool that create a graph of code to show dependencies between code entities (methods, classes and etc).
4+
CodeGraph does not execute code, it is based only on lex and syntax parse'
5+
6+
commands:
7+
- keys: ['-v', '--version']
8+
help: "show CodeGraph version"
9+
action: show_version()
10+
default: True
11+
- keys: ['paths']
12+
nargs: '*'
13+
help: "Provide path to code base"
14+
- keys: ['-o', '--object-only']
15+
help: "Provide flag if you don't want to visualise your code dependencies as graph"
16+
action: 'store_true'
17+
default: False

codegraph/core.py

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import os
2+
from typing import Text, Tuple, Dict, List
3+
from argparse import Namespace
4+
from collections import defaultdict
5+
from codegraph.parser import create_objects_array, Import
6+
from codegraph.utils import get_paths_list
7+
8+
9+
aliases = {}
10+
11+
12+
def read_file_content(path: Text) -> Text:
13+
with open(path, 'r+') as file_read:
14+
return file_read.read()
15+
16+
17+
def parse_code_file(path: Text) -> List:
18+
""" read module source and parse to get objects array """
19+
source = read_file_content(path)
20+
parsed_module = create_objects_array(source=source, fname=os.path.basename(path))
21+
return parsed_module
22+
23+
24+
def get_code_objects(paths_list: List) -> Dict:
25+
"""
26+
get all code files data for paths list
27+
:param paths_list: list with paths to code files to parse
28+
:return:
29+
"""
30+
all_data = {}
31+
for path in paths_list:
32+
content = parse_code_file(path)
33+
all_data[path] = content
34+
return all_data
35+
36+
37+
def create_graph(args: Namespace) -> Dict:
38+
"""
39+
method to create list of objects from py modules
40+
:param args:
41+
:return:
42+
"""
43+
# get py modules list
44+
paths_list = get_paths_list(args.paths)
45+
# get py modules list
46+
add_dict = get_code_objects(paths_list)
47+
modules_entities = usage_graph(add_dict)
48+
return modules_entities
49+
50+
51+
def get_module_name(code_path: Text) -> Text:
52+
module_name = os.path.basename(code_path).replace('.py', '')
53+
return module_name
54+
55+
56+
def module_name_in_imports(imports: List, module_name: Text) -> bool:
57+
for import_ in imports:
58+
if module_name in import_:
59+
return True
60+
return False
61+
62+
63+
def get_imports_and_entities_lines(code_objects: Dict) -> Tuple[Dict, Dict, Dict]:
64+
"""
65+
joined together to avoid iteration several time
66+
imports - list of modules in code_objects Dict that used in current module
67+
"""
68+
entities_lines = defaultdict(dict)
69+
imports = defaultdict(list)
70+
modules_ = code_objects.keys()
71+
names_map = {}
72+
for path in code_objects:
73+
_base_folder = os.path.basename(os.path.dirname(path))
74+
names_map[get_module_name(path)] = path
75+
# for each module in list
76+
if code_objects[path] and isinstance(code_objects[path][-1], Import):
77+
# extract imports if exist
78+
for import_ in code_objects[path].pop(-1).modules:
79+
pathed_import = import_
80+
alias = None
81+
if ' as ' in pathed_import:
82+
pathed_import, alias = pathed_import.split(' as ')
83+
if _base_folder+'.' in pathed_import:
84+
pathed_import = pathed_import.replace('.', '/').split(_base_folder+'/')[1]
85+
if '/' in pathed_import:
86+
pathed_import = pathed_import.split('/')[0]
87+
for module_ in modules_:
88+
if pathed_import and pathed_import in module_:
89+
if alias:
90+
aliases[pathed_import] = alias
91+
imports[path].append(pathed_import)
92+
for entity in code_objects[path]:
93+
# create a dict with lines of start and end for each entity in module
94+
entities_lines[path][(entity.lineno, entity.endno)] = entity.name
95+
return entities_lines, imports, names_map
96+
97+
98+
def search_entities_from_list_in_code(entities_list: List, module_name: Text, line:Text) -> Text:
99+
for entity in entities_list:
100+
if search_entity_usage(module_name, entity.name, line):
101+
yield entity
102+
103+
104+
def search_entities_from_module_in_code(
105+
_module: Text, _path: Text, code_objects: Dict, code: List, current: bool =False) -> Dict:
106+
found_entities = defaultdict(list)
107+
for num, line in enumerate(code):
108+
if not line.startswith("#") and not line.startswith("\"") and not line.startswith("\'"):
109+
entities_in_line = [x for x in
110+
search_entities_from_list_in_code(code_objects[_path], _module, line)]
111+
for entity in entities_in_line:
112+
prefix = f'{_module}.' if not current else ''
113+
found_entities[f'{prefix}{entity.name}'].append(num + 1)
114+
return found_entities
115+
116+
117+
def collect_entities_usage_in_modules(code_objects: Dict, imports: Dict, modules_names_map: Dict) -> Dict:
118+
entities_usage_in_modules = defaultdict(dict)
119+
for path in code_objects:
120+
entities_usage_in_modules[path] = defaultdict(list)
121+
# print(f"Start to work with module: {path}")
122+
# print(f"Imports in module: {imports}")
123+
module_content = read_file_content(path)
124+
# to reduce count of iteration, we not need lines with functions and classes defenitions
125+
module_content = module_content.replace("async ", "# async ").replace(
126+
"def ", "# def ").replace("class ", "# class ")
127+
# split by line
128+
code = module_content.split('\n')
129+
for _module in imports[path]:
130+
# search entities from other modules
131+
_path = modules_names_map[_module]
132+
entities_usage_in_modules[path].update(search_entities_from_module_in_code(
133+
_module, _path, code_objects, code))
134+
# search entities from current module
135+
entities_usage_in_modules[path].update(
136+
search_entities_from_module_in_code(get_module_name(path), path, code_objects, code, current=True))
137+
return entities_usage_in_modules
138+
139+
140+
def populate_free_nodes(code_objects: Dict, dependencies: Dict) -> Dict:
141+
for path in code_objects:
142+
for entity in code_objects[path]:
143+
if entity.name not in dependencies[path]:
144+
dependencies[path][entity.name] = []
145+
return dependencies
146+
147+
148+
def usage_graph(code_objects: Dict) -> Dict:
149+
"""
150+
module name: function
151+
:param code_objects:
152+
:return:
153+
"""
154+
entities_lines, imports, modules_names_map = get_imports_and_entities_lines(code_objects)
155+
entities_usage_in_modules = collect_entities_usage_in_modules(code_objects, imports, modules_names_map )
156+
# create edges
157+
dependencies = defaultdict(dict)
158+
for module in entities_usage_in_modules:
159+
dependencies[module] = defaultdict(list)
160+
for method_that_used in entities_usage_in_modules[module]:
161+
method_usage_lines = entities_usage_in_modules[module][method_that_used]
162+
for method_usage_line in method_usage_lines:
163+
for entity in entities_lines[module]:
164+
if entity[0] <= method_usage_line <= entity[1]:
165+
dependencies[module][entities_lines[module][entity]].append(method_that_used)
166+
break
167+
else:
168+
# mean in global of module
169+
dependencies[module]['_'].append(method_that_used)
170+
dependencies = populate_free_nodes(code_objects, dependencies)
171+
return dependencies
172+
173+
174+
def search_entity_usage(module_name: Text, name: Text, line: Text) -> bool:
175+
""" check exist method or entity usage in line or not """
176+
method_call = name + "("
177+
dot_access = name + "."
178+
if method_call in line or " " + dot_access in line \
179+
or f"{module_name}." + method_call in line or f"{module_name}." + dot_access in line:
180+
return True
181+
elif module_name in aliases:
182+
if aliases[module_name] + '.' + method_call in line:
183+
return True
184+
return False
185+
298 KB
Loading

codegraph/docs/img/normal_code.png

264 KB
Loading

codegraph/main.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
""" main module of testsdiffer for console (cli) usage"""
2+
import os
3+
import pprint
4+
5+
import clifier
6+
7+
from codegraph import __version__
8+
from codegraph import core
9+
10+
CLI_CFG_NAME = "conf/cli.yml"
11+
12+
13+
def cli():
14+
config_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), CLI_CFG_NAME)
15+
_cli = clifier.Clifier(config_path, prog_version=__version__)
16+
parser = _cli.create_parser()
17+
args = parser.parse_args()
18+
main(args)
19+
20+
21+
def main(args):
22+
modules_entities = core.create_graph(args)
23+
pprint.pprint(modules_entities)
24+
if not args.object_only:
25+
# to make more quick work if not needed to visualize
26+
import codegraph.vizualyzer as vz
27+
vz.draw_graph(modules_entities)

0 commit comments

Comments
 (0)