explr 0.1.0__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.
@@ -0,0 +1,55 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*.*.*"
7
+
8
+ jobs:
9
+ build:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ with:
14
+ fetch-depth: 0
15
+
16
+ - name: Fetch tags
17
+ run: git fetch --tags --force
18
+
19
+ - uses: actions/setup-python@v5
20
+ with:
21
+ python-version: "3.11"
22
+
23
+ - name: Install build tools
24
+ run: pip install build
25
+
26
+ - name: Build distribution
27
+ run: |
28
+ rm -rf dist/
29
+ python -m build
30
+ echo "--- built files ---"
31
+ ls dist/
32
+
33
+ - uses: actions/upload-artifact@v4
34
+ with:
35
+ name: dist
36
+ path: dist/
37
+
38
+ publish:
39
+ needs: build
40
+ runs-on: ubuntu-latest
41
+ environment: pypi
42
+ permissions:
43
+ id-token: write
44
+
45
+ steps:
46
+ - uses: actions/download-artifact@v4
47
+ with:
48
+ name: dist
49
+ path: dist/
50
+
51
+ - name: List files to publish
52
+ run: ls dist/
53
+
54
+ - name: Publish to PyPI
55
+ uses: pypa/gh-action-pypi-publish@release/v1
explr-0.1.0/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ explr_env
2
+ .claude
3
+ dist/
4
+ build/
5
+ *.egg-info
6
+ **/__pycache__/
7
+ *.pyc
explr-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 omavashia2005
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 BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,5 @@
1
+ include LICENSE README.md pyproject.toml
2
+ recursive-exclude explr_diagrams *
3
+ recursive-exclude test_files *
4
+ recursive-exclude __pycache__ *
5
+ global-exclude *.pyc *.pyo
explr-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,243 @@
1
+ Metadata-Version: 2.4
2
+ Name: explr
3
+ Version: 0.1.0
4
+ Summary: Trace any Python process and generate a clean call graph diagram
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/omavashia2005/explr
7
+ Project-URL: Repository, https://github.com/omavashia2005/explr
8
+ Project-URL: Bug Tracker, https://github.com/omavashia2005/explr/issues
9
+ Keywords: tracing,call-graph,debugging,visualization,profiling
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Software Development :: Debuggers
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: graphviz>=0.20
24
+ Dynamic: license-file
25
+
26
+ # explr
27
+
28
+ Trace any Python process and generate a clean call graph diagram.
29
+
30
+ Best suited for debugging small-to-medium synchronous Python programs (for now).
31
+
32
+ ![example diagram](explr_diagrams/t6_cross_module_diagram.png)
33
+
34
+
35
+ ## How it works
36
+
37
+ `explr` injects Python's `sys.settrace` at runtime, records every function call, filters out noise (stdlib, dunders, private functions), and renders a flow diagram showing how control moves through your code.
38
+
39
+ The diagram has a **horizontal spine** of entry points in execution order, with each node's sub-calls hanging below it:
40
+
41
+ ```
42
+ (S) → run → (E)
43
+ ├── auth.register → db.save_user
44
+ │ → db.get_user
45
+ ├── auth.login → db.get_user
46
+ └── report → db.all_users
47
+ ```
48
+
49
+ - **S / E** = start and end of execution
50
+ - **Green nodes** = entry points (called from top-level code), in the order they ran
51
+ - **Blue nodes** = sub-calls
52
+
53
+
54
+ ## Installation
55
+
56
+ ### Prerequisites
57
+
58
+ `explr` requires **Graphviz** to render diagrams. Install it for your OS:
59
+
60
+ | OS | Command |
61
+ |---|---|
62
+ | macOS (Homebrew) | `brew install graphviz` |
63
+ | Ubuntu / Debian | `sudo apt install graphviz` |
64
+ | Fedora / RHEL | `sudo dnf install graphviz` |
65
+ | Windows | [Download installer](https://graphviz.org/download/) — make sure `dot` is added to PATH |
66
+
67
+ ### Install explr
68
+
69
+ ```bash
70
+ pip install -e .
71
+ ```
72
+
73
+ Or with uv:
74
+
75
+ ```bash
76
+ uv pip install -e .
77
+ ```
78
+
79
+ > **Note:** Use `--no-build-isolation` if your environment already has setuptools:
80
+ > ```bash
81
+ > pip install -e . --no-build-isolation
82
+ > ```
83
+
84
+ ---
85
+
86
+ ## CLI usage
87
+
88
+ ```
89
+ explr [options] <target> [target-args ...]
90
+ ```
91
+
92
+ ### Examples
93
+
94
+ ```bash
95
+ # Trace a .py file
96
+ explr myscript.py
97
+
98
+ # Trace with the python prefix (same result)
99
+ explr python myscript.py
100
+ explr python3 myscript.py
101
+
102
+ # Pass arguments through to your script
103
+ explr myscript.py --config dev
104
+
105
+ # Trace a module-style tool (e.g. pytest, flask)
106
+ explr pytest tests/
107
+ explr python -m mypackage
108
+ ```
109
+
110
+ ### Options
111
+
112
+ | Flag | Description |
113
+ |---|---|
114
+ | `--depth N` | Limit call depth (default: unlimited) |
115
+ | `--no-stdlib` | Skip tracing stdlib frames (faster, same visual result) |
116
+ | `--output NAME` | Override output filename (no extension needed) |
117
+
118
+ ```bash
119
+ explr --depth 5 myscript.py
120
+ explr --no-stdlib myscript.py
121
+ explr --output my_graph myscript.py
122
+ ```
123
+
124
+ ### Output
125
+
126
+ Diagrams are saved to `./explr_diagrams/` in the current working directory:
127
+
128
+ ```
129
+ explr_diagrams/
130
+ myscript_diagram.png
131
+ ```
132
+
133
+
134
+
135
+ ## Python API
136
+
137
+ Trace a specific function from within your own code using `explr.trace()`. Works with both **sync and async** functions.
138
+
139
+ ```python
140
+ import explr
141
+
142
+ # Sync function
143
+ explr.trace(my_function, args=(1, 2))
144
+
145
+ # Async function — explr handles the event loop automatically
146
+ explr.trace(my_async_function, kwargs={"url": "...", "headers": {}})
147
+
148
+ # With keyword args
149
+ explr.trace(my_function, args=(x,), kwargs={"flag": True})
150
+
151
+ # All options
152
+ explr.trace(
153
+ my_function,
154
+ args=(x,),
155
+ output="my_graph", # custom output filename (no extension)
156
+ depth=5, # limit call depth
157
+ no_stdlib=True, # skip stdlib frames (faster)
158
+ )
159
+
160
+ # Returns the path to the generated PNG (or None if nothing was captured)
161
+ path = explr.trace(my_function, args=(x,))
162
+ ```
163
+
164
+ Diagrams are written to `./explr_diagrams/<func_name>_diagram.png` (or your `output` name).
165
+
166
+ `explr.trace()` runs entirely in-process using `sys.settrace` — no subprocess or temp files. Any existing trace hook is saved and restored around the call.
167
+
168
+ ### Async functions
169
+
170
+ For async functions, `explr.trace()` automatically runs the coroutine via `asyncio.run()`. Mock out any network/IO calls so the function executes without side effects:
171
+
172
+ ```python
173
+ import explr
174
+
175
+ # Mock the network call
176
+ async def fetch(url, headers):
177
+ return b"mock response"
178
+
179
+ async def my_pipeline(url: str, headers: dict):
180
+ raw = await fetch(url=url, headers=headers)
181
+ result = process(raw)
182
+ return result
183
+
184
+ explr.trace(
185
+ my_pipeline,
186
+ kwargs={"url": "https://example.com", "headers": {}},
187
+ output="my_pipeline",
188
+ no_stdlib=True,
189
+ )
190
+ ```
191
+
192
+ > **Jupyter / running event loop:** `asyncio.run()` cannot be called from inside an already-running loop. Install `nest_asyncio` to work around this:
193
+ > ```bash
194
+ > pip install nest_asyncio
195
+ > ```
196
+ > ```python
197
+ > import nest_asyncio
198
+ > nest_asyncio.apply()
199
+ > explr.trace(my_async_function, kwargs={...})
200
+ > ```
201
+
202
+ ---
203
+
204
+ ## What gets shown
205
+
206
+ | Included | Excluded |
207
+ |---|---|
208
+ | User-defined functions | stdlib functions |
209
+ | Cross-module calls | Dunder methods (`__init__`, etc.) |
210
+ | Recursive calls (self-loops) | Private functions/modules (leading `_`) |
211
+ | Class methods | Synthetic names (`<listcomp>`, `<lambda>`, etc.) |
212
+
213
+ If a function has no user-defined sub-calls, it still appears on the spine as `S → fn → E`.
214
+
215
+ ---
216
+
217
+ ## Test files
218
+
219
+ The `test_files/` directory contains examples covering common patterns:
220
+
221
+ ```bash
222
+ explr test_files/simple.py # linear call chain
223
+ explr test_files/recursive.py # recursive functions
224
+ explr test_files/classes.py # class methods
225
+ explr test_files/branching.py # conditional branches
226
+ explr test_files/multi_module/main.py # calls across multiple files
227
+ explr test_files/no_calls.py # no sub-calls (spine only)
228
+ ```
229
+
230
+ ---
231
+
232
+ ## Project structure
233
+
234
+ ```
235
+ explr/
236
+ __init__.py # explr.trace() Python API
237
+ cli.py # entry point, argument parsing, process detection
238
+ tracer.py # sys.settrace bootstrap (CLI) and in-process tracer (API)
239
+ renderer.py # graphviz diagram rendering
240
+ models.py # CallNode / CallEdge / CallGraph dataclasses
241
+ test_files/ # example scripts for testing
242
+ pyproject.toml
243
+ ```
explr-0.1.0/README.md ADDED
@@ -0,0 +1,218 @@
1
+ # explr
2
+
3
+ Trace any Python process and generate a clean call graph diagram.
4
+
5
+ Best suited for debugging small-to-medium synchronous Python programs (for now).
6
+
7
+ ![example diagram](explr_diagrams/t6_cross_module_diagram.png)
8
+
9
+
10
+ ## How it works
11
+
12
+ `explr` injects Python's `sys.settrace` at runtime, records every function call, filters out noise (stdlib, dunders, private functions), and renders a flow diagram showing how control moves through your code.
13
+
14
+ The diagram has a **horizontal spine** of entry points in execution order, with each node's sub-calls hanging below it:
15
+
16
+ ```
17
+ (S) → run → (E)
18
+ ├── auth.register → db.save_user
19
+ │ → db.get_user
20
+ ├── auth.login → db.get_user
21
+ └── report → db.all_users
22
+ ```
23
+
24
+ - **S / E** = start and end of execution
25
+ - **Green nodes** = entry points (called from top-level code), in the order they ran
26
+ - **Blue nodes** = sub-calls
27
+
28
+
29
+ ## Installation
30
+
31
+ ### Prerequisites
32
+
33
+ `explr` requires **Graphviz** to render diagrams. Install it for your OS:
34
+
35
+ | OS | Command |
36
+ |---|---|
37
+ | macOS (Homebrew) | `brew install graphviz` |
38
+ | Ubuntu / Debian | `sudo apt install graphviz` |
39
+ | Fedora / RHEL | `sudo dnf install graphviz` |
40
+ | Windows | [Download installer](https://graphviz.org/download/) — make sure `dot` is added to PATH |
41
+
42
+ ### Install explr
43
+
44
+ ```bash
45
+ pip install -e .
46
+ ```
47
+
48
+ Or with uv:
49
+
50
+ ```bash
51
+ uv pip install -e .
52
+ ```
53
+
54
+ > **Note:** Use `--no-build-isolation` if your environment already has setuptools:
55
+ > ```bash
56
+ > pip install -e . --no-build-isolation
57
+ > ```
58
+
59
+ ---
60
+
61
+ ## CLI usage
62
+
63
+ ```
64
+ explr [options] <target> [target-args ...]
65
+ ```
66
+
67
+ ### Examples
68
+
69
+ ```bash
70
+ # Trace a .py file
71
+ explr myscript.py
72
+
73
+ # Trace with the python prefix (same result)
74
+ explr python myscript.py
75
+ explr python3 myscript.py
76
+
77
+ # Pass arguments through to your script
78
+ explr myscript.py --config dev
79
+
80
+ # Trace a module-style tool (e.g. pytest, flask)
81
+ explr pytest tests/
82
+ explr python -m mypackage
83
+ ```
84
+
85
+ ### Options
86
+
87
+ | Flag | Description |
88
+ |---|---|
89
+ | `--depth N` | Limit call depth (default: unlimited) |
90
+ | `--no-stdlib` | Skip tracing stdlib frames (faster, same visual result) |
91
+ | `--output NAME` | Override output filename (no extension needed) |
92
+
93
+ ```bash
94
+ explr --depth 5 myscript.py
95
+ explr --no-stdlib myscript.py
96
+ explr --output my_graph myscript.py
97
+ ```
98
+
99
+ ### Output
100
+
101
+ Diagrams are saved to `./explr_diagrams/` in the current working directory:
102
+
103
+ ```
104
+ explr_diagrams/
105
+ myscript_diagram.png
106
+ ```
107
+
108
+
109
+
110
+ ## Python API
111
+
112
+ Trace a specific function from within your own code using `explr.trace()`. Works with both **sync and async** functions.
113
+
114
+ ```python
115
+ import explr
116
+
117
+ # Sync function
118
+ explr.trace(my_function, args=(1, 2))
119
+
120
+ # Async function — explr handles the event loop automatically
121
+ explr.trace(my_async_function, kwargs={"url": "...", "headers": {}})
122
+
123
+ # With keyword args
124
+ explr.trace(my_function, args=(x,), kwargs={"flag": True})
125
+
126
+ # All options
127
+ explr.trace(
128
+ my_function,
129
+ args=(x,),
130
+ output="my_graph", # custom output filename (no extension)
131
+ depth=5, # limit call depth
132
+ no_stdlib=True, # skip stdlib frames (faster)
133
+ )
134
+
135
+ # Returns the path to the generated PNG (or None if nothing was captured)
136
+ path = explr.trace(my_function, args=(x,))
137
+ ```
138
+
139
+ Diagrams are written to `./explr_diagrams/<func_name>_diagram.png` (or your `output` name).
140
+
141
+ `explr.trace()` runs entirely in-process using `sys.settrace` — no subprocess or temp files. Any existing trace hook is saved and restored around the call.
142
+
143
+ ### Async functions
144
+
145
+ For async functions, `explr.trace()` automatically runs the coroutine via `asyncio.run()`. Mock out any network/IO calls so the function executes without side effects:
146
+
147
+ ```python
148
+ import explr
149
+
150
+ # Mock the network call
151
+ async def fetch(url, headers):
152
+ return b"mock response"
153
+
154
+ async def my_pipeline(url: str, headers: dict):
155
+ raw = await fetch(url=url, headers=headers)
156
+ result = process(raw)
157
+ return result
158
+
159
+ explr.trace(
160
+ my_pipeline,
161
+ kwargs={"url": "https://example.com", "headers": {}},
162
+ output="my_pipeline",
163
+ no_stdlib=True,
164
+ )
165
+ ```
166
+
167
+ > **Jupyter / running event loop:** `asyncio.run()` cannot be called from inside an already-running loop. Install `nest_asyncio` to work around this:
168
+ > ```bash
169
+ > pip install nest_asyncio
170
+ > ```
171
+ > ```python
172
+ > import nest_asyncio
173
+ > nest_asyncio.apply()
174
+ > explr.trace(my_async_function, kwargs={...})
175
+ > ```
176
+
177
+ ---
178
+
179
+ ## What gets shown
180
+
181
+ | Included | Excluded |
182
+ |---|---|
183
+ | User-defined functions | stdlib functions |
184
+ | Cross-module calls | Dunder methods (`__init__`, etc.) |
185
+ | Recursive calls (self-loops) | Private functions/modules (leading `_`) |
186
+ | Class methods | Synthetic names (`<listcomp>`, `<lambda>`, etc.) |
187
+
188
+ If a function has no user-defined sub-calls, it still appears on the spine as `S → fn → E`.
189
+
190
+ ---
191
+
192
+ ## Test files
193
+
194
+ The `test_files/` directory contains examples covering common patterns:
195
+
196
+ ```bash
197
+ explr test_files/simple.py # linear call chain
198
+ explr test_files/recursive.py # recursive functions
199
+ explr test_files/classes.py # class methods
200
+ explr test_files/branching.py # conditional branches
201
+ explr test_files/multi_module/main.py # calls across multiple files
202
+ explr test_files/no_calls.py # no sub-calls (spine only)
203
+ ```
204
+
205
+ ---
206
+
207
+ ## Project structure
208
+
209
+ ```
210
+ explr/
211
+ __init__.py # explr.trace() Python API
212
+ cli.py # entry point, argument parsing, process detection
213
+ tracer.py # sys.settrace bootstrap (CLI) and in-process tracer (API)
214
+ renderer.py # graphviz diagram rendering
215
+ models.py # CallNode / CallEdge / CallGraph dataclasses
216
+ test_files/ # example scripts for testing
217
+ pyproject.toml
218
+ ```
@@ -0,0 +1,59 @@
1
+ import os
2
+ import sys
3
+ from typing import Callable, Optional
4
+
5
+ from .tracer import trace_func
6
+ from .renderer import render
7
+
8
+
9
+ def trace(
10
+ func: Callable,
11
+ args: tuple = (),
12
+ kwargs: Optional[dict] = None,
13
+ *,
14
+ output: Optional[str] = None,
15
+ depth: Optional[int] = None,
16
+ no_stdlib: bool = False,
17
+ ) -> Optional[str]:
18
+ """
19
+ Trace *func* and write a call graph diagram to ./explr_diagrams/.
20
+
21
+ Usage::
22
+
23
+ import explr
24
+
25
+ explr.trace(my_function, args=(1, 2))
26
+ explr.trace(my_function, args=(x,), kwargs={"flag": True}, output="my_graph")
27
+
28
+ Args:
29
+ func: The callable to trace.
30
+ args: Positional arguments to pass to func.
31
+ kwargs: Keyword arguments to pass to func.
32
+ output: Output filename stem (no extension). Defaults to func.__name__.
33
+ depth: Limit call depth captured (default: unlimited).
34
+ no_stdlib: Exclude stdlib calls from the diagram.
35
+
36
+ Returns:
37
+ Path to the generated PNG, or None if no calls were captured.
38
+ """
39
+ call_graph = trace_func(func, args=args, kwargs=kwargs,
40
+ max_depth=depth, no_stdlib=no_stdlib)
41
+
42
+ node_count = len(call_graph.nodes)
43
+ edge_count = len(call_graph.edges)
44
+ print(f"[explr] captured {node_count} nodes, {edge_count} edges")
45
+
46
+ if node_count == 0:
47
+ print("[explr] nothing to render – no calls were captured")
48
+ return None
49
+
50
+ out_dir = os.path.join(os.getcwd(), "explr_diagrams")
51
+ os.makedirs(out_dir, exist_ok=True)
52
+
53
+ name = output or func.__name__
54
+ out_path = os.path.join(out_dir, f"{name}_diagram.png")
55
+
56
+ _gv_extra = "/opt/homebrew/bin" if sys.platform == "darwin" else None
57
+ render(call_graph, out_path, target_name=func.__name__, _graphviz_path=_gv_extra)
58
+
59
+ return out_path