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.
- explr-0.1.0/.github/workflows/publish.yml +55 -0
- explr-0.1.0/.gitignore +7 -0
- explr-0.1.0/LICENSE +21 -0
- explr-0.1.0/MANIFEST.in +5 -0
- explr-0.1.0/PKG-INFO +243 -0
- explr-0.1.0/README.md +218 -0
- explr-0.1.0/explr/__init__.py +59 -0
- explr-0.1.0/explr/cli.py +196 -0
- explr-0.1.0/explr/models.py +66 -0
- explr-0.1.0/explr/renderer.py +270 -0
- explr-0.1.0/explr/tracer.py +291 -0
- explr-0.1.0/explr.egg-info/PKG-INFO +243 -0
- explr-0.1.0/explr.egg-info/SOURCES.txt +18 -0
- explr-0.1.0/explr.egg-info/dependency_links.txt +1 -0
- explr-0.1.0/explr.egg-info/entry_points.txt +2 -0
- explr-0.1.0/explr.egg-info/requires.txt +1 -0
- explr-0.1.0/explr.egg-info/top_level.txt +1 -0
- explr-0.1.0/pyproject.toml +41 -0
- explr-0.1.0/requirements.txt +14 -0
- explr-0.1.0/setup.cfg +4 -0
|
@@ -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
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.
|
explr-0.1.0/MANIFEST.in
ADDED
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
|
+

|
|
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
|
+

|
|
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
|