atrace 0.1.3__tar.gz → 0.1.5__tar.gz

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.
@@ -25,6 +25,8 @@ jobs:
25
25
  uses: astral-sh/setup-uv@v7
26
26
  - name: Install Python 3.13
27
27
  run: uv python install 3.13
28
+ - name: Check
29
+ run: uv run ruff check src
28
30
  - name: Build
29
31
  run: uv build
30
32
  - name: Publish
@@ -11,3 +11,6 @@ wheels/
11
11
 
12
12
  # Mac
13
13
  .DS_Store
14
+
15
+ # IDEs
16
+ .vscode
atrace-0.1.5/PKG-INFO ADDED
@@ -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
atrace-0.1.5/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # Usage
2
+
3
+ Automatically prints a trace table of a program once the execution is finished.
4
+
5
+ Just import the module.
6
+
7
+ An animated example of a trace table: https://www.101computing.net/using-trace-tables/
8
+
9
+ An idea of how things look:
10
+
11
+ ```
12
+ +-----------+-----+-----+---------+---------------+-----------------------+-------------------------+
13
+ | ligne | x | y | (f) a | (f) b | (f) c | affichage |
14
+ +===========+=====+=====+=========+===============+=======================+=========================+
15
+ | 5 | 1 | 3 | | | | |
16
+ | 7 | 2 | | | | | |
17
+ | 5 | | 2 | | | | |
18
+ | 7 | 3 | | | | | |
19
+ | 5 | | 1 | | | | |
20
+ | 7 | 4 | | | | | |
21
+ | 5 | | | | | | |
22
+ | 9 | | | | | | somme: 4 |
23
+ | 12 | | | Bonjour | tout le monde | | |
24
+ | 14 | | | | | Bonjour tout le monde | Bonjour tout le monde ! |
25
+ +-----------+-----+-----+---------+---------------+-----------------------+-------------------------+
26
+ ```
27
+
28
+ Does not work with :
29
+
30
+ - Multithreaded programs
31
+ - Multi-module programs
32
+ - Classes
33
+
34
+ # TODO
35
+
36
+ - 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
37
+ This is a bitch
38
+ - Thonny, which adds a lot of indirection and magic
39
+
40
+ # Later
41
+
42
+ - Make robust. In other words should never raise an exception.
43
+ - mypy, unit-tests. Integrate in build pipeline
44
+ - localize the names of the line and output columns in the report
45
+ - Handle classes better
46
+
47
+ # Possible enhancements
48
+
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
+ - Parallel assignations show up properly
61
+
62
+ # Build
63
+
64
+ Automatically deployed to pypi every time a new tag is pushed: https://pypi.org/project/atrace/
65
+
66
+ # Inspiration
67
+
68
+ https://github.com/DarshanLakshman/PyTracerTool/blob/master/PyTracerTool/pytracertool.py
69
+
70
+ Does almost everything I want, but has many flaws:
71
+
72
+ - it chokes trying to deepcopy some objects
73
+ - it cannot be simply imported into a module
74
+ - it fumbles the handling of output (preventing programs with input from working properly).
75
+ - it repeats values for no good reason
76
+ - its line numbers are very confused
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "atrace"
3
- version = "0.1.3"
3
+ version = "0.1.5"
4
4
  description = "Generate trace tables for programs"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "Nicholas Wolff", email = "nwolff@gmail.com" }]
@@ -16,3 +16,6 @@ build-backend = "hatchling.build"
16
16
 
17
17
  [dependency-groups]
18
18
  dev = ["mypy>=1.19.1", "ruff>=0.14.14"]
19
+
20
+ [tool.uv.workspace]
21
+ members = ["src/examples"]
@@ -5,7 +5,7 @@ import sys
5
5
  from types import FrameType
6
6
  from typing import Any, 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
 
@@ -32,11 +32,14 @@ def exit_handler():
32
32
  report.dump_report(trace)
33
33
 
34
34
 
35
+ # https://stackoverflow.com/questions/23468042/the-invocation-of-signal-handler-and-atexit-handler-in-python
35
36
  def sig_handler(_signo, _frame):
36
37
  sys.exit(0)
37
38
 
38
39
 
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
+
40
43
  # This kicks off the tracing machinery (so that the lines below work)
41
44
  sys.settrace(just_kicking_off)
42
45
 
@@ -45,16 +48,15 @@ def setup():
45
48
  if importer_frame:
46
49
  module_of_interest = inspect.getmodule(importer_frame)
47
50
  if module_of_interest:
48
- var_tracer = tracers.VarTracer(
49
- trace=trace, module_of_interest=module_of_interest
50
- )
51
+ var_tracer = vartracer.VarTracer(trace, module_of_interest)
52
+
51
53
  # Setup tracing outside of functions
52
54
  importer_frame.f_trace = var_tracer.trace_vars
53
55
 
54
56
  # Setup tracing inside of functions
55
57
  sys.settrace(var_tracer.trace_vars)
56
58
 
57
- sys.stdout = tracers.OutputLogger(trace=trace, stdout=sys.stdout)
59
+ sys.stdout = outputlogger.OutputLogger(trace=trace, stdout=sys.stdout)
58
60
 
59
61
  atexit.register(exit_handler)
60
62
  catchable_sigs = set(signal.Signals) - {signal.SIGKILL, signal.SIGSTOP}
@@ -7,13 +7,13 @@ 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
+ function_name: str | None
13
13
  name: str
14
14
 
15
15
 
16
- @dataclass
16
+ @dataclass(frozen=True)
17
17
  class VariableChangeEvent:
18
18
  variable: Variable
19
19
  value: Any
@@ -23,7 +23,7 @@ class VariableChangeEvent:
23
23
  class TraceItem:
24
24
  line_no: int
25
25
  function_name: str
26
- event: PrintEvent | VariableChangeEvent | None
26
+ event: PrintEvent | VariableChangeEvent
27
27
 
28
28
 
29
29
  Trace: TypeAlias = list[TraceItem]
@@ -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()
@@ -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"))
@@ -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
@@ -41,7 +40,6 @@ class VarTracer:
41
40
  return
42
41
  if event == "return":
43
42
  return
44
- # print("####", event, frame.f_lineno, frame.f_code)
45
43
 
46
44
  code = frame.f_code
47
45
 
@@ -51,17 +49,17 @@ class VarTracer:
51
49
  old_locals = self.last_locals[code.co_name]
52
50
 
53
51
  locals_now = copy_carefully(filtered_variables(frame.f_locals))
54
- # print("locals_now", locals_now)
55
52
 
56
53
  for var, new_val in filtered_variables(locals_now).items():
57
54
  if var not in old_locals or old_locals[var] != new_val:
58
- # print("çççç", frame.f_lineno, var)
59
55
  self.trace.append(
60
56
  model.TraceItem(
61
57
  line_no=frame.f_lineno,
62
58
  function_name=frame.f_code.co_name,
63
59
  event=model.VariableChangeEvent(
64
- variable=model.Variable(module=code.co_name, name=var),
60
+ variable=model.Variable(
61
+ function_name=code.co_name, name=var
62
+ ),
65
63
  value=new_val,
66
64
  ),
67
65
  )
@@ -69,34 +67,3 @@ class VarTracer:
69
67
 
70
68
  self.last_locals[code.co_name] = locals_now
71
69
  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()
@@ -1,17 +1,19 @@
1
- import atrace
1
+ import atrace # noqa
2
2
 
3
3
  x, y = 3, 6
4
4
 
5
5
  while x < y:
6
6
  x = x + 1
7
- y = y - 1
8
7
 
9
8
  print("x", x)
10
- print("y", x)
11
9
 
12
- l = ["riri", "fifi", "loulou"]
13
- while l:
14
- print(l.pop(0))
10
+ kids = ["riri", "fifi", "loulou"]
11
+ while kids:
12
+ print(kids.pop(0))
13
+
14
+
15
+ for i in range(5):
16
+ print(i)
15
17
 
16
18
 
17
19
  def double(a):
@@ -19,6 +21,8 @@ def double(a):
19
21
  return b
20
22
 
21
23
 
24
+ ligne = 666 # To test if it messes up the table header ligne
25
+
22
26
  print(double(6))
23
27
 
24
28
 
@@ -1,4 +1,4 @@
1
- import atrace
1
+ import atrace # noqa
2
2
 
3
3
 
4
4
  class Animal:
@@ -1,7 +1,9 @@
1
1
  [project]
2
- name = "inspiration"
2
+ name = "examples"
3
3
  version = "0.1.0"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
- requires-python = ">=3.7"
7
- dependencies = ["pytracertool>=2.0.4"]
6
+ requires-python = ">=3.10"
7
+ dependencies = [
8
+ "tabulate>=0.9.0",
9
+ ]
@@ -1,4 +1,4 @@
1
- import atrace
1
+ import atrace # noqa
2
2
 
3
3
  x, y = 1, 3
4
4
 
@@ -6,12 +6,12 @@ while y > 0:
6
6
  x = x + 1
7
7
  y = y - 1
8
8
 
9
- print("sum", x)
9
+ print("somme: ", x)
10
10
 
11
11
 
12
12
  def f(a, b):
13
13
  c = a + " " + b
14
- print(c)
14
+ print(c, "!")
15
15
 
16
16
 
17
- f("hello", "everyone")
17
+ f("Bonjour", "tout le monde")
@@ -0,0 +1,4 @@
1
+ import atrace # noqa
2
+
3
+ x = 1
4
+ print(x)
@@ -2,9 +2,15 @@ version = 1
2
2
  revision = 2
3
3
  requires-python = ">=3.10"
4
4
 
5
+ [manifest]
6
+ members = [
7
+ "atrace",
8
+ "examples",
9
+ ]
10
+
5
11
  [[package]]
6
12
  name = "atrace"
7
- version = "0.1.2"
13
+ version = "0.1.5"
8
14
  source = { editable = "." }
9
15
  dependencies = [
10
16
  { name = "tabulate" },
@@ -25,6 +31,17 @@ dev = [
25
31
  { name = "ruff", specifier = ">=0.14.14" },
26
32
  ]
27
33
 
34
+ [[package]]
35
+ name = "examples"
36
+ version = "0.1.0"
37
+ source = { virtual = "src/examples" }
38
+ dependencies = [
39
+ { name = "tabulate" },
40
+ ]
41
+
42
+ [package.metadata]
43
+ requires-dist = [{ name = "tabulate", specifier = ">=0.9.0" }]
44
+
28
45
  [[package]]
29
46
  name = "librt"
30
47
  version = "0.7.8"
@@ -1,11 +0,0 @@
1
- {
2
- "spellright.language": [
3
- "fr",
4
- "en"
5
- ],
6
- "spellright.documentTypes": [
7
- "markdown",
8
- "latex",
9
- "plaintext"
10
- ]
11
- }