Skip to content

Commit 964a19d

Browse files
Merge pull request #60 from LucasAndradeDias/multi-files-tracing
Multi files tracing
2 parents d300d4b + f0bff91 commit 964a19d

File tree

14 files changed

+248
-109
lines changed

14 files changed

+248
-109
lines changed

src/outliner/cli.py renamed to src/outliner/__main__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,13 @@ def parser_arguments():
99
"-fp",
1010
help="Path to the file you want to trace",
1111
required=True,
12-
nargs=1,
1312
)
1413
parser.add_argument(
1514
"--object_invoke",
1615
"-o",
1716
help="The invoking statement of the object",
1817
type=str,
1918
required=True,
20-
nargs=1,
2119
)
2220
parser.add_argument(
2321
"--mode",
@@ -33,11 +31,13 @@ def parser_arguments():
3331

3432
def main():
3533
args = parser_arguments()
36-
3734
instance_class = Outliner(
3835
args.file_path,
3936
args.object_invoke,
4037
args.mode,
4138
)
4239

4340
instance_class.run()
41+
42+
if __name__ == "__main__":
43+
main()

src/outliner/modules/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from .trace import Trace
2+
from .display import Display

src/outliner/modules/display.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,11 @@ def tree(self):
3737
connect_branch = (
3838
lines
3939
+ self.ansi_for_tree[
40-
"CHILD_OBJECT_ANSI"
41-
if not func_With_exception
42-
else "CHILD_WITH_EXECEPTION"
40+
(
41+
"CHILD_OBJECT_ANSI"
42+
if not func_With_exception
43+
else "CHILD_WITH_EXECEPTION"
44+
)
4345
]
4446
+ f"{position}. "
4547
+ func

src/outliner/modules/trace.py

Lines changed: 10 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
import sys
2-
32
import collections
4-
import importlib.util
53

6-
from functools import partial
7-
from pathlib import Path
8-
from typing import Optional
4+
from ..views import RunningObject
95

106

117
class ExceptionWhileTracing(Exception):
@@ -14,7 +10,10 @@ class ExceptionWhileTracing(Exception):
1410
pass
1511

1612

13+
14+
1715
class Trace:
16+
1817
def __init__(self):
1918
self.detailed_data = collections.defaultdict(
2019
lambda: {"call": 0, "return": 0, "line": 0, "exception": 0}
@@ -32,64 +31,19 @@ def _trace_function(self, frame, event, arg):
3231

3332
def _running_trace(self, obj) -> None:
3433
try:
34+
instance = obj.instance()
3535
sys.settrace(self._trace_function)
36-
obj()
36+
instance()
3737
except ExceptionWhileTracing as error:
3838
return
39-
except Exception as error:
40-
raise error
39+
except Exception:
40+
raise Exception("Erro durante execução")
4141
finally:
4242
sys.settrace(None)
4343

44-
def _get_object_arguments(self, obj: str):
45-
parenthesis_1 = obj.index("(")
46-
parenthesis_2 = obj.index(")")
47-
obj_arguments = obj[parenthesis_1 + 1 : parenthesis_2].split(",")
48-
49-
return None if not any(obj_arguments) else obj_arguments
50-
51-
def _create_obj_instance(self, module: any, object_: str):
52-
object_name = object_.split("(")[0]
53-
object_arguments = self._get_object_arguments(object_)
54-
55-
obj_instance = getattr(module, object_name, None)
56-
57-
if object_arguments:
58-
obj_instance = partial(obj_instance, *object_arguments)
59-
60-
return obj_instance
61-
62-
def run_file(
63-
self,
64-
module_path: Path,
65-
object_to_run: str,
66-
) -> None:
67-
"""
68-
Trace a file
69-
70-
:param Path-like module_path: The path to the module
71-
:param str object_to_run: The object to be traced inside the module
72-
"""
73-
module_name = module_path.stem
74-
75-
moduleSpec = importlib.util.spec_from_file_location(module_name, module_path)
76-
module_obj = importlib.util.module_from_spec(moduleSpec)
77-
78-
moduleSpec.loader.exec_module(module_obj)
79-
80-
if len(object_to_run.split(".")) >= 2:
81-
obj_class_instance = self._create_obj_instance(
82-
module_obj, object_to_run.split(".")[0]
83-
)()
84-
running_obj = self._create_obj_instance(
85-
obj_class_instance, object_to_run.split(".")[1]
86-
)
87-
else:
88-
running_obj = self._create_obj_instance(module_obj, object_to_run)
89-
44+
def trace_obj(self, running_obj: RunningObject):
9045
if not callable(running_obj):
91-
raise Exception("given object '%s' is not callable." % (module_name))
92-
46+
raise Exception(f"Given object '{running_obj}' is not callable.")
9347
self._running_trace(running_obj)
9448

9549
def __str__(self):

src/outliner/outliner.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,44 @@
11
import re
22

33
from pathlib import Path
4+
45
from .modules.trace import Trace
56
from .modules.display import Display
67

8+
from .views import ModuleObject, RunningObject
9+
710

811
class Outliner:
9-
def __init__(self, file_path, object_name, display_type):
12+
def __init__(self, file_path, object_name: str, display_type: str):
1013
self.file = file_path
1114
self.obj_name = object_name
1215
self.display_type = display_type
1316
self.callable_object_parttern = re.compile(
1417
r"\w+\((?:'\w+')?(?:\.\w+\('(?:\w+)'\))?\)"
1518
)
19+
self._check_file()
1620

1721
def run(self):
1822
"""
1923
This method trace an object and prints out a tree with found data
2024
"""
21-
self._check_file()
2225

2326
if not re.match(self.callable_object_parttern, self.obj_name):
2427
raise ValueError("Not valid callable syntax")
2528

29+
module_obj = ModuleObject(Path(self.file))
30+
running_obj = RunningObject(module_obj, self.obj_name)
31+
2632
trace = Trace()
33+
trace.trace_obj(running_obj)
2734

28-
trace.run_file(Path(self.file), self.obj_name)
2935
display_class = Display(trace.detailed_data, trace.functions_flow)
3036

3137
running_display = getattr(display_class, self.display_type)
3238
running_display()
3339

3440
def _check_file(self):
3541
if not Path(self.file).is_file():
36-
raise ("Not a valid file")
42+
raise Exception(f"Not a valid file\nPath:'{self.file}'")
3743
if not ".py" in self.file:
38-
raise ("Not a python file")
44+
raise Exception("Not a python file")

src/outliner/utils/__init__.py

Whitespace-only changes.

src/outliner/utils/loader.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import importlib
2+
import importlib.abc
3+
from types import ModuleType
4+
5+
6+
class CustomLoader(importlib.abc.Loader):
7+
def __init__(self) -> None:
8+
super().__init__()
9+
10+
def create_module(spec) -> object:
11+
return None
12+
13+
def exec_module(module: ModuleType) -> None:
14+
with open(module.__spec__.origin, "rb") as f:
15+
code = compile(f.read(), module.__spec__.origin, "exec")
16+
exec(code, module.__dict__)

src/outliner/views/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from .module_obj import ModuleObject
2+
from .running_obj import RunningObject

src/outliner/views/module_obj.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import sys
2+
import ast
3+
import importlib
4+
5+
from importlib import util
6+
from pathlib import Path
7+
from ..utils.loader import CustomLoader
8+
9+
10+
class ModuleObject:
11+
"""
12+
Run all needed pre-sets to run the trace_object.
13+
14+
Class create to transforms the script into an better handling object.
15+
That way, it is faster to handle with it because will use ast module to get infos about it
16+
"""
17+
18+
def __init__(
19+
self,
20+
module_path: Path,
21+
):
22+
self.module_path = module_path
23+
self.module_ast = self._load_file_ast()
24+
self.found_submodules = self._find_imported_modules()
25+
26+
sys.path.append(str(module_path.parent))
27+
28+
for submodule in self.found_submodules:
29+
try:
30+
importlib.import_module(submodule)
31+
except ModuleNotFoundError:
32+
raise ModuleNotFoundError(
33+
f"Could not add the submodule '{submodule}' to the namespace.\n check if the module is installed."
34+
)
35+
36+
def _find_imported_modules(self) -> iter:
37+
"""
38+
A generator with all imports the ast object contains in its namespace
39+
"""
40+
for node in ast.walk(self.module_ast):
41+
if isinstance(node, ast.Import):
42+
for alias in node.names:
43+
yield alias.name
44+
elif isinstance(node, ast.ImportFrom):
45+
if node.module is not None:
46+
yield node.module
47+
return []
48+
49+
def _load_file_ast(self) -> ast.parse:
50+
with open(self.module_path, "r") as file:
51+
return ast.parse(file.read())
52+
53+
def module(self):
54+
"""
55+
Return a the ModuleObj and the ModuleSpec (https://peps.python.org/pep-0451/) of the module
56+
"""
57+
module_name = self.module_path.stem
58+
module_spec = util.spec_from_file_location(
59+
name=module_name, location=self.module_path, loader=CustomLoader
60+
)
61+
module_obj = util.module_from_spec(module_spec)
62+
63+
return module_obj, module_spec

src/outliner/views/running_obj.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from .module_obj import ModuleObject
2+
from functools import partial
3+
4+
5+
class RunningObject:
6+
"""
7+
Create an running object from a module obj
8+
"""
9+
10+
def __init__(self, script_obj: ModuleObject, obj_invoking: str):
11+
self.obj_invoking = obj_invoking
12+
self.script_obj = script_obj
13+
self.father_module = self.script_obj.module()
14+
15+
self.module_obj = self.father_module[0]
16+
self.module_spec = self.father_module[1]
17+
18+
# load the module to the global namespace
19+
self.module_spec.loader.exec_module(self.module_obj)
20+
21+
self.running_obj = None
22+
self.instance()
23+
24+
def _get_object_arguments(self, obj: str):
25+
parenthesis_1 = obj.index("(")
26+
parenthesis_2 = obj.index(")")
27+
obj_arguments = obj[parenthesis_1 + 1 : parenthesis_2].split(",")
28+
29+
return obj_arguments if any(obj_arguments) else None
30+
31+
def _create_obj_instance(self, namespace: any, object_: str):
32+
object_name = object_.split("(")[0]
33+
object_arguments = self._get_object_arguments(object_)
34+
obj_instance = getattr(namespace, object_name, None)
35+
36+
if object_arguments is not None:
37+
obj_instance = partial(obj_instance, *object_arguments)
38+
39+
return obj_instance
40+
41+
def instance(self):
42+
"""Creates an instance of the object"""
43+
running_obj = None
44+
father = self.module_obj
45+
objs = self.obj_invoking.split(".") or [self.obj_invoking]
46+
for number, _ in enumerate(objs):
47+
running_obj = self._create_obj_instance(father, objs[number])
48+
if len(objs) == number:
49+
break
50+
father = running_obj
51+
52+
self.running_obj = father
53+
return running_obj
54+
55+
def __call__(self):
56+
self.instance()

tests/data/multifiles/module.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from module_2 import final_function
2+
3+
class Dummy:
4+
5+
def __init__(self):
6+
self.method1()
7+
8+
def method1(self):
9+
return self.method2()
10+
11+
def method2(self):
12+
return self.method_math()
13+
14+
def method_math(self):
15+
return final_function()
16+
17+
def final():
18+
Dummy()

tests/data/multifiles/module_2.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
2+
def final_function():
3+
return 1 + 2

tests/data/multifiles/origin.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from module import Dummy, final
2+
3+
4+
5+
def test4():
6+
test5()
7+
8+
def test3():
9+
return test4()
10+
11+
def test2():
12+
test3()
13+
14+
def test1():
15+
test2()
16+
17+
def test5():
18+
final()
19+
20+
#test = Dummy

0 commit comments

Comments
 (0)