pyfirstaid 0.1.0__tar.gz → 0.2.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.
- pyfirstaid-0.2.0/.gitignore +9 -0
- pyfirstaid-0.2.0/CHANGELOG.md +31 -0
- {pyfirstaid-0.1.0 → pyfirstaid-0.2.0}/PKG-INFO +87 -30
- pyfirstaid-0.2.0/README.md +208 -0
- {pyfirstaid-0.1.0 → pyfirstaid-0.2.0}/src/pyfirstaid/__init__.py +1 -1
- pyfirstaid-0.2.0/src/pyfirstaid/__main__.py +13 -0
- pyfirstaid-0.2.0/src/pyfirstaid/checks/__init__.py +36 -0
- pyfirstaid-0.2.0/src/pyfirstaid/checks/broken_installs.py +69 -0
- pyfirstaid-0.2.0/src/pyfirstaid/checks/compiled.py +76 -0
- pyfirstaid-0.2.0/src/pyfirstaid/checks/dependencies.py +42 -0
- pyfirstaid-0.2.0/src/pyfirstaid/checks/encoding.py +33 -0
- pyfirstaid-0.2.0/src/pyfirstaid/checks/leaks.py +62 -0
- pyfirstaid-0.2.0/src/pyfirstaid/checks/path_check.py +60 -0
- pyfirstaid-0.2.0/src/pyfirstaid/checks/permissions.py +37 -0
- pyfirstaid-0.2.0/src/pyfirstaid/checks/python_version.py +91 -0
- pyfirstaid-0.2.0/src/pyfirstaid/checks/shadowing.py +86 -0
- {pyfirstaid-0.1.0 → pyfirstaid-0.2.0}/src/pyfirstaid/cli.py +1 -1
- {pyfirstaid-0.1.0 → pyfirstaid-0.2.0}/src/pyfirstaid/util.py +26 -0
- pyfirstaid-0.2.0/tests/test_more_checks.py +148 -0
- pyfirstaid-0.1.0/CHANGELOG.md +0 -14
- pyfirstaid-0.1.0/README.md +0 -151
- pyfirstaid-0.1.0/src/pyfirstaid/__main__.py +0 -5
- pyfirstaid-0.1.0/src/pyfirstaid/checks/__init__.py +0 -14
- {pyfirstaid-0.1.0 → pyfirstaid-0.2.0}/.github/workflows/ci.yml +0 -0
- {pyfirstaid-0.1.0 → pyfirstaid-0.2.0}/.github/workflows/publish.yml +0 -0
- {pyfirstaid-0.1.0 → pyfirstaid-0.2.0}/LICENSE +0 -0
- {pyfirstaid-0.1.0 → pyfirstaid-0.2.0}/pyproject.toml +0 -0
- {pyfirstaid-0.1.0 → pyfirstaid-0.2.0}/scripts/build_pyz.py +0 -0
- {pyfirstaid-0.1.0 → pyfirstaid-0.2.0}/src/pyfirstaid/checks/pip_mismatch.py +0 -0
- {pyfirstaid-0.1.0 → pyfirstaid-0.2.0}/src/pyfirstaid/checks/ssl_check.py +0 -0
- {pyfirstaid-0.1.0 → pyfirstaid-0.2.0}/src/pyfirstaid/checks/venv.py +0 -0
- {pyfirstaid-0.1.0 → pyfirstaid-0.2.0}/src/pyfirstaid/model.py +0 -0
- {pyfirstaid-0.1.0 → pyfirstaid-0.2.0}/src/pyfirstaid/report.py +0 -0
- {pyfirstaid-0.1.0 → pyfirstaid-0.2.0}/tests/test_checks.py +0 -0
- {pyfirstaid-0.1.0 → pyfirstaid-0.2.0}/tests/test_cli.py +0 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
Nine new checks, bringing the total to 12:
|
|
6
|
+
|
|
7
|
+
- **python-version**: Python past (or close to) end of life, pre-release Pythons, and Apple's
|
|
8
|
+
built-in Command Line Tools Python on macOS.
|
|
9
|
+
- **path**: typing `python` / `python3` runs a *different* Python, or opens the Windows
|
|
10
|
+
Microsoft Store shortcut.
|
|
11
|
+
- **leaks**: `PYTHONPATH` / `PYTHONHOME` set, and venvs that can see system packages.
|
|
12
|
+
- **shadowing**: files like `random.py` or `requests.py` in the current folder that hide real
|
|
13
|
+
modules.
|
|
14
|
+
- **compiled**: packages with compiled files built for a different Python version.
|
|
15
|
+
- **broken-installs**: packages installed twice, `~` leftovers from interrupted installs,
|
|
16
|
+
and corrupted metadata.
|
|
17
|
+
- **dependencies**: dependency conflicts from `pip check`, each with a suggested fix.
|
|
18
|
+
- **permissions**: venvs that are not writable (e.g. created with sudo).
|
|
19
|
+
- **encoding**: default text encoding that is not UTF-8.
|
|
20
|
+
|
|
21
|
+
## 0.1.0
|
|
22
|
+
|
|
23
|
+
First release, with 3 checks:
|
|
24
|
+
|
|
25
|
+
- **venv**: is a virtual environment active, is an unused `.venv` sitting in the folder,
|
|
26
|
+
is a *different* venv activated in your shell, and does the system Python block pip (PEP 668)?
|
|
27
|
+
- **pip-mismatch**: does the `pip` command install into a different Python than the one you run?
|
|
28
|
+
- **ssl**: can Python reach pypi.org over HTTPS, and are certificate settings
|
|
29
|
+
(`SSL_CERT_FILE`, `REQUESTS_CA_BUNDLE`, `PIP_CERT`, macOS certificates) valid?
|
|
30
|
+
|
|
31
|
+
Options: `--json`, `--share`, `--offline`, `--strict`, `--only`, `--skip`, `--list`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pyfirstaid
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: First aid for broken Python environments: finds what's wrong and tells you how to fix it.
|
|
5
5
|
Project-URL: Homepage, https://github.com/sai-sakalam/pyfirstaid
|
|
6
6
|
Project-URL: Issues, https://github.com/sai-sakalam/pyfirstaid/issues
|
|
@@ -26,7 +26,7 @@ Description-Content-Type: text/markdown
|
|
|
26
26
|
|
|
27
27
|
**First aid for broken Python environments.** One command tells you *what's broken* and *exactly how to fix it*.
|
|
28
28
|
|
|
29
|
-
> **Status: v0.
|
|
29
|
+
> **Status: v0.2.** 12 checks, tested on Windows, macOS and Linux (Python 3.9–3.14).
|
|
30
30
|
> Feedback is very welcome in [Issues](https://github.com/sai-sakalam/pyfirstaid/issues). *Which problems do you hit most?*
|
|
31
31
|
|
|
32
32
|
---
|
|
@@ -41,40 +41,51 @@ This was discussed on the Python forum: [Standard Library Health Check Module](h
|
|
|
41
41
|
|
|
42
42
|
## Example
|
|
43
43
|
|
|
44
|
-
A
|
|
44
|
+
A run inside a project folder where the venv's Python is running, `pip` belongs to the
|
|
45
|
+
system Python, and a file called `random.py` is hiding the real `random` module:
|
|
45
46
|
|
|
46
47
|
```console
|
|
47
|
-
$ python -m pyfirstaid
|
|
48
|
+
$ python -m pyfirstaid --share
|
|
48
49
|
|
|
49
|
-
pyfirstaid 0.
|
|
50
|
+
pyfirstaid 0.2.0: checking your Python environment
|
|
50
51
|
Python 3.13.1 (~/project/.venv/bin/python)
|
|
51
52
|
|
|
53
|
+
✔ Python 3.13.1 (supported until October 2029)
|
|
52
54
|
✔ Virtual environment active: ~/project/.venv
|
|
53
55
|
✘ `pip` installs into a DIFFERENT Python
|
|
54
|
-
pip -> /
|
|
56
|
+
pip -> /opt/homebrew/lib/python3.13/site-packages/pip (python 3.13)
|
|
55
57
|
python -> ~/project/.venv/bin/python (python 3.13)
|
|
56
58
|
fix: Use `python -m pip install <package>` instead of `pip install`. If a virtual environment should be active, activate it first.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
! Typing `python3` runs a DIFFERENT Python than this one
|
|
60
|
+
python3 -> /opt/homebrew
|
|
61
|
+
this Python -> ~/project/.venv
|
|
62
|
+
fix: Activate the virtual environment you want, or call Python by its full path. To change the default, put the right Python's folder first in PATH.
|
|
63
|
+
✔ No PYTHONPATH / PYTHONHOME leaks
|
|
64
|
+
! `random.py` in this folder hides the standard library module `random`
|
|
65
|
+
`import random` will load your file instead, which causes confusing errors like "module 'random' has no attribute ...".
|
|
66
|
+
fix: Rename it (e.g. my_random.py) and delete any __pycache__ folder next to it.
|
|
67
|
+
✔ HTTPS to pypi.org works (OpenSSL 3.4.0 22 Oct 2024)
|
|
68
|
+
✔ 12 compiled package(s) match this Python
|
|
69
|
+
✔ No broken or duplicate installs (1 folder(s) scanned)
|
|
70
|
+
✔ No dependency conflicts (pip check)
|
|
71
|
+
✔ site-packages is writable
|
|
72
|
+
✔ Default text encoding is UTF-8
|
|
73
|
+
|
|
74
|
+
1 problem(s), 2 warning(s).
|
|
60
75
|
```
|
|
61
76
|
|
|
62
77
|
## Try it
|
|
63
78
|
|
|
64
|
-
pyfirstaid is not on PyPI yet. To run it from source:
|
|
65
|
-
|
|
66
79
|
```bash
|
|
67
|
-
|
|
68
|
-
cd pyfirstaid
|
|
69
|
-
python -m pip install .
|
|
80
|
+
pip install pyfirstaid # or: pipx install pyfirstaid
|
|
70
81
|
python -m pyfirstaid
|
|
71
82
|
```
|
|
72
83
|
|
|
73
|
-
|
|
84
|
+
No install possible (for example, pip itself is broken)? Download `pyfirstaid.pyz` from the
|
|
85
|
+
[latest release](https://github.com/sai-sakalam/pyfirstaid/releases) and run:
|
|
74
86
|
|
|
75
87
|
```bash
|
|
76
|
-
python
|
|
77
|
-
python dist/pyfirstaid.pyz
|
|
88
|
+
python pyfirstaid.pyz
|
|
78
89
|
```
|
|
79
90
|
|
|
80
91
|
### Options
|
|
@@ -104,6 +115,48 @@ Usually `pip` installed into a different Python, or your virtual environment isn
|
|
|
104
115
|
|
|
105
116
|
This checks whether a company proxy is intercepting HTTPS and whether your certificate settings point at real files.
|
|
106
117
|
|
|
118
|
+
**"`module 'random' has no attribute 'randint'`" (or the same with `json`, `requests`, ...)**
|
|
119
|
+
|
|
120
|
+
python -m pyfirstaid --only shadowing
|
|
121
|
+
|
|
122
|
+
A file in your folder with the same name as a real module (`random.py`, `json.py`, `requests.py`) is being imported instead of the real one. pyfirstaid names the file to rename.
|
|
123
|
+
|
|
124
|
+
**"I upgraded Python and now a package fails with `ImportError` / `undefined symbol` / `DLL load failed`."**
|
|
125
|
+
|
|
126
|
+
python -m pyfirstaid --only compiled,broken-installs
|
|
127
|
+
|
|
128
|
+
Compiled packages built for your *old* Python version, or half-removed installs, are the usual cause. You get the exact reinstall command.
|
|
129
|
+
|
|
130
|
+
**"`python3 --version` still shows the old version after I installed a new Python."**
|
|
131
|
+
|
|
132
|
+
python3 -m pyfirstaid --only path,python-version
|
|
133
|
+
|
|
134
|
+
Shows which Python `python` / `python3` actually start, warns about Apple's built-in Python on macOS and the Microsoft Store shortcut on Windows, and flags Pythons past end of life.
|
|
135
|
+
|
|
136
|
+
**"pip says ERROR: Could not install packages due to an OSError: Permission denied."**
|
|
137
|
+
|
|
138
|
+
python -m pyfirstaid --only permissions,venv
|
|
139
|
+
|
|
140
|
+
Tells you whether you're installing into a read-only system Python (use a venv or pipx) or into a venv that was created with `sudo`.
|
|
141
|
+
|
|
142
|
+
**"pip check shows conflicts and I don't know what to install."**
|
|
143
|
+
|
|
144
|
+
python -m pyfirstaid --only dependencies
|
|
145
|
+
|
|
146
|
+
Each conflict comes with a ready-to-run `pip install` command.
|
|
147
|
+
|
|
148
|
+
**"`UnicodeDecodeError` when reading a file, but it works on my colleague's machine."**
|
|
149
|
+
|
|
150
|
+
python -m pyfirstaid --only encoding
|
|
151
|
+
|
|
152
|
+
Your default text encoding is not UTF-8 (common on Windows and minimal Linux/Docker images). pyfirstaid shows the one-line fix.
|
|
153
|
+
|
|
154
|
+
**"My venv picks up packages I never installed in it."**
|
|
155
|
+
|
|
156
|
+
python -m pyfirstaid --only leaks
|
|
157
|
+
|
|
158
|
+
`PYTHONPATH` or `PYTHONHOME` set in your shell profile is the usual reason.
|
|
159
|
+
|
|
107
160
|
**"I'm reporting a bug and the maintainer asked for my environment details."**
|
|
108
161
|
|
|
109
162
|
python -m pyfirstaid --share
|
|
@@ -124,18 +177,22 @@ Download `pyfirstaid.pyz` from the [latest release](https://github.com/sai-sakal
|
|
|
124
177
|
|
|
125
178
|
## Checks
|
|
126
179
|
|
|
127
|
-
|
|
|
128
|
-
|
|
129
|
-
|
|
|
130
|
-
|
|
|
131
|
-
|
|
|
132
|
-
|
|
|
133
|
-
|
|
|
134
|
-
|
|
|
135
|
-
|
|
|
136
|
-
|
|
|
137
|
-
|
|
|
138
|
-
|
|
|
180
|
+
| Check | What it catches |
|
|
181
|
+
|---|---|
|
|
182
|
+
| `python-version` | Python past (or close to) end of life, pre-release Pythons, Apple's built-in Command Line Tools Python |
|
|
183
|
+
| `venv` | No venv active, an unused `.venv` in the folder, a *different* venv activated in your shell, a system Python that blocks pip (PEP 668) |
|
|
184
|
+
| `pip-mismatch` | `pip` installs into a different Python than the one you run, pip missing, a broken `pip` command |
|
|
185
|
+
| `path` | Typing `python` / `python3` runs a *different* Python, or the Windows Microsoft Store shortcut |
|
|
186
|
+
| `leaks` | `PYTHONPATH` / `PYTHONHOME` set, venvs that can see system packages |
|
|
187
|
+
| `shadowing` | Files like `random.py` or `requests.py` in your folder that hide real modules |
|
|
188
|
+
| `ssl` | Certificate failures reaching PyPI (corporate proxies), certificate variables pointing at missing files, macOS certificates not installed, Python built without SSL |
|
|
189
|
+
| `compiled` | Packages with compiled files built for a different Python version |
|
|
190
|
+
| `broken-installs` | Packages installed twice, `~` leftovers from interrupted installs, corrupted metadata |
|
|
191
|
+
| `dependencies` | Dependency conflicts (`pip check`), each with a suggested fix |
|
|
192
|
+
| `permissions` | Virtual environments you can't install into (e.g. created with sudo) |
|
|
193
|
+
| `encoding` | Default text encoding that is not UTF-8 |
|
|
194
|
+
|
|
195
|
+
Run one or a few: `python -m pyfirstaid --only shadowing,path`. List them all: `python -m pyfirstaid --list`.
|
|
139
196
|
|
|
140
197
|
## How is this different?
|
|
141
198
|
|
|
@@ -151,7 +208,7 @@ Download `pyfirstaid.pyz` from the [latest release](https://github.com/sai-sakal
|
|
|
151
208
|
|
|
152
209
|
## Design principles
|
|
153
210
|
|
|
154
|
-
- **Works when pip is broken.**
|
|
211
|
+
- **Works when pip is broken.** Install with `pip install pyfirstaid`, or run the single file `python pyfirstaid.pyz` without installing.
|
|
155
212
|
- **Zero dependencies.** Standard library only, Python 3.9+.
|
|
156
213
|
- **Every problem comes with a fix command,** not just a description.
|
|
157
214
|
- **Conservative.** It's better to miss an edge case than to raise a false alarm.
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# pyfirstaid 🩹
|
|
2
|
+
[](https://github.com/sai-sakalam/pyfirstaid/actions/workflows/ci.yml)
|
|
3
|
+
|
|
4
|
+
**First aid for broken Python environments.** One command tells you *what's broken* and *exactly how to fix it*.
|
|
5
|
+
|
|
6
|
+
> **Status: v0.2.** 12 checks, tested on Windows, macOS and Linux (Python 3.9–3.14).
|
|
7
|
+
> Feedback is very welcome in [Issues](https://github.com/sai-sakalam/pyfirstaid/issues). *Which problems do you hit most?*
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Why
|
|
12
|
+
|
|
13
|
+
"It works on my machine" usually comes down to a broken environment: `pip` installing into a different Python than you run, SSL errors behind a corporate proxy, or a virtual environment that isn't active.
|
|
14
|
+
|
|
15
|
+
The errors are cryptic, the fixes are scattered across Stack Overflow, and there's no single command that checks it all.
|
|
16
|
+
|
|
17
|
+
This was discussed on the Python forum: [Standard Library Health Check Module](https://discuss.python.org/t/standard-library-health-check-module/105153). The advice there was to start it as a package on PyPI.
|
|
18
|
+
|
|
19
|
+
## Example
|
|
20
|
+
|
|
21
|
+
A run inside a project folder where the venv's Python is running, `pip` belongs to the
|
|
22
|
+
system Python, and a file called `random.py` is hiding the real `random` module:
|
|
23
|
+
|
|
24
|
+
```console
|
|
25
|
+
$ python -m pyfirstaid --share
|
|
26
|
+
|
|
27
|
+
pyfirstaid 0.2.0: checking your Python environment
|
|
28
|
+
Python 3.13.1 (~/project/.venv/bin/python)
|
|
29
|
+
|
|
30
|
+
✔ Python 3.13.1 (supported until October 2029)
|
|
31
|
+
✔ Virtual environment active: ~/project/.venv
|
|
32
|
+
✘ `pip` installs into a DIFFERENT Python
|
|
33
|
+
pip -> /opt/homebrew/lib/python3.13/site-packages/pip (python 3.13)
|
|
34
|
+
python -> ~/project/.venv/bin/python (python 3.13)
|
|
35
|
+
fix: Use `python -m pip install <package>` instead of `pip install`. If a virtual environment should be active, activate it first.
|
|
36
|
+
! Typing `python3` runs a DIFFERENT Python than this one
|
|
37
|
+
python3 -> /opt/homebrew
|
|
38
|
+
this Python -> ~/project/.venv
|
|
39
|
+
fix: Activate the virtual environment you want, or call Python by its full path. To change the default, put the right Python's folder first in PATH.
|
|
40
|
+
✔ No PYTHONPATH / PYTHONHOME leaks
|
|
41
|
+
! `random.py` in this folder hides the standard library module `random`
|
|
42
|
+
`import random` will load your file instead, which causes confusing errors like "module 'random' has no attribute ...".
|
|
43
|
+
fix: Rename it (e.g. my_random.py) and delete any __pycache__ folder next to it.
|
|
44
|
+
✔ HTTPS to pypi.org works (OpenSSL 3.4.0 22 Oct 2024)
|
|
45
|
+
✔ 12 compiled package(s) match this Python
|
|
46
|
+
✔ No broken or duplicate installs (1 folder(s) scanned)
|
|
47
|
+
✔ No dependency conflicts (pip check)
|
|
48
|
+
✔ site-packages is writable
|
|
49
|
+
✔ Default text encoding is UTF-8
|
|
50
|
+
|
|
51
|
+
1 problem(s), 2 warning(s).
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Try it
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install pyfirstaid # or: pipx install pyfirstaid
|
|
58
|
+
python -m pyfirstaid
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
No install possible (for example, pip itself is broken)? Download `pyfirstaid.pyz` from the
|
|
62
|
+
[latest release](https://github.com/sai-sakalam/pyfirstaid/releases) and run:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
python pyfirstaid.pyz
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Options
|
|
69
|
+
|
|
70
|
+
| Option | What it does |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `--share` | Hides your username and home folder, so the report is safe to paste into a bug report |
|
|
73
|
+
| `--json` | Machine-readable output for CI and scripts |
|
|
74
|
+
| `--offline` | Skips checks that need the internet |
|
|
75
|
+
| `--strict` | Exits with code 1 on warnings too (useful in CI) |
|
|
76
|
+
| `--only venv,ssl` / `--skip ssl` | Runs only some checks, or skips some |
|
|
77
|
+
| `--list` | Lists all checks |
|
|
78
|
+
|
|
79
|
+
Exit codes: `0` means no problems, `1` means problems were found, `2` means pyfirstaid itself failed.
|
|
80
|
+
|
|
81
|
+
## Common situations
|
|
82
|
+
|
|
83
|
+
**"I installed a package, but `import` says it doesn't exist."**
|
|
84
|
+
|
|
85
|
+
python -m pyfirstaid --only pip-mismatch,venv
|
|
86
|
+
|
|
87
|
+
Usually `pip` installed into a different Python, or your virtual environment isn't active. pyfirstaid tells you which, and how to fix it.
|
|
88
|
+
|
|
89
|
+
**"pip install fails with SSL: CERTIFICATE_VERIFY_FAILED at work."**
|
|
90
|
+
|
|
91
|
+
python -m pyfirstaid --only ssl
|
|
92
|
+
|
|
93
|
+
This checks whether a company proxy is intercepting HTTPS and whether your certificate settings point at real files.
|
|
94
|
+
|
|
95
|
+
**"`module 'random' has no attribute 'randint'`" (or the same with `json`, `requests`, ...)**
|
|
96
|
+
|
|
97
|
+
python -m pyfirstaid --only shadowing
|
|
98
|
+
|
|
99
|
+
A file in your folder with the same name as a real module (`random.py`, `json.py`, `requests.py`) is being imported instead of the real one. pyfirstaid names the file to rename.
|
|
100
|
+
|
|
101
|
+
**"I upgraded Python and now a package fails with `ImportError` / `undefined symbol` / `DLL load failed`."**
|
|
102
|
+
|
|
103
|
+
python -m pyfirstaid --only compiled,broken-installs
|
|
104
|
+
|
|
105
|
+
Compiled packages built for your *old* Python version, or half-removed installs, are the usual cause. You get the exact reinstall command.
|
|
106
|
+
|
|
107
|
+
**"`python3 --version` still shows the old version after I installed a new Python."**
|
|
108
|
+
|
|
109
|
+
python3 -m pyfirstaid --only path,python-version
|
|
110
|
+
|
|
111
|
+
Shows which Python `python` / `python3` actually start, warns about Apple's built-in Python on macOS and the Microsoft Store shortcut on Windows, and flags Pythons past end of life.
|
|
112
|
+
|
|
113
|
+
**"pip says ERROR: Could not install packages due to an OSError: Permission denied."**
|
|
114
|
+
|
|
115
|
+
python -m pyfirstaid --only permissions,venv
|
|
116
|
+
|
|
117
|
+
Tells you whether you're installing into a read-only system Python (use a venv or pipx) or into a venv that was created with `sudo`.
|
|
118
|
+
|
|
119
|
+
**"pip check shows conflicts and I don't know what to install."**
|
|
120
|
+
|
|
121
|
+
python -m pyfirstaid --only dependencies
|
|
122
|
+
|
|
123
|
+
Each conflict comes with a ready-to-run `pip install` command.
|
|
124
|
+
|
|
125
|
+
**"`UnicodeDecodeError` when reading a file, but it works on my colleague's machine."**
|
|
126
|
+
|
|
127
|
+
python -m pyfirstaid --only encoding
|
|
128
|
+
|
|
129
|
+
Your default text encoding is not UTF-8 (common on Windows and minimal Linux/Docker images). pyfirstaid shows the one-line fix.
|
|
130
|
+
|
|
131
|
+
**"My venv picks up packages I never installed in it."**
|
|
132
|
+
|
|
133
|
+
python -m pyfirstaid --only leaks
|
|
134
|
+
|
|
135
|
+
`PYTHONPATH` or `PYTHONHOME` set in your shell profile is the usual reason.
|
|
136
|
+
|
|
137
|
+
**"I'm reporting a bug and the maintainer asked for my environment details."**
|
|
138
|
+
|
|
139
|
+
python -m pyfirstaid --share
|
|
140
|
+
|
|
141
|
+
Paste the output into the issue. Your username and home folder are hidden.
|
|
142
|
+
|
|
143
|
+
**"I want CI to fail if the environment is broken."**
|
|
144
|
+
|
|
145
|
+
python -m pyfirstaid --offline --strict --json > env-report.json
|
|
146
|
+
|
|
147
|
+
The exit code is `1` if there are problems, and the JSON report can be saved as a build artifact.
|
|
148
|
+
|
|
149
|
+
**"pip itself is broken, so I can't install anything."**
|
|
150
|
+
|
|
151
|
+
Download `pyfirstaid.pyz` from the [latest release](https://github.com/sai-sakalam/pyfirstaid/releases) and run:
|
|
152
|
+
|
|
153
|
+
python pyfirstaid.pyz
|
|
154
|
+
|
|
155
|
+
## Checks
|
|
156
|
+
|
|
157
|
+
| Check | What it catches |
|
|
158
|
+
|---|---|
|
|
159
|
+
| `python-version` | Python past (or close to) end of life, pre-release Pythons, Apple's built-in Command Line Tools Python |
|
|
160
|
+
| `venv` | No venv active, an unused `.venv` in the folder, a *different* venv activated in your shell, a system Python that blocks pip (PEP 668) |
|
|
161
|
+
| `pip-mismatch` | `pip` installs into a different Python than the one you run, pip missing, a broken `pip` command |
|
|
162
|
+
| `path` | Typing `python` / `python3` runs a *different* Python, or the Windows Microsoft Store shortcut |
|
|
163
|
+
| `leaks` | `PYTHONPATH` / `PYTHONHOME` set, venvs that can see system packages |
|
|
164
|
+
| `shadowing` | Files like `random.py` or `requests.py` in your folder that hide real modules |
|
|
165
|
+
| `ssl` | Certificate failures reaching PyPI (corporate proxies), certificate variables pointing at missing files, macOS certificates not installed, Python built without SSL |
|
|
166
|
+
| `compiled` | Packages with compiled files built for a different Python version |
|
|
167
|
+
| `broken-installs` | Packages installed twice, `~` leftovers from interrupted installs, corrupted metadata |
|
|
168
|
+
| `dependencies` | Dependency conflicts (`pip check`), each with a suggested fix |
|
|
169
|
+
| `permissions` | Virtual environments you can't install into (e.g. created with sudo) |
|
|
170
|
+
| `encoding` | Default text encoding that is not UTF-8 |
|
|
171
|
+
|
|
172
|
+
Run one or a few: `python -m pyfirstaid --only shadowing,path`. List them all: `python -m pyfirstaid --list`.
|
|
173
|
+
|
|
174
|
+
## How is this different?
|
|
175
|
+
|
|
176
|
+
| Tool | What it does | Gap pyfirstaid fills |
|
|
177
|
+
|---|---|---|
|
|
178
|
+
| `pip check` | Finds dependency conflicts | Only covers one kind of problem |
|
|
179
|
+
| `conda doctor` | Health checks for conda environments | Doesn't cover pip, venv or uv |
|
|
180
|
+
| pymedic | Lists environment info (versions, packages) | Reports, but doesn't diagnose or suggest fixes |
|
|
181
|
+
| pyenv-doctor | Early-stage environment checks | Single release so far |
|
|
182
|
+
| env-repair | Repairs conda and pip environments | Changes your environment; pyfirstaid only diagnoses, safely |
|
|
183
|
+
|
|
184
|
+
**pyfirstaid's focus:** diagnose the problem, explain it in plain English, and give a copy-paste fix.
|
|
185
|
+
|
|
186
|
+
## Design principles
|
|
187
|
+
|
|
188
|
+
- **Works when pip is broken.** Install with `pip install pyfirstaid`, or run the single file `python pyfirstaid.pyz` without installing.
|
|
189
|
+
- **Zero dependencies.** Standard library only, Python 3.9+.
|
|
190
|
+
- **Every problem comes with a fix command,** not just a description.
|
|
191
|
+
- **Conservative.** It's better to miss an edge case than to raise a false alarm.
|
|
192
|
+
- **Never crashes.** A check that fails internally is reported, and the rest still run.
|
|
193
|
+
- **Diagnose only.** It never changes your environment.
|
|
194
|
+
|
|
195
|
+
## Scope
|
|
196
|
+
|
|
197
|
+
**In:** pip, venv and uv on Windows, macOS and Linux.
|
|
198
|
+
**Out (for now):** conda (use `conda doctor`), Poetry and pyenv specifics, automatic repair.
|
|
199
|
+
|
|
200
|
+
## Contributing
|
|
201
|
+
|
|
202
|
+
- Hit an environment error? [Open an issue](https://github.com/sai-sakalam/pyfirstaid/issues) with the error message, the cause and the fix. Real cases decide which checks come next.
|
|
203
|
+
- Development: `python -m pip install -e ".[dev]"`, then `python -m pytest` and `ruff check src tests`.
|
|
204
|
+
- A new check is one file in `src/pyfirstaid/checks/` that returns a list of `Finding`s, registered in `checks/__init__.py`.
|
|
205
|
+
|
|
206
|
+
## License
|
|
207
|
+
|
|
208
|
+
MIT
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
# `python -m pyfirstaid` puts the current folder first on sys.path, so a user's
|
|
5
|
+
# random.py, json.py, ... would be imported instead of the real modules that
|
|
6
|
+
# pyfirstaid needs. Drop it: pyfirstaid itself is already imported at this point.
|
|
7
|
+
_cwd = os.path.normcase(os.path.abspath(os.getcwd()))
|
|
8
|
+
sys.path[:] = [p for p in sys.path
|
|
9
|
+
if p and os.path.normcase(os.path.abspath(p)) != _cwd]
|
|
10
|
+
|
|
11
|
+
from pyfirstaid.cli import main # noqa: E402
|
|
12
|
+
|
|
13
|
+
sys.exit(main())
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Registry of all checks. Add new checks to ALL_CHECKS."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
from pyfirstaid.checks import (
|
|
8
|
+
broken_installs,
|
|
9
|
+
compiled,
|
|
10
|
+
dependencies,
|
|
11
|
+
encoding,
|
|
12
|
+
leaks,
|
|
13
|
+
path_check,
|
|
14
|
+
permissions,
|
|
15
|
+
pip_mismatch,
|
|
16
|
+
python_version,
|
|
17
|
+
shadowing,
|
|
18
|
+
ssl_check,
|
|
19
|
+
venv,
|
|
20
|
+
)
|
|
21
|
+
from pyfirstaid.model import Check
|
|
22
|
+
|
|
23
|
+
ALL_CHECKS: List[Check] = [
|
|
24
|
+
Check(python_version.CHECK_ID, "Python version", python_version.run_check),
|
|
25
|
+
Check(venv.CHECK_ID, "Virtual environment", venv.run_check),
|
|
26
|
+
Check(pip_mismatch.CHECK_ID, "pip / python match", pip_mismatch.run_check),
|
|
27
|
+
Check(path_check.CHECK_ID, "python on PATH", path_check.run_check),
|
|
28
|
+
Check(leaks.CHECK_ID, "PYTHONPATH / PYTHONHOME", leaks.run_check),
|
|
29
|
+
Check(shadowing.CHECK_ID, "Files hiding real modules", shadowing.run_check),
|
|
30
|
+
Check(ssl_check.CHECK_ID, "SSL / HTTPS to PyPI", ssl_check.run_check),
|
|
31
|
+
Check(compiled.CHECK_ID, "Compiled packages", compiled.run_check),
|
|
32
|
+
Check(broken_installs.CHECK_ID, "Broken installs", broken_installs.run_check),
|
|
33
|
+
Check(dependencies.CHECK_ID, "Dependency conflicts", dependencies.run_check),
|
|
34
|
+
Check(permissions.CHECK_ID, "Install permissions", permissions.run_check),
|
|
35
|
+
Check(encoding.CHECK_ID, "Text encoding", encoding.run_check),
|
|
36
|
+
]
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Check: duplicate, half-removed or corrupted package installs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
from typing import Dict, List
|
|
8
|
+
|
|
9
|
+
from pyfirstaid.model import Finding, Options, Status
|
|
10
|
+
from pyfirstaid.util import site_package_dirs
|
|
11
|
+
|
|
12
|
+
CHECK_ID = "broken-installs"
|
|
13
|
+
|
|
14
|
+
_META_RE = re.compile(r"^(?P<name>.+?)-(?P<ver>[^-]+)\.(?:dist-info|egg-info)$")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def normalize(name: str) -> str:
|
|
18
|
+
return re.sub(r"[-_.]+", "-", name).lower()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def scan_dir(directory: str) -> List[Finding]:
|
|
22
|
+
findings: List[Finding] = []
|
|
23
|
+
try:
|
|
24
|
+
entries = os.listdir(directory)
|
|
25
|
+
except OSError:
|
|
26
|
+
return findings
|
|
27
|
+
|
|
28
|
+
versions: Dict[str, List[str]] = {}
|
|
29
|
+
for entry in entries:
|
|
30
|
+
path = os.path.join(directory, entry)
|
|
31
|
+
if entry.startswith("~"):
|
|
32
|
+
findings.append(Finding(
|
|
33
|
+
CHECK_ID, Status.WARN, "Leftover from an interrupted install: %s" % entry,
|
|
34
|
+
detail="pip renames packages to '~...' while removing them; this one was "
|
|
35
|
+
"never cleaned up and can break imports.",
|
|
36
|
+
fix="Delete the folder: %s" % path,
|
|
37
|
+
))
|
|
38
|
+
continue
|
|
39
|
+
m = _META_RE.match(entry)
|
|
40
|
+
if not m:
|
|
41
|
+
continue
|
|
42
|
+
versions.setdefault(normalize(m.group("name")), []).append(entry)
|
|
43
|
+
if entry.endswith(".dist-info") and not os.path.isfile(os.path.join(path, "METADATA")):
|
|
44
|
+
findings.append(Finding(
|
|
45
|
+
CHECK_ID, Status.ERROR, "Corrupted install: %s has no METADATA" % entry,
|
|
46
|
+
fix="python -m pip install --force-reinstall %s" % m.group("name"),
|
|
47
|
+
))
|
|
48
|
+
|
|
49
|
+
for name, metas in sorted(versions.items()):
|
|
50
|
+
if len(metas) > 1:
|
|
51
|
+
findings.append(Finding(
|
|
52
|
+
CHECK_ID, Status.WARN, "%s is installed more than once" % name,
|
|
53
|
+
detail="in %s:\n%s" % (directory, "\n".join(sorted(metas))),
|
|
54
|
+
fix="python -m pip uninstall -y %s, repeat until it is gone, "
|
|
55
|
+
"then python -m pip install %s" % (name, name),
|
|
56
|
+
))
|
|
57
|
+
return findings
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def run_check(opts: Options) -> List[Finding]:
|
|
61
|
+
findings: List[Finding] = []
|
|
62
|
+
dirs = site_package_dirs()
|
|
63
|
+
for d in dirs:
|
|
64
|
+
findings.extend(scan_dir(d))
|
|
65
|
+
if not findings:
|
|
66
|
+
findings.append(Finding(
|
|
67
|
+
CHECK_ID, Status.OK,
|
|
68
|
+
"No broken or duplicate installs (%d folder(s) scanned)" % len(dirs)))
|
|
69
|
+
return findings
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Check: compiled extension files built for a different Python version."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.machinery
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
from typing import Dict, List, Optional, Sequence, Set
|
|
9
|
+
|
|
10
|
+
from pyfirstaid.model import Finding, Options, Status
|
|
11
|
+
|
|
12
|
+
CHECK_ID = "compiled"
|
|
13
|
+
|
|
14
|
+
# Matches version-tagged extension files, e.g. _core.cpython-312-x86_64-linux-gnu.so,
|
|
15
|
+
# _core.cp312-win_amd64.pyd, _core.abi3.so. Untagged "_core.so" is always accepted.
|
|
16
|
+
_TAG_RE = re.compile(r"\.((?:cpython-|cp|pypy|graalpy)[0-9][^.]*|abi3)\.(so|pyd)$")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def mismatched_tag(filename: str, valid_suffixes: Sequence[str]) -> Optional[str]:
|
|
20
|
+
"""Return the ABI tag if `filename` is tagged for a different interpreter, else None."""
|
|
21
|
+
base = os.path.basename(filename.replace("\\", "/"))
|
|
22
|
+
m = _TAG_RE.search(base)
|
|
23
|
+
if not m:
|
|
24
|
+
return None
|
|
25
|
+
suffix = ".%s.%s" % (m.group(1), m.group(2))
|
|
26
|
+
return None if suffix in valid_suffixes else m.group(1)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def running_tag() -> str:
|
|
30
|
+
for s in importlib.machinery.EXTENSION_SUFFIXES:
|
|
31
|
+
m = _TAG_RE.search("x" + s)
|
|
32
|
+
if m and m.group(1) != "abi3":
|
|
33
|
+
return m.group(1)
|
|
34
|
+
return "this Python"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def run_check(opts: Options) -> List[Finding]:
|
|
38
|
+
import importlib.metadata as md
|
|
39
|
+
|
|
40
|
+
valid = importlib.machinery.EXTENSION_SUFFIXES
|
|
41
|
+
bad: Dict[str, Set[str]] = {}
|
|
42
|
+
compiled_count = 0
|
|
43
|
+
seen: Set[str] = set()
|
|
44
|
+
|
|
45
|
+
for dist in md.distributions():
|
|
46
|
+
name = (dist.metadata["Name"] or "").strip()
|
|
47
|
+
if not name or name.lower() in seen:
|
|
48
|
+
continue
|
|
49
|
+
seen.add(name.lower())
|
|
50
|
+
has_ext = False
|
|
51
|
+
for f in dist.files or []:
|
|
52
|
+
s = str(f)
|
|
53
|
+
if not s.endswith((".so", ".pyd")) or ".libs/" in s.replace("\\", "/"):
|
|
54
|
+
continue
|
|
55
|
+
has_ext = True
|
|
56
|
+
tag = mismatched_tag(s, valid)
|
|
57
|
+
if tag:
|
|
58
|
+
bad.setdefault(name, set()).add(tag)
|
|
59
|
+
compiled_count += has_ext
|
|
60
|
+
|
|
61
|
+
if not bad:
|
|
62
|
+
return [Finding(CHECK_ID, Status.OK,
|
|
63
|
+
"%d compiled package(s) match this Python" % compiled_count)]
|
|
64
|
+
|
|
65
|
+
findings = []
|
|
66
|
+
for name, tags in sorted(bad.items())[:10]:
|
|
67
|
+
findings.append(Finding(
|
|
68
|
+
CHECK_ID, Status.ERROR,
|
|
69
|
+
"%s was built for a different Python" % name,
|
|
70
|
+
detail="files are tagged %s, but this is %s" % (", ".join(sorted(tags)), running_tag()),
|
|
71
|
+
fix="python -m pip install --force-reinstall --no-cache-dir %s" % name,
|
|
72
|
+
))
|
|
73
|
+
if len(bad) > 10:
|
|
74
|
+
findings.append(Finding(CHECK_ID, Status.INFO,
|
|
75
|
+
"...and %d more packages with the same problem" % (len(bad) - 10)))
|
|
76
|
+
return findings
|