methodgraph 0.1.0__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.
methodgraph/tracer.py ADDED
@@ -0,0 +1,297 @@
1
+ import functools
2
+ import inspect
3
+ import os
4
+ import sys
5
+ import threading
6
+ import atexit
7
+ from typing import Any, Callable, Dict, List, Optional, Type, Union
8
+
9
+ from .collector import ExecutionTraceCollector, safe_serialize_val
10
+ from .renderer import render_and_save, open_in_browser
11
+
12
+ _report_scheduled = False
13
+
14
+ def _generate_atexit_report(report_path: str):
15
+ global _report_scheduled
16
+ if not _report_scheduled:
17
+ return
18
+ collector = get_active_collector()
19
+ summary = collector.build_summary()
20
+ html_path = render_and_save(summary, report_path)
21
+ open_in_browser(html_path)
22
+
23
+ # Thread-local storage for managing nested call stacks per thread
24
+ _thread_local = threading.local()
25
+
26
+ def _get_call_stack() -> List[int]:
27
+ if not hasattr(_thread_local, "call_stack"):
28
+ _thread_local.call_stack = []
29
+ return _thread_local.call_stack
30
+
31
+ _global_collector: Optional[ExecutionTraceCollector] = None
32
+ _global_collector_lock = threading.Lock()
33
+
34
+ def get_active_collector() -> ExecutionTraceCollector:
35
+ global _global_collector
36
+ with _global_collector_lock:
37
+ if _global_collector is None:
38
+ _global_collector = ExecutionTraceCollector()
39
+ return _global_collector
40
+
41
+ def reset_active_collector() -> ExecutionTraceCollector:
42
+ global _global_collector
43
+ with _global_collector_lock:
44
+ _global_collector = ExecutionTraceCollector()
45
+ return _global_collector
46
+
47
+
48
+ def _capture_function_args(func: Callable, args: tuple, kwargs: dict) -> tuple:
49
+ """Parses positional args and kwargs into structured data list and dict."""
50
+ args_data = []
51
+ kwargs_data = {}
52
+
53
+ try:
54
+ sig = inspect.signature(func)
55
+ bound_args = sig.bind(*args, **kwargs)
56
+ bound_args.apply_defaults()
57
+
58
+ for name, val in bound_args.arguments.items():
59
+ param_kind = sig.parameters[name].kind if name in sig.parameters else None
60
+
61
+ if param_kind == inspect.Parameter.VAR_POSITIONAL: # *args
62
+ for idx, item in enumerate(val):
63
+ args_data.append({
64
+ "name": f"*{name}[{idx}]",
65
+ "val_info": safe_serialize_val(item)
66
+ })
67
+ elif param_kind == inspect.Parameter.VAR_KEYWORD: # **kwargs
68
+ for k, item in val.items():
69
+ kwargs_data[k] = safe_serialize_val(item)
70
+ else:
71
+ # Regular parameter or self/cls
72
+ if name in ("self", "cls"):
73
+ # Record self/cls cleanly
74
+ args_data.append({
75
+ "name": name,
76
+ "val_info": safe_serialize_val(val, max_depth=1)
77
+ })
78
+ else:
79
+ kwargs_data[name] = safe_serialize_val(val)
80
+ except Exception:
81
+ # Fallback if signature binding fails (C extensions or dynamic builtins)
82
+ for idx, item in enumerate(args):
83
+ args_data.append({
84
+ "name": f"arg_{idx}",
85
+ "val_info": safe_serialize_val(item)
86
+ })
87
+ for k, v in kwargs.items():
88
+ kwargs_data[k] = safe_serialize_val(v)
89
+
90
+ return args_data, kwargs_data
91
+
92
+
93
+ class trace:
94
+ """Decorator to trace a function or method execution."""
95
+
96
+ def __init__(self, func: Optional[Callable] = None, *, show_on_exit: bool = False, report_path: str = "methodgraph_report.html"):
97
+ self.func = func
98
+ self.show_on_exit = show_on_exit
99
+ self.report_path = report_path
100
+
101
+ if func is not None:
102
+ functools.update_wrapper(self, func)
103
+
104
+ def __call__(self, *args, **kwargs):
105
+ if self.func is None:
106
+ # Used as @trace(...) with arguments
107
+ func_to_wrap = args[0]
108
+ @functools.wraps(func_to_wrap)
109
+ def wrapper(*w_args, **w_kwargs):
110
+ return self._invoke(func_to_wrap, w_args, w_kwargs)
111
+ return wrapper
112
+ else:
113
+ # Used as @trace without parentheses
114
+ return self._invoke(self.func, args, kwargs)
115
+
116
+ def __get__(self, instance, owner):
117
+ """Supports binding to instance methods."""
118
+ if self.func is None:
119
+ return self
120
+ return functools.partial(self.__call__, instance)
121
+
122
+ def _invoke(self, func: Callable, args: tuple, kwargs: dict) -> Any:
123
+ collector = get_active_collector()
124
+ call_stack = _get_call_stack()
125
+
126
+ parent_id = call_stack[-1] if call_stack else None
127
+ depth = len(call_stack)
128
+
129
+ module = getattr(func, "__module__", "") or ""
130
+ qualname = getattr(func, "__qualname__", func.__name__)
131
+ func_name = getattr(func, "__name__", "function")
132
+
133
+ try:
134
+ file_path = inspect.getfile(func)
135
+ line_no = inspect.getsourcelines(func)[1]
136
+ except Exception:
137
+ file_path = "unknown"
138
+ line_no = 0
139
+
140
+ args_data, kwargs_data = _capture_function_args(func, args, kwargs)
141
+
142
+ record = collector.add_call(
143
+ parent_id=parent_id,
144
+ depth=depth,
145
+ module=module,
146
+ qualname=qualname,
147
+ func_name=func_name,
148
+ file_path=file_path,
149
+ line_no=line_no,
150
+ args_data=args_data,
151
+ kwargs_data=kwargs_data,
152
+ )
153
+
154
+ call_stack.append(record.call_id)
155
+
156
+ try:
157
+ result = func(*args, **kwargs)
158
+ record.complete(return_val=result)
159
+ return result
160
+ except BaseException as exc:
161
+ record.complete(exception=exc)
162
+ raise
163
+ finally:
164
+ call_stack.pop()
165
+ if self.show_on_exit:
166
+ global _report_scheduled
167
+ if not _report_scheduled:
168
+ _report_scheduled = True
169
+ atexit.register(_generate_atexit_report, self.report_path)
170
+
171
+
172
+ def trace_class(cls: Type) -> Type:
173
+ """Class decorator to apply @trace to all callable methods in a class."""
174
+ for attr_name, attr_val in list(cls.__dict__.items()):
175
+ if attr_name.startswith("__") and attr_name.endswith("__"):
176
+ continue
177
+
178
+ if inspect.isfunction(attr_val) or inspect.ismethod(attr_val):
179
+ setattr(cls, attr_name, trace(attr_val))
180
+ elif isinstance(attr_val, classmethod):
181
+ original_fn = attr_val.__func__
182
+ setattr(cls, attr_name, classmethod(trace(original_fn)))
183
+ elif isinstance(attr_val, staticmethod):
184
+ original_fn = attr_val.__func__
185
+ setattr(cls, attr_name, staticmethod(trace(original_fn)))
186
+
187
+ return cls
188
+
189
+
190
+ class TraceSession:
191
+ """Context manager for recording execution blocks."""
192
+
193
+ def __init__(self, report_path: str = "methodgraph_report.html", auto_open: bool = False):
194
+ self.report_path = report_path
195
+ self.auto_open = auto_open
196
+ self.collector: Optional[ExecutionTraceCollector] = None
197
+
198
+ def __enter__(self):
199
+ self.collector = reset_active_collector()
200
+ return self
201
+
202
+ def __exit__(self, exc_type, exc_val, exc_tb):
203
+ if self.collector:
204
+ summary = self.collector.build_summary()
205
+ saved_file = render_and_save(summary, self.report_path)
206
+ if self.auto_open:
207
+ open_in_browser(saved_file)
208
+
209
+
210
+ class SystemTracer:
211
+ """sys.settrace hook for dynamic tracing of scripts without decorators."""
212
+
213
+ def __init__(self, include_stdlib: bool = False):
214
+ self.include_stdlib = include_stdlib
215
+ self.collector = get_active_collector()
216
+ self.frame_to_record: Dict[int, Any] = {}
217
+ self.py_stdlib_dir = os.path.dirname(os.path.dirname(inspect.__file__)).lower()
218
+
219
+ def _should_trace_file(self, file_path: str) -> bool:
220
+ if not file_path or file_path.startswith("<"):
221
+ return False
222
+ fp_lower = os.path.abspath(file_path).lower()
223
+
224
+ # Prevent tracing methodgraph's own internal source files
225
+ tracer_dir = os.path.dirname(os.path.abspath(__file__)).lower()
226
+ if fp_lower.startswith(tracer_dir):
227
+ return False
228
+
229
+ if not self.include_stdlib:
230
+ if "site-packages" in fp_lower or "dist-packages" in fp_lower:
231
+ return False
232
+ if fp_lower.startswith(self.py_stdlib_dir):
233
+ return False
234
+ return True
235
+
236
+ def trace_dispatch(self, frame, event, arg):
237
+ code = frame.f_code
238
+ file_path = code.co_filename
239
+
240
+ if not self._should_trace_file(file_path):
241
+ return self.trace_dispatch
242
+
243
+ frame_id = id(frame)
244
+ call_stack = _get_call_stack()
245
+
246
+ if event == "call":
247
+ func_name = code.co_name
248
+ module = frame.f_globals.get("__name__", "")
249
+ qualname = func_name
250
+ line_no = code.co_firstlineno
251
+
252
+ # Extract local arguments from frame
253
+ args_data = []
254
+ kwargs_data = {}
255
+ argcount = code.co_argcount + code.co_kwonlyargcount
256
+ var_names = code.co_varnames
257
+
258
+ for i in range(argcount):
259
+ if i < len(var_names):
260
+ var_name = var_names[i]
261
+ val = frame.f_locals.get(var_name)
262
+ if var_name in ("self", "cls"):
263
+ args_data.append({"name": var_name, "val_info": safe_serialize_val(val, max_depth=1)})
264
+ else:
265
+ kwargs_data[var_name] = safe_serialize_val(val)
266
+
267
+ parent_id = call_stack[-1] if call_stack else None
268
+ depth = len(call_stack)
269
+
270
+ record = self.collector.add_call(
271
+ parent_id=parent_id,
272
+ depth=depth,
273
+ module=module,
274
+ qualname=qualname,
275
+ func_name=func_name,
276
+ file_path=file_path,
277
+ line_no=line_no,
278
+ args_data=args_data,
279
+ kwargs_data=kwargs_data,
280
+ )
281
+
282
+ self.frame_to_record[frame_id] = record
283
+ call_stack.append(record.call_id)
284
+
285
+ elif event == "return":
286
+ record = self.frame_to_record.pop(frame_id, None)
287
+ if record:
288
+ record.complete(return_val=arg)
289
+ if call_stack and call_stack[-1] == record.call_id:
290
+ call_stack.pop()
291
+
292
+ elif event == "exception":
293
+ record = self.frame_to_record.get(frame_id)
294
+ if record and isinstance(arg[1], BaseException):
295
+ record.complete(exception=arg[1])
296
+
297
+ return self.trace_dispatch
@@ -0,0 +1,154 @@
1
+ Metadata-Version: 2.4
2
+ Name: methodgraph
3
+ Version: 0.1.0
4
+ Summary: Graphical flow debugger for Python method calls, arguments, return values, and execution flow.
5
+ Author-email: Sagar Kariya <sbkariya99@gmail.com>
6
+ License-Expression: MIT
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Topic :: Software Development :: Debuggers
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.8
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Dynamic: license-file
16
+
17
+ # 🔍 methodgraph
18
+
19
+ **Graphical Style Flow Visual Debugger for Python Method Calls, Passed Values, and Execution Flow**
20
+
21
+ - ⚡ **Graphical Style Flow Canvas**: Interactive node-edge workflow DAG with `__start__` and `__end__` boundary capsules, smooth curved Bezier connectors, card-style nodes, pan/zoom, and minimap.
22
+ - 🎬 **Step-by-Step Playback & Time-Travel Scrubber**: Animate function execution flows with live pulsing halos, edge particle streams, and synchronized state inspection.
23
+ - 🔬 **State & Run Inspector**: Split-screen drawer featuring collapsible syntax-highlighted object trees for inputs, outputs, exceptions, execution metadata, and run history.
24
+ - 🌓 **Light & Dark Theme Toggle**: Built-in dark and light mode themes with persistent preferences and auto OS detection.
25
+ - ⏱️ **Trace Waterfall Timeline**: Execution timing visualization broken down by function call duration and concurrency spans.
26
+ - 🌳 **Interactive Call Tree**: Nested collapsible hierarchy of function invocations with inline parameter chips and return status.
27
+ - 📊 **Searchable Data Matrix**: Filter, search, and inspect argument values, types, return values, and error tracebacks.
28
+ - 🖥️ **CLI Runner & Auto Browser Launcher**: Trace scripts automatically without changing source code.
29
+
30
+ ---
31
+
32
+ ## 🚀 Quick Start
33
+
34
+ ### 1. Installation
35
+
36
+ ```bash
37
+ pip install methodgraph
38
+ ```
39
+
40
+ Or install locally in editable mode:
41
+
42
+ ```bash
43
+ git clone https://github.com/example/methodgraph.git
44
+ cd methodgraph
45
+ pip install -e .
46
+ ```
47
+
48
+ ---
49
+
50
+ ## 💡 Usage Modes
51
+
52
+ ### Option A: Function Decorator `@trace`
53
+
54
+ Trace specific functions and open the graphical visualization when executed:
55
+
56
+ ```python
57
+ from methodgraph import trace, show
58
+
59
+ @trace(show_on_exit=True)
60
+ def calculate_tax(amount, rate=0.2):
61
+ return amount * rate
62
+
63
+ @trace
64
+ def process_order(item_id, price, quantity):
65
+ tax = calculate_tax(price * quantity)
66
+ total = (price * quantity) + tax
67
+ return {"item": item_id, "total": total}
68
+
69
+ # Execute methods
70
+ process_order("ITEM-102", price=49.99, quantity=3)
71
+
72
+ # Generates 'methodgraph_report.html' and opens browser
73
+ ```
74
+
75
+ ---
76
+
77
+ ### Option B: Class Decorator `@trace_class`
78
+
79
+ Trace all methods within a class automatically:
80
+
81
+ ```python
82
+ from methodgraph import trace_class, save_report
83
+
84
+ @trace_class
85
+ class DataPipeline:
86
+ def fetch_data(self, source):
87
+ return [10, 20, 30, 40]
88
+
89
+ def transform(self, data, multiplier=2):
90
+ return [x * multiplier for x in data]
91
+
92
+ def run(self):
93
+ raw = self.fetch_data("database")
94
+ return self.transform(raw, multiplier=3)
95
+
96
+ pipeline = DataPipeline()
97
+ pipeline.run()
98
+
99
+ # Save interactive visual report
100
+ save_report("pipeline_report.html")
101
+ ```
102
+
103
+ ---
104
+
105
+ ### Option C: Context Manager `TraceSession`
106
+
107
+ Trace a specific block of code:
108
+
109
+ ```python
110
+ from methodgraph import TraceSession
111
+
112
+ with TraceSession(report_path="session_report.html", auto_open=True) as session:
113
+ data = [5, 12, 18, 24]
114
+ avg = sum(data) / len(data)
115
+ print(f"Average: {avg}")
116
+ ```
117
+
118
+ ---
119
+
120
+ ### Option D: CLI Script Tracer (`methodgraph run`)
121
+
122
+ Trace any existing Python script without modifying a single line of code!
123
+
124
+ ```bash
125
+ methodgraph run my_script.py --open
126
+ ```
127
+
128
+ Additional CLI options:
129
+ - `--open`: Open generated HTML report in browser automatically.
130
+ - `--output report.html`: Specify custom report file path.
131
+ - `--include-stdlib`: Include standard library modules in tracing (disabled by default for clean graphs).
132
+
133
+ ---
134
+
135
+ ## 🎨 Interactive Features in Graphical Presentation
136
+
137
+ 1. **Parameter Inspection**: Click any method node to view exact positional `args` and keyword `kwargs`, object types, formatted values, and line numbers.
138
+ 2. **Return & Exception Inspector**: Clear visual distinction between successful returns and unhandled exceptions (highlighted in crimson red with traceback stack).
139
+ 3. **Execution Bottleneck Finder**: Identify slowest methods visually on the Gantt timeline or graph heatmap.
140
+ 4. **Live Search**: Filter method calls in real-time by method name, argument name, or argument value substring.
141
+
142
+ ---
143
+
144
+ ## 🛠️ Requirements
145
+
146
+ - Python >= 3.8
147
+ - No heavy third-party dependencies required! Generates self-contained HTML/CSS/JS visualizers.
148
+
149
+ ---
150
+
151
+ ## 📜 License
152
+
153
+ MIT License. See [LICENSE](LICENSE) for details.
154
+
@@ -0,0 +1,13 @@
1
+ methodgraph/__init__.py,sha256=xlR9DNAITQ4pEg7WpMEoy-c03fqgspxFU7YTG_pzolc,871
2
+ methodgraph/cli.py,sha256=OvZOPxuq6v2J4JTQ78v0zDhXH_DWLCMeNkz2DI192zc,3241
3
+ methodgraph/collector.py,sha256=v8K1h_fgV2BcRnmJ89KQelUv14k9Jm0K5p_dCLplCKU,12246
4
+ methodgraph/renderer.py,sha256=r_iFicA_hHXkzC9SLyoH3fPM2OTmEAsTYKGSyAm-EOU,1539
5
+ methodgraph/server.py,sha256=wo00ZfaCvTmL2tlUvnvWAkLdxbKVH6uiDfQAJGMKIuc,1096
6
+ methodgraph/tracer.py,sha256=YhYXXRLb-Y9jzzQNGCUC9bvrvpPl0I7CVfKqfGLJTyM,10710
7
+ methodgraph/templates/report.html,sha256=WsckjiJw29Idkp2-5jZXw-AkPVABXFkfKBx7W2tbOS8,88402
8
+ methodgraph-0.1.0.dist-info/licenses/LICENSE,sha256=wfJJ1UL-WsyIlxt2aTqCOIvRIBO4Qf_OLMZkOs6kRSw,1048
9
+ methodgraph-0.1.0.dist-info/METADATA,sha256=zfWOz_aLwLlUi8dMKM1obcgcEDBQEceQ-_0NEWket4g,4978
10
+ methodgraph-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
11
+ methodgraph-0.1.0.dist-info/entry_points.txt,sha256=7x-v0Xna1UqlasiF3YgHps7MJc0HNqb0TpsOXCyHaMM,53
12
+ methodgraph-0.1.0.dist-info/top_level.txt,sha256=gi84YkdFhN2ckhnBgE6l1gY3Fv9s3rM91W1nbpcG3EM,12
13
+ methodgraph-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ methodgraph = methodgraph.cli:main
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Antigravity
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS IN Schnitt ODER OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
20
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1 @@
1
+ methodgraph