atrace 0.1.1__py3-none-any.whl → 0.1.3__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 CHANGED
@@ -1,101 +1,65 @@
1
- import copy
1
+ import atexit
2
2
  import inspect
3
+ import signal
3
4
  import sys
4
5
  from types import FrameType
5
- from typing import Any
6
+ from typing import Any, Optional
6
7
 
7
- paused = True
8
- last_locals = {}
8
+ from . import model, report, tracers
9
9
 
10
- ignored_variables = set(
11
- [
12
- "tid",
13
- ]
14
- )
10
+ trace: model.Trace = []
15
11
 
16
12
 
17
- def ignore_variable(var: str):
18
- return var in ignored_variables
19
-
20
-
21
- def copy_carefully_using_ignored_variables(d: dict[str, Any]):
22
- res = {}
23
- for k, v in d.items():
24
- if not ignore_variable(k):
25
- res[k] = copy.deepcopy(v)
26
- return res
27
-
28
-
29
- def copy_carefully(d: dict[str, Any]):
30
- res = {}
31
- for k, v in d.items():
32
- try:
33
- v_copy = copy.deepcopy(v)
34
- except:
35
- v_copy = v
36
- res[k] = v_copy
37
- return res
13
+ def just_kicking_off(frame: FrameType, event: str, arg: Any):
14
+ return None
38
15
 
39
16
 
40
- def trace_vars(frame: FrameType, event: str, arg: Any):
41
- if paused:
42
- return
43
- # print(frame.f_lineno, frame.f_code.co_name, frame.f_locals, event)
44
- if event != "line":
45
- return trace_vars
46
- code = frame.f_code
47
- lineno = frame.f_lineno
48
- locals_now = copy_carefully(frame.f_locals)
49
- global last_locals
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
50
25
 
51
- if last_locals is None: # We're being unloaded, it's the end of the program
52
- return None
53
26
 
54
- if code.co_name not in last_locals:
55
- last_locals[code.co_name] = locals_now
56
- return trace_vars
27
+ original_stdout = sys.stdout
57
28
 
58
- old_locals = last_locals[code.co_name]
59
29
 
60
- for var, new_val in locals_now.items():
61
- if not ignore_variable(var):
62
- if var not in old_locals:
63
- print(f"[{lineno}] NEW {var} = {new_val}")
64
- elif old_locals[var] != new_val:
65
- print(f"[{lineno}] MODIFIED {var}: {old_locals[var]} → {new_val}")
30
+ def exit_handler():
31
+ sys.stdout = original_stdout
32
+ report.dump_report(trace)
66
33
 
67
- for var in old_locals:
68
- if not ignore_variable(var):
69
- if var not in locals_now:
70
- print(f"[{lineno}] DELETED {var}")
71
34
 
72
- last_locals[code.co_name] = locals_now
73
- return trace_vars
35
+ def sig_handler(_signo, _frame):
36
+ sys.exit(0)
74
37
 
75
38
 
76
- def just_kicking_off(frame: FrameType, event: str, arg: Any):
77
- return None
39
+ def setup():
40
+ # This kicks off the tracing machinery (so that the lines below work)
41
+ sys.settrace(just_kicking_off)
78
42
 
43
+ # We want to only trace the module that imports us
44
+ importer_frame = get_importer_frame()
45
+ if importer_frame:
46
+ module_of_interest = inspect.getmodule(importer_frame)
47
+ if module_of_interest:
48
+ var_tracer = tracers.VarTracer(
49
+ trace=trace, module_of_interest=module_of_interest
50
+ )
51
+ # Setup tracing outside of functions
52
+ importer_frame.f_trace = var_tracer.trace_vars
79
53
 
80
- def get_importer_frame():
81
- # Get the current call stack
82
- for frame_info in inspect.stack():
83
- # Filter out internal importlib frames and the current module's frame
84
- filename = frame_info.filename
85
- if not filename.startswith("<") and filename != __file__:
86
- return frame_info.frame
87
- return None
54
+ # Setup tracing inside of functions
55
+ sys.settrace(var_tracer.trace_vars)
88
56
 
57
+ sys.stdout = tracers.OutputLogger(trace=trace, stdout=sys.stdout)
89
58
 
90
- # This will work next time we enter a function.
91
- # It also kicks off the tracing machinery (so that the lines below work)
92
- sys.settrace(trace_vars)
59
+ atexit.register(exit_handler)
60
+ catchable_sigs = set(signal.Signals) - {signal.SIGKILL, signal.SIGSTOP}
61
+ for sig in catchable_sigs:
62
+ signal.signal(sig, sig_handler)
93
63
 
94
64
 
95
- # This reaches into the importing module's frame to setup tracing
96
- importer_frame = get_importer_frame()
97
- if importer_frame:
98
- importer_frame.f_trace = trace_vars
99
- paused = False
100
- else:
101
- print("Cannot trace")
65
+ 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
11
+ class Variable:
12
+ module: str | None
13
+ name: str
14
+
15
+
16
+ @dataclass
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 | None
27
+
28
+
29
+ Trace: TypeAlias = list[TraceItem]
atrace/report.py ADDED
@@ -0,0 +1,38 @@
1
+ from . import model
2
+
3
+
4
+ def collect_variable_list(trace: model.Trace) -> list[model.Variable]:
5
+ variable_list = []
6
+ for traceItem in trace:
7
+ match traceItem.event:
8
+ case model.VariableChangeEvent(variable_name, _):
9
+ if variable_name not in variable_list:
10
+ variable_list.append(variable_name)
11
+ return variable_list
12
+
13
+
14
+ def coalesce_print_events(trace: model.Trace) -> model.Trace:
15
+ last_print_item = None
16
+ for trace_item in trace:
17
+ match trace_item.event:
18
+ case model.PrintEvent(text):
19
+ if (
20
+ last_print_item is None
21
+ or trace_item.line_no != last_print_item.line_no
22
+ ):
23
+ last_print_item = trace_item
24
+ else:
25
+ last_print_item.event.text += text
26
+ trace_item.event = None # Mark for deletion
27
+
28
+ return [i for i in trace if i.event is not None]
29
+
30
+
31
+ def dump_report(trace: model.Trace) -> None:
32
+ trace = coalesce_print_events(trace)
33
+
34
+ variable_list = collect_variable_list(trace)
35
+ print("vars:", variable_list)
36
+
37
+ for trace_item in trace:
38
+ print(trace_item.line_no, trace_item.event)
atrace/tracers.py ADDED
@@ -0,0 +1,102 @@
1
+ import copy
2
+ import inspect
3
+ import sys
4
+ from types import FrameType, ModuleType
5
+ from typing import Any, TextIO
6
+
7
+ from atrace import model
8
+
9
+
10
+ def ignore_variable(name: str, value: Any):
11
+ return name.startswith("__") or callable(value) or isinstance(value, ModuleType)
12
+
13
+
14
+ def filtered_variables(variables: dict[str, Any]) -> dict[str, Any]:
15
+ return {
16
+ name: value
17
+ for name, value in variables.items()
18
+ if not ignore_variable(name, value)
19
+ }
20
+
21
+
22
+ def copy_carefully(d: dict[str, Any]):
23
+ res = {}
24
+ for k, v in d.items():
25
+ try:
26
+ v_copy = copy.deepcopy(v)
27
+ except:
28
+ v_copy = v
29
+ res[k] = v_copy
30
+ return res
31
+
32
+
33
+ class VarTracer:
34
+ def __init__(self, trace: model.Trace, module_of_interest: ModuleType):
35
+ self.trace = trace
36
+ self.module_of_interest = module_of_interest
37
+ self.last_locals = {}
38
+
39
+ def trace_vars(self, frame: FrameType, event: str, arg: Any):
40
+ if inspect.getmodule(frame) != self.module_of_interest:
41
+ return
42
+ if event == "return":
43
+ return
44
+ # print("####", event, frame.f_lineno, frame.f_code)
45
+
46
+ code = frame.f_code
47
+
48
+ if code.co_name not in self.last_locals:
49
+ old_locals = {}
50
+ else:
51
+ old_locals = self.last_locals[code.co_name]
52
+
53
+ locals_now = copy_carefully(filtered_variables(frame.f_locals))
54
+ # print("locals_now", locals_now)
55
+
56
+ for var, new_val in filtered_variables(locals_now).items():
57
+ if var not in old_locals or old_locals[var] != new_val:
58
+ # print("çççç", frame.f_lineno, var)
59
+ self.trace.append(
60
+ model.TraceItem(
61
+ line_no=frame.f_lineno,
62
+ function_name=frame.f_code.co_name,
63
+ event=model.VariableChangeEvent(
64
+ variable=model.Variable(module=code.co_name, name=var),
65
+ value=new_val,
66
+ ),
67
+ )
68
+ )
69
+
70
+ self.last_locals[code.co_name] = locals_now
71
+ return self.trace_vars
72
+
73
+
74
+ class OutputLogger(object):
75
+ """
76
+ OutputLogger captures and logs the output produced by print statements during code execution.
77
+ """
78
+
79
+ def __init__(self, trace: model.Trace, stdout: TextIO):
80
+ self.trace = trace
81
+ self.stdout = stdout
82
+
83
+ def write(self, text: str) -> None:
84
+ """
85
+ Write to stdout and record it in the trace
86
+ """
87
+ self.stdout.write(text)
88
+
89
+ frame = sys._getframe(1)
90
+ # An alternative that uses a public API, but then the type checker bothers me
91
+ # frame = inspect.currentframe().f_back
92
+
93
+ self.trace.append(
94
+ model.TraceItem(
95
+ line_no=frame.f_lineno,
96
+ function_name=frame.f_code.co_name,
97
+ event=model.PrintEvent(text),
98
+ )
99
+ )
100
+
101
+ def flush(self):
102
+ self.stdout.flush()
@@ -0,0 +1,77 @@
1
+ Metadata-Version: 2.4
2
+ Name: atrace
3
+ Version: 0.1.3
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
+ | Line | (double)a | (double)b | (main)l | (main)x | (main)y | OUTPUT |
24
+ +========+=============+=============+============================+===========+===========+==========+
25
+ | 2 | | | | 0 | | |
26
+ +--------+-------------+-------------+----------------------------+-----------+-----------+----------+
27
+ | 3 | | | | 0 | 10 | |
28
+ +--------+-------------+-------------+----------------------------+-----------+-----------+----------+
29
+ | 5 | | | | 0 | 10 | 10 |
30
+ +--------+-------------+-------------+----------------------------+-----------+-----------+----------+
31
+ ```
32
+
33
+ Does not work with :
34
+
35
+ - Multithreaded programs
36
+ - Multi-module programs
37
+
38
+ # Requirements/TODO
39
+
40
+ - Build the goddam table
41
+ - Fix line numbers in trace_vars
42
+ - Parallel assignations show up properly
43
+ - Thonny, which adds a shitload of indirection and magic
44
+
45
+ # Later
46
+
47
+ - Make robust. In other words should never raise an exception. ruff check, mypy, unit-tests.
48
+ - Handle classes better
49
+ - More details when recursive invocations
50
+ - Think about how to show returns
51
+ - Find if there could be a good use for colors in the trace
52
+
53
+ # Done
54
+
55
+ - Sets the program print to stdout unhindered (this is important for input to work properly),
56
+ but captures the prints at the same time to show in the trace at the end.
57
+ - Emits the trace at the end if the application ends normally and abruptly (exception, signal, etc.)
58
+ - Shows bindings to local variables when entering a function
59
+ - Handles mutations to objects like lists (by copying the previous version and then comparing)
60
+
61
+ # Build
62
+
63
+ Automatically deployed to pypi every time a new tag is pushed: https://pypi.org/project/atrace/
64
+
65
+ # Refs
66
+
67
+ - https://stackoverflow.com/questions/16258553/how-can-i-define-algebraic-data-types-in-python To model the events in the trace
68
+
69
+ - https://docs.python.org/3/library/sys.html#sys.settrace The python doc for settrace
70
+
71
+ - https://stackoverflow.com/questions/23468042/the-invocation-of-signal-handler-and-atexit-handler-in-python to dump the trace when the program stops, no matter how.
72
+
73
+ # Inspiration
74
+
75
+ - https://github.com/DarshanLakshman/PyTracerTool Does almost everything I want, but has flaws: it chokes trying to deepcopy some objects, it cannot be simply imported into a module.
76
+
77
+ - https://stackoverflow.com/questions/1645028/trace-table-for-python-programs Some ideas in there
@@ -0,0 +1,8 @@
1
+ atrace/__init__.py,sha256=qFhpaphaeyb40MQKk-N2AC17HsZyg8sjOCN__ukhSnY,1715
2
+ atrace/model.py,sha256=_Fd1XWH5CThBjpZJEWm7rBkxP79IcKDPB8JQq_ksa6Y,415
3
+ atrace/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ atrace/report.py,sha256=c5H8EVhyoHLygI3DVkSv96Mn_bZxPcdmHt1TcY8HFqI,1218
5
+ atrace/tracers.py,sha256=TxTxZGe6Oxn-RKoI5nDFT71kA3Aa1oGxnWMbRrsELtE,2987
6
+ atrace-0.1.3.dist-info/METADATA,sha256=iGTG8NhZ7ZGsW8WtsEZzSaZcXWJbhInL-8HIuYNAHoo,3212
7
+ atrace-0.1.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
8
+ atrace-0.1.3.dist-info/RECORD,,
@@ -1,65 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: atrace
3
- Version: 0.1.1
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.7
8
- Description-Content-Type: text/markdown
9
-
10
- # TODO:
11
-
12
- - entering a function, binding the local arguments, returning
13
- - ignore function definitions (look at the type of object)
14
-
15
- - display at end only
16
-
17
- # Usage and intent
18
-
19
- A package that automatically prints a trace table of a program, just by importing the module
20
-
21
- - Should not interfere with the running program (apart from capturing stdout)
22
- - Should display the trace at the end of execution (not while the program is interacting with the user)
23
- - Should display the trace even if an exception interrupts the program
24
- - Should display the trace even if the user interrupts the program
25
- - Should handle mutations to objects like lists
26
- - Should handle functions properly:
27
- - entering the function
28
- - binding the local arguments
29
- - returning
30
-
31
- An animated example of a trace table: https://www.101computing.net/using-trace-tables/
32
-
33
- # Not in scope
34
-
35
- - Multithreaded programs
36
-
37
- # Implementation
38
-
39
- To trace variables :
40
-
41
- - Either https://docs.python.org/3/library/sys.html#sys.settrace
42
- - Or https://docs.python.org/3/library/trace.html
43
-
44
- To capture stdout :
45
-
46
- - Either https://docs.python.org/3/library/contextlib.html#contextlib.redirect_stdout
47
- - Or just reassign stdout
48
-
49
- To trap sigint:
50
-
51
- - https://stackoverflow.com/questions/1112343/how-do-i-capture-sigint-in-python
52
-
53
- # Build
54
-
55
- Automatically deployed to pypi every time a new tag is pushed: https://pypi.org/project/atrace/
56
-
57
- # Technical Refs
58
-
59
- - First similar thing I found, doesn't work (chokes on deep copying some variables): https://github.com/DarshanLakshman/PyTracerTool
60
-
61
- - 11 years old, doesn't work: https://github.com/mihneadb/python-execution-trace
62
-
63
- - A hot mess: https://stackoverflow.com/questions/1645028/trace-table-for-python-programs
64
-
65
- - A tutorial on the trace module: https://pymotw.com/2/trace/
@@ -1,5 +0,0 @@
1
- atrace/__init__.py,sha256=wUmeppysEV7VYt8ZpVnhX9IL_Pec_50FjGqHmS5gEwc,2550
2
- atrace/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- atrace-0.1.1.dist-info/METADATA,sha256=4oknmUUp4BvNtji0hRV121JKvtkjC2BOO6pt9zPqDgI,2026
4
- atrace-0.1.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
5
- atrace-0.1.1.dist-info/RECORD,,
File without changes