atrace 0.1.3__py3-none-any.whl → 0.1.6__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
@@ -3,27 +3,13 @@ import inspect
3
3
  import signal
4
4
  import sys
5
5
  from types import FrameType
6
- from typing import Any, Optional
6
+ from typing import Optional
7
7
 
8
- from . import model, report, tracers
8
+ from . import model, outputlogger, report, vartracer
9
9
 
10
10
  trace: model.Trace = []
11
11
 
12
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
13
  original_stdout = sys.stdout
28
14
 
29
15
 
@@ -32,29 +18,39 @@ def exit_handler():
32
18
  report.dump_report(trace)
33
19
 
34
20
 
21
+ # https://stackoverflow.com/questions/23468042/the-invocation-of-signal-handler-and-atexit-handler-in-python
35
22
  def sig_handler(_signo, _frame):
36
23
  sys.exit(0)
37
24
 
38
25
 
26
+ def get_importer_frame() -> Optional[FrameType]:
27
+ # Get the current call stack
28
+ for frame_info in inspect.stack():
29
+ # Filter out internal importlib frames and the current module's frame
30
+ filename = frame_info.filename
31
+ if not filename.startswith("<") and filename != __file__:
32
+ return frame_info.frame
33
+ return None
34
+
35
+
39
36
  def setup():
40
- # This kicks off the tracing machinery (so that the lines below work)
41
- sys.settrace(just_kicking_off)
37
+ """See https://docs.python.org/3/library/sys.html#sys.settrace for an explanation of all the convoluted things in here"""
42
38
 
43
39
  # We want to only trace the module that imports us
44
40
  importer_frame = get_importer_frame()
45
41
  if importer_frame:
46
42
  module_of_interest = inspect.getmodule(importer_frame)
47
43
  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
44
+ var_tracer = vartracer.VarTracer(trace, module_of_interest)
53
45
 
54
- # Setup tracing inside of functions
46
+ # Setup tracing inside of functions.
47
+ # Order is important. This must be called before seting the trace function for the current frame
55
48
  sys.settrace(var_tracer.trace_vars)
56
49
 
57
- sys.stdout = tracers.OutputLogger(trace=trace, stdout=sys.stdout)
50
+ # Setup tracing outside of functions
51
+ importer_frame.f_trace = var_tracer.trace_vars
52
+
53
+ sys.stdout = outputlogger.OutputLogger(trace=trace, stdout=sys.stdout)
58
54
 
59
55
  atexit.register(exit_handler)
60
56
  catchable_sigs = set(signal.Signals) - {signal.SIGKILL, signal.SIGSTOP}
@@ -62,4 +58,21 @@ def setup():
62
58
  signal.signal(sig, sig_handler)
63
59
 
64
60
 
65
- setup()
61
+ def var_trace_for_code(code: str) -> model.Trace:
62
+ trace: model.Trace = []
63
+ # We want to only trace the module that imports us
64
+ importer_frame = get_importer_frame()
65
+ if importer_frame:
66
+ module_of_interest = inspect.getmodule(importer_frame)
67
+ if module_of_interest:
68
+ var_tracer = vartracer.VarTracer(trace, module_of_interest)
69
+ try:
70
+ sys.settrace(var_tracer.trace_vars)
71
+ exec(code)
72
+ finally:
73
+ sys.settrace(None)
74
+ return trace
75
+
76
+
77
+ if "pytest" not in sys.modules:
78
+ setup()
atrace/model.py CHANGED
@@ -2,28 +2,34 @@ from dataclasses import dataclass
2
2
  from typing import Any, TypeAlias
3
3
 
4
4
 
5
- @dataclass
5
+ @dataclass(frozen=True)
6
6
  class PrintEvent:
7
7
  text: str
8
8
 
9
9
 
10
- @dataclass
10
+ @dataclass(frozen=True)
11
11
  class Variable:
12
- module: str | None
12
+ scope: str
13
13
  name: str
14
14
 
15
15
 
16
- @dataclass
16
+ @dataclass(frozen=True)
17
+ class ReturnEvent:
18
+ function_name: str
19
+ return_value: Any
20
+
21
+
22
+ @dataclass(frozen=True)
17
23
  class VariableChangeEvent:
18
24
  variable: Variable
19
25
  value: Any
20
26
 
21
27
 
22
- @dataclass
28
+ @dataclass(frozen=True)
23
29
  class TraceItem:
24
30
  line_no: int
25
31
  function_name: str
26
- event: PrintEvent | VariableChangeEvent | None
32
+ event: PrintEvent | VariableChangeEvent | ReturnEvent
27
33
 
28
34
 
29
35
  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, passing down writes and flushes to it, while 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/report.py CHANGED
@@ -1,38 +1,74 @@
1
+ from dataclasses import dataclass
2
+ from typing import Any, TypeAlias
3
+
4
+ from tabulate import tabulate
5
+
1
6
  from . import model
2
7
 
3
8
 
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
9
+ @dataclass
10
+ class Instant:
11
+ line_no: int
12
+ variable_changes: dict[model.Variable, Any]
13
+ output: str
14
+
15
+
16
+ Instants: TypeAlias = list[Instant]
12
17
 
13
18
 
14
- def coalesce_print_events(trace: model.Trace) -> model.Trace:
15
- last_print_item = None
19
+ def trace_to_instants(trace: model.Trace) -> Instants:
20
+ instants = []
21
+ instant = None
16
22
  for trace_item in trace:
23
+ if instant is None or instant.line_no != trace_item.line_no:
24
+ instant = Instant(trace_item.line_no, {}, "")
25
+ instants.append(instant)
26
+
17
27
  match trace_item.event:
18
28
  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
29
+ instant.output += text
30
+ case model.VariableChangeEvent(variable, value):
31
+ # Don't clobber an existing value a variable if it changes again on the same line
32
+ # (this happens for example in tight loops)
33
+ if variable in instant.variable_changes:
34
+ instant = Instant(trace_item.line_no, {}, "")
35
+ instants.append(instant)
36
+ instant.variable_changes[variable] = value
37
+ case model.ReturnEvent():
38
+ pass # XXX
27
39
 
28
- return [i for i in trace if i.event is not None]
40
+ return instants
29
41
 
30
42
 
31
- def dump_report(trace: model.Trace) -> None:
32
- trace = coalesce_print_events(trace)
43
+ def variable_to_column_name(var: model.Variable) -> str:
44
+ return var.name if var.scope == "<module>" else f"({var.scope}) {var.name}"
33
45
 
34
- variable_list = collect_variable_list(trace)
35
- print("vars:", variable_list)
36
46
 
37
- for trace_item in trace:
38
- print(trace_item.line_no, trace_item.event)
47
+ def formatted_table_from_instants(instants: Instants) -> str:
48
+ variables = []
49
+
50
+ for instant in instants:
51
+ for variable in instant.variable_changes:
52
+ if variable not in variables:
53
+ variables.append(variable)
54
+
55
+ LINE_NO, OUTPUT = "ligne", "affichage"
56
+
57
+ table = []
58
+ for instant in instants:
59
+ row: dict[str, Any] = {}
60
+ table.append(row)
61
+
62
+ row[LINE_NO] = instant.line_no
63
+ for variable in variables:
64
+ cell_for_variable = instant.variable_changes.get(variable, "")
65
+ row[variable_to_column_name(variable)] = cell_for_variable
66
+ row[OUTPUT] = instant.output
67
+
68
+ return tabulate(table, headers="keys", tablefmt="simple_outline")
69
+
70
+
71
+ def dump_report(trace: model.Trace) -> None:
72
+ instants = trace_to_instants(trace)
73
+ formatted_table = formatted_table_from_instants(instants)
74
+ print(formatted_table)
@@ -1,8 +1,7 @@
1
1
  import copy
2
2
  import inspect
3
- import sys
4
3
  from types import FrameType, ModuleType
5
- from typing import Any, TextIO
4
+ from typing import Any
6
5
 
7
6
  from atrace import model
8
7
 
@@ -24,7 +23,7 @@ def copy_carefully(d: dict[str, Any]):
24
23
  for k, v in d.items():
25
24
  try:
26
25
  v_copy = copy.deepcopy(v)
27
- except:
26
+ except Exception:
28
27
  v_copy = v
29
28
  res[k] = v_copy
30
29
  return res
@@ -34,14 +33,21 @@ class VarTracer:
34
33
  def __init__(self, trace: model.Trace, module_of_interest: ModuleType):
35
34
  self.trace = trace
36
35
  self.module_of_interest = module_of_interest
37
- self.last_locals = {}
36
+ self.last_locals: dict[str, Any] = {}
38
37
 
39
38
  def trace_vars(self, frame: FrameType, event: str, arg: Any):
40
39
  if inspect.getmodule(frame) != self.module_of_interest:
41
40
  return
42
41
  if event == "return":
43
- return
44
- # print("####", event, frame.f_lineno, frame.f_code)
42
+ self.trace.append(
43
+ model.TraceItem(
44
+ line_no=frame.f_lineno,
45
+ function_name=frame.f_code.co_name,
46
+ event=model.ReturnEvent(
47
+ function_name=frame.f_code.co_name, return_value=arg # XXX
48
+ ),
49
+ )
50
+ )
45
51
 
46
52
  code = frame.f_code
47
53
 
@@ -51,17 +57,15 @@ class VarTracer:
51
57
  old_locals = self.last_locals[code.co_name]
52
58
 
53
59
  locals_now = copy_carefully(filtered_variables(frame.f_locals))
54
- # print("locals_now", locals_now)
55
60
 
56
61
  for var, new_val in filtered_variables(locals_now).items():
57
62
  if var not in old_locals or old_locals[var] != new_val:
58
- # print("çççç", frame.f_lineno, var)
59
63
  self.trace.append(
60
64
  model.TraceItem(
61
65
  line_no=frame.f_lineno,
62
66
  function_name=frame.f_code.co_name,
63
67
  event=model.VariableChangeEvent(
64
- variable=model.Variable(module=code.co_name, name=var),
68
+ variable=model.Variable(scope=code.co_name, name=var),
65
69
  value=new_val,
66
70
  ),
67
71
  )
@@ -69,34 +73,3 @@ class VarTracer:
69
73
 
70
74
  self.last_locals[code.co_name] = locals_now
71
75
  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,67 @@
1
+ Metadata-Version: 2.4
2
+ Name: atrace
3
+ Version: 0.1.6
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 idea of how things look:
18
+
19
+ ```
20
+
21
+ ```
22
+
23
+ Does not work with :
24
+
25
+ - Multithreaded programs
26
+ - Multi-module programs
27
+ - Classes
28
+
29
+ # TODO
30
+
31
+ - Unit tests for the capture part (because the line numbers are so hard). If pytest adds too much magic, then revert to UnitTest
32
+ - 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
33
+ This might not even be possible. Start solving it without functions in the scope.
34
+
35
+ - Handle returns (with and without return statements)
36
+
37
+ - Thonny, which adds a lot of indirection and magic. Try and find a way to edit the package directly when running in thonny
38
+
39
+ # Later
40
+
41
+ - localize the names of the line and output columns in the report
42
+
43
+ # Done
44
+
45
+ - Sets the program print to stdout unhindered (this is important for input to work properly),
46
+ but captures the prints at the same time to show in the trace at the end.
47
+ - Emits the trace at the end if the application ends normally and abruptly (exception, signal, etc.)
48
+ - Shows bindings to local variables when entering a function
49
+ - Handles mutations to objects like lists (by copying the previous version and then comparing)
50
+ - Parallel assignations show up properly
51
+ - Changes to variables in other scopes (for instance global)
52
+
53
+ # Build
54
+
55
+ Automatically deployed to pypi every time a new tag is pushed: https://pypi.org/project/atrace/
56
+
57
+ # Inspiration
58
+
59
+ https://github.com/DarshanLakshman/PyTracerTool/blob/master/PyTracerTool/pytracertool.py
60
+
61
+ Does almost everything I want, but has many flaws:
62
+
63
+ - it chokes trying to deepcopy some objects
64
+ - it cannot be simply imported into a module
65
+ - it fumbles the handling of output (preventing programs with input from working properly).
66
+ - it repeats values for no good reason
67
+ - its line numbers are very confused
@@ -0,0 +1,9 @@
1
+ atrace/__init__.py,sha256=FBVbvNBcU8agaIv62OT0PjhccY2-bhe0mkWOKT1H2fg,2406
2
+ atrace/model.py,sha256=yhBJuciqRc4X_10QuHv18y4lB2N35m39DvpIpYYtiIQ,556
3
+ atrace/outputlogger.py,sha256=gAM15WF7DOVxc6ZDBnMmutUJ7okeRoc9JUC4iqJaN9I,928
4
+ atrace/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ atrace/report.py,sha256=P6fgj-dJUZYk1wbW_Q-MATXoedXUvZ28VR3RdBjU8dA,2240
6
+ atrace/vartracer.py,sha256=bmENaHoSnh8-kdlJMaopWj5W8kPHjKC2E4k2Rx03f0k,2316
7
+ atrace-0.1.6.dist-info/METADATA,sha256=vHaImexdGhE25FbLMO7e3vN3gyskzuub90fY6RIw9ZU,2200
8
+ atrace-0.1.6.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
9
+ atrace-0.1.6.dist-info/RECORD,,
@@ -1,77 +0,0 @@
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
@@ -1,8 +0,0 @@
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,,
File without changes