pyprobe-plots 0.0.4__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 (113) hide show
  1. pyprobe_plots-0.0.4/PKG-INFO +31 -0
  2. pyprobe_plots-0.0.4/pyprobe/__init__.py +3 -0
  3. pyprobe_plots-0.0.4/pyprobe/__main__.py +121 -0
  4. pyprobe_plots-0.0.4/pyprobe/analysis/__init__.py +4 -0
  5. pyprobe_plots-0.0.4/pyprobe/analysis/anchor_mapper.py +220 -0
  6. pyprobe_plots-0.0.4/pyprobe/analysis/ast_locator.py +265 -0
  7. pyprobe_plots-0.0.4/pyprobe/analysis/tests/__init__.py +1 -0
  8. pyprobe_plots-0.0.4/pyprobe/analysis/tests/test_ast_locator.py +50 -0
  9. pyprobe_plots-0.0.4/pyprobe/core/__init__.py +1 -0
  10. pyprobe_plots-0.0.4/pyprobe/core/anchor.py +49 -0
  11. pyprobe_plots-0.0.4/pyprobe/core/anchor_matcher.py +71 -0
  12. pyprobe_plots-0.0.4/pyprobe/core/capture_manager.py +175 -0
  13. pyprobe_plots-0.0.4/pyprobe/core/capture_record.py +44 -0
  14. pyprobe_plots-0.0.4/pyprobe/core/data_classifier.py +328 -0
  15. pyprobe_plots-0.0.4/pyprobe/core/probe_persistence.py +184 -0
  16. pyprobe_plots-0.0.4/pyprobe/core/runner.py +391 -0
  17. pyprobe_plots-0.0.4/pyprobe/core/sequence.py +18 -0
  18. pyprobe_plots-0.0.4/pyprobe/core/settings.py +48 -0
  19. pyprobe_plots-0.0.4/pyprobe/core/tracer.py +386 -0
  20. pyprobe_plots-0.0.4/pyprobe/gui/__init__.py +1 -0
  21. pyprobe_plots-0.0.4/pyprobe/gui/animations.py +72 -0
  22. pyprobe_plots-0.0.4/pyprobe/gui/app.py +70 -0
  23. pyprobe_plots-0.0.4/pyprobe/gui/axis_editor.py +116 -0
  24. pyprobe_plots-0.0.4/pyprobe/gui/code_gutter.py +209 -0
  25. pyprobe_plots-0.0.4/pyprobe/gui/code_highlighter.py +309 -0
  26. pyprobe_plots-0.0.4/pyprobe/gui/code_viewer.py +573 -0
  27. pyprobe_plots-0.0.4/pyprobe/gui/collapsible_pane.py +92 -0
  28. pyprobe_plots-0.0.4/pyprobe/gui/color_manager.py +160 -0
  29. pyprobe_plots-0.0.4/pyprobe/gui/constrained_viewbox.py +56 -0
  30. pyprobe_plots-0.0.4/pyprobe/gui/control_bar.py +214 -0
  31. pyprobe_plots-0.0.4/pyprobe/gui/debug_overlay.py +84 -0
  32. pyprobe_plots-0.0.4/pyprobe/gui/dock_bar.py +171 -0
  33. pyprobe_plots-0.0.4/pyprobe/gui/drag_helpers.py +68 -0
  34. pyprobe_plots-0.0.4/pyprobe/gui/edge_strip.py +101 -0
  35. pyprobe_plots-0.0.4/pyprobe/gui/file_tree.py +199 -0
  36. pyprobe_plots-0.0.4/pyprobe/gui/file_watcher.py +40 -0
  37. pyprobe_plots-0.0.4/pyprobe/gui/focus_manager.py +88 -0
  38. pyprobe_plots-0.0.4/pyprobe/gui/layout_manager.py +96 -0
  39. pyprobe_plots-0.0.4/pyprobe/gui/lens_dropdown.py +137 -0
  40. pyprobe_plots-0.0.4/pyprobe/gui/main_window.py +1395 -0
  41. pyprobe_plots-0.0.4/pyprobe/gui/message_handler.py +235 -0
  42. pyprobe_plots-0.0.4/pyprobe/gui/panel_container.py +332 -0
  43. pyprobe_plots-0.0.4/pyprobe/gui/plot_toolbar.py +181 -0
  44. pyprobe_plots-0.0.4/pyprobe/gui/probe_buffer.py +71 -0
  45. pyprobe_plots-0.0.4/pyprobe/gui/probe_controller.py +695 -0
  46. pyprobe_plots-0.0.4/pyprobe/gui/probe_panel.py +708 -0
  47. pyprobe_plots-0.0.4/pyprobe/gui/probe_registry.py +184 -0
  48. pyprobe_plots-0.0.4/pyprobe/gui/probe_state.py +10 -0
  49. pyprobe_plots-0.0.4/pyprobe/gui/probe_state_indicator.py +134 -0
  50. pyprobe_plots-0.0.4/pyprobe/gui/redraw_throttler.py +52 -0
  51. pyprobe_plots-0.0.4/pyprobe/gui/scalar_watch_window.py +259 -0
  52. pyprobe_plots-0.0.4/pyprobe/gui/script_runner.py +376 -0
  53. pyprobe_plots-0.0.4/pyprobe/gui/theme/__init__.py +29 -0
  54. pyprobe_plots-0.0.4/pyprobe/gui/theme/anthropic.py +318 -0
  55. pyprobe_plots-0.0.4/pyprobe/gui/theme/base.py +16 -0
  56. pyprobe_plots-0.0.4/pyprobe/gui/theme/cyberpunk.py +380 -0
  57. pyprobe_plots-0.0.4/pyprobe/gui/theme/instrument_panel.py +318 -0
  58. pyprobe_plots-0.0.4/pyprobe/gui/theme/monokai.py +314 -0
  59. pyprobe_plots-0.0.4/pyprobe/gui/theme/ocean.py +314 -0
  60. pyprobe_plots-0.0.4/pyprobe/gui/theme/theme_manager.py +67 -0
  61. pyprobe_plots-0.0.4/pyprobe/gui/watch_list.py +145 -0
  62. pyprobe_plots-0.0.4/pyprobe/ipc/__init__.py +1 -0
  63. pyprobe_plots-0.0.4/pyprobe/ipc/channels.py +227 -0
  64. pyprobe_plots-0.0.4/pyprobe/ipc/messages.py +186 -0
  65. pyprobe_plots-0.0.4/pyprobe/ipc/socket_channel.py +134 -0
  66. pyprobe_plots-0.0.4/pyprobe/ipc/socket_transport.py +125 -0
  67. pyprobe_plots-0.0.4/pyprobe/logging.py +103 -0
  68. pyprobe_plots-0.0.4/pyprobe/plots/__init__.py +1 -0
  69. pyprobe_plots-0.0.4/pyprobe/plots/axis_controller.py +86 -0
  70. pyprobe_plots-0.0.4/pyprobe/plots/base_plot.py +44 -0
  71. pyprobe_plots-0.0.4/pyprobe/plots/constellation.py +233 -0
  72. pyprobe_plots-0.0.4/pyprobe/plots/draw_mode.py +42 -0
  73. pyprobe_plots-0.0.4/pyprobe/plots/editable_axis.py +69 -0
  74. pyprobe_plots-0.0.4/pyprobe/plots/pin_indicator.py +190 -0
  75. pyprobe_plots-0.0.4/pyprobe/plots/pin_layout_mixin.py +74 -0
  76. pyprobe_plots-0.0.4/pyprobe/plots/plot_factory.py +68 -0
  77. pyprobe_plots-0.0.4/pyprobe/plots/scalar_display.py +108 -0
  78. pyprobe_plots-0.0.4/pyprobe/plots/scalar_history_chart.py +248 -0
  79. pyprobe_plots-0.0.4/pyprobe/plugins/__init__.py +5 -0
  80. pyprobe_plots-0.0.4/pyprobe/plugins/base.py +80 -0
  81. pyprobe_plots-0.0.4/pyprobe/plugins/builtins/__init__.py +33 -0
  82. pyprobe_plots-0.0.4/pyprobe/plugins/builtins/complex_plots.py +462 -0
  83. pyprobe_plots-0.0.4/pyprobe/plugins/builtins/constellation.py +283 -0
  84. pyprobe_plots-0.0.4/pyprobe/plugins/builtins/scalar_display.py +112 -0
  85. pyprobe_plots-0.0.4/pyprobe/plugins/builtins/scalar_history.py +258 -0
  86. pyprobe_plots-0.0.4/pyprobe/plugins/builtins/waveform.py +730 -0
  87. pyprobe_plots-0.0.4/pyprobe/plugins/registry.py +78 -0
  88. pyprobe_plots-0.0.4/pyprobe/state_tracer.py +443 -0
  89. pyprobe_plots-0.0.4/pyprobe_plots.egg-info/PKG-INFO +31 -0
  90. pyprobe_plots-0.0.4/pyprobe_plots.egg-info/SOURCES.txt +111 -0
  91. pyprobe_plots-0.0.4/pyprobe_plots.egg-info/dependency_links.txt +1 -0
  92. pyprobe_plots-0.0.4/pyprobe_plots.egg-info/entry_points.txt +2 -0
  93. pyprobe_plots-0.0.4/pyprobe_plots.egg-info/requires.txt +13 -0
  94. pyprobe_plots-0.0.4/pyprobe_plots.egg-info/top_level.txt +1 -0
  95. pyprobe_plots-0.0.4/pyproject.toml +60 -0
  96. pyprobe_plots-0.0.4/setup.cfg +4 -0
  97. pyprobe_plots-0.0.4/tests/test_axis_controller.py +100 -0
  98. pyprobe_plots-0.0.4/tests/test_axis_editor.py +66 -0
  99. pyprobe_plots-0.0.4/tests/test_capture_pipeline.py +179 -0
  100. pyprobe_plots-0.0.4/tests/test_cli_automation.py +109 -0
  101. pyprobe_plots-0.0.4/tests/test_code_viewer_highlight.py +137 -0
  102. pyprobe_plots-0.0.4/tests/test_color_manager.py +134 -0
  103. pyprobe_plots-0.0.4/tests/test_constellation_verify.py +152 -0
  104. pyprobe_plots-0.0.4/tests/test_dock_bar.py +66 -0
  105. pyprobe_plots-0.0.4/tests/test_e2e_capture_pipeline.py +182 -0
  106. pyprobe_plots-0.0.4/tests/test_e2e_folder_browsing.py +239 -0
  107. pyprobe_plots-0.0.4/tests/test_focus_manager.py +105 -0
  108. pyprobe_plots-0.0.4/tests/test_layout_manager.py +91 -0
  109. pyprobe_plots-0.0.4/tests/test_overlay_drag_drop_single_frame.py +207 -0
  110. pyprobe_plots-0.0.4/tests/test_overlay_drag_drop_two_frames.py +265 -0
  111. pyprobe_plots-0.0.4/tests/test_plot_toolbar.py +52 -0
  112. pyprobe_plots-0.0.4/tests/test_plugin_registry.py +103 -0
  113. pyprobe_plots-0.0.4/tests/test_signal_overlay.py +52 -0
@@ -0,0 +1,31 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyprobe-plots
3
+ Version: 0.0.4
4
+ Summary: Variable probing based debugger for Python DSP debugging
5
+ Author: PyProbe Contributors
6
+ License: MIT
7
+ Keywords: dsp,debugging,visualization,probe
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Scientific/Engineering
17
+ Classifier: Topic :: Software Development :: Debuggers
18
+ Requires-Python: >=3.12
19
+ Description-Content-Type: text/markdown
20
+ Requires-Dist: PyQt6>=6.4.0
21
+ Requires-Dist: pyqtgraph>=0.13.0
22
+ Requires-Dist: numpy>=1.24.0
23
+ Requires-Dist: pytest>=9.0.2
24
+ Requires-Dist: matplotlib>=3.10.8
25
+ Requires-Dist: scipy>=1.15.3
26
+ Requires-Dist: pytest-qt>=4.5.0
27
+ Requires-Dist: pytest-xdist>=3.8.0
28
+ Requires-Dist: pytest-forked>=1.6.0
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
31
+ Requires-Dist: pytest-qt>=4.0.0; extra == "dev"
@@ -0,0 +1,3 @@
1
+ """PyProbe: Variable probing based debugger for Python DSP debugging."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,121 @@
1
+ """
2
+ PyProbe entry point.
3
+
4
+ Usage:
5
+ python -m pyprobe [script.py]
6
+ python -m pyprobe --loglevel DEBUG examples/dsp_demo.py
7
+ python -m pyprobe --trace-states examples/dsp_demo.py
8
+ """
9
+
10
+ import sys
11
+ import os
12
+ import argparse
13
+
14
+
15
+ def main():
16
+ """Main entry point for PyProbe."""
17
+ parser = argparse.ArgumentParser(
18
+ description="PyProbe - Variable probing based debugger for Python DSP debugging"
19
+ )
20
+ parser.add_argument(
21
+ "script",
22
+ nargs="?",
23
+ help="Python script to probe (optional, can be loaded from GUI)"
24
+ )
25
+ parser.add_argument(
26
+ "--auto-run",
27
+ action="store_true",
28
+ help="Automatically run the script after loading"
29
+ )
30
+ parser.add_argument(
31
+ "--auto-quit",
32
+ action="store_true",
33
+ help="Automatically quit the application when the script finishes"
34
+ )
35
+ parser.add_argument(
36
+ "--auto-quit-timeout",
37
+ type=float,
38
+ default=None,
39
+ help="Force quit after specified seconds, even if errors prevent normal auto-quit (default: infinite)"
40
+ )
41
+ parser.add_argument(
42
+ "-p", "--probe",
43
+ action="append",
44
+ help="Add graphical probe. Format: line:symbol:instance (e.g., 4:x:1)"
45
+ )
46
+ parser.add_argument(
47
+ "-w", "--watch",
48
+ action="append",
49
+ help="Add scalar watch. Format: line:symbol:instance (e.g., 4:x:1)"
50
+ )
51
+ parser.add_argument(
52
+ "--overlay",
53
+ action="append",
54
+ help="Overlay a signal on an existing probe. Format: target_symbol:line:symbol:instance "
55
+ "(e.g., signal_i:75:received_symbols:1 overlays received_symbols onto signal_i)"
56
+ )
57
+ parser.add_argument(
58
+ "-l", "--loglevel",
59
+ choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
60
+ default="WARNING",
61
+ help="Set logging level (default: WARNING). DEBUG writes to /tmp/pyprobe_debug.log"
62
+ )
63
+ parser.add_argument(
64
+ "--logfile",
65
+ default="/tmp/pyprobe_debug.log",
66
+ help="Log file path (default: /tmp/pyprobe_debug.log)"
67
+ )
68
+ parser.add_argument(
69
+ "--log-console",
70
+ action="store_true",
71
+ help="Also log to console (stderr)"
72
+ )
73
+
74
+ parser.add_argument(
75
+ "--trace-states",
76
+ action="store_true",
77
+ help="Enable detailed state tracing. Logs all user actions and reactions to /tmp/pyprobe_state_trace.log"
78
+ )
79
+
80
+ args = parser.parse_args()
81
+
82
+ # Initialize state tracer FIRST if requested
83
+ if args.trace_states:
84
+ from pyprobe.state_tracer import init_tracer
85
+ init_tracer(enabled=True)
86
+ print("State tracing enabled. Log: /tmp/pyprobe_state_trace.log")
87
+
88
+ # Setup logging before importing anything else
89
+ from pyprobe.logging import setup_logging
90
+ setup_logging(
91
+ level=args.loglevel,
92
+ log_file=args.logfile,
93
+ console=args.log_console
94
+ )
95
+
96
+ # Import here to avoid slow startup for --help
97
+ from pyprobe.gui.app import run_app
98
+
99
+ # Auto-detect file vs. folder
100
+ script_path = args.script
101
+ folder_path = None
102
+ if script_path and os.path.isdir(script_path):
103
+ folder_path = os.path.abspath(script_path)
104
+ script_path = None
105
+
106
+ # Run the application
107
+ sys.exit(run_app(
108
+ script_path=script_path,
109
+ folder_path=folder_path,
110
+ probes=args.probe,
111
+ watches=args.watch,
112
+ overlays=args.overlay,
113
+ auto_run=args.auto_run,
114
+ auto_quit=args.auto_quit,
115
+ auto_quit_timeout=args.auto_quit_timeout
116
+ ))
117
+
118
+
119
+ if __name__ == "__main__":
120
+ main()
121
+
@@ -0,0 +1,4 @@
1
+ """Source code analysis utilities."""
2
+ from .ast_locator import ASTLocator
3
+
4
+ __all__ = ['ASTLocator']
@@ -0,0 +1,220 @@
1
+ """Anchor mapping for preserving probe locations across file edits."""
2
+ import difflib
3
+ from dataclasses import dataclass
4
+ from typing import Dict, List, Optional, Set
5
+
6
+ from pyprobe.core.anchor import ProbeAnchor
7
+ from pyprobe.analysis.ast_locator import ASTLocator
8
+
9
+
10
+ @dataclass
11
+ class AnchorMapping:
12
+ """Result of mapping an anchor from old source to new source."""
13
+ old_anchor: ProbeAnchor
14
+ new_anchor: Optional[ProbeAnchor] # None if invalid
15
+ confidence: float # 0.0 to 1.0
16
+
17
+
18
+ class AnchorMapper:
19
+ """Maps probe anchors from old source to new source after file edits.
20
+
21
+ Mapping Strategy (4-tier confidence):
22
+ - 1.0 (Exact): Same (line, col, symbol, func) exists after line shift
23
+ - 0.7 (Near): Same (symbol, func) exists nearby (within same function)
24
+ - 0.4 (Weak): Only symbol exists anywhere in file
25
+ - 0.0 (Invalid): Cannot find symbol at all
26
+ """
27
+
28
+ def __init__(self, old_source: str, new_source: str, filepath: str):
29
+ """Initialize mapper with old and new source code.
30
+
31
+ Args:
32
+ old_source: The original source code
33
+ new_source: The modified source code
34
+ filepath: The file path (for creating new anchors)
35
+ """
36
+ self._old_source = old_source
37
+ self._new_source = new_source
38
+ self._filepath = filepath
39
+
40
+ # Parse both sources
41
+ self._old_locator = ASTLocator(old_source, filepath)
42
+ self._new_locator = ASTLocator(new_source, filepath)
43
+
44
+ # Compute line mapping from old to new
45
+ self._line_map = self._compute_line_map()
46
+
47
+ def _compute_line_map(self) -> Dict[int, int]:
48
+ """Use difflib to map old line numbers to new line numbers.
49
+
50
+ Returns:
51
+ Dict mapping old line number (1-indexed) to new line number (1-indexed).
52
+ Only includes lines that exist in both versions (unchanged lines).
53
+ """
54
+ old_lines = self._old_source.splitlines()
55
+ new_lines = self._new_source.splitlines()
56
+
57
+ matcher = difflib.SequenceMatcher(None, old_lines, new_lines)
58
+ line_map: Dict[int, int] = {}
59
+
60
+ for tag, i1, i2, j1, j2 in matcher.get_opcodes():
61
+ if tag == 'equal':
62
+ # Lines are identical - map each old line to corresponding new line
63
+ for offset in range(i2 - i1):
64
+ old_line = i1 + offset + 1 # 1-indexed
65
+ new_line = j1 + offset + 1 # 1-indexed
66
+ line_map[old_line] = new_line
67
+
68
+ return line_map
69
+
70
+ def map_anchor(self, anchor: ProbeAnchor) -> AnchorMapping:
71
+ """Map a single anchor to its new location.
72
+
73
+ Args:
74
+ anchor: The probe anchor to map
75
+
76
+ Returns:
77
+ AnchorMapping with new_anchor (or None) and confidence score
78
+ """
79
+ # Tier 1: Exact match after line shift (confidence 1.0)
80
+ exact_result = self._try_exact_match(anchor)
81
+ if exact_result is not None:
82
+ return AnchorMapping(anchor, exact_result, 1.0)
83
+
84
+ # Tier 2: Symbol in same function nearby (confidence 0.7)
85
+ func_result = self._find_symbol_in_function(anchor)
86
+ if func_result is not None:
87
+ return AnchorMapping(anchor, func_result, 0.7)
88
+
89
+ # Tier 3: Symbol exists anywhere (confidence 0.4)
90
+ anywhere_result = self._find_symbol_anywhere(anchor)
91
+ if anywhere_result is not None:
92
+ return AnchorMapping(anchor, anywhere_result, 0.4)
93
+
94
+ # Tier 4: Invalid - symbol not found (confidence 0.0)
95
+ return AnchorMapping(anchor, None, 0.0)
96
+
97
+ def _try_exact_match(self, anchor: ProbeAnchor) -> Optional[ProbeAnchor]:
98
+ """Try to find exact match at mapped line position.
99
+
100
+ Returns new anchor if the same symbol exists at the same column
101
+ on the mapped line within the same function.
102
+ """
103
+ # Check if old line maps to a new line
104
+ if anchor.line not in self._line_map:
105
+ return None
106
+
107
+ new_line = self._line_map[anchor.line]
108
+
109
+ # Check if same symbol exists at same column on new line
110
+ var_name = self._new_locator.get_var_at_cursor(new_line, anchor.col)
111
+ if var_name != anchor.symbol:
112
+ return None
113
+
114
+ # Verify same enclosing function
115
+ new_func = self._new_locator.get_enclosing_function(new_line) or ""
116
+ if new_func != anchor.func:
117
+ return None
118
+
119
+ # Exact match found
120
+ return ProbeAnchor(
121
+ file=self._filepath,
122
+ line=new_line,
123
+ col=anchor.col,
124
+ symbol=anchor.symbol,
125
+ func=new_func,
126
+ )
127
+
128
+ def _find_symbol_in_function(self, anchor: ProbeAnchor) -> Optional[ProbeAnchor]:
129
+ """Find the same symbol within the same function.
130
+
131
+ Searches for the symbol in the new source, preferring occurrences
132
+ within the same function.
133
+ """
134
+ if not anchor.func:
135
+ # No function context to search within
136
+ return None
137
+
138
+ # Find all variables in new source
139
+ new_lines = self._new_source.splitlines()
140
+ for line_num in range(1, len(new_lines) + 1):
141
+ # Check if this line is in the same function
142
+ func_name = self._new_locator.get_enclosing_function(line_num)
143
+ if func_name != anchor.func:
144
+ continue
145
+
146
+ # Check for symbol on this line
147
+ vars_on_line = self._new_locator.get_all_variables_on_line(line_num)
148
+ for var in vars_on_line:
149
+ if var.name == anchor.symbol:
150
+ return ProbeAnchor(
151
+ file=self._filepath,
152
+ line=line_num,
153
+ col=var.col_start,
154
+ symbol=anchor.symbol,
155
+ func=func_name or "",
156
+ )
157
+
158
+ return None
159
+
160
+ def _find_symbol_anywhere(self, anchor: ProbeAnchor) -> Optional[ProbeAnchor]:
161
+ """Find first occurrence of symbol anywhere in new source."""
162
+ new_lines = self._new_source.splitlines()
163
+ for line_num in range(1, len(new_lines) + 1):
164
+ vars_on_line = self._new_locator.get_all_variables_on_line(line_num)
165
+ for var in vars_on_line:
166
+ if var.name == anchor.symbol:
167
+ func_name = self._new_locator.get_enclosing_function(line_num)
168
+ return ProbeAnchor(
169
+ file=self._filepath,
170
+ line=line_num,
171
+ col=var.col_start,
172
+ symbol=anchor.symbol,
173
+ func=func_name or "",
174
+ )
175
+
176
+ return None
177
+
178
+ def map_all(self, anchors: List[ProbeAnchor]) -> List[AnchorMapping]:
179
+ """Map all anchors and return list of mappings.
180
+
181
+ Args:
182
+ anchors: List of probe anchors to map
183
+
184
+ Returns:
185
+ List of AnchorMapping results
186
+ """
187
+ return [self.map_anchor(anchor) for anchor in anchors]
188
+
189
+ def get_invalidated(self, anchors: List[ProbeAnchor]) -> Set[ProbeAnchor]:
190
+ """Return set of anchors that could not be mapped (invalid).
191
+
192
+ Args:
193
+ anchors: List of probe anchors to check
194
+
195
+ Returns:
196
+ Set of anchors with confidence 0.0 (no new_anchor)
197
+ """
198
+ invalid: Set[ProbeAnchor] = set()
199
+ for anchor in anchors:
200
+ mapping = self.map_anchor(anchor)
201
+ if mapping.new_anchor is None:
202
+ invalid.add(anchor)
203
+ return invalid
204
+
205
+ def get_valid_mappings(self, anchors: List[ProbeAnchor]) -> Dict[ProbeAnchor, ProbeAnchor]:
206
+ """Return dict of old anchor -> new anchor for valid mappings.
207
+
208
+ Args:
209
+ anchors: List of probe anchors to map
210
+
211
+ Returns:
212
+ Dict mapping old anchors to their new locations
213
+ (only includes anchors with confidence > 0.0)
214
+ """
215
+ valid: Dict[ProbeAnchor, ProbeAnchor] = {}
216
+ for anchor in anchors:
217
+ mapping = self.map_anchor(anchor)
218
+ if mapping.new_anchor is not None:
219
+ valid[mapping.old_anchor] = mapping.new_anchor
220
+ return valid
@@ -0,0 +1,265 @@
1
+ """AST-based source code analysis for variable location."""
2
+ import ast
3
+ from enum import Enum
4
+ from typing import Optional, List, Tuple, Set
5
+ from dataclasses import dataclass, field
6
+
7
+ from pyprobe.logging import get_logger, trace_print
8
+
9
+ logger = get_logger(__name__)
10
+
11
+
12
+ class SymbolType(Enum):
13
+ """Classification of symbol types for probeability determination."""
14
+ DATA_VARIABLE = "data" # LHS of assignment → PROBEABLE
15
+ FUNCTION_CALL = "call" # Function being called → NOT probeable
16
+ MODULE_REF = "module" # Module access (np.xyz) → NOT probeable
17
+ FUNCTION_DEF = "func_def" # Function name in def → NOT probeable
18
+ UNKNOWN = "unknown" # Default → NOT probeable
19
+
20
+
21
+ @dataclass
22
+ class VariableLocation:
23
+ """Location of a variable in source code."""
24
+ name: str
25
+ line: int # 1-indexed
26
+ col_start: int # 0-indexed
27
+ col_end: int # 0-indexed, exclusive
28
+ is_lhs: bool # True if assignment target
29
+ symbol_type: SymbolType = field(default=SymbolType.UNKNOWN)
30
+
31
+
32
+ class ASTLocator:
33
+ """Maps cursor positions to AST nodes and variable names.
34
+
35
+ Key features:
36
+ - Column-aware variable detection
37
+ - LHS preference for ambiguous positions (x = x + 1)
38
+ - Function scope detection
39
+ """
40
+
41
+ def __init__(self, source: str, filename: str = "<string>"):
42
+ self._source = source
43
+ self._filename = filename
44
+ self._tree: Optional[ast.AST] = None
45
+ self._variables: List[VariableLocation] = []
46
+ self._functions: List[Tuple[str, int, int]] = [] # (name, start_line, end_line)
47
+ self._parse()
48
+
49
+ def _parse(self) -> None:
50
+ """Parse source and extract variable locations."""
51
+ try:
52
+ self._tree = ast.parse(self._source, filename=self._filename)
53
+ except SyntaxError:
54
+ self._tree = None
55
+ return
56
+
57
+ self._extract_variables()
58
+ self._extract_functions()
59
+
60
+ def _extract_variables(self) -> None:
61
+ """Extract all Name nodes with their locations and symbol types."""
62
+ if self._tree is None:
63
+ return
64
+
65
+ # Collect positions for each symbol type
66
+ lhs_positions: Set[Tuple[int, int]] = set()
67
+ call_positions: Set[Tuple[int, int]] = set() # Function calls
68
+ module_positions: Set[Tuple[int, int]] = set() # Attribute base (e.g., np in np.sin)
69
+ func_def_positions: Set[Tuple[int, int]] = set() # Function/class def names
70
+
71
+ self._collect_lhs_positions_from_tree(self._tree, lhs_positions)
72
+ self._collect_special_positions(self._tree, call_positions, module_positions, func_def_positions)
73
+
74
+ # Now collect all Name nodes and classify them
75
+ for node in ast.walk(self._tree):
76
+ if isinstance(node, ast.Name):
77
+ pos = (node.lineno, node.col_offset)
78
+
79
+ # Determine symbol type (priority order matters)
80
+ if pos in lhs_positions:
81
+ symbol_type = SymbolType.DATA_VARIABLE
82
+ elif pos in func_def_positions:
83
+ symbol_type = SymbolType.FUNCTION_DEF
84
+ elif pos in call_positions:
85
+ symbol_type = SymbolType.FUNCTION_CALL
86
+ elif pos in module_positions:
87
+ symbol_type = SymbolType.MODULE_REF
88
+ else:
89
+ symbol_type = SymbolType.UNKNOWN
90
+
91
+ is_lhs = pos in lhs_positions
92
+ var_loc = VariableLocation(
93
+ name=node.id,
94
+ line=node.lineno,
95
+ col_start=node.col_offset,
96
+ col_end=node.end_col_offset or (node.col_offset + len(node.id)),
97
+ is_lhs=is_lhs,
98
+ symbol_type=symbol_type,
99
+ )
100
+ self._variables.append(var_loc)
101
+ # Debug trace for line 72 variables
102
+ if node.lineno == 72 and node.id in ('received_symbols', 'signal_i', 'signal_q'):
103
+ trace_print(f"ASTLocator: {node.id} at L{node.lineno}:C{node.col_offset} pos={pos}, is_lhs={is_lhs}, lhs_positions contains? {pos in lhs_positions}")
104
+ logger.debug(f"Classified {node.id} at L{node.lineno}:C{node.col_offset} as {symbol_type}")
105
+
106
+ def _collect_special_positions(
107
+ self,
108
+ tree: ast.AST,
109
+ call_positions: Set[Tuple[int, int]],
110
+ module_positions: Set[Tuple[int, int]],
111
+ func_def_positions: Set[Tuple[int, int]]
112
+ ) -> None:
113
+ """Collect positions of function calls, module refs, and function defs."""
114
+ for node in ast.walk(tree):
115
+ # Function definitions - the function name itself
116
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
117
+ # Note: node.name is just a string, but we need to find corresponding Name nodes
118
+ # Function defs don't have Name nodes for their own name, so we track line/col
119
+ # Actually FunctionDef.name is a str, not a Name node, so this won't be in _variables
120
+ pass
121
+
122
+ # Class definitions - similar, class name is a string attribute
123
+ if isinstance(node, ast.ClassDef):
124
+ pass
125
+
126
+ # Function calls
127
+ if isinstance(node, ast.Call):
128
+ func = node.func
129
+ # Direct call: foo()
130
+ if isinstance(func, ast.Name):
131
+ call_positions.add((func.lineno, func.col_offset))
132
+ # Method call: obj.method() - mark 'obj' as module ref
133
+ elif isinstance(func, ast.Attribute):
134
+ self._collect_attribute_base_positions(func, module_positions)
135
+
136
+ # Attribute access (not a call, just np.something)
137
+ if isinstance(node, ast.Attribute):
138
+ # If this attribute is the value of another attribute (nested: np.sin.something)
139
+ # or if it's part of a call (np.sin()), we handle module refs
140
+ self._collect_attribute_base_positions(node, module_positions)
141
+
142
+ def _collect_attribute_base_positions(
143
+ self,
144
+ node: ast.Attribute,
145
+ positions: Set[Tuple[int, int]]
146
+ ) -> None:
147
+ """Recursively collect base Name positions from attribute chains."""
148
+ val = node.value
149
+ if isinstance(val, ast.Name):
150
+ # This is the base of the attribute chain (e.g., 'np' in np.sin)
151
+ positions.add((val.lineno, val.col_offset))
152
+ elif isinstance(val, ast.Attribute):
153
+ # Nested attribute, recurse
154
+ self._collect_attribute_base_positions(val, positions)
155
+
156
+ def _collect_lhs_positions_from_tree(self, tree: ast.AST, positions: Set[Tuple[int, int]]) -> None:
157
+ """Walk tree and collect all assignment target positions."""
158
+ for node in ast.walk(tree):
159
+ if isinstance(node, ast.Assign):
160
+ for target in node.targets:
161
+ self._collect_lhs_positions(target, positions)
162
+ elif isinstance(node, ast.AnnAssign) and node.target:
163
+ self._collect_lhs_positions(node.target, positions)
164
+ elif isinstance(node, ast.AugAssign):
165
+ self._collect_lhs_positions(node.target, positions)
166
+ elif isinstance(node, (ast.For, ast.comprehension)):
167
+ self._collect_lhs_positions(node.target, positions)
168
+
169
+ def _collect_lhs_positions(self, node: ast.AST, positions: Set[Tuple[int, int]]) -> None:
170
+ """Recursively collect positions of assignment targets."""
171
+ if isinstance(node, ast.Name):
172
+ positions.add((node.lineno, node.col_offset))
173
+ elif isinstance(node, (ast.Tuple, ast.List)):
174
+ for elt in node.elts:
175
+ self._collect_lhs_positions(elt, positions)
176
+
177
+ def _extract_functions(self) -> None:
178
+ """Extract function definitions with their line ranges."""
179
+ if self._tree is None:
180
+ return
181
+
182
+ for node in ast.walk(self._tree):
183
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
184
+ end_line = node.end_lineno or node.lineno
185
+ self._functions.append((node.name, node.lineno, end_line))
186
+
187
+ def get_var_at_cursor(self, line: int, col: int) -> Optional[str]:
188
+ """Return variable name at cursor position, or None.
189
+
190
+ If multiple variables overlap (rare), prefer LHS.
191
+ """
192
+ candidates = []
193
+ for var in self._variables:
194
+ if var.line == line and var.col_start <= col < var.col_end:
195
+ candidates.append(var)
196
+
197
+ if not candidates:
198
+ return None
199
+
200
+ # Prefer LHS if ambiguous
201
+ lhs_candidates = [v for v in candidates if v.is_lhs]
202
+ if lhs_candidates:
203
+ return lhs_candidates[0].name
204
+
205
+ return candidates[0].name
206
+
207
+ def get_var_location_at_cursor(self, line: int, col: int) -> Optional[VariableLocation]:
208
+ """Return full VariableLocation at cursor, or None."""
209
+ for var in self._variables:
210
+ if var.line == line and var.col_start <= col < var.col_end:
211
+ return var
212
+ return None
213
+
214
+ def get_all_variables_on_line(self, line: int) -> List[VariableLocation]:
215
+ """Return all variables on a given line."""
216
+ return [v for v in self._variables if v.line == line]
217
+
218
+ def get_enclosing_function(self, line: int) -> Optional[str]:
219
+ """Return name of function containing this line, or None."""
220
+ for name, start, end in self._functions:
221
+ if start <= line <= end:
222
+ return name
223
+ return None
224
+
225
+ def get_nearest_variable(self, line: int, col: int) -> Optional[VariableLocation]:
226
+ """Find nearest variable to cursor position.
227
+
228
+ First checks exact match, then looks on same line within 3 chars.
229
+ """
230
+ # Exact match
231
+ exact = self.get_var_location_at_cursor(line, col)
232
+ if exact:
233
+ return exact
234
+
235
+ # Same line, within proximity
236
+ line_vars = self.get_all_variables_on_line(line)
237
+ if not line_vars:
238
+ return None
239
+
240
+ # Find closest by column distance
241
+ def distance(v: VariableLocation) -> int:
242
+ if v.col_start <= col < v.col_end:
243
+ return 0
244
+ return min(abs(col - v.col_start), abs(col - v.col_end))
245
+
246
+ closest = min(line_vars, key=distance)
247
+ if distance(closest) <= 3: # Snap within 3 chars
248
+ return closest
249
+
250
+ return None
251
+
252
+ def is_probeable(self, var_loc: VariableLocation) -> bool:
253
+ """Return True if this variable can be probed.
254
+
255
+ Only DATA_VARIABLE symbols (LHS of assignments) are probeable.
256
+ Function calls, module references, and unknown symbols are not.
257
+ """
258
+ result = var_loc.symbol_type == SymbolType.DATA_VARIABLE
259
+ logger.debug(f"is_probeable({var_loc.name}): type={var_loc.symbol_type}, result={result}")
260
+ return result
261
+
262
+ @property
263
+ def is_valid(self) -> bool:
264
+ """Return True if source was parsed successfully."""
265
+ return self._tree is not None
@@ -0,0 +1 @@
1
+ """Tests for analysis module."""