atrace 0.1.5__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.
- atrace/__init__.py +67 -0
- atrace/model.py +29 -0
- atrace/outputlogger.py +35 -0
- atrace/py.typed +0 -0
- atrace/report.py +98 -0
- atrace/vartracer.py +69 -0
- atrace-0.1.5.dist-info/METADATA +86 -0
- atrace-0.1.5.dist-info/RECORD +9 -0
- atrace-0.1.5.dist-info/WHEEL +4 -0
atrace/__init__.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import atexit
|
|
2
|
+
import inspect
|
|
3
|
+
import signal
|
|
4
|
+
import sys
|
|
5
|
+
from types import FrameType
|
|
6
|
+
from typing import Any, Optional
|
|
7
|
+
|
|
8
|
+
from . import model, outputlogger, report, vartracer
|
|
9
|
+
|
|
10
|
+
trace: model.Trace = []
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def just_kicking_off(frame: FrameType, event: str, arg: Any):
|
|
14
|
+
return None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_importer_frame() -> Optional[FrameType]:
|
|
18
|
+
# Get the current call stack
|
|
19
|
+
for frame_info in inspect.stack():
|
|
20
|
+
# Filter out internal importlib frames and the current module's frame
|
|
21
|
+
filename = frame_info.filename
|
|
22
|
+
if not filename.startswith("<") and filename != __file__:
|
|
23
|
+
return frame_info.frame
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
original_stdout = sys.stdout
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def exit_handler():
|
|
31
|
+
sys.stdout = original_stdout
|
|
32
|
+
report.dump_report(trace)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# https://stackoverflow.com/questions/23468042/the-invocation-of-signal-handler-and-atexit-handler-in-python
|
|
36
|
+
def sig_handler(_signo, _frame):
|
|
37
|
+
sys.exit(0)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def setup():
|
|
41
|
+
"""See https://docs.python.org/3/library/sys.html#sys.settrace for an explanation of all the convoluted things in here"""
|
|
42
|
+
|
|
43
|
+
# This kicks off the tracing machinery (so that the lines below work)
|
|
44
|
+
sys.settrace(just_kicking_off)
|
|
45
|
+
|
|
46
|
+
# We want to only trace the module that imports us
|
|
47
|
+
importer_frame = get_importer_frame()
|
|
48
|
+
if importer_frame:
|
|
49
|
+
module_of_interest = inspect.getmodule(importer_frame)
|
|
50
|
+
if module_of_interest:
|
|
51
|
+
var_tracer = vartracer.VarTracer(trace, module_of_interest)
|
|
52
|
+
|
|
53
|
+
# Setup tracing outside of functions
|
|
54
|
+
importer_frame.f_trace = var_tracer.trace_vars
|
|
55
|
+
|
|
56
|
+
# Setup tracing inside of functions
|
|
57
|
+
sys.settrace(var_tracer.trace_vars)
|
|
58
|
+
|
|
59
|
+
sys.stdout = outputlogger.OutputLogger(trace=trace, stdout=sys.stdout)
|
|
60
|
+
|
|
61
|
+
atexit.register(exit_handler)
|
|
62
|
+
catchable_sigs = set(signal.Signals) - {signal.SIGKILL, signal.SIGSTOP}
|
|
63
|
+
for sig in catchable_sigs:
|
|
64
|
+
signal.signal(sig, sig_handler)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
setup()
|
atrace/model.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Any, TypeAlias
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class PrintEvent:
|
|
7
|
+
text: str
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class Variable:
|
|
12
|
+
function_name: str | None
|
|
13
|
+
name: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class VariableChangeEvent:
|
|
18
|
+
variable: Variable
|
|
19
|
+
value: Any
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class TraceItem:
|
|
24
|
+
line_no: int
|
|
25
|
+
function_name: str
|
|
26
|
+
event: PrintEvent | VariableChangeEvent
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
Trace: TypeAlias = list[TraceItem]
|
atrace/outputlogger.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from typing import TextIO
|
|
3
|
+
|
|
4
|
+
from . import model
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class OutputLogger:
|
|
8
|
+
"""
|
|
9
|
+
OutputLogger wraps stdout, writing to it and simultaneously capturing the text to the trace.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, trace: model.Trace, stdout: TextIO):
|
|
13
|
+
self.trace = trace
|
|
14
|
+
self.stdout = stdout
|
|
15
|
+
|
|
16
|
+
def write(self, text: str) -> None:
|
|
17
|
+
"""
|
|
18
|
+
Write to stdout and record the text in the trace
|
|
19
|
+
"""
|
|
20
|
+
self.stdout.write(text)
|
|
21
|
+
|
|
22
|
+
frame = sys._getframe(1)
|
|
23
|
+
# An alternative that uses a public API, but then the type checker bothers me
|
|
24
|
+
# frame = inspect.currentframe().f_back
|
|
25
|
+
|
|
26
|
+
self.trace.append(
|
|
27
|
+
model.TraceItem(
|
|
28
|
+
line_no=frame.f_lineno,
|
|
29
|
+
function_name=frame.f_code.co_name,
|
|
30
|
+
event=model.PrintEvent(text),
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def flush(self):
|
|
35
|
+
self.stdout.flush()
|
atrace/py.typed
ADDED
|
File without changes
|
atrace/report.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from tabulate import tabulate
|
|
5
|
+
|
|
6
|
+
from . import model
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def collect_variable_list(trace: model.Trace) -> list[model.Variable]:
|
|
10
|
+
variable_list = []
|
|
11
|
+
for traceItem in trace:
|
|
12
|
+
match traceItem.event:
|
|
13
|
+
case model.VariableChangeEvent(variable_name, _):
|
|
14
|
+
if variable_name not in variable_list:
|
|
15
|
+
variable_list.append(variable_name)
|
|
16
|
+
return variable_list
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def coalesce_print_events(trace: model.Trace) -> model.Trace:
|
|
20
|
+
last_print_item = None
|
|
21
|
+
for trace_item in trace:
|
|
22
|
+
match trace_item.event:
|
|
23
|
+
case model.PrintEvent(text):
|
|
24
|
+
if (
|
|
25
|
+
last_print_item is None
|
|
26
|
+
or trace_item.line_no != last_print_item.line_no
|
|
27
|
+
):
|
|
28
|
+
last_print_item = trace_item
|
|
29
|
+
else:
|
|
30
|
+
last_print_item.event.text += text
|
|
31
|
+
trace_item.event = None # Mark for deletion
|
|
32
|
+
case _:
|
|
33
|
+
last_print_item = None
|
|
34
|
+
|
|
35
|
+
return [item for item in trace if item.event is not None]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def contains_prints(trace: model.Trace) -> bool:
|
|
39
|
+
for trace_item in trace:
|
|
40
|
+
match trace_item.event:
|
|
41
|
+
case model.PrintEvent:
|
|
42
|
+
return True
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def variable_to_column_name(var: model.Variable) -> str:
|
|
47
|
+
context = None if var.function_name == "<module>" else var.function_name
|
|
48
|
+
return "(" + context + ") " + var.name if context else var.name
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class Line:
|
|
53
|
+
line_no: int
|
|
54
|
+
variable_changes: dict[str, Any]
|
|
55
|
+
output: str | None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def dump_report(trace: model.Trace) -> None:
|
|
59
|
+
trace = coalesce_print_events(trace)
|
|
60
|
+
|
|
61
|
+
# Build intermediate "lines" datastructure, that collects all events that happen on a given line
|
|
62
|
+
lines = []
|
|
63
|
+
current_line = None
|
|
64
|
+
for trace_item in trace:
|
|
65
|
+
if current_line is None or current_line.line_no != trace_item.line_no:
|
|
66
|
+
current_line = Line(trace_item.line_no, {}, None)
|
|
67
|
+
lines.append(current_line)
|
|
68
|
+
|
|
69
|
+
match trace_item.event:
|
|
70
|
+
case model.VariableChangeEvent(variable, value):
|
|
71
|
+
# Don't clobber a new value if it changes on the same line
|
|
72
|
+
# (this happens for example in tight loops)
|
|
73
|
+
if variable in current_line.variable_changes:
|
|
74
|
+
current_line = Line(trace_item.line_no, {}, None)
|
|
75
|
+
lines.append(current_line)
|
|
76
|
+
|
|
77
|
+
current_line.variable_changes[variable_to_column_name(variable)] = value
|
|
78
|
+
case model.PrintEvent(text):
|
|
79
|
+
current_line.output = text
|
|
80
|
+
|
|
81
|
+
# The extra spaces so the names cannot collide with variable names
|
|
82
|
+
LINE_NO, OUTPUT = " ligne", "affichage "
|
|
83
|
+
|
|
84
|
+
variable_list = collect_variable_list(trace)
|
|
85
|
+
headers = [LINE_NO] + [variable_to_column_name(v) for v in variable_list] + [OUTPUT]
|
|
86
|
+
table = []
|
|
87
|
+
for line in lines:
|
|
88
|
+
table_row = [line.line_no]
|
|
89
|
+
for column_name in headers[1:-1]:
|
|
90
|
+
variable_value = line.variable_changes.get(column_name)
|
|
91
|
+
if variable_value:
|
|
92
|
+
table_row.append(variable_value)
|
|
93
|
+
else:
|
|
94
|
+
table_row.append(None)
|
|
95
|
+
table_row.append(line.output)
|
|
96
|
+
table.append(table_row)
|
|
97
|
+
|
|
98
|
+
print(tabulate(table, headers=headers, tablefmt="outline"))
|
atrace/vartracer.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import inspect
|
|
3
|
+
from types import FrameType, ModuleType
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from atrace import model
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def ignore_variable(name: str, value: Any):
|
|
10
|
+
return name.startswith("__") or callable(value) or isinstance(value, ModuleType)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def filtered_variables(variables: dict[str, Any]) -> dict[str, Any]:
|
|
14
|
+
return {
|
|
15
|
+
name: value
|
|
16
|
+
for name, value in variables.items()
|
|
17
|
+
if not ignore_variable(name, value)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def copy_carefully(d: dict[str, Any]):
|
|
22
|
+
res = {}
|
|
23
|
+
for k, v in d.items():
|
|
24
|
+
try:
|
|
25
|
+
v_copy = copy.deepcopy(v)
|
|
26
|
+
except Exception:
|
|
27
|
+
v_copy = v
|
|
28
|
+
res[k] = v_copy
|
|
29
|
+
return res
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class VarTracer:
|
|
33
|
+
def __init__(self, trace: model.Trace, module_of_interest: ModuleType):
|
|
34
|
+
self.trace = trace
|
|
35
|
+
self.module_of_interest = module_of_interest
|
|
36
|
+
self.last_locals = {}
|
|
37
|
+
|
|
38
|
+
def trace_vars(self, frame: FrameType, event: str, arg: Any):
|
|
39
|
+
if inspect.getmodule(frame) != self.module_of_interest:
|
|
40
|
+
return
|
|
41
|
+
if event == "return":
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
code = frame.f_code
|
|
45
|
+
|
|
46
|
+
if code.co_name not in self.last_locals:
|
|
47
|
+
old_locals = {}
|
|
48
|
+
else:
|
|
49
|
+
old_locals = self.last_locals[code.co_name]
|
|
50
|
+
|
|
51
|
+
locals_now = copy_carefully(filtered_variables(frame.f_locals))
|
|
52
|
+
|
|
53
|
+
for var, new_val in filtered_variables(locals_now).items():
|
|
54
|
+
if var not in old_locals or old_locals[var] != new_val:
|
|
55
|
+
self.trace.append(
|
|
56
|
+
model.TraceItem(
|
|
57
|
+
line_no=frame.f_lineno,
|
|
58
|
+
function_name=frame.f_code.co_name,
|
|
59
|
+
event=model.VariableChangeEvent(
|
|
60
|
+
variable=model.Variable(
|
|
61
|
+
function_name=code.co_name, name=var
|
|
62
|
+
),
|
|
63
|
+
value=new_val,
|
|
64
|
+
),
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
self.last_locals[code.co_name] = locals_now
|
|
69
|
+
return self.trace_vars
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: atrace
|
|
3
|
+
Version: 0.1.5
|
|
4
|
+
Summary: Generate trace tables for programs
|
|
5
|
+
Project-URL: Repository, https://github.com/nwolff/atrace.git
|
|
6
|
+
Author-email: Nicholas Wolff <nwolff@gmail.com>
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: tabulate>=0.9.0
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# Usage
|
|
12
|
+
|
|
13
|
+
Automatically prints a trace table of a program once the execution is finished.
|
|
14
|
+
|
|
15
|
+
Just import the module.
|
|
16
|
+
|
|
17
|
+
An animated example of a trace table: https://www.101computing.net/using-trace-tables/
|
|
18
|
+
|
|
19
|
+
An idea of how things look:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
+-----------+-----+-----+---------+---------------+-----------------------+-------------------------+
|
|
23
|
+
| ligne | x | y | (f) a | (f) b | (f) c | affichage |
|
|
24
|
+
+===========+=====+=====+=========+===============+=======================+=========================+
|
|
25
|
+
| 5 | 1 | 3 | | | | |
|
|
26
|
+
| 7 | 2 | | | | | |
|
|
27
|
+
| 5 | | 2 | | | | |
|
|
28
|
+
| 7 | 3 | | | | | |
|
|
29
|
+
| 5 | | 1 | | | | |
|
|
30
|
+
| 7 | 4 | | | | | |
|
|
31
|
+
| 5 | | | | | | |
|
|
32
|
+
| 9 | | | | | | somme: 4 |
|
|
33
|
+
| 12 | | | Bonjour | tout le monde | | |
|
|
34
|
+
| 14 | | | | | Bonjour tout le monde | Bonjour tout le monde ! |
|
|
35
|
+
+-----------+-----+-----+---------+---------------+-----------------------+-------------------------+
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Does not work with :
|
|
39
|
+
|
|
40
|
+
- Multithreaded programs
|
|
41
|
+
- Multi-module programs
|
|
42
|
+
- Classes
|
|
43
|
+
|
|
44
|
+
# TODO
|
|
45
|
+
|
|
46
|
+
- Fix line numbers in trace_vars. https://discuss.python.org/t/trace-a-line-after-the-line-but-not-only-before-the-line/89475/7
|
|
47
|
+
This is a bitch
|
|
48
|
+
- Thonny, which adds a lot of indirection and magic
|
|
49
|
+
|
|
50
|
+
# Later
|
|
51
|
+
|
|
52
|
+
- Make robust. In other words should never raise an exception.
|
|
53
|
+
- mypy, unit-tests. Integrate in build pipeline
|
|
54
|
+
- localize the names of the line and output columns in the report
|
|
55
|
+
- Handle classes better
|
|
56
|
+
|
|
57
|
+
# Possible enhancements
|
|
58
|
+
|
|
59
|
+
- More details when recursive invocations
|
|
60
|
+
- Think about how to show returns
|
|
61
|
+
- Find if there could be a good use for colors in the trace
|
|
62
|
+
|
|
63
|
+
# Done
|
|
64
|
+
|
|
65
|
+
- Sets the program print to stdout unhindered (this is important for input to work properly),
|
|
66
|
+
but captures the prints at the same time to show in the trace at the end.
|
|
67
|
+
- Emits the trace at the end if the application ends normally and abruptly (exception, signal, etc.)
|
|
68
|
+
- Shows bindings to local variables when entering a function
|
|
69
|
+
- Handles mutations to objects like lists (by copying the previous version and then comparing)
|
|
70
|
+
- Parallel assignations show up properly
|
|
71
|
+
|
|
72
|
+
# Build
|
|
73
|
+
|
|
74
|
+
Automatically deployed to pypi every time a new tag is pushed: https://pypi.org/project/atrace/
|
|
75
|
+
|
|
76
|
+
# Inspiration
|
|
77
|
+
|
|
78
|
+
https://github.com/DarshanLakshman/PyTracerTool/blob/master/PyTracerTool/pytracertool.py
|
|
79
|
+
|
|
80
|
+
Does almost everything I want, but has many flaws:
|
|
81
|
+
|
|
82
|
+
- it chokes trying to deepcopy some objects
|
|
83
|
+
- it cannot be simply imported into a module
|
|
84
|
+
- it fumbles the handling of output (preventing programs with input from working properly).
|
|
85
|
+
- it repeats values for no good reason
|
|
86
|
+
- its line numbers are very confused
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
atrace/__init__.py,sha256=MdubXhWJW6S5OqDrA3-YYOjl91EomeoCmBC4MwInPmQ,1920
|
|
2
|
+
atrace/model.py,sha256=uNX2rjYOZYIATY_ZvXFYiD5FhLeQ02Bqy8ExnRFPiK8,441
|
|
3
|
+
atrace/outputlogger.py,sha256=ct1jlUlK03ZI5hy45aysdb2I9UDWPLK4hxRJ_aU83Xk,901
|
|
4
|
+
atrace/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
atrace/report.py,sha256=6p19dK3YWI8mn-HPiGRp55UE7ddJg7TjGgw1b2S0CCA,3350
|
|
6
|
+
atrace/vartracer.py,sha256=O3FPZhWl2nHcmtQHu1HECwP1ZF8X9XGNuk9xTe7Y1Og,2041
|
|
7
|
+
atrace-0.1.5.dist-info/METADATA,sha256=AtkCObDbIF95triDjCOqba1Xx93zWm7Wqb9MhKuh2Qc,3628
|
|
8
|
+
atrace-0.1.5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
9
|
+
atrace-0.1.5.dist-info/RECORD,,
|