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.
Files changed (35) hide show
  1. pyfirstaid-0.2.0/.gitignore +9 -0
  2. pyfirstaid-0.2.0/CHANGELOG.md +31 -0
  3. {pyfirstaid-0.1.0 → pyfirstaid-0.2.0}/PKG-INFO +87 -30
  4. pyfirstaid-0.2.0/README.md +208 -0
  5. {pyfirstaid-0.1.0 → pyfirstaid-0.2.0}/src/pyfirstaid/__init__.py +1 -1
  6. pyfirstaid-0.2.0/src/pyfirstaid/__main__.py +13 -0
  7. pyfirstaid-0.2.0/src/pyfirstaid/checks/__init__.py +36 -0
  8. pyfirstaid-0.2.0/src/pyfirstaid/checks/broken_installs.py +69 -0
  9. pyfirstaid-0.2.0/src/pyfirstaid/checks/compiled.py +76 -0
  10. pyfirstaid-0.2.0/src/pyfirstaid/checks/dependencies.py +42 -0
  11. pyfirstaid-0.2.0/src/pyfirstaid/checks/encoding.py +33 -0
  12. pyfirstaid-0.2.0/src/pyfirstaid/checks/leaks.py +62 -0
  13. pyfirstaid-0.2.0/src/pyfirstaid/checks/path_check.py +60 -0
  14. pyfirstaid-0.2.0/src/pyfirstaid/checks/permissions.py +37 -0
  15. pyfirstaid-0.2.0/src/pyfirstaid/checks/python_version.py +91 -0
  16. pyfirstaid-0.2.0/src/pyfirstaid/checks/shadowing.py +86 -0
  17. {pyfirstaid-0.1.0 → pyfirstaid-0.2.0}/src/pyfirstaid/cli.py +1 -1
  18. {pyfirstaid-0.1.0 → pyfirstaid-0.2.0}/src/pyfirstaid/util.py +26 -0
  19. pyfirstaid-0.2.0/tests/test_more_checks.py +148 -0
  20. pyfirstaid-0.1.0/CHANGELOG.md +0 -14
  21. pyfirstaid-0.1.0/README.md +0 -151
  22. pyfirstaid-0.1.0/src/pyfirstaid/__main__.py +0 -5
  23. pyfirstaid-0.1.0/src/pyfirstaid/checks/__init__.py +0 -14
  24. {pyfirstaid-0.1.0 → pyfirstaid-0.2.0}/.github/workflows/ci.yml +0 -0
  25. {pyfirstaid-0.1.0 → pyfirstaid-0.2.0}/.github/workflows/publish.yml +0 -0
  26. {pyfirstaid-0.1.0 → pyfirstaid-0.2.0}/LICENSE +0 -0
  27. {pyfirstaid-0.1.0 → pyfirstaid-0.2.0}/pyproject.toml +0 -0
  28. {pyfirstaid-0.1.0 → pyfirstaid-0.2.0}/scripts/build_pyz.py +0 -0
  29. {pyfirstaid-0.1.0 → pyfirstaid-0.2.0}/src/pyfirstaid/checks/pip_mismatch.py +0 -0
  30. {pyfirstaid-0.1.0 → pyfirstaid-0.2.0}/src/pyfirstaid/checks/ssl_check.py +0 -0
  31. {pyfirstaid-0.1.0 → pyfirstaid-0.2.0}/src/pyfirstaid/checks/venv.py +0 -0
  32. {pyfirstaid-0.1.0 → pyfirstaid-0.2.0}/src/pyfirstaid/model.py +0 -0
  33. {pyfirstaid-0.1.0 → pyfirstaid-0.2.0}/src/pyfirstaid/report.py +0 -0
  34. {pyfirstaid-0.1.0 → pyfirstaid-0.2.0}/tests/test_checks.py +0 -0
  35. {pyfirstaid-0.1.0 → pyfirstaid-0.2.0}/tests/test_cli.py +0 -0
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .venv/
5
+ venv/
6
+ build/
7
+ dist/
8
+ .pytest_cache/
9
+ .ruff_cache/
@@ -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.1.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.1, early.** 3 checks work today, and more are on the roadmap.
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 real run: the virtual environment's Python is running, but the `pip` command on PATH belongs to the system Python:
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.1.0: checking your Python environment
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 -> /usr/lib/python3/dist-packages/pip (python 3.13)
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
- ✔ HTTPS to pypi.org works (OpenSSL 3.0.13 30 Jan 2024)
58
-
59
- 1 problem(s), 0 warning(s).
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
- git clone https://github.com/sai-sakalam/pyfirstaid
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
- Or build the **single file**, which needs no install and works even when pip is broken:
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 scripts/build_pyz.py
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
- | Status | Check | What it catches |
128
- |---|---|---|
129
- | ✅ v0.1 | `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) |
130
- | ✅ v0.1 | `pip-mismatch` | `pip` installs into a different Python than the one you run, pip missing, a broken `pip` command |
131
- | ✅ v0.1 | `ssl` | Certificate failures reaching PyPI (corporate proxies), certificate variables pointing at missing files, macOS certificates not installed, Python built without SSL |
132
- | 🔜 planned | `compiled` | Compiled packages built for a different Python version |
133
- | 🔜 planned | `broken-installs` | Duplicate or half-removed packages |
134
- | 🔜 planned | `dependencies` | Dependency conflicts (`pip check`) |
135
- | 🔜 planned | `path` | Several Pythons on PATH hiding each other |
136
- | 🔜 planned | `leaks` | `PYTHONPATH` or user-site packages leaking into a venv |
137
- | 🔜 planned | `permissions` | No write access to site-packages |
138
- | 🔜 planned | `encoding` | Locale and encoding problems |
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.** It runs as a single file (`python pyfirstaid.pyz`). A PyPI release is coming.
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
+ [![CI](https://github.com/sai-sakalam/pyfirstaid/actions/workflows/ci.yml/badge.svg)](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
@@ -1,3 +1,3 @@
1
1
  """pyfirstaid: first aid for broken Python environments."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.2.0"
@@ -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