pyfirstaid 0.2.3__tar.gz → 0.4.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.3 → pyfirstaid-0.4.0}/.github/workflows/ci.yml +8 -0
- {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/.github/workflows/publish.yml +1 -1
- {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/CHANGELOG.md +30 -0
- {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/PKG-INFO +39 -10
- {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/README.md +36 -8
- {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/pyproject.toml +3 -1
- {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/src/pyfirstaid/__init__.py +1 -1
- {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/src/pyfirstaid/checks/__init__.py +10 -0
- pyfirstaid-0.4.0/src/pyfirstaid/checks/conda.py +166 -0
- pyfirstaid-0.4.0/src/pyfirstaid/checks/jupyter.py +183 -0
- {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/src/pyfirstaid/checks/pip_mismatch.py +8 -1
- pyfirstaid-0.4.0/src/pyfirstaid/checks/poetry.py +153 -0
- pyfirstaid-0.4.0/src/pyfirstaid/checks/pyenv.py +125 -0
- pyfirstaid-0.4.0/src/pyfirstaid/checks/uv.py +100 -0
- {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/src/pyfirstaid/report.py +15 -1
- pyfirstaid-0.4.0/src/pyfirstaid/util.py +162 -0
- {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/tests/test_cli.py +16 -0
- pyfirstaid-0.4.0/tests/test_conda.py +139 -0
- pyfirstaid-0.4.0/tests/test_jupyter.py +115 -0
- {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/tests/test_more_checks.py +1 -1
- pyfirstaid-0.4.0/tests/test_project_tools.py +290 -0
- pyfirstaid-0.2.3/src/pyfirstaid/util.py +0 -81
- {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/.gitignore +0 -0
- {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/LICENSE +0 -0
- {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/scripts/build_pyz.py +0 -0
- {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/src/pyfirstaid/__main__.py +0 -0
- {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/src/pyfirstaid/checks/broken_installs.py +0 -0
- {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/src/pyfirstaid/checks/compiled.py +0 -0
- {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/src/pyfirstaid/checks/dependencies.py +0 -0
- {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/src/pyfirstaid/checks/encoding.py +0 -0
- {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/src/pyfirstaid/checks/leaks.py +0 -0
- {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/src/pyfirstaid/checks/path_check.py +0 -0
- {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/src/pyfirstaid/checks/permissions.py +0 -0
- {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/src/pyfirstaid/checks/python_version.py +0 -0
- {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/src/pyfirstaid/checks/shadowing.py +0 -0
- {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/src/pyfirstaid/checks/ssl_check.py +0 -0
- {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/src/pyfirstaid/checks/venv.py +0 -0
- {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/src/pyfirstaid/cli.py +0 -0
- {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/src/pyfirstaid/delegate.py +0 -0
- {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/src/pyfirstaid/model.py +0 -0
- {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/tests/test_checks.py +0 -0
- {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/tests/test_delegate.py +0 -0
|
@@ -13,6 +13,14 @@ jobs:
|
|
|
13
13
|
matrix:
|
|
14
14
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
15
15
|
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
16
|
+
# Python 3.9 is end-of-life and not built for ubuntu-latest (26.04),
|
|
17
|
+
# so test it on Ubuntu 24.04 instead.
|
|
18
|
+
exclude:
|
|
19
|
+
- os: ubuntu-latest
|
|
20
|
+
python-version: "3.9"
|
|
21
|
+
include:
|
|
22
|
+
- os: ubuntu-24.04
|
|
23
|
+
python-version: "3.9"
|
|
16
24
|
steps:
|
|
17
25
|
- uses: actions/checkout@v4
|
|
18
26
|
- uses: actions/setup-python@v5
|
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
Four new checks for the tools people use to manage Pythons and projects. Like the conda
|
|
6
|
+
check, they only read files on disk and run no uv, Poetry, pyenv or Jupyter commands.
|
|
7
|
+
|
|
8
|
+
- **New `jupyter` check**: Jupyter kernels whose Python no longer exists (deleted venv,
|
|
9
|
+
upgraded Python), an environment that has ipykernel but is not registered as a kernel,
|
|
10
|
+
and several kernels with the same name in the kernel picker.
|
|
11
|
+
- **New `uv` check**: a uv project whose `.venv` was never created, is broken (its base
|
|
12
|
+
Python is gone), or is not the Python running. Also respects `UV_PROJECT_ENVIRONMENT`.
|
|
13
|
+
- **New `poetry` check**: finds the project's Poetry environment (in-project `.venv` or
|
|
14
|
+
Poetry's cache folder), and warns when it is broken or not the Python running.
|
|
15
|
+
- **New `pyenv` check**: a `.python-version` file (or `PYENV_VERSION`) asking for a different
|
|
16
|
+
Python than the one running, a pinned version pyenv hasn't installed, and pyenv not set
|
|
17
|
+
up in the shell. uv reads `.python-version` too, so this also helps uv users.
|
|
18
|
+
- `pip-mismatch`: in an environment made by uv (which has no pip by design), it now
|
|
19
|
+
suggests `uv pip install` instead of reporting pip as missing.
|
|
20
|
+
- Report: checks for tools you don't use are shown on one line ("Not used here: ...").
|
|
21
|
+
Multi-line fixes are indented properly.
|
|
22
|
+
|
|
23
|
+
## 0.3.0
|
|
24
|
+
|
|
25
|
+
- **New `conda` check** for problems from mixing conda and pip (something `conda doctor`
|
|
26
|
+
doesn't look at):
|
|
27
|
+
- a package conda installed that pip later replaced, so both tools think they own it,
|
|
28
|
+
- pip packages installed into the conda *base* environment,
|
|
29
|
+
- a conda environment that is activated, but isn't the Python actually running.
|
|
30
|
+
It only reads files on disk, runs no conda or pip commands, and stays quiet when unsure.
|
|
31
|
+
- README: up-to-date example output; author link on PyPI.
|
|
32
|
+
|
|
3
33
|
## 0.2.3
|
|
4
34
|
|
|
5
35
|
- Clearer PEP 668 message: it now says the block can come from your OS *or* a package
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pyfirstaid
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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: Source, https://github.com/sai-sakalam/pyfirstaid
|
|
7
7
|
Project-URL: Issues, https://github.com/sai-sakalam/pyfirstaid/issues
|
|
8
8
|
Project-URL: Changelog, https://github.com/sai-sakalam/pyfirstaid/blob/main/CHANGELOG.md
|
|
9
|
+
Project-URL: Author, https://sai-sakalam.github.io/
|
|
9
10
|
Author: Sai Gavaskar Sakalam
|
|
10
11
|
License-Expression: MIT
|
|
11
12
|
License-File: LICENSE
|
|
12
|
-
Keywords: diagnostics,environment,pip,ssl,troubleshooting,venv,virtualenv
|
|
13
|
+
Keywords: conda,diagnostics,environment,jupyter,pip,poetry,pyenv,ssl,troubleshooting,uv,venv,virtualenv
|
|
13
14
|
Classifier: Development Status :: 3 - Alpha
|
|
14
15
|
Classifier: Environment :: Console
|
|
15
16
|
Classifier: Intended Audience :: Developers
|
|
@@ -32,14 +33,14 @@ Description-Content-Type: text/markdown
|
|
|
32
33
|
|
|
33
34
|
# pyfirstaid 🩹
|
|
34
35
|
[](https://pypi.org/project/pyfirstaid/)
|
|
35
|
-
[](https://pepy.tech/projects/pyfirstaid)
|
|
36
|
+
[](https://pypi.org/project/pyfirstaid/)
|
|
37
37
|
[](https://github.com/sai-sakalam/pyfirstaid/actions/workflows/ci.yml)
|
|
38
38
|
[](https://github.com/sai-sakalam/pyfirstaid/blob/main/LICENSE)
|
|
39
|
+
[](https://hub.xerj.org/contributors/sai-sakalam)
|
|
39
40
|
|
|
40
41
|
**First aid for broken Python environments.** One command tells you *what's broken* and *exactly how to fix it*.
|
|
41
42
|
|
|
42
|
-
> **Status: early, actively developed.**
|
|
43
|
+
> **Status: early, actively developed.** 17 checks, tested on Windows, macOS and Linux (Python 3.9–3.14).
|
|
43
44
|
> Feedback is very welcome in [Issues](https://github.com/sai-sakalam/pyfirstaid/issues). *Which problems do you hit most?*
|
|
44
45
|
|
|
45
46
|
---
|
|
@@ -60,7 +61,7 @@ system Python, and a file called `random.py` is hiding the real `random` module:
|
|
|
60
61
|
```console
|
|
61
62
|
$ python -m pyfirstaid --share
|
|
62
63
|
|
|
63
|
-
pyfirstaid 0.
|
|
64
|
+
pyfirstaid 0.4.0: checking your Python environment
|
|
64
65
|
Python 3.13.1 (~/project/.venv/bin/python)
|
|
65
66
|
|
|
66
67
|
✔ Python 3.13.1 (supported until October 2029)
|
|
@@ -83,6 +84,7 @@ Python 3.13.1 (~/project/.venv/bin/python)
|
|
|
83
84
|
✔ No dependency conflicts (pip check)
|
|
84
85
|
✔ site-packages is writable
|
|
85
86
|
✔ Default text encoding is UTF-8
|
|
87
|
+
- Not used here: conda, uv, poetry, pyenv, jupyter
|
|
86
88
|
|
|
87
89
|
1 problem(s), 2 warning(s).
|
|
88
90
|
```
|
|
@@ -162,6 +164,24 @@ Shows which Python `python` / `python3` actually start, warns about Apple's buil
|
|
|
162
164
|
|
|
163
165
|
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`.
|
|
164
166
|
|
|
167
|
+
**"I installed a package, but my Jupyter notebook still says `ModuleNotFoundError`."**
|
|
168
|
+
|
|
169
|
+
python -m pyfirstaid --only jupyter,pip-mismatch
|
|
170
|
+
|
|
171
|
+
The notebook runs a *kernel*, and the kernel may use a different Python than your terminal. pyfirstaid finds kernels that point at a deleted or moved Python, and tells you when your environment isn't registered as a kernel at all. Tip: inside a notebook, use `%pip install <package>` (not `!pip install`), so it installs into the kernel's Python.
|
|
172
|
+
|
|
173
|
+
**"It works with `uv run` / `poetry run`, but not when I run `python` myself."**
|
|
174
|
+
|
|
175
|
+
python -m pyfirstaid --only uv,poetry,venv
|
|
176
|
+
|
|
177
|
+
The project's packages live in its own environment. pyfirstaid tells you whether the Python running is that environment, whether it exists yet (`uv sync` / `poetry install`), or whether it broke after a Python upgrade.
|
|
178
|
+
|
|
179
|
+
**"pyenv says one version, but `python --version` shows another."**
|
|
180
|
+
|
|
181
|
+
python -m pyfirstaid --only pyenv,path
|
|
182
|
+
|
|
183
|
+
Compares the Python running with your `.python-version` file (or `PYENV_VERSION`), and tells you if the version isn't installed in pyenv or pyenv isn't set up in your shell.
|
|
184
|
+
|
|
165
185
|
**"pip check shows conflicts and I don't know what to install."**
|
|
166
186
|
|
|
167
187
|
python -m pyfirstaid --only dependencies
|
|
@@ -204,7 +224,12 @@ Download `pyfirstaid.pyz` from the [latest release](https://github.com/sai-sakal
|
|
|
204
224
|
|---|---|
|
|
205
225
|
| `python-version` | Python past (or close to) end of life, pre-release Pythons, Apple's built-in Command Line Tools Python |
|
|
206
226
|
| `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) |
|
|
207
|
-
| `
|
|
227
|
+
| `conda` | A package conda installed that pip later replaced, pip packages in the conda *base* environment, a conda environment that is activated but isn't the Python running |
|
|
228
|
+
| `uv` | A uv project whose environment isn't created yet, is broken (its base Python is gone), or isn't the Python running |
|
|
229
|
+
| `poetry` | A Poetry project whose environment is missing, broken, or isn't the Python running |
|
|
230
|
+
| `pyenv` | `.python-version` / `PYENV_VERSION` asking for a different Python than the one running, a version pyenv hasn't installed, pyenv not set up in your shell |
|
|
231
|
+
| `jupyter` | Kernels pointing to a Python that no longer exists, an environment with ipykernel that isn't registered as a kernel, kernels with the same name |
|
|
232
|
+
| `pip-mismatch` | `pip` installs into a different Python than the one you run, pip missing (with the `uv pip` fix in uv environments), a broken `pip` command |
|
|
208
233
|
| `path` | Typing `python` / `python3` runs a *different* Python, or the Windows Microsoft Store shortcut |
|
|
209
234
|
| `leaks` | `PYTHONPATH` / `PYTHONHOME` set, venvs that can see system packages |
|
|
210
235
|
| `shadowing` | Files like `random.py` or `requests.py` in your folder that hide real modules |
|
|
@@ -222,7 +247,7 @@ Run one or a few: `python -m pyfirstaid --only shadowing,path`. List them all: `
|
|
|
222
247
|
| Tool | What it does | Gap pyfirstaid fills |
|
|
223
248
|
|---|---|---|
|
|
224
249
|
| `pip check` | Finds dependency conflicts | Only covers one kind of problem |
|
|
225
|
-
| `conda doctor` | Health checks for conda environments | Doesn't
|
|
250
|
+
| `conda doctor` | Health checks for conda environments | Doesn't check what pip did inside conda, or venv / uv |
|
|
226
251
|
| pymedic | Lists environment info (versions, packages) | Reports, but doesn't diagnose or suggest fixes |
|
|
227
252
|
| pyenv-doctor | Early-stage environment checks | Single release so far |
|
|
228
253
|
| env-repair | Repairs conda and pip environments | Changes your environment; pyfirstaid only diagnoses, safely |
|
|
@@ -240,8 +265,8 @@ Run one or a few: `python -m pyfirstaid --only shadowing,path`. List them all: `
|
|
|
240
265
|
|
|
241
266
|
## Scope
|
|
242
267
|
|
|
243
|
-
**In:** pip, venv and uv on Windows, macOS and Linux.
|
|
244
|
-
**Out (for now):** conda (use `conda doctor`), Poetry and pyenv specifics, automatic repair.
|
|
268
|
+
**In:** pip, venv and uv on Windows, macOS and Linux, plus conda / pip mixing problems.
|
|
269
|
+
**Out (for now):** conda's own internals (use `conda doctor`), Poetry and pyenv specifics, automatic repair.
|
|
245
270
|
|
|
246
271
|
## Contributing
|
|
247
272
|
|
|
@@ -249,6 +274,10 @@ Run one or a few: `python -m pyfirstaid --only shadowing,path`. List them all: `
|
|
|
249
274
|
- Development: `python -m pip install -e ".[dev]"`, then `python -m pytest` and `ruff check src tests`.
|
|
250
275
|
- A new check is one file in `src/pyfirstaid/checks/` that returns a list of `Finding`s, registered in `checks/__init__.py`.
|
|
251
276
|
|
|
277
|
+
## Author
|
|
278
|
+
|
|
279
|
+
Made by [Sai Sakalam](https://sai-sakalam.github.io/), a data engineer who kept fixing the same broken Python environments for colleagues and decided to automate it. Feedback and ideas are always welcome.
|
|
280
|
+
|
|
252
281
|
## License
|
|
253
282
|
|
|
254
283
|
MIT
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# pyfirstaid 🩹
|
|
2
2
|
[](https://pypi.org/project/pyfirstaid/)
|
|
3
|
-
[](https://pepy.tech/projects/pyfirstaid)
|
|
3
|
+
[](https://pypi.org/project/pyfirstaid/)
|
|
5
4
|
[](https://github.com/sai-sakalam/pyfirstaid/actions/workflows/ci.yml)
|
|
6
5
|
[](https://github.com/sai-sakalam/pyfirstaid/blob/main/LICENSE)
|
|
6
|
+
[](https://hub.xerj.org/contributors/sai-sakalam)
|
|
7
7
|
|
|
8
8
|
**First aid for broken Python environments.** One command tells you *what's broken* and *exactly how to fix it*.
|
|
9
9
|
|
|
10
|
-
> **Status: early, actively developed.**
|
|
10
|
+
> **Status: early, actively developed.** 17 checks, tested on Windows, macOS and Linux (Python 3.9–3.14).
|
|
11
11
|
> Feedback is very welcome in [Issues](https://github.com/sai-sakalam/pyfirstaid/issues). *Which problems do you hit most?*
|
|
12
12
|
|
|
13
13
|
---
|
|
@@ -28,7 +28,7 @@ system Python, and a file called `random.py` is hiding the real `random` module:
|
|
|
28
28
|
```console
|
|
29
29
|
$ python -m pyfirstaid --share
|
|
30
30
|
|
|
31
|
-
pyfirstaid 0.
|
|
31
|
+
pyfirstaid 0.4.0: checking your Python environment
|
|
32
32
|
Python 3.13.1 (~/project/.venv/bin/python)
|
|
33
33
|
|
|
34
34
|
✔ Python 3.13.1 (supported until October 2029)
|
|
@@ -51,6 +51,7 @@ Python 3.13.1 (~/project/.venv/bin/python)
|
|
|
51
51
|
✔ No dependency conflicts (pip check)
|
|
52
52
|
✔ site-packages is writable
|
|
53
53
|
✔ Default text encoding is UTF-8
|
|
54
|
+
- Not used here: conda, uv, poetry, pyenv, jupyter
|
|
54
55
|
|
|
55
56
|
1 problem(s), 2 warning(s).
|
|
56
57
|
```
|
|
@@ -130,6 +131,24 @@ Shows which Python `python` / `python3` actually start, warns about Apple's buil
|
|
|
130
131
|
|
|
131
132
|
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`.
|
|
132
133
|
|
|
134
|
+
**"I installed a package, but my Jupyter notebook still says `ModuleNotFoundError`."**
|
|
135
|
+
|
|
136
|
+
python -m pyfirstaid --only jupyter,pip-mismatch
|
|
137
|
+
|
|
138
|
+
The notebook runs a *kernel*, and the kernel may use a different Python than your terminal. pyfirstaid finds kernels that point at a deleted or moved Python, and tells you when your environment isn't registered as a kernel at all. Tip: inside a notebook, use `%pip install <package>` (not `!pip install`), so it installs into the kernel's Python.
|
|
139
|
+
|
|
140
|
+
**"It works with `uv run` / `poetry run`, but not when I run `python` myself."**
|
|
141
|
+
|
|
142
|
+
python -m pyfirstaid --only uv,poetry,venv
|
|
143
|
+
|
|
144
|
+
The project's packages live in its own environment. pyfirstaid tells you whether the Python running is that environment, whether it exists yet (`uv sync` / `poetry install`), or whether it broke after a Python upgrade.
|
|
145
|
+
|
|
146
|
+
**"pyenv says one version, but `python --version` shows another."**
|
|
147
|
+
|
|
148
|
+
python -m pyfirstaid --only pyenv,path
|
|
149
|
+
|
|
150
|
+
Compares the Python running with your `.python-version` file (or `PYENV_VERSION`), and tells you if the version isn't installed in pyenv or pyenv isn't set up in your shell.
|
|
151
|
+
|
|
133
152
|
**"pip check shows conflicts and I don't know what to install."**
|
|
134
153
|
|
|
135
154
|
python -m pyfirstaid --only dependencies
|
|
@@ -172,7 +191,12 @@ Download `pyfirstaid.pyz` from the [latest release](https://github.com/sai-sakal
|
|
|
172
191
|
|---|---|
|
|
173
192
|
| `python-version` | Python past (or close to) end of life, pre-release Pythons, Apple's built-in Command Line Tools Python |
|
|
174
193
|
| `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) |
|
|
175
|
-
| `
|
|
194
|
+
| `conda` | A package conda installed that pip later replaced, pip packages in the conda *base* environment, a conda environment that is activated but isn't the Python running |
|
|
195
|
+
| `uv` | A uv project whose environment isn't created yet, is broken (its base Python is gone), or isn't the Python running |
|
|
196
|
+
| `poetry` | A Poetry project whose environment is missing, broken, or isn't the Python running |
|
|
197
|
+
| `pyenv` | `.python-version` / `PYENV_VERSION` asking for a different Python than the one running, a version pyenv hasn't installed, pyenv not set up in your shell |
|
|
198
|
+
| `jupyter` | Kernels pointing to a Python that no longer exists, an environment with ipykernel that isn't registered as a kernel, kernels with the same name |
|
|
199
|
+
| `pip-mismatch` | `pip` installs into a different Python than the one you run, pip missing (with the `uv pip` fix in uv environments), a broken `pip` command |
|
|
176
200
|
| `path` | Typing `python` / `python3` runs a *different* Python, or the Windows Microsoft Store shortcut |
|
|
177
201
|
| `leaks` | `PYTHONPATH` / `PYTHONHOME` set, venvs that can see system packages |
|
|
178
202
|
| `shadowing` | Files like `random.py` or `requests.py` in your folder that hide real modules |
|
|
@@ -190,7 +214,7 @@ Run one or a few: `python -m pyfirstaid --only shadowing,path`. List them all: `
|
|
|
190
214
|
| Tool | What it does | Gap pyfirstaid fills |
|
|
191
215
|
|---|---|---|
|
|
192
216
|
| `pip check` | Finds dependency conflicts | Only covers one kind of problem |
|
|
193
|
-
| `conda doctor` | Health checks for conda environments | Doesn't
|
|
217
|
+
| `conda doctor` | Health checks for conda environments | Doesn't check what pip did inside conda, or venv / uv |
|
|
194
218
|
| pymedic | Lists environment info (versions, packages) | Reports, but doesn't diagnose or suggest fixes |
|
|
195
219
|
| pyenv-doctor | Early-stage environment checks | Single release so far |
|
|
196
220
|
| env-repair | Repairs conda and pip environments | Changes your environment; pyfirstaid only diagnoses, safely |
|
|
@@ -208,8 +232,8 @@ Run one or a few: `python -m pyfirstaid --only shadowing,path`. List them all: `
|
|
|
208
232
|
|
|
209
233
|
## Scope
|
|
210
234
|
|
|
211
|
-
**In:** pip, venv and uv on Windows, macOS and Linux.
|
|
212
|
-
**Out (for now):** conda (use `conda doctor`), Poetry and pyenv specifics, automatic repair.
|
|
235
|
+
**In:** pip, venv and uv on Windows, macOS and Linux, plus conda / pip mixing problems.
|
|
236
|
+
**Out (for now):** conda's own internals (use `conda doctor`), Poetry and pyenv specifics, automatic repair.
|
|
213
237
|
|
|
214
238
|
## Contributing
|
|
215
239
|
|
|
@@ -217,6 +241,10 @@ Run one or a few: `python -m pyfirstaid --only shadowing,path`. List them all: `
|
|
|
217
241
|
- Development: `python -m pip install -e ".[dev]"`, then `python -m pytest` and `ruff check src tests`.
|
|
218
242
|
- A new check is one file in `src/pyfirstaid/checks/` that returns a list of `Finding`s, registered in `checks/__init__.py`.
|
|
219
243
|
|
|
244
|
+
## Author
|
|
245
|
+
|
|
246
|
+
Made by [Sai Sakalam](https://sai-sakalam.github.io/), a data engineer who kept fixing the same broken Python environments for colleagues and decided to automate it. Feedback and ideas are always welcome.
|
|
247
|
+
|
|
220
248
|
## License
|
|
221
249
|
|
|
222
250
|
MIT
|
|
@@ -11,7 +11,8 @@ license = "MIT"
|
|
|
11
11
|
license-files = ["LICENSE"]
|
|
12
12
|
requires-python = ">=3.9"
|
|
13
13
|
authors = [{ name = "Sai Gavaskar Sakalam" }]
|
|
14
|
-
keywords = ["pip", "virtualenv", "venv", "diagnostics", "troubleshooting", "ssl", "environment"
|
|
14
|
+
keywords = ["pip", "virtualenv", "venv", "diagnostics", "troubleshooting", "ssl", "environment",
|
|
15
|
+
"conda", "uv", "poetry", "pyenv", "jupyter"]
|
|
15
16
|
classifiers = [
|
|
16
17
|
"Development Status :: 3 - Alpha",
|
|
17
18
|
"Environment :: Console",
|
|
@@ -41,6 +42,7 @@ Homepage = "https://github.com/sai-sakalam/pyfirstaid"
|
|
|
41
42
|
Source = "https://github.com/sai-sakalam/pyfirstaid"
|
|
42
43
|
Issues = "https://github.com/sai-sakalam/pyfirstaid/issues"
|
|
43
44
|
Changelog = "https://github.com/sai-sakalam/pyfirstaid/blob/main/CHANGELOG.md"
|
|
45
|
+
Author = "https://sai-sakalam.github.io/"
|
|
44
46
|
|
|
45
47
|
[tool.hatch.version]
|
|
46
48
|
path = "src/pyfirstaid/__init__.py"
|
|
@@ -7,15 +7,20 @@ from typing import List
|
|
|
7
7
|
from pyfirstaid.checks import (
|
|
8
8
|
broken_installs,
|
|
9
9
|
compiled,
|
|
10
|
+
conda,
|
|
10
11
|
dependencies,
|
|
11
12
|
encoding,
|
|
13
|
+
jupyter,
|
|
12
14
|
leaks,
|
|
13
15
|
path_check,
|
|
14
16
|
permissions,
|
|
15
17
|
pip_mismatch,
|
|
18
|
+
poetry,
|
|
19
|
+
pyenv,
|
|
16
20
|
python_version,
|
|
17
21
|
shadowing,
|
|
18
22
|
ssl_check,
|
|
23
|
+
uv,
|
|
19
24
|
venv,
|
|
20
25
|
)
|
|
21
26
|
from pyfirstaid.model import Check
|
|
@@ -23,6 +28,11 @@ from pyfirstaid.model import Check
|
|
|
23
28
|
ALL_CHECKS: List[Check] = [
|
|
24
29
|
Check(python_version.CHECK_ID, "Python version", python_version.run_check),
|
|
25
30
|
Check(venv.CHECK_ID, "Virtual environment", venv.run_check),
|
|
31
|
+
Check(conda.CHECK_ID, "conda / pip mixing", conda.run_check),
|
|
32
|
+
Check(uv.CHECK_ID, "uv project environment", uv.run_check),
|
|
33
|
+
Check(poetry.CHECK_ID, "Poetry project environment", poetry.run_check),
|
|
34
|
+
Check(pyenv.CHECK_ID, "pyenv / .python-version", pyenv.run_check),
|
|
35
|
+
Check(jupyter.CHECK_ID, "Jupyter kernels", jupyter.run_check),
|
|
26
36
|
Check(pip_mismatch.CHECK_ID, "pip / python match", pip_mismatch.run_check),
|
|
27
37
|
Check(path_check.CHECK_ID, "python on PATH", path_check.run_check),
|
|
28
38
|
Check(leaks.CHECK_ID, "PYTHONPATH / PYTHONHOME", leaks.run_check),
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Check: problems from mixing conda and pip in a conda environment.
|
|
2
|
+
|
|
3
|
+
`conda doctor` checks conda's own files but not what pip did to the environment.
|
|
4
|
+
This check looks only at files on disk (no conda or pip commands are run):
|
|
5
|
+
|
|
6
|
+
- a package that conda installed and pip later replaced (two tools fighting over it),
|
|
7
|
+
- packages pip-installed into the conda *base* environment,
|
|
8
|
+
- a conda environment that is activated but is not the Python actually running.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import glob
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import re
|
|
17
|
+
import sys
|
|
18
|
+
from typing import Dict, List, Optional, Set
|
|
19
|
+
|
|
20
|
+
from pyfirstaid.model import Finding, Options, Status
|
|
21
|
+
from pyfirstaid.util import norm, site_package_dirs
|
|
22
|
+
|
|
23
|
+
CHECK_ID = "conda"
|
|
24
|
+
|
|
25
|
+
_META_RE = re.compile(r"^(?P<name>.+?)-(?P<ver>[^-]+)\.(?:dist-info|egg-info)$")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def normalize(name: str) -> str:
|
|
29
|
+
return re.sub(r"[-_.]+", "-", name).lower()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def is_conda_env(prefix: str) -> bool:
|
|
33
|
+
"""Every conda environment (base included) has a conda-meta folder."""
|
|
34
|
+
return os.path.isdir(os.path.join(prefix, "conda-meta"))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def is_base_env(prefix: str) -> bool:
|
|
38
|
+
"""The base environment holds conda itself: it has condabin/ or an envs/ folder."""
|
|
39
|
+
return (os.path.isdir(os.path.join(prefix, "condabin"))
|
|
40
|
+
or os.path.isdir(os.path.join(prefix, "envs")))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def conda_records(prefix: str) -> Dict[str, Set[str]]:
|
|
44
|
+
"""Map normalized conda package name -> metadata folder names conda installed for it.
|
|
45
|
+
|
|
46
|
+
Reads conda-meta/*.json. The set is empty for packages that ship no Python metadata.
|
|
47
|
+
"""
|
|
48
|
+
records: Dict[str, Set[str]] = {}
|
|
49
|
+
for path in glob.glob(os.path.join(prefix, "conda-meta", "*.json")):
|
|
50
|
+
try:
|
|
51
|
+
with open(path, encoding="utf-8") as fh:
|
|
52
|
+
data = json.load(fh)
|
|
53
|
+
except (OSError, ValueError):
|
|
54
|
+
continue
|
|
55
|
+
name = data.get("name")
|
|
56
|
+
if not isinstance(name, str):
|
|
57
|
+
continue
|
|
58
|
+
metas: Set[str] = set()
|
|
59
|
+
for f in data.get("files") or []:
|
|
60
|
+
if not isinstance(f, str):
|
|
61
|
+
continue
|
|
62
|
+
for part in f.replace("\\", "/").split("/"):
|
|
63
|
+
if part.endswith((".dist-info", ".egg-info")):
|
|
64
|
+
metas.add(part)
|
|
65
|
+
break
|
|
66
|
+
records.setdefault(normalize(name), set()).update(metas)
|
|
67
|
+
return records
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def installer_of(meta_dir: str) -> str:
|
|
71
|
+
try:
|
|
72
|
+
with open(os.path.join(meta_dir, "INSTALLER"), encoding="utf-8") as fh:
|
|
73
|
+
return fh.read().strip().lower()
|
|
74
|
+
except OSError:
|
|
75
|
+
return ""
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def find_problems(site_dirs: List[str], records: Dict[str, Set[str]]):
|
|
79
|
+
"""Return (replaced, pip_only): package names pip replaced over conda, and pip-only ones."""
|
|
80
|
+
owned_by_conda: Set[str] = set()
|
|
81
|
+
for metas in records.values():
|
|
82
|
+
owned_by_conda.update(metas)
|
|
83
|
+
|
|
84
|
+
replaced: List[str] = []
|
|
85
|
+
pip_only: List[str] = []
|
|
86
|
+
for d in site_dirs:
|
|
87
|
+
try:
|
|
88
|
+
entries = os.listdir(d)
|
|
89
|
+
except OSError:
|
|
90
|
+
continue
|
|
91
|
+
for entry in entries:
|
|
92
|
+
m = _META_RE.match(entry)
|
|
93
|
+
if not m or entry in owned_by_conda:
|
|
94
|
+
continue
|
|
95
|
+
if installer_of(os.path.join(d, entry)) != "pip":
|
|
96
|
+
continue # conservative: only packages pip clearly says it installed
|
|
97
|
+
name = normalize(m.group("name"))
|
|
98
|
+
if name in records:
|
|
99
|
+
replaced.append("%s %s" % (name, m.group("ver")))
|
|
100
|
+
else:
|
|
101
|
+
pip_only.append(name)
|
|
102
|
+
return sorted(set(replaced)), sorted(set(pip_only))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def run_check(opts: Options, prefix: Optional[str] = None,
|
|
106
|
+
site_dirs: Optional[List[str]] = None) -> List[Finding]:
|
|
107
|
+
prefix = prefix or sys.prefix
|
|
108
|
+
findings: List[Finding] = []
|
|
109
|
+
active = os.environ.get("CONDA_PREFIX")
|
|
110
|
+
|
|
111
|
+
if not is_conda_env(prefix):
|
|
112
|
+
if active and is_conda_env(active):
|
|
113
|
+
findings.append(Finding(
|
|
114
|
+
CHECK_ID, Status.WARN,
|
|
115
|
+
"A conda environment is activated, but this Python is not from it",
|
|
116
|
+
detail="activated: %s\nrunning: %s" % (active, sys.executable),
|
|
117
|
+
fix="Use the environment's own Python: python -m pyfirstaid after "
|
|
118
|
+
"`conda activate <env>`, or check which python with `which python` "
|
|
119
|
+
"(Windows: `where python`).",
|
|
120
|
+
))
|
|
121
|
+
return findings
|
|
122
|
+
return [Finding(CHECK_ID, Status.SKIP, "Not a conda environment")]
|
|
123
|
+
|
|
124
|
+
records = conda_records(prefix)
|
|
125
|
+
replaced, pip_only = find_problems(
|
|
126
|
+
site_dirs if site_dirs is not None else site_package_dirs(), records)
|
|
127
|
+
base = is_base_env(prefix)
|
|
128
|
+
env_hint = "the base environment" if base else "this conda environment"
|
|
129
|
+
|
|
130
|
+
if replaced:
|
|
131
|
+
findings.append(Finding(
|
|
132
|
+
CHECK_ID, Status.WARN,
|
|
133
|
+
"%d package(s) installed by conda were later replaced by pip" % len(replaced),
|
|
134
|
+
detail="conda and pip now both think they own these, which can cause "
|
|
135
|
+
"import errors and broken updates:\n" + "\n".join(replaced[:8])
|
|
136
|
+
+ ("\n...and %d more" % (len(replaced) - 8) if len(replaced) > 8 else ""),
|
|
137
|
+
fix="Pick one tool per package. Usually: conda install --force-reinstall <name>. "
|
|
138
|
+
"Rule of thumb: install with conda first, and use pip only for packages "
|
|
139
|
+
"conda doesn't have.",
|
|
140
|
+
))
|
|
141
|
+
|
|
142
|
+
if base and pip_only:
|
|
143
|
+
findings.append(Finding(
|
|
144
|
+
CHECK_ID, Status.WARN,
|
|
145
|
+
"%d package(s) pip-installed into the conda base environment" % len(pip_only),
|
|
146
|
+
detail="The base environment runs conda itself; extra pip packages there can "
|
|
147
|
+
"break conda updates:\n" + ", ".join(pip_only[:10])
|
|
148
|
+
+ (" ..." if len(pip_only) > 10 else ""),
|
|
149
|
+
fix="Create a separate environment for your work: conda create -n myenv python, "
|
|
150
|
+
"then conda activate myenv",
|
|
151
|
+
))
|
|
152
|
+
|
|
153
|
+
if active and norm(active) != norm(prefix):
|
|
154
|
+
findings.append(Finding(
|
|
155
|
+
CHECK_ID, Status.WARN,
|
|
156
|
+
"Your shell activated a DIFFERENT conda environment",
|
|
157
|
+
detail="activated: %s\nrunning: %s" % (active, prefix),
|
|
158
|
+
fix="Run `conda activate <the env you meant>`, then run pyfirstaid again.",
|
|
159
|
+
))
|
|
160
|
+
|
|
161
|
+
if not findings:
|
|
162
|
+
findings.append(Finding(
|
|
163
|
+
CHECK_ID, Status.OK,
|
|
164
|
+
"No conda / pip conflicts in %s" % env_hint,
|
|
165
|
+
))
|
|
166
|
+
return findings
|