atrace 0.1.3__tar.gz → 0.1.6__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.
Files changed (32) hide show
  1. {atrace-0.1.3 → atrace-0.1.6}/.github/workflows/publish-to-pypi.yml +6 -0
  2. {atrace-0.1.3 → atrace-0.1.6}/.gitignore +3 -0
  3. atrace-0.1.6/PKG-INFO +67 -0
  4. atrace-0.1.6/README.md +57 -0
  5. {atrace-0.1.3 → atrace-0.1.6}/pyproject.toml +10 -2
  6. {atrace-0.1.3 → atrace-0.1.6}/src/atrace/__init__.py +39 -26
  7. {atrace-0.1.3 → atrace-0.1.6}/src/atrace/model.py +12 -6
  8. atrace-0.1.6/src/atrace/outputlogger.py +35 -0
  9. atrace-0.1.6/src/atrace/report.py +74 -0
  10. atrace-0.1.3/src/atrace/tracers.py → atrace-0.1.6/src/atrace/vartracer.py +13 -40
  11. {atrace-0.1.3/src → atrace-0.1.6/src/examples}/complete_example.py +10 -6
  12. atrace-0.1.6/src/examples/function_example.py +9 -0
  13. atrace-0.1.6/src/examples/global_example.py +13 -0
  14. {atrace-0.1.3/inspiration → atrace-0.1.6/src/examples}/pyproject.toml +5 -3
  15. atrace-0.1.6/src/examples/small_example.py +18 -0
  16. atrace-0.1.6/src/examples/tiny_example.py +4 -0
  17. atrace-0.1.6/tests/no_test_sample.py +16 -0
  18. atrace-0.1.6/tests/test_report.py +151 -0
  19. {atrace-0.1.3 → atrace-0.1.6}/uv.lock +106 -1
  20. atrace-0.1.3/.vscode/settings.json +0 -11
  21. atrace-0.1.3/PKG-INFO +0 -77
  22. atrace-0.1.3/README.md +0 -67
  23. atrace-0.1.3/inspiration/.python-version +0 -1
  24. atrace-0.1.3/inspiration/devto_example.py +0 -56
  25. atrace-0.1.3/inspiration/pytracetool_example.py +0 -43
  26. atrace-0.1.3/inspiration/simple_program.py +0 -10
  27. atrace-0.1.3/inspiration/stack_overflow_example.py +0 -67
  28. atrace-0.1.3/inspiration/uv.lock +0 -378
  29. atrace-0.1.3/src/atrace/report.py +0 -38
  30. atrace-0.1.3/src/example_with_classes.py +0 -14
  31. atrace-0.1.3/src/small_example.py +0 -17
  32. {atrace-0.1.3 → atrace-0.1.6}/src/atrace/py.typed +0 -0
@@ -25,6 +25,12 @@ 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: Lint
29
+ run: uv run ruff check src tests
30
+ - name: Type check
31
+ run: uv run mypy src tests
32
+ - name: Run tests
33
+ run: uv run pytest
28
34
  - name: Build
29
35
  run: uv build
30
36
  - name: Publish
@@ -11,3 +11,6 @@ wheels/
11
11
 
12
12
  # Mac
13
13
  .DS_Store
14
+
15
+ # IDEs
16
+ .vscode
atrace-0.1.6/PKG-INFO ADDED
@@ -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
atrace-0.1.6/README.md ADDED
@@ -0,0 +1,57 @@
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 idea of how things look:
8
+
9
+ ```
10
+
11
+ ```
12
+
13
+ Does not work with :
14
+
15
+ - Multithreaded programs
16
+ - Multi-module programs
17
+ - Classes
18
+
19
+ # TODO
20
+
21
+ - Unit tests for the capture part (because the line numbers are so hard). If pytest adds too much magic, then revert to UnitTest
22
+ - 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
23
+ This might not even be possible. Start solving it without functions in the scope.
24
+
25
+ - Handle returns (with and without return statements)
26
+
27
+ - Thonny, which adds a lot of indirection and magic. Try and find a way to edit the package directly when running in thonny
28
+
29
+ # Later
30
+
31
+ - localize the names of the line and output columns in the report
32
+
33
+ # Done
34
+
35
+ - Sets the program print to stdout unhindered (this is important for input to work properly),
36
+ but captures the prints at the same time to show in the trace at the end.
37
+ - Emits the trace at the end if the application ends normally and abruptly (exception, signal, etc.)
38
+ - Shows bindings to local variables when entering a function
39
+ - Handles mutations to objects like lists (by copying the previous version and then comparing)
40
+ - Parallel assignations show up properly
41
+ - Changes to variables in other scopes (for instance global)
42
+
43
+ # Build
44
+
45
+ Automatically deployed to pypi every time a new tag is pushed: https://pypi.org/project/atrace/
46
+
47
+ # Inspiration
48
+
49
+ https://github.com/DarshanLakshman/PyTracerTool/blob/master/PyTracerTool/pytracertool.py
50
+
51
+ Does almost everything I want, but has many flaws:
52
+
53
+ - it chokes trying to deepcopy some objects
54
+ - it cannot be simply imported into a module
55
+ - it fumbles the handling of output (preventing programs with input from working properly).
56
+ - it repeats values for no good reason
57
+ - 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.6"
4
4
  description = "Generate trace tables for programs"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "Nicholas Wolff", email = "nwolff@gmail.com" }]
@@ -15,4 +15,12 @@ requires = ["hatchling"]
15
15
  build-backend = "hatchling.build"
16
16
 
17
17
  [dependency-groups]
18
- dev = ["mypy>=1.19.1", "ruff>=0.14.14"]
18
+ dev = [
19
+ "mypy>=1.19.1",
20
+ "pytest>=9.0.2",
21
+ "ruff>=0.14.14",
22
+ "types-tabulate>=0.9.0.20241207",
23
+ ]
24
+
25
+ [tool.uv.workspace]
26
+ members = ["src/examples"]
@@ -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()
@@ -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]
@@ -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()
@@ -0,0 +1,74 @@
1
+ from dataclasses import dataclass
2
+ from typing import Any, TypeAlias
3
+
4
+ from tabulate import tabulate
5
+
6
+ from . import model
7
+
8
+
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]
17
+
18
+
19
+ def trace_to_instants(trace: model.Trace) -> Instants:
20
+ instants = []
21
+ instant = None
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
+
27
+ match trace_item.event:
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
41
+
42
+
43
+ def variable_to_column_name(var: model.Variable) -> str:
44
+ return var.name if var.scope == "<module>" else f"({var.scope}) {var.name}"
45
+
46
+
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()
@@ -1,17 +1,21 @@
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
+ t = (1, 2)
11
+
12
+ kids = ["riri", "fifi", "loulou"]
13
+ while kids:
14
+ print(kids.pop(0))
15
+
16
+
17
+ for i in range(5):
18
+ print(i)
15
19
 
16
20
 
17
21
  def double(a):
@@ -0,0 +1,9 @@
1
+ import atrace # noqa
2
+
3
+
4
+ def f(a, b):
5
+ c = a + b
6
+ return c
7
+
8
+
9
+ print(f(3, 14))
@@ -0,0 +1,13 @@
1
+ import atrace # noqa
2
+
3
+ c = "first value of c"
4
+
5
+
6
+ def f(a, b):
7
+ global c
8
+ c = a + b
9
+ d = a - b
10
+ return c, d
11
+
12
+
13
+ print(f(3, 14))
@@ -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
+ ]
@@ -0,0 +1,18 @@
1
+ import atrace # noqa
2
+
3
+ x, y = 1, 3
4
+
5
+ while x < y:
6
+ x = x + 1
7
+
8
+ print("x:", x)
9
+
10
+ t = 1, 2
11
+
12
+
13
+ def f(a, b):
14
+ c = a + " " + b
15
+ return c + "!"
16
+
17
+
18
+ print(f("Bonjour", "tout le monde"))
@@ -0,0 +1,4 @@
1
+ import atrace # noqa
2
+
3
+ x = 1
4
+ print(x)
@@ -0,0 +1,16 @@
1
+ # import sys
2
+
3
+ import atrace
4
+
5
+ # from atrace import model, vartracer
6
+
7
+ code = """
8
+ print("hello")
9
+ x = 1
10
+ x = x + 1
11
+ y, z = 7, 8
12
+ """
13
+
14
+ trace = atrace.var_trace_for_code(code)
15
+ print(trace)
16
+ assert 1 == 0 # To show the output