pipbreak 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.
- pipbreak-0.1.0/LICENSE +21 -0
- pipbreak-0.1.0/PKG-INFO +209 -0
- pipbreak-0.1.0/README.md +188 -0
- pipbreak-0.1.0/pipbreak/__init__.py +4 -0
- pipbreak-0.1.0/pipbreak/__main__.py +5 -0
- pipbreak-0.1.0/pipbreak/checker.py +1110 -0
- pipbreak-0.1.0/pipbreak/cli.py +84 -0
- pipbreak-0.1.0/pipbreak/compare.py +142 -0
- pipbreak-0.1.0/pipbreak.egg-info/PKG-INFO +209 -0
- pipbreak-0.1.0/pipbreak.egg-info/SOURCES.txt +14 -0
- pipbreak-0.1.0/pipbreak.egg-info/dependency_links.txt +1 -0
- pipbreak-0.1.0/pipbreak.egg-info/entry_points.txt +2 -0
- pipbreak-0.1.0/pipbreak.egg-info/top_level.txt +1 -0
- pipbreak-0.1.0/pyproject.toml +34 -0
- pipbreak-0.1.0/setup.cfg +4 -0
- pipbreak-0.1.0/tests/test_checker.py +593 -0
pipbreak-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Youssef Elaasri
|
|
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.
|
pipbreak-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pipbreak
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Find the installed packages a dependency update breaks - the calls pip's version ranges let through.
|
|
5
|
+
Author: Youssef Elaasri
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/youssef-elaasri/pipbreak
|
|
8
|
+
Project-URL: Issues, https://github.com/youssef-elaasri/pipbreak/issues
|
|
9
|
+
Keywords: dependencies,dependabot,renovate,upgrade,compatibility,static-analysis
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
16
|
+
Classifier: Topic :: System :: Software Distribution
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# pipbreak
|
|
23
|
+
|
|
24
|
+
**`pip install -U werkzeug` succeeds. Your Flask app is now broken.**
|
|
25
|
+
|
|
26
|
+
pip only checks the version ranges packages declare, and those are usually open-ended - Flask 2.2
|
|
27
|
+
asks for `Werkzeug>=2.2.2`, so pip happily installs Werkzeug 3, which removed APIs Flask 2.2 calls.
|
|
28
|
+
Dependabot opens the PR, pip resolves it, and you find out at runtime - or not at all, if your
|
|
29
|
+
tests don't happen to go through that code.
|
|
30
|
+
|
|
31
|
+
pipbreak reads the code of your installed packages and checks every call one package makes into
|
|
32
|
+
another against the versions actually installed. On a dependency-update PR it compares before and
|
|
33
|
+
after, and tells you exactly what the update breaks:
|
|
34
|
+
|
|
35
|
+
> ### pipbreak: this update breaks 2 call(s) in installed packages
|
|
36
|
+
>
|
|
37
|
+
> Changed: Werkzeug 2.3.8 → 3.1.3
|
|
38
|
+
>
|
|
39
|
+
> | Package | Where | Problem |
|
|
40
|
+
> |---|---|---|
|
|
41
|
+
> | Flask 2.2.5 | `flask/cli.py:250` | 'werkzeug' has no attribute '__version__' in Werkzeug==3.1.3. |
|
|
42
|
+
> | Flask 2.2.5 | `flask/testing.py:116` | 'werkzeug' has no attribute '__version__' in Werkzeug==3.1.3. |
|
|
43
|
+
>
|
|
44
|
+
> These packages accept the new version in their metadata but call APIs it no longer has, so pip
|
|
45
|
+
> installs them without complaint and they fail at runtime. Upgrade them too, or hold this update back.
|
|
46
|
+
|
|
47
|
+
No configuration, no type stubs, zero dependencies. Python 3.10+.
|
|
48
|
+
|
|
49
|
+
## How well does it work?
|
|
50
|
+
|
|
51
|
+
- **Known breaks:** on 12 documented "update broke a package that didn't pin it" cases, it flagged
|
|
52
|
+
**8 of the 9 real breaks** (5 at the exact failing line) and stayed **quiet on all 3 safe updates**
|
|
53
|
+
([benchmark](research/KNOWN_BREAKS.md)).
|
|
54
|
+
- **Real Dependabot PRs:** on 40 open-source bump PRs it caught the Werkzeug 3 breaks in Flask,
|
|
55
|
+
Flask-Login and Flask-WTF in every PR that had them - including one whose CI passed
|
|
56
|
+
([study](research/UPGRADES.md)).
|
|
57
|
+
- **What it can't see:** arguments forwarded through `**kwargs`, methods on objects whose type is
|
|
58
|
+
only known at runtime, binary (C extension) incompatibilities, and behaviour changes behind an
|
|
59
|
+
unchanged API. It complements your tests; it doesn't replace them.
|
|
60
|
+
|
|
61
|
+
## On Dependabot / Renovate PRs
|
|
62
|
+
|
|
63
|
+
```yaml
|
|
64
|
+
# .github/workflows/pipbreak.yml
|
|
65
|
+
name: pipbreak
|
|
66
|
+
on: pull_request
|
|
67
|
+
permissions:
|
|
68
|
+
contents: read
|
|
69
|
+
pull-requests: write # for the comment; Dependabot PRs get a read-only token without this
|
|
70
|
+
jobs:
|
|
71
|
+
dependency-update:
|
|
72
|
+
if: github.actor == 'dependabot[bot]' || github.actor == 'renovate[bot]'
|
|
73
|
+
runs-on: ubuntu-latest
|
|
74
|
+
steps:
|
|
75
|
+
- uses: actions/checkout@v5
|
|
76
|
+
- uses: actions/setup-python@v6
|
|
77
|
+
with:
|
|
78
|
+
python-version: "3.12" # the Python you deploy on
|
|
79
|
+
- uses: youssef-elaasri/pipbreak@v0
|
|
80
|
+
# with:
|
|
81
|
+
# install: pip install -r requirements/prod.txt # default: requirements.txt, else `pip install .`
|
|
82
|
+
# working-directory: backend
|
|
83
|
+
# fail-on-break: false
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The action installs your dependencies twice - from the PR's base commit and from the PR - finds
|
|
87
|
+
which packages changed, and reports only what the change breaks: as a job summary, as one PR comment
|
|
88
|
+
it keeps up to date, and as a failed check. A safe update stays silent.
|
|
89
|
+
|
|
90
|
+
Use `pull_request`, not `pull_request_target`: installing the PR's dependencies runs their build
|
|
91
|
+
code, which must not see a write token for your repository.
|
|
92
|
+
|
|
93
|
+
## On your machine
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
pip install pipbreak
|
|
97
|
+
|
|
98
|
+
pip install -U werkzeug
|
|
99
|
+
pipbreak --deps --uses werkzeug # what in my environment calls Werkzeug APIs that are gone?
|
|
100
|
+
pipbreak --deps # every installed package against every other
|
|
101
|
+
pipbreak --deps --python .venv/bin/python # another environment
|
|
102
|
+
|
|
103
|
+
# before/after, like the action does
|
|
104
|
+
python -m pipbreak.compare --base .venv-old/bin/python --head .venv/bin/python --markdown report.md
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
- `--uses LIB` (repeatable; import or distribution name) narrows the check to packages that import
|
|
108
|
+
LIB. That takes seconds; a whole environment takes a minute or two.
|
|
109
|
+
- Only calls from one distribution into another count. A package's own internals, the stdlib,
|
|
110
|
+
missing optional extras, vendored copies and shipped test suites are skipped.
|
|
111
|
+
- Exit code is 1 when there are errors (`--strict` makes warnings fail too).
|
|
112
|
+
|
|
113
|
+
## Checking your own code
|
|
114
|
+
|
|
115
|
+
The same engine checks your project's code against your installed versions - useful for code
|
|
116
|
+
written for a different version of a library (by you, a tutorial, or an AI assistant):
|
|
117
|
+
|
|
118
|
+
```text
|
|
119
|
+
$ pipbreak examples/
|
|
120
|
+
examples/app.py:9:1: error API002 'pydantic.BaseSettings' is not available in pydantic==2.9.2: `BaseSettings` has been moved to the `pydantic-settings` package. ...
|
|
121
|
+
examples/app.py:9:1: warning API101 'pydantic.validator' is deprecated in pydantic==2.9.2: Pydantic V1 style `@validator` validators are deprecated. ...
|
|
122
|
+
examples/app.py:31:8: error API002 'os.path' has no attribute 'exist' in Python 3.13. Did you mean 'exists'?
|
|
123
|
+
examples/app.py:33:27: error API003 shutil.copy() has no parameter 'overwrite' in Python 3.13.
|
|
124
|
+
examples/app.py:37:39: error API003 random.choices() has no parameter 'unique' in Python 3.13.
|
|
125
|
+
examples/app.py:45:30: error API002 'collections' has no attribute 'Mapping' in Python 3.13. Did you mean 'collections.abc.Mapping'?
|
|
126
|
+
examples/app.py:50:12: error API002 'Path' object has no attribute 'exist' in Python 3.13. Did you mean 'exists'?
|
|
127
|
+
examples/app.py:52:27: error API003 path.read_text() has no parameter 'encodings' in Python 3.13. Did you mean 'encoding'?
|
|
128
|
+
examples/app.py:56:12: warning API101 'User.dict' is deprecated in pydantic==2.9.2: The `dict` method is deprecated; use `model_dump` instead.
|
|
129
|
+
examples/app.py:60:12: warning API101 'User.parse_obj' is deprecated in pydantic==2.9.2: The `parse_obj` method is deprecated; use `model_validate` instead.
|
|
130
|
+
examples/app.py:71:12: warning API005 'Job' object has no attribute 'retry_count': it is never assigned in the class or its bases.
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Run the same file against pydantic 1.10 and the answer changes: `BaseSettings`, `.dict()` and
|
|
134
|
+
`.parse_obj()` are fine, but `field_validator` doesn't exist yet (`Did you mean 'validator'?`).
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
pipbreak src/ # check against the current interpreter
|
|
138
|
+
pipbreak --python .venv/bin/python src/ # check against another environment
|
|
139
|
+
pipbreak --format github src/ # inline annotations on GitHub PRs
|
|
140
|
+
pipbreak --format json src/ # machine-readable
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
In CI, after installing your pinned dependencies: `pip install pipbreak && pipbreak --format github src/`.
|
|
144
|
+
|
|
145
|
+
## Checks
|
|
146
|
+
|
|
147
|
+
| Code | Severity | Meaning |
|
|
148
|
+
|--------|----------|---------|
|
|
149
|
+
| API001 | error | Package or module doesn't exist / isn't installed |
|
|
150
|
+
| API002 | error | Name or attribute doesn't exist in the installed version (with "did you mean") |
|
|
151
|
+
| API003 | error | Call uses a keyword argument the function doesn't accept |
|
|
152
|
+
| API004 | error | Call passes more positional arguments than the function accepts |
|
|
153
|
+
| API005 | warning | Attribute is never assigned anywhere in the object's class (instances with a `__dict__`) |
|
|
154
|
+
| API101 | warning | API is deprecated (PEP 702 `@deprecated` or a `.. deprecated::` docstring note) |
|
|
155
|
+
|
|
156
|
+
## What it knows about your variables
|
|
157
|
+
|
|
158
|
+
A light, flow-insensitive inference tracks what each name holds, per function scope:
|
|
159
|
+
|
|
160
|
+
| Code | Inferred as | Checks |
|
|
161
|
+
|------|-------------|--------|
|
|
162
|
+
| `p = Path(name)`, `Path(name).read_text()` | a `Path`, exactly | missing attributes, method signatures, deprecations |
|
|
163
|
+
| `def f(user: User)`, `x: Client \| None` | a `User` or subclass | method signatures, deprecations |
|
|
164
|
+
| `client.get(url)` with `-> Response` | a `Response` or subclass | method signatures, deprecations |
|
|
165
|
+
| `self` in a method | the class or a subclass | method signatures, deprecations |
|
|
166
|
+
| `class Job: ...` in your file | modelled from its source and its real base classes | all of the above |
|
|
167
|
+
|
|
168
|
+
Missing attributes are only reported for objects we saw constructed: an annotated parameter may be a
|
|
169
|
+
subclass with more attributes. For classes whose instances have no `__dict__` (`__slots__`, builtins)
|
|
170
|
+
that's an **error**. Otherwise attributes are read from the assignments in the class source, so it's a
|
|
171
|
+
**warning** (API005): code elsewhere could still set the attribute.
|
|
172
|
+
|
|
173
|
+
## Avoiding false positives
|
|
174
|
+
|
|
175
|
+
- Imports and calls under feature probes are skipped: `if hasattr(...)`, `sys.platform`,
|
|
176
|
+
`sys.version_info`, `os.name`, `TYPE_CHECKING`, `try/except ImportError|AttributeError`, and
|
|
177
|
+
flags assigned from these (`HAVE_FORK = hasattr(os, "fork")`).
|
|
178
|
+
- Your own modules, relative imports, and names you reassign are never checked. A name bound to
|
|
179
|
+
different things in one scope (or used as a loop/comprehension/lambda variable) isn't inferred.
|
|
180
|
+
- Classes with a custom `__getattr__`/`__getattribute__`, a custom metaclass (ORMs, pydantic, enums),
|
|
181
|
+
class decorators, or code that writes `self.__dict__`/`setattr(self, ...)` are never reported as
|
|
182
|
+
missing attributes - only deprecations and signatures are checked.
|
|
183
|
+
- `super()`, proxies, and functions taking `**kwargs` / `*args` aren't checked for those arguments.
|
|
184
|
+
- Silence a line with `# pipbreak: ignore`.
|
|
185
|
+
|
|
186
|
+
Checked against pydantic 2.9 and 21 stdlib packages (429 files), the only errors left are real for the
|
|
187
|
+
environment: Unix-only APIs (`os.fork`, `socket.AF_UNIX`) in modules that only run on Unix.
|
|
188
|
+
|
|
189
|
+
## Limitations
|
|
190
|
+
|
|
191
|
+
- **Imports installed modules** to inspect them, so their import-time code runs. Use it on
|
|
192
|
+
environments you'd run anyway (the action only installs what your PR already would).
|
|
193
|
+
- Inference is per scope and doesn't follow `isinstance` narrowing, container contents (`users[0].x`),
|
|
194
|
+
or return types without annotations.
|
|
195
|
+
- Python only.
|
|
196
|
+
|
|
197
|
+
## Development
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
pip install -e .
|
|
201
|
+
python -m unittest discover -s tests
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
The `research/` folder holds the studies behind the numbers above, run in a locked-down Docker
|
|
205
|
+
sandbox (`research/run-docker.ps1`).
|
|
206
|
+
|
|
207
|
+
## License
|
|
208
|
+
|
|
209
|
+
MIT
|
pipbreak-0.1.0/README.md
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# pipbreak
|
|
2
|
+
|
|
3
|
+
**`pip install -U werkzeug` succeeds. Your Flask app is now broken.**
|
|
4
|
+
|
|
5
|
+
pip only checks the version ranges packages declare, and those are usually open-ended - Flask 2.2
|
|
6
|
+
asks for `Werkzeug>=2.2.2`, so pip happily installs Werkzeug 3, which removed APIs Flask 2.2 calls.
|
|
7
|
+
Dependabot opens the PR, pip resolves it, and you find out at runtime - or not at all, if your
|
|
8
|
+
tests don't happen to go through that code.
|
|
9
|
+
|
|
10
|
+
pipbreak reads the code of your installed packages and checks every call one package makes into
|
|
11
|
+
another against the versions actually installed. On a dependency-update PR it compares before and
|
|
12
|
+
after, and tells you exactly what the update breaks:
|
|
13
|
+
|
|
14
|
+
> ### pipbreak: this update breaks 2 call(s) in installed packages
|
|
15
|
+
>
|
|
16
|
+
> Changed: Werkzeug 2.3.8 → 3.1.3
|
|
17
|
+
>
|
|
18
|
+
> | Package | Where | Problem |
|
|
19
|
+
> |---|---|---|
|
|
20
|
+
> | Flask 2.2.5 | `flask/cli.py:250` | 'werkzeug' has no attribute '__version__' in Werkzeug==3.1.3. |
|
|
21
|
+
> | Flask 2.2.5 | `flask/testing.py:116` | 'werkzeug' has no attribute '__version__' in Werkzeug==3.1.3. |
|
|
22
|
+
>
|
|
23
|
+
> These packages accept the new version in their metadata but call APIs it no longer has, so pip
|
|
24
|
+
> installs them without complaint and they fail at runtime. Upgrade them too, or hold this update back.
|
|
25
|
+
|
|
26
|
+
No configuration, no type stubs, zero dependencies. Python 3.10+.
|
|
27
|
+
|
|
28
|
+
## How well does it work?
|
|
29
|
+
|
|
30
|
+
- **Known breaks:** on 12 documented "update broke a package that didn't pin it" cases, it flagged
|
|
31
|
+
**8 of the 9 real breaks** (5 at the exact failing line) and stayed **quiet on all 3 safe updates**
|
|
32
|
+
([benchmark](research/KNOWN_BREAKS.md)).
|
|
33
|
+
- **Real Dependabot PRs:** on 40 open-source bump PRs it caught the Werkzeug 3 breaks in Flask,
|
|
34
|
+
Flask-Login and Flask-WTF in every PR that had them - including one whose CI passed
|
|
35
|
+
([study](research/UPGRADES.md)).
|
|
36
|
+
- **What it can't see:** arguments forwarded through `**kwargs`, methods on objects whose type is
|
|
37
|
+
only known at runtime, binary (C extension) incompatibilities, and behaviour changes behind an
|
|
38
|
+
unchanged API. It complements your tests; it doesn't replace them.
|
|
39
|
+
|
|
40
|
+
## On Dependabot / Renovate PRs
|
|
41
|
+
|
|
42
|
+
```yaml
|
|
43
|
+
# .github/workflows/pipbreak.yml
|
|
44
|
+
name: pipbreak
|
|
45
|
+
on: pull_request
|
|
46
|
+
permissions:
|
|
47
|
+
contents: read
|
|
48
|
+
pull-requests: write # for the comment; Dependabot PRs get a read-only token without this
|
|
49
|
+
jobs:
|
|
50
|
+
dependency-update:
|
|
51
|
+
if: github.actor == 'dependabot[bot]' || github.actor == 'renovate[bot]'
|
|
52
|
+
runs-on: ubuntu-latest
|
|
53
|
+
steps:
|
|
54
|
+
- uses: actions/checkout@v5
|
|
55
|
+
- uses: actions/setup-python@v6
|
|
56
|
+
with:
|
|
57
|
+
python-version: "3.12" # the Python you deploy on
|
|
58
|
+
- uses: youssef-elaasri/pipbreak@v0
|
|
59
|
+
# with:
|
|
60
|
+
# install: pip install -r requirements/prod.txt # default: requirements.txt, else `pip install .`
|
|
61
|
+
# working-directory: backend
|
|
62
|
+
# fail-on-break: false
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The action installs your dependencies twice - from the PR's base commit and from the PR - finds
|
|
66
|
+
which packages changed, and reports only what the change breaks: as a job summary, as one PR comment
|
|
67
|
+
it keeps up to date, and as a failed check. A safe update stays silent.
|
|
68
|
+
|
|
69
|
+
Use `pull_request`, not `pull_request_target`: installing the PR's dependencies runs their build
|
|
70
|
+
code, which must not see a write token for your repository.
|
|
71
|
+
|
|
72
|
+
## On your machine
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pip install pipbreak
|
|
76
|
+
|
|
77
|
+
pip install -U werkzeug
|
|
78
|
+
pipbreak --deps --uses werkzeug # what in my environment calls Werkzeug APIs that are gone?
|
|
79
|
+
pipbreak --deps # every installed package against every other
|
|
80
|
+
pipbreak --deps --python .venv/bin/python # another environment
|
|
81
|
+
|
|
82
|
+
# before/after, like the action does
|
|
83
|
+
python -m pipbreak.compare --base .venv-old/bin/python --head .venv/bin/python --markdown report.md
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
- `--uses LIB` (repeatable; import or distribution name) narrows the check to packages that import
|
|
87
|
+
LIB. That takes seconds; a whole environment takes a minute or two.
|
|
88
|
+
- Only calls from one distribution into another count. A package's own internals, the stdlib,
|
|
89
|
+
missing optional extras, vendored copies and shipped test suites are skipped.
|
|
90
|
+
- Exit code is 1 when there are errors (`--strict` makes warnings fail too).
|
|
91
|
+
|
|
92
|
+
## Checking your own code
|
|
93
|
+
|
|
94
|
+
The same engine checks your project's code against your installed versions - useful for code
|
|
95
|
+
written for a different version of a library (by you, a tutorial, or an AI assistant):
|
|
96
|
+
|
|
97
|
+
```text
|
|
98
|
+
$ pipbreak examples/
|
|
99
|
+
examples/app.py:9:1: error API002 'pydantic.BaseSettings' is not available in pydantic==2.9.2: `BaseSettings` has been moved to the `pydantic-settings` package. ...
|
|
100
|
+
examples/app.py:9:1: warning API101 'pydantic.validator' is deprecated in pydantic==2.9.2: Pydantic V1 style `@validator` validators are deprecated. ...
|
|
101
|
+
examples/app.py:31:8: error API002 'os.path' has no attribute 'exist' in Python 3.13. Did you mean 'exists'?
|
|
102
|
+
examples/app.py:33:27: error API003 shutil.copy() has no parameter 'overwrite' in Python 3.13.
|
|
103
|
+
examples/app.py:37:39: error API003 random.choices() has no parameter 'unique' in Python 3.13.
|
|
104
|
+
examples/app.py:45:30: error API002 'collections' has no attribute 'Mapping' in Python 3.13. Did you mean 'collections.abc.Mapping'?
|
|
105
|
+
examples/app.py:50:12: error API002 'Path' object has no attribute 'exist' in Python 3.13. Did you mean 'exists'?
|
|
106
|
+
examples/app.py:52:27: error API003 path.read_text() has no parameter 'encodings' in Python 3.13. Did you mean 'encoding'?
|
|
107
|
+
examples/app.py:56:12: warning API101 'User.dict' is deprecated in pydantic==2.9.2: The `dict` method is deprecated; use `model_dump` instead.
|
|
108
|
+
examples/app.py:60:12: warning API101 'User.parse_obj' is deprecated in pydantic==2.9.2: The `parse_obj` method is deprecated; use `model_validate` instead.
|
|
109
|
+
examples/app.py:71:12: warning API005 'Job' object has no attribute 'retry_count': it is never assigned in the class or its bases.
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Run the same file against pydantic 1.10 and the answer changes: `BaseSettings`, `.dict()` and
|
|
113
|
+
`.parse_obj()` are fine, but `field_validator` doesn't exist yet (`Did you mean 'validator'?`).
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
pipbreak src/ # check against the current interpreter
|
|
117
|
+
pipbreak --python .venv/bin/python src/ # check against another environment
|
|
118
|
+
pipbreak --format github src/ # inline annotations on GitHub PRs
|
|
119
|
+
pipbreak --format json src/ # machine-readable
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
In CI, after installing your pinned dependencies: `pip install pipbreak && pipbreak --format github src/`.
|
|
123
|
+
|
|
124
|
+
## Checks
|
|
125
|
+
|
|
126
|
+
| Code | Severity | Meaning |
|
|
127
|
+
|--------|----------|---------|
|
|
128
|
+
| API001 | error | Package or module doesn't exist / isn't installed |
|
|
129
|
+
| API002 | error | Name or attribute doesn't exist in the installed version (with "did you mean") |
|
|
130
|
+
| API003 | error | Call uses a keyword argument the function doesn't accept |
|
|
131
|
+
| API004 | error | Call passes more positional arguments than the function accepts |
|
|
132
|
+
| API005 | warning | Attribute is never assigned anywhere in the object's class (instances with a `__dict__`) |
|
|
133
|
+
| API101 | warning | API is deprecated (PEP 702 `@deprecated` or a `.. deprecated::` docstring note) |
|
|
134
|
+
|
|
135
|
+
## What it knows about your variables
|
|
136
|
+
|
|
137
|
+
A light, flow-insensitive inference tracks what each name holds, per function scope:
|
|
138
|
+
|
|
139
|
+
| Code | Inferred as | Checks |
|
|
140
|
+
|------|-------------|--------|
|
|
141
|
+
| `p = Path(name)`, `Path(name).read_text()` | a `Path`, exactly | missing attributes, method signatures, deprecations |
|
|
142
|
+
| `def f(user: User)`, `x: Client \| None` | a `User` or subclass | method signatures, deprecations |
|
|
143
|
+
| `client.get(url)` with `-> Response` | a `Response` or subclass | method signatures, deprecations |
|
|
144
|
+
| `self` in a method | the class or a subclass | method signatures, deprecations |
|
|
145
|
+
| `class Job: ...` in your file | modelled from its source and its real base classes | all of the above |
|
|
146
|
+
|
|
147
|
+
Missing attributes are only reported for objects we saw constructed: an annotated parameter may be a
|
|
148
|
+
subclass with more attributes. For classes whose instances have no `__dict__` (`__slots__`, builtins)
|
|
149
|
+
that's an **error**. Otherwise attributes are read from the assignments in the class source, so it's a
|
|
150
|
+
**warning** (API005): code elsewhere could still set the attribute.
|
|
151
|
+
|
|
152
|
+
## Avoiding false positives
|
|
153
|
+
|
|
154
|
+
- Imports and calls under feature probes are skipped: `if hasattr(...)`, `sys.platform`,
|
|
155
|
+
`sys.version_info`, `os.name`, `TYPE_CHECKING`, `try/except ImportError|AttributeError`, and
|
|
156
|
+
flags assigned from these (`HAVE_FORK = hasattr(os, "fork")`).
|
|
157
|
+
- Your own modules, relative imports, and names you reassign are never checked. A name bound to
|
|
158
|
+
different things in one scope (or used as a loop/comprehension/lambda variable) isn't inferred.
|
|
159
|
+
- Classes with a custom `__getattr__`/`__getattribute__`, a custom metaclass (ORMs, pydantic, enums),
|
|
160
|
+
class decorators, or code that writes `self.__dict__`/`setattr(self, ...)` are never reported as
|
|
161
|
+
missing attributes - only deprecations and signatures are checked.
|
|
162
|
+
- `super()`, proxies, and functions taking `**kwargs` / `*args` aren't checked for those arguments.
|
|
163
|
+
- Silence a line with `# pipbreak: ignore`.
|
|
164
|
+
|
|
165
|
+
Checked against pydantic 2.9 and 21 stdlib packages (429 files), the only errors left are real for the
|
|
166
|
+
environment: Unix-only APIs (`os.fork`, `socket.AF_UNIX`) in modules that only run on Unix.
|
|
167
|
+
|
|
168
|
+
## Limitations
|
|
169
|
+
|
|
170
|
+
- **Imports installed modules** to inspect them, so their import-time code runs. Use it on
|
|
171
|
+
environments you'd run anyway (the action only installs what your PR already would).
|
|
172
|
+
- Inference is per scope and doesn't follow `isinstance` narrowing, container contents (`users[0].x`),
|
|
173
|
+
or return types without annotations.
|
|
174
|
+
- Python only.
|
|
175
|
+
|
|
176
|
+
## Development
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
pip install -e .
|
|
180
|
+
python -m unittest discover -s tests
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
The `research/` folder holds the studies behind the numbers above, run in a locked-down Docker
|
|
184
|
+
sandbox (`research/run-docker.ps1`).
|
|
185
|
+
|
|
186
|
+
## License
|
|
187
|
+
|
|
188
|
+
MIT
|