atrace 0.1.5__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
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
 
@@ -37,12 +23,19 @@ def sig_handler(_signo, _frame):
37
23
  sys.exit(0)
38
24
 
39
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
+
40
36
  def setup():
41
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
- # This kicks off the tracing machinery (so that the lines below work)
44
- sys.settrace(just_kicking_off)
45
-
46
39
  # We want to only trace the module that imports us
47
40
  importer_frame = get_importer_frame()
48
41
  if importer_frame:
@@ -50,12 +43,13 @@ def setup():
50
43
  if module_of_interest:
51
44
  var_tracer = vartracer.VarTracer(trace, module_of_interest)
52
45
 
46
+ # Setup tracing inside of functions.
47
+ # Order is important. This must be called before seting the trace function for the current frame
48
+ sys.settrace(var_tracer.trace_vars)
49
+
53
50
  # Setup tracing outside of functions
54
51
  importer_frame.f_trace = var_tracer.trace_vars
55
52
 
56
- # Setup tracing inside of functions
57
- sys.settrace(var_tracer.trace_vars)
58
-
59
53
  sys.stdout = outputlogger.OutputLogger(trace=trace, stdout=sys.stdout)
60
54
 
61
55
  atexit.register(exit_handler)
@@ -64,4 +58,21 @@ def setup():
64
58
  signal.signal(sig, sig_handler)
65
59
 
66
60
 
67
- 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
10
  @dataclass(frozen=True)
11
11
  class Variable:
12
- function_name: str | None
12
+ scope: str
13
13
  name: str
14
14
 
15
15
 
16
+ @dataclass(frozen=True)
17
+ class ReturnEvent:
18
+ function_name: str
19
+ return_value: Any
20
+
21
+
16
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
32
+ event: PrintEvent | VariableChangeEvent | ReturnEvent
27
33
 
28
34
 
29
35
  Trace: TypeAlias = list[TraceItem]
atrace/outputlogger.py CHANGED
@@ -6,7 +6,7 @@ from . import model
6
6
 
7
7
  class OutputLogger:
8
8
  """
9
- OutputLogger wraps stdout, writing to it and simultaneously capturing the text to the trace.
9
+ OutputLogger wraps stdout, passing down writes and flushes to it, while simultaneously capturing the text to the trace.
10
10
  """
11
11
 
12
12
  def __init__(self, trace: model.Trace, stdout: TextIO):
atrace/report.py CHANGED
@@ -1,98 +1,74 @@
1
1
  from dataclasses import dataclass
2
- from typing import Any
2
+ from typing import Any, TypeAlias
3
3
 
4
4
  from tabulate import tabulate
5
5
 
6
6
  from . import model
7
7
 
8
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
-
9
+ @dataclass
10
+ class Instant:
11
+ line_no: int
12
+ variable_changes: dict[model.Variable, Any]
13
+ output: str
18
14
 
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
15
 
35
- return [item for item in trace if item.event is not None]
16
+ Instants: TypeAlias = list[Instant]
36
17
 
37
18
 
38
- def contains_prints(trace: model.Trace) -> bool:
19
+ def trace_to_instants(trace: model.Trace) -> Instants:
20
+ instants = []
21
+ instant = None
39
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
+
40
27
  match trace_item.event:
41
- case model.PrintEvent:
42
- return True
43
- return False
28
+ case model.PrintEvent(text):
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
39
+
40
+ return instants
44
41
 
45
42
 
46
43
  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
44
+ return var.name if var.scope == "<module>" else f"({var.scope}) {var.name}"
49
45
 
50
46
 
51
- @dataclass
52
- class Line:
53
- line_no: int
54
- variable_changes: dict[str, Any]
55
- output: str | None
47
+ def formatted_table_from_instants(instants: Instants) -> str:
48
+ variables = []
56
49
 
50
+ for instant in instants:
51
+ for variable in instant.variable_changes:
52
+ if variable not in variables:
53
+ variables.append(variable)
57
54
 
58
- def dump_report(trace: model.Trace) -> None:
59
- trace = coalesce_print_events(trace)
55
+ LINE_NO, OUTPUT = "ligne", "affichage"
60
56
 
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)
57
+ table = []
58
+ for instant in instants:
59
+ row: dict[str, Any] = {}
60
+ table.append(row)
68
61
 
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)
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
76
67
 
77
- current_line.variable_changes[variable_to_column_name(variable)] = value
78
- case model.PrintEvent(text):
79
- current_line.output = text
68
+ return tabulate(table, headers="keys", tablefmt="simple_outline")
80
69
 
81
- # The extra spaces so the names cannot collide with variable names
82
- LINE_NO, OUTPUT = " ligne", "affichage "
83
70
 
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"))
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)
atrace/vartracer.py CHANGED
@@ -33,13 +33,21 @@ class VarTracer:
33
33
  def __init__(self, trace: model.Trace, module_of_interest: ModuleType):
34
34
  self.trace = trace
35
35
  self.module_of_interest = module_of_interest
36
- self.last_locals = {}
36
+ self.last_locals: dict[str, Any] = {}
37
37
 
38
38
  def trace_vars(self, frame: FrameType, event: str, arg: Any):
39
39
  if inspect.getmodule(frame) != self.module_of_interest:
40
40
  return
41
41
  if event == "return":
42
- return
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
+ )
43
51
 
44
52
  code = frame.f_code
45
53
 
@@ -57,9 +65,7 @@ class VarTracer:
57
65
  line_no=frame.f_lineno,
58
66
  function_name=frame.f_code.co_name,
59
67
  event=model.VariableChangeEvent(
60
- variable=model.Variable(
61
- function_name=code.co_name, name=var
62
- ),
68
+ variable=model.Variable(scope=code.co_name, name=var),
63
69
  value=new_val,
64
70
  ),
65
71
  )
@@ -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,86 +0,0 @@
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
@@ -1,9 +0,0 @@
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,,
File without changes