warntrace 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.
- warntrace-0.1.0/PKG-INFO +286 -0
- warntrace-0.1.0/README.md +276 -0
- warntrace-0.1.0/pyproject.toml +48 -0
- warntrace-0.1.0/src/warntrace/__init__.py +34 -0
- warntrace-0.1.0/src/warntrace/__main__.py +5 -0
- warntrace-0.1.0/src/warntrace/_bootstrap.py +83 -0
- warntrace-0.1.0/src/warntrace/api.py +163 -0
- warntrace-0.1.0/src/warntrace/capture.py +245 -0
- warntrace-0.1.0/src/warntrace/classifier.py +151 -0
- warntrace-0.1.0/src/warntrace/cli.py +290 -0
- warntrace-0.1.0/src/warntrace/dependencies.py +306 -0
- warntrace-0.1.0/src/warntrace/models.py +134 -0
- warntrace-0.1.0/src/warntrace/ownership.py +66 -0
- warntrace-0.1.0/src/warntrace/renderer.py +270 -0
- warntrace-0.1.0/src/warntrace/report.py +114 -0
- warntrace-0.1.0/src/warntrace/utils.py +197 -0
warntrace-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: warntrace
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Find which package caused a Python warning and where your code triggered it.
|
|
5
|
+
Author: Kunal Kaul
|
|
6
|
+
Author-email: Kunal Kaul <54585925+kunalkaul@users.noreply.github.com>
|
|
7
|
+
Requires-Dist: packaging>=24
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# Warntrace
|
|
12
|
+
|
|
13
|
+
Trace the origin of Python warnings — classify them by ownership (application, direct
|
|
14
|
+
dependency, transitive, stdlib), suppress noise, and fail CI when policy is violated.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
uv add --dev warntrace
|
|
20
|
+
# or
|
|
21
|
+
pip install warntrace
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Requires Python >= 3.10.
|
|
25
|
+
|
|
26
|
+
## Quick start
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Capture warnings from any Python command
|
|
30
|
+
warntrace run python your_script.py
|
|
31
|
+
|
|
32
|
+
# Capture warnings from your test suite
|
|
33
|
+
warntrace run pytest
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Warntrace intercepts every `warnings.warn()` call in the child process, classifies each
|
|
37
|
+
warning by origin, and writes a JSON report to stdout after the child exits.
|
|
38
|
+
|
|
39
|
+
## CLI reference
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
warntrace run [options] -- <command> [args...]
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
| Flag | Default | Description |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `--format` | `json` | Output format: `json`, `detailed`, or `summary` |
|
|
48
|
+
| `--output FILE` | stdout | Write report to a file instead of stdout |
|
|
49
|
+
| `--max-frames N` | unlimited | Limit stack frames per warning in terminal output |
|
|
50
|
+
| `--show-all` | off | Capture normally-suppressed warnings (e.g. `BytesWarning`) |
|
|
51
|
+
| `--no-passthrough` | off | Suppress original warning output from the child process |
|
|
52
|
+
| `--fail-on POLICY` | off | Exit code 3 when a warning matches the policy (see below) |
|
|
53
|
+
| `--root PATH` | child's CWD | Application root directory for classification |
|
|
54
|
+
| `--no-color` | off | Disable ANSI color in terminal output |
|
|
55
|
+
|
|
56
|
+
### Examples
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# Basic — run a script and see the default JSON report
|
|
60
|
+
warntrace run python -c "import warnings; warnings.warn('hello')"
|
|
61
|
+
|
|
62
|
+
# Detailed terminal output with limited stack frames
|
|
63
|
+
warntrace run --format detailed --max-frames 5 pytest
|
|
64
|
+
|
|
65
|
+
# Summary — one line per warning with total counts
|
|
66
|
+
warntrace run --format summary pytest
|
|
67
|
+
|
|
68
|
+
# Write report to a file (no ANSI codes in file output)
|
|
69
|
+
warntrace run --output report.json pytest
|
|
70
|
+
|
|
71
|
+
# Collect all warnings silently, even normally-suppressed ones
|
|
72
|
+
warntrace run --no-passthrough --show-all pytest
|
|
73
|
+
|
|
74
|
+
# Fail CI when application-origin warnings are found
|
|
75
|
+
warntrace run --fail-on application pytest
|
|
76
|
+
|
|
77
|
+
# Treat triggered dependency warnings as application-level too
|
|
78
|
+
warntrace run --fail-on application pytest
|
|
79
|
+
|
|
80
|
+
# Fail on any warning (application + deps + stdlib + unknown)
|
|
81
|
+
warntrace run --fail-on all pytest
|
|
82
|
+
|
|
83
|
+
# Opt out of policy failure explicitly
|
|
84
|
+
warntrace run --fail-on none pytest
|
|
85
|
+
|
|
86
|
+
# Specify granular origins to fail on
|
|
87
|
+
warntrace run --fail-on direct_dependency transitive_dependency pytest
|
|
88
|
+
|
|
89
|
+
# Combined: silent, fail on app warnings, save report
|
|
90
|
+
warntrace run --no-passthrough --fail-on application --output ci-report.json pytest
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Python API
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from warntrace import capture_warnings
|
|
97
|
+
|
|
98
|
+
with capture_warnings() as tracer:
|
|
99
|
+
import warnings
|
|
100
|
+
warnings.warn("something is deprecated", DeprecationWarning)
|
|
101
|
+
|
|
102
|
+
report = tracer.stop()
|
|
103
|
+
print(report.to_dict())
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The context manager installs the capture hook, runs your code, restores the
|
|
107
|
+
original warning state, and returns a classified `WarningReport`. The report
|
|
108
|
+
contains the full warning list with origin, stack, dependency paths, and
|
|
109
|
+
occurrence counts.
|
|
110
|
+
|
|
111
|
+
### Lower-level API
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from warntrace import WarningTracer
|
|
115
|
+
|
|
116
|
+
tracer = WarningTracer(show_all=True, passthrough=False)
|
|
117
|
+
tracer.start()
|
|
118
|
+
# ... your code ...
|
|
119
|
+
report = tracer.stop()
|
|
120
|
+
print(report.to_dict())
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Configuration
|
|
124
|
+
|
|
125
|
+
| Parameter | Default | Description |
|
|
126
|
+
|---|---|---|
|
|
127
|
+
| `root` | `Path.cwd()` | Application root for origin classification |
|
|
128
|
+
| `show_all` | `False` | Capture normally-suppressed warning types |
|
|
129
|
+
| `passthrough` | `True` | Forward warnings to the original handler |
|
|
130
|
+
|
|
131
|
+
## Output formats
|
|
132
|
+
|
|
133
|
+
### JSON (default)
|
|
134
|
+
|
|
135
|
+
```json
|
|
136
|
+
{
|
|
137
|
+
"schema_version": "0.1",
|
|
138
|
+
"summary": {
|
|
139
|
+
"unique_warnings": 2,
|
|
140
|
+
"total_occurrences": 5,
|
|
141
|
+
"application": 1,
|
|
142
|
+
"direct_dependency": 1,
|
|
143
|
+
"transitive_dependency": 0,
|
|
144
|
+
"standard_library": 0,
|
|
145
|
+
"unknown": 0
|
|
146
|
+
},
|
|
147
|
+
"warnings": [
|
|
148
|
+
{
|
|
149
|
+
"category": "DeprecationWarning",
|
|
150
|
+
"message": "use foobar() instead of deprecated_func()",
|
|
151
|
+
"origin": "direct_dependency",
|
|
152
|
+
"triggered_directly_by_application": true,
|
|
153
|
+
"occurrences": 3,
|
|
154
|
+
"emitted_from": {
|
|
155
|
+
"filename": "/path/to/site-packages/oldpkg/utils.py",
|
|
156
|
+
"lineno": 42,
|
|
157
|
+
"distribution": {
|
|
158
|
+
"name": "oldpkg",
|
|
159
|
+
"normalized_name": "oldpkg",
|
|
160
|
+
"version": "1.2.3"
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
"application_frame": {
|
|
164
|
+
"filename": "/home/user/project/app.py",
|
|
165
|
+
"lineno": 15,
|
|
166
|
+
"function": "run",
|
|
167
|
+
"source_line": "oldpkg.deprecated_func()"
|
|
168
|
+
},
|
|
169
|
+
"dependency_path": [
|
|
170
|
+
{"name": "myproject", "normalized_name": "myproject", "version": null},
|
|
171
|
+
{"name": "oldpkg", "normalized_name": "oldpkg", "version": "1.2.3"}
|
|
172
|
+
],
|
|
173
|
+
"stack": [
|
|
174
|
+
{"filename": "/home/user/project/app.py", "lineno": 15, "function": "run", "source_line": "oldpkg.deprecated_func()"},
|
|
175
|
+
{"filename": "/path/to/site-packages/oldpkg/utils.py", "lineno": 42, "function": "deprecated_func", "source_line": "warnings.warn(...)"}
|
|
176
|
+
]
|
|
177
|
+
}
|
|
178
|
+
]
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Detailed terminal output
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
warntrace report — 1 unique warning, 3 total occurrences
|
|
186
|
+
|
|
187
|
+
─── Warning 1 of 1 ───────────────────────────────────────────────────────────
|
|
188
|
+
Origin: ! direct_dependency (triggered by your code)
|
|
189
|
+
Category: DeprecationWarning
|
|
190
|
+
Message: use foobar() instead of deprecated_func()
|
|
191
|
+
Location: oldpkg/utils.py:42
|
|
192
|
+
Occurrences: 3
|
|
193
|
+
Application:
|
|
194
|
+
myproject/app.py:15 run()
|
|
195
|
+
-> oldpkg.deprecated_func()
|
|
196
|
+
Dependency path:
|
|
197
|
+
myproject → oldpkg 1.2.3
|
|
198
|
+
Stack:
|
|
199
|
+
1 myproject/app.py:15 run()
|
|
200
|
+
2 oldpkg/utils.py:42 deprecated_func()
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Summary output
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
application 1 DeprecationWarning: use foobar() instead...
|
|
207
|
+
direct_dependency 3 DeprecationWarning: use foobar() instead...
|
|
208
|
+
────────────────────────────────────────────────────────
|
|
209
|
+
warntrace report — 2 unique warnings, 4 total occurrences
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Classification
|
|
213
|
+
|
|
214
|
+
Warntrace assigns an **origin** to every captured warning by walking the stack and
|
|
215
|
+
mapping each frame to a package or to the Python standard library. There are five
|
|
216
|
+
origin levels:
|
|
217
|
+
|
|
218
|
+
| Origin | Meaning |
|
|
219
|
+
|---|---|
|
|
220
|
+
| `application` | The warning was emitted from your own code (under the project root) |
|
|
221
|
+
| `direct_dependency` | The warning was emitted by a package listed in your dependencies |
|
|
222
|
+
| `transitive_dependency` | The warning was emitted by a dependency of one of your dependencies |
|
|
223
|
+
| `standard_library` | The warning was emitted from CPython stdlib |
|
|
224
|
+
| `unknown` | The file could not be mapped to any known package |
|
|
225
|
+
|
|
226
|
+
Additionally, each warning has a `triggered_directly_by_application` boolean.
|
|
227
|
+
This is `true` when an application frame appears in the call stack below the
|
|
228
|
+
warning emission — meaning your code called the code that warned, even if the
|
|
229
|
+
warning itself comes from a dependency.
|
|
230
|
+
|
|
231
|
+
See `docs/CLASSIFICATION.md` for the detailed pipeline documentation.
|
|
232
|
+
|
|
233
|
+
## CI integration
|
|
234
|
+
|
|
235
|
+
Policies are expressed via `--fail-on`:
|
|
236
|
+
|
|
237
|
+
| Policy | Warnings that trigger failure |
|
|
238
|
+
|---|---|
|
|
239
|
+
| `none` | Never (explicit opt-out) |
|
|
240
|
+
| `application` | Origin is `application` **or** `triggered_directly_by_application` is true |
|
|
241
|
+
| `direct` | Origin is `application` or `direct_dependency` |
|
|
242
|
+
| `all` | Any captured warning |
|
|
243
|
+
| *(individual origins)* | `direct_dependency`, `transitive_dependency`, `standard_library`, `unknown` |
|
|
244
|
+
|
|
245
|
+
Exit codes:
|
|
246
|
+
|
|
247
|
+
| Code | Meaning |
|
|
248
|
+
|---|---|
|
|
249
|
+
| 0 | Success — no policy failure |
|
|
250
|
+
| 2 | Warntrace usage error (e.g. missing command) |
|
|
251
|
+
| 3 | Policy matched — warnings exceed the configured threshold |
|
|
252
|
+
|
|
253
|
+
> Child-process failure (e.g. pytest test failures) takes priority over policy:
|
|
254
|
+
> if the child exits non-zero, that exit code is returned even when a policy
|
|
255
|
+
> would have matched.
|
|
256
|
+
|
|
257
|
+
### Example: CI workflow
|
|
258
|
+
|
|
259
|
+
```yaml
|
|
260
|
+
# .github/workflows/ci.yml
|
|
261
|
+
- run: warntrace run --no-passthrough --fail-on application --output warnings.json pytest
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
The test suite runs normally. If any application-origin or application-triggered
|
|
265
|
+
warnings are found, the step exits with code 3. The full report is saved to
|
|
266
|
+
`warnings.json` for inspection.
|
|
267
|
+
|
|
268
|
+
## Privacy
|
|
269
|
+
|
|
270
|
+
Warntrace makes **no network requests**. All analysis is local. It reads:
|
|
271
|
+
- Your project's dependency metadata (`importlib.metadata`)
|
|
272
|
+
- The filesystem path of each stack frame
|
|
273
|
+
- Environment variables (for configuration only)
|
|
274
|
+
|
|
275
|
+
No data is collected, logged, or transmitted.
|
|
276
|
+
|
|
277
|
+
## Known limitations
|
|
278
|
+
|
|
279
|
+
- **Pytest stack frames.** Under pytest, captured stacks include ~30 frames of
|
|
280
|
+
pytest/pluggy internals. Use `--max-frames` to limit terminal output.
|
|
281
|
+
- **Self-testing.** Running `warntrace run pytest` on warntrace's own test suite
|
|
282
|
+
reports 0 warnings because the test files manipulate capture state at import
|
|
283
|
+
time. This does not affect normal projects.
|
|
284
|
+
- **Dependency graph accuracy.** The graph is built from installed package
|
|
285
|
+
metadata (`Requires-Dist`). Optional/extra dependencies are excluded.
|
|
286
|
+
Namespace packages with ambiguous ownership return `unknown`.
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# Warntrace
|
|
2
|
+
|
|
3
|
+
Trace the origin of Python warnings — classify them by ownership (application, direct
|
|
4
|
+
dependency, transitive, stdlib), suppress noise, and fail CI when policy is violated.
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
uv add --dev warntrace
|
|
10
|
+
# or
|
|
11
|
+
pip install warntrace
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Requires Python >= 3.10.
|
|
15
|
+
|
|
16
|
+
## Quick start
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# Capture warnings from any Python command
|
|
20
|
+
warntrace run python your_script.py
|
|
21
|
+
|
|
22
|
+
# Capture warnings from your test suite
|
|
23
|
+
warntrace run pytest
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Warntrace intercepts every `warnings.warn()` call in the child process, classifies each
|
|
27
|
+
warning by origin, and writes a JSON report to stdout after the child exits.
|
|
28
|
+
|
|
29
|
+
## CLI reference
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
warntrace run [options] -- <command> [args...]
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
| Flag | Default | Description |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| `--format` | `json` | Output format: `json`, `detailed`, or `summary` |
|
|
38
|
+
| `--output FILE` | stdout | Write report to a file instead of stdout |
|
|
39
|
+
| `--max-frames N` | unlimited | Limit stack frames per warning in terminal output |
|
|
40
|
+
| `--show-all` | off | Capture normally-suppressed warnings (e.g. `BytesWarning`) |
|
|
41
|
+
| `--no-passthrough` | off | Suppress original warning output from the child process |
|
|
42
|
+
| `--fail-on POLICY` | off | Exit code 3 when a warning matches the policy (see below) |
|
|
43
|
+
| `--root PATH` | child's CWD | Application root directory for classification |
|
|
44
|
+
| `--no-color` | off | Disable ANSI color in terminal output |
|
|
45
|
+
|
|
46
|
+
### Examples
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# Basic — run a script and see the default JSON report
|
|
50
|
+
warntrace run python -c "import warnings; warnings.warn('hello')"
|
|
51
|
+
|
|
52
|
+
# Detailed terminal output with limited stack frames
|
|
53
|
+
warntrace run --format detailed --max-frames 5 pytest
|
|
54
|
+
|
|
55
|
+
# Summary — one line per warning with total counts
|
|
56
|
+
warntrace run --format summary pytest
|
|
57
|
+
|
|
58
|
+
# Write report to a file (no ANSI codes in file output)
|
|
59
|
+
warntrace run --output report.json pytest
|
|
60
|
+
|
|
61
|
+
# Collect all warnings silently, even normally-suppressed ones
|
|
62
|
+
warntrace run --no-passthrough --show-all pytest
|
|
63
|
+
|
|
64
|
+
# Fail CI when application-origin warnings are found
|
|
65
|
+
warntrace run --fail-on application pytest
|
|
66
|
+
|
|
67
|
+
# Treat triggered dependency warnings as application-level too
|
|
68
|
+
warntrace run --fail-on application pytest
|
|
69
|
+
|
|
70
|
+
# Fail on any warning (application + deps + stdlib + unknown)
|
|
71
|
+
warntrace run --fail-on all pytest
|
|
72
|
+
|
|
73
|
+
# Opt out of policy failure explicitly
|
|
74
|
+
warntrace run --fail-on none pytest
|
|
75
|
+
|
|
76
|
+
# Specify granular origins to fail on
|
|
77
|
+
warntrace run --fail-on direct_dependency transitive_dependency pytest
|
|
78
|
+
|
|
79
|
+
# Combined: silent, fail on app warnings, save report
|
|
80
|
+
warntrace run --no-passthrough --fail-on application --output ci-report.json pytest
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Python API
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from warntrace import capture_warnings
|
|
87
|
+
|
|
88
|
+
with capture_warnings() as tracer:
|
|
89
|
+
import warnings
|
|
90
|
+
warnings.warn("something is deprecated", DeprecationWarning)
|
|
91
|
+
|
|
92
|
+
report = tracer.stop()
|
|
93
|
+
print(report.to_dict())
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The context manager installs the capture hook, runs your code, restores the
|
|
97
|
+
original warning state, and returns a classified `WarningReport`. The report
|
|
98
|
+
contains the full warning list with origin, stack, dependency paths, and
|
|
99
|
+
occurrence counts.
|
|
100
|
+
|
|
101
|
+
### Lower-level API
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from warntrace import WarningTracer
|
|
105
|
+
|
|
106
|
+
tracer = WarningTracer(show_all=True, passthrough=False)
|
|
107
|
+
tracer.start()
|
|
108
|
+
# ... your code ...
|
|
109
|
+
report = tracer.stop()
|
|
110
|
+
print(report.to_dict())
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Configuration
|
|
114
|
+
|
|
115
|
+
| Parameter | Default | Description |
|
|
116
|
+
|---|---|---|
|
|
117
|
+
| `root` | `Path.cwd()` | Application root for origin classification |
|
|
118
|
+
| `show_all` | `False` | Capture normally-suppressed warning types |
|
|
119
|
+
| `passthrough` | `True` | Forward warnings to the original handler |
|
|
120
|
+
|
|
121
|
+
## Output formats
|
|
122
|
+
|
|
123
|
+
### JSON (default)
|
|
124
|
+
|
|
125
|
+
```json
|
|
126
|
+
{
|
|
127
|
+
"schema_version": "0.1",
|
|
128
|
+
"summary": {
|
|
129
|
+
"unique_warnings": 2,
|
|
130
|
+
"total_occurrences": 5,
|
|
131
|
+
"application": 1,
|
|
132
|
+
"direct_dependency": 1,
|
|
133
|
+
"transitive_dependency": 0,
|
|
134
|
+
"standard_library": 0,
|
|
135
|
+
"unknown": 0
|
|
136
|
+
},
|
|
137
|
+
"warnings": [
|
|
138
|
+
{
|
|
139
|
+
"category": "DeprecationWarning",
|
|
140
|
+
"message": "use foobar() instead of deprecated_func()",
|
|
141
|
+
"origin": "direct_dependency",
|
|
142
|
+
"triggered_directly_by_application": true,
|
|
143
|
+
"occurrences": 3,
|
|
144
|
+
"emitted_from": {
|
|
145
|
+
"filename": "/path/to/site-packages/oldpkg/utils.py",
|
|
146
|
+
"lineno": 42,
|
|
147
|
+
"distribution": {
|
|
148
|
+
"name": "oldpkg",
|
|
149
|
+
"normalized_name": "oldpkg",
|
|
150
|
+
"version": "1.2.3"
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
"application_frame": {
|
|
154
|
+
"filename": "/home/user/project/app.py",
|
|
155
|
+
"lineno": 15,
|
|
156
|
+
"function": "run",
|
|
157
|
+
"source_line": "oldpkg.deprecated_func()"
|
|
158
|
+
},
|
|
159
|
+
"dependency_path": [
|
|
160
|
+
{"name": "myproject", "normalized_name": "myproject", "version": null},
|
|
161
|
+
{"name": "oldpkg", "normalized_name": "oldpkg", "version": "1.2.3"}
|
|
162
|
+
],
|
|
163
|
+
"stack": [
|
|
164
|
+
{"filename": "/home/user/project/app.py", "lineno": 15, "function": "run", "source_line": "oldpkg.deprecated_func()"},
|
|
165
|
+
{"filename": "/path/to/site-packages/oldpkg/utils.py", "lineno": 42, "function": "deprecated_func", "source_line": "warnings.warn(...)"}
|
|
166
|
+
]
|
|
167
|
+
}
|
|
168
|
+
]
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Detailed terminal output
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
warntrace report — 1 unique warning, 3 total occurrences
|
|
176
|
+
|
|
177
|
+
─── Warning 1 of 1 ───────────────────────────────────────────────────────────
|
|
178
|
+
Origin: ! direct_dependency (triggered by your code)
|
|
179
|
+
Category: DeprecationWarning
|
|
180
|
+
Message: use foobar() instead of deprecated_func()
|
|
181
|
+
Location: oldpkg/utils.py:42
|
|
182
|
+
Occurrences: 3
|
|
183
|
+
Application:
|
|
184
|
+
myproject/app.py:15 run()
|
|
185
|
+
-> oldpkg.deprecated_func()
|
|
186
|
+
Dependency path:
|
|
187
|
+
myproject → oldpkg 1.2.3
|
|
188
|
+
Stack:
|
|
189
|
+
1 myproject/app.py:15 run()
|
|
190
|
+
2 oldpkg/utils.py:42 deprecated_func()
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Summary output
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
application 1 DeprecationWarning: use foobar() instead...
|
|
197
|
+
direct_dependency 3 DeprecationWarning: use foobar() instead...
|
|
198
|
+
────────────────────────────────────────────────────────
|
|
199
|
+
warntrace report — 2 unique warnings, 4 total occurrences
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Classification
|
|
203
|
+
|
|
204
|
+
Warntrace assigns an **origin** to every captured warning by walking the stack and
|
|
205
|
+
mapping each frame to a package or to the Python standard library. There are five
|
|
206
|
+
origin levels:
|
|
207
|
+
|
|
208
|
+
| Origin | Meaning |
|
|
209
|
+
|---|---|
|
|
210
|
+
| `application` | The warning was emitted from your own code (under the project root) |
|
|
211
|
+
| `direct_dependency` | The warning was emitted by a package listed in your dependencies |
|
|
212
|
+
| `transitive_dependency` | The warning was emitted by a dependency of one of your dependencies |
|
|
213
|
+
| `standard_library` | The warning was emitted from CPython stdlib |
|
|
214
|
+
| `unknown` | The file could not be mapped to any known package |
|
|
215
|
+
|
|
216
|
+
Additionally, each warning has a `triggered_directly_by_application` boolean.
|
|
217
|
+
This is `true` when an application frame appears in the call stack below the
|
|
218
|
+
warning emission — meaning your code called the code that warned, even if the
|
|
219
|
+
warning itself comes from a dependency.
|
|
220
|
+
|
|
221
|
+
See `docs/CLASSIFICATION.md` for the detailed pipeline documentation.
|
|
222
|
+
|
|
223
|
+
## CI integration
|
|
224
|
+
|
|
225
|
+
Policies are expressed via `--fail-on`:
|
|
226
|
+
|
|
227
|
+
| Policy | Warnings that trigger failure |
|
|
228
|
+
|---|---|
|
|
229
|
+
| `none` | Never (explicit opt-out) |
|
|
230
|
+
| `application` | Origin is `application` **or** `triggered_directly_by_application` is true |
|
|
231
|
+
| `direct` | Origin is `application` or `direct_dependency` |
|
|
232
|
+
| `all` | Any captured warning |
|
|
233
|
+
| *(individual origins)* | `direct_dependency`, `transitive_dependency`, `standard_library`, `unknown` |
|
|
234
|
+
|
|
235
|
+
Exit codes:
|
|
236
|
+
|
|
237
|
+
| Code | Meaning |
|
|
238
|
+
|---|---|
|
|
239
|
+
| 0 | Success — no policy failure |
|
|
240
|
+
| 2 | Warntrace usage error (e.g. missing command) |
|
|
241
|
+
| 3 | Policy matched — warnings exceed the configured threshold |
|
|
242
|
+
|
|
243
|
+
> Child-process failure (e.g. pytest test failures) takes priority over policy:
|
|
244
|
+
> if the child exits non-zero, that exit code is returned even when a policy
|
|
245
|
+
> would have matched.
|
|
246
|
+
|
|
247
|
+
### Example: CI workflow
|
|
248
|
+
|
|
249
|
+
```yaml
|
|
250
|
+
# .github/workflows/ci.yml
|
|
251
|
+
- run: warntrace run --no-passthrough --fail-on application --output warnings.json pytest
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
The test suite runs normally. If any application-origin or application-triggered
|
|
255
|
+
warnings are found, the step exits with code 3. The full report is saved to
|
|
256
|
+
`warnings.json` for inspection.
|
|
257
|
+
|
|
258
|
+
## Privacy
|
|
259
|
+
|
|
260
|
+
Warntrace makes **no network requests**. All analysis is local. It reads:
|
|
261
|
+
- Your project's dependency metadata (`importlib.metadata`)
|
|
262
|
+
- The filesystem path of each stack frame
|
|
263
|
+
- Environment variables (for configuration only)
|
|
264
|
+
|
|
265
|
+
No data is collected, logged, or transmitted.
|
|
266
|
+
|
|
267
|
+
## Known limitations
|
|
268
|
+
|
|
269
|
+
- **Pytest stack frames.** Under pytest, captured stacks include ~30 frames of
|
|
270
|
+
pytest/pluggy internals. Use `--max-frames` to limit terminal output.
|
|
271
|
+
- **Self-testing.** Running `warntrace run pytest` on warntrace's own test suite
|
|
272
|
+
reports 0 warnings because the test files manipulate capture state at import
|
|
273
|
+
time. This does not affect normal projects.
|
|
274
|
+
- **Dependency graph accuracy.** The graph is built from installed package
|
|
275
|
+
metadata (`Requires-Dist`). Optional/extra dependencies are excluded.
|
|
276
|
+
Namespace packages with ambiguous ownership return `unknown`.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "warntrace"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Find which package caused a Python warning and where your code triggered it."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Kunal Kaul", email = "54585925+kunalkaul@users.noreply.github.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
dependencies = ["packaging>=24"]
|
|
11
|
+
|
|
12
|
+
[project.scripts]
|
|
13
|
+
warntrace = "warntrace.cli:main"
|
|
14
|
+
|
|
15
|
+
[build-system]
|
|
16
|
+
requires = ["uv_build>=0.11.22,<0.12.0"]
|
|
17
|
+
build-backend = "uv_build"
|
|
18
|
+
|
|
19
|
+
[tool.ruff]
|
|
20
|
+
line-length = 100
|
|
21
|
+
target-version = "py310"
|
|
22
|
+
|
|
23
|
+
[tool.ruff.lint]
|
|
24
|
+
select = ["E", "F", "I", "B", "UP", "SIM"]
|
|
25
|
+
|
|
26
|
+
[tool.mypy]
|
|
27
|
+
python_version = "3.10"
|
|
28
|
+
strict = true
|
|
29
|
+
|
|
30
|
+
[tool.pytest.ini_options]
|
|
31
|
+
testpaths = ["tests"]
|
|
32
|
+
addopts = "-ra"
|
|
33
|
+
|
|
34
|
+
[tool.coverage.run]
|
|
35
|
+
branch = true
|
|
36
|
+
source = ["warntrace"]
|
|
37
|
+
|
|
38
|
+
[tool.coverage.report]
|
|
39
|
+
fail_under = 85
|
|
40
|
+
show_missing = true
|
|
41
|
+
|
|
42
|
+
[dependency-groups]
|
|
43
|
+
dev = [
|
|
44
|
+
"mypy>=2.1.0",
|
|
45
|
+
"pytest>=9.1.0",
|
|
46
|
+
"pytest-cov>=7.1.0",
|
|
47
|
+
"ruff>=0.15.18",
|
|
48
|
+
]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Warntrace - Find which package caused a Python warning and where your code triggered it."""
|
|
2
|
+
|
|
3
|
+
from warntrace.api import WarningTracer, capture_warnings
|
|
4
|
+
from warntrace.capture import (
|
|
5
|
+
clear_captured_warnings,
|
|
6
|
+
get_aggregator,
|
|
7
|
+
get_captured_warnings,
|
|
8
|
+
install_hook,
|
|
9
|
+
is_hook_installed,
|
|
10
|
+
uninstall_hook,
|
|
11
|
+
)
|
|
12
|
+
from warntrace.models import (
|
|
13
|
+
CapturedWarning,
|
|
14
|
+
DistributionInfo,
|
|
15
|
+
FrameInfo,
|
|
16
|
+
WarningOrigin,
|
|
17
|
+
WarningReport,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"CapturedWarning",
|
|
22
|
+
"capture_warnings",
|
|
23
|
+
"clear_captured_warnings",
|
|
24
|
+
"DistributionInfo",
|
|
25
|
+
"FrameInfo",
|
|
26
|
+
"get_aggregator",
|
|
27
|
+
"get_captured_warnings",
|
|
28
|
+
"install_hook",
|
|
29
|
+
"is_hook_installed",
|
|
30
|
+
"uninstall_hook",
|
|
31
|
+
"WarningOrigin",
|
|
32
|
+
"WarningReport",
|
|
33
|
+
"WarningTracer",
|
|
34
|
+
]
|