pyfirstaid 0.3.0__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.3.0 → pyfirstaid-0.4.0}/.github/workflows/ci.yml +8 -0
- {pyfirstaid-0.3.0 → pyfirstaid-0.4.0}/.github/workflows/publish.yml +1 -1
- {pyfirstaid-0.3.0 → pyfirstaid-0.4.0}/CHANGELOG.md +22 -2
- {pyfirstaid-0.3.0 → pyfirstaid-0.4.0}/PKG-INFO +30 -7
- {pyfirstaid-0.3.0 → pyfirstaid-0.4.0}/README.md +28 -5
- {pyfirstaid-0.3.0 → pyfirstaid-0.4.0}/pyproject.toml +2 -1
- {pyfirstaid-0.3.0 → pyfirstaid-0.4.0}/src/pyfirstaid/__init__.py +1 -1
- {pyfirstaid-0.3.0 → pyfirstaid-0.4.0}/src/pyfirstaid/checks/__init__.py +8 -0
- pyfirstaid-0.4.0/src/pyfirstaid/checks/jupyter.py +183 -0
- {pyfirstaid-0.3.0 → 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.3.0 → pyfirstaid-0.4.0}/src/pyfirstaid/report.py +15 -1
- pyfirstaid-0.4.0/src/pyfirstaid/util.py +162 -0
- {pyfirstaid-0.3.0 → pyfirstaid-0.4.0}/tests/test_cli.py +16 -0
- pyfirstaid-0.4.0/tests/test_jupyter.py +115 -0
- {pyfirstaid-0.3.0 → pyfirstaid-0.4.0}/tests/test_more_checks.py +1 -1
- pyfirstaid-0.4.0/tests/test_project_tools.py +290 -0
- pyfirstaid-0.3.0/src/pyfirstaid/util.py +0 -81
- {pyfirstaid-0.3.0 → pyfirstaid-0.4.0}/.gitignore +0 -0
- {pyfirstaid-0.3.0 → pyfirstaid-0.4.0}/LICENSE +0 -0
- {pyfirstaid-0.3.0 → pyfirstaid-0.4.0}/scripts/build_pyz.py +0 -0
- {pyfirstaid-0.3.0 → pyfirstaid-0.4.0}/src/pyfirstaid/__main__.py +0 -0
- {pyfirstaid-0.3.0 → pyfirstaid-0.4.0}/src/pyfirstaid/checks/broken_installs.py +0 -0
- {pyfirstaid-0.3.0 → pyfirstaid-0.4.0}/src/pyfirstaid/checks/compiled.py +0 -0
- {pyfirstaid-0.3.0 → pyfirstaid-0.4.0}/src/pyfirstaid/checks/conda.py +0 -0
- {pyfirstaid-0.3.0 → pyfirstaid-0.4.0}/src/pyfirstaid/checks/dependencies.py +0 -0
- {pyfirstaid-0.3.0 → pyfirstaid-0.4.0}/src/pyfirstaid/checks/encoding.py +0 -0
- {pyfirstaid-0.3.0 → pyfirstaid-0.4.0}/src/pyfirstaid/checks/leaks.py +0 -0
- {pyfirstaid-0.3.0 → pyfirstaid-0.4.0}/src/pyfirstaid/checks/path_check.py +0 -0
- {pyfirstaid-0.3.0 → pyfirstaid-0.4.0}/src/pyfirstaid/checks/permissions.py +0 -0
- {pyfirstaid-0.3.0 → pyfirstaid-0.4.0}/src/pyfirstaid/checks/python_version.py +0 -0
- {pyfirstaid-0.3.0 → pyfirstaid-0.4.0}/src/pyfirstaid/checks/shadowing.py +0 -0
- {pyfirstaid-0.3.0 → pyfirstaid-0.4.0}/src/pyfirstaid/checks/ssl_check.py +0 -0
- {pyfirstaid-0.3.0 → pyfirstaid-0.4.0}/src/pyfirstaid/checks/venv.py +0 -0
- {pyfirstaid-0.3.0 → pyfirstaid-0.4.0}/src/pyfirstaid/cli.py +0 -0
- {pyfirstaid-0.3.0 → pyfirstaid-0.4.0}/src/pyfirstaid/delegate.py +0 -0
- {pyfirstaid-0.3.0 → pyfirstaid-0.4.0}/src/pyfirstaid/model.py +0 -0
- {pyfirstaid-0.3.0 → pyfirstaid-0.4.0}/tests/test_checks.py +0 -0
- {pyfirstaid-0.3.0 → pyfirstaid-0.4.0}/tests/test_conda.py +0 -0
- {pyfirstaid-0.3.0 → 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,6 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
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
|
|
4
24
|
|
|
5
25
|
- **New `conda` check** for problems from mixing conda and pip (something `conda doctor`
|
|
6
26
|
doesn't look at):
|
|
@@ -8,7 +28,7 @@
|
|
|
8
28
|
- pip packages installed into the conda *base* environment,
|
|
9
29
|
- a conda environment that is activated, but isn't the Python actually running.
|
|
10
30
|
It only reads files on disk, runs no conda or pip commands, and stays quiet when unsure.
|
|
11
|
-
- README: up-to-date example output
|
|
31
|
+
- README: up-to-date example output; author link on PyPI.
|
|
12
32
|
|
|
13
33
|
## 0.2.3
|
|
14
34
|
|
|
@@ -1,6 +1,6 @@
|
|
|
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
|
|
@@ -10,7 +10,7 @@ Project-URL: Author, https://sai-sakalam.github.io/
|
|
|
10
10
|
Author: Sai Gavaskar Sakalam
|
|
11
11
|
License-Expression: MIT
|
|
12
12
|
License-File: LICENSE
|
|
13
|
-
Keywords: diagnostics,environment,pip,ssl,troubleshooting,venv,virtualenv
|
|
13
|
+
Keywords: conda,diagnostics,environment,jupyter,pip,poetry,pyenv,ssl,troubleshooting,uv,venv,virtualenv
|
|
14
14
|
Classifier: Development Status :: 3 - Alpha
|
|
15
15
|
Classifier: Environment :: Console
|
|
16
16
|
Classifier: Intended Audience :: Developers
|
|
@@ -33,14 +33,14 @@ Description-Content-Type: text/markdown
|
|
|
33
33
|
|
|
34
34
|
# pyfirstaid 🩹
|
|
35
35
|
[](https://pypi.org/project/pyfirstaid/)
|
|
36
|
-
[](https://pypistats.org/packages/pyfirstaid)
|
|
36
|
+
[](https://pypi.org/project/pyfirstaid/)
|
|
38
37
|
[](https://github.com/sai-sakalam/pyfirstaid/actions/workflows/ci.yml)
|
|
39
38
|
[](https://github.com/sai-sakalam/pyfirstaid/blob/main/LICENSE)
|
|
39
|
+
[](https://hub.xerj.org/contributors/sai-sakalam)
|
|
40
40
|
|
|
41
41
|
**First aid for broken Python environments.** One command tells you *what's broken* and *exactly how to fix it*.
|
|
42
42
|
|
|
43
|
-
> **Status: early, actively developed.**
|
|
43
|
+
> **Status: early, actively developed.** 17 checks, tested on Windows, macOS and Linux (Python 3.9–3.14).
|
|
44
44
|
> Feedback is very welcome in [Issues](https://github.com/sai-sakalam/pyfirstaid/issues). *Which problems do you hit most?*
|
|
45
45
|
|
|
46
46
|
---
|
|
@@ -61,7 +61,7 @@ system Python, and a file called `random.py` is hiding the real `random` module:
|
|
|
61
61
|
```console
|
|
62
62
|
$ python -m pyfirstaid --share
|
|
63
63
|
|
|
64
|
-
pyfirstaid 0.
|
|
64
|
+
pyfirstaid 0.4.0: checking your Python environment
|
|
65
65
|
Python 3.13.1 (~/project/.venv/bin/python)
|
|
66
66
|
|
|
67
67
|
✔ Python 3.13.1 (supported until October 2029)
|
|
@@ -84,6 +84,7 @@ Python 3.13.1 (~/project/.venv/bin/python)
|
|
|
84
84
|
✔ No dependency conflicts (pip check)
|
|
85
85
|
✔ site-packages is writable
|
|
86
86
|
✔ Default text encoding is UTF-8
|
|
87
|
+
- Not used here: conda, uv, poetry, pyenv, jupyter
|
|
87
88
|
|
|
88
89
|
1 problem(s), 2 warning(s).
|
|
89
90
|
```
|
|
@@ -163,6 +164,24 @@ Shows which Python `python` / `python3` actually start, warns about Apple's buil
|
|
|
163
164
|
|
|
164
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`.
|
|
165
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
|
+
|
|
166
185
|
**"pip check shows conflicts and I don't know what to install."**
|
|
167
186
|
|
|
168
187
|
python -m pyfirstaid --only dependencies
|
|
@@ -206,7 +225,11 @@ Download `pyfirstaid.pyz` from the [latest release](https://github.com/sai-sakal
|
|
|
206
225
|
| `python-version` | Python past (or close to) end of life, pre-release Pythons, Apple's built-in Command Line Tools Python |
|
|
207
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) |
|
|
208
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 |
|
|
209
|
-
| `
|
|
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 |
|
|
210
233
|
| `path` | Typing `python` / `python3` runs a *different* Python, or the Windows Microsoft Store shortcut |
|
|
211
234
|
| `leaks` | `PYTHONPATH` / `PYTHONHOME` set, venvs that can see system packages |
|
|
212
235
|
| `shadowing` | Files like `random.py` or `requests.py` in your folder that hide real modules |
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# pyfirstaid 🩹
|
|
2
2
|
[](https://pypi.org/project/pyfirstaid/)
|
|
3
|
-
[](https://pypistats.org/packages/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
|
|
@@ -173,7 +192,11 @@ Download `pyfirstaid.pyz` from the [latest release](https://github.com/sai-sakal
|
|
|
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 |
|
|
176
|
-
| `
|
|
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 |
|
|
177
200
|
| `path` | Typing `python` / `python3` runs a *different* Python, or the Windows Microsoft Store shortcut |
|
|
178
201
|
| `leaks` | `PYTHONPATH` / `PYTHONHOME` set, venvs that can see system packages |
|
|
179
202
|
| `shadowing` | Files like `random.py` or `requests.py` in your folder that hide real modules |
|
|
@@ -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",
|
|
@@ -10,13 +10,17 @@ from pyfirstaid.checks import (
|
|
|
10
10
|
conda,
|
|
11
11
|
dependencies,
|
|
12
12
|
encoding,
|
|
13
|
+
jupyter,
|
|
13
14
|
leaks,
|
|
14
15
|
path_check,
|
|
15
16
|
permissions,
|
|
16
17
|
pip_mismatch,
|
|
18
|
+
poetry,
|
|
19
|
+
pyenv,
|
|
17
20
|
python_version,
|
|
18
21
|
shadowing,
|
|
19
22
|
ssl_check,
|
|
23
|
+
uv,
|
|
20
24
|
venv,
|
|
21
25
|
)
|
|
22
26
|
from pyfirstaid.model import Check
|
|
@@ -25,6 +29,10 @@ ALL_CHECKS: List[Check] = [
|
|
|
25
29
|
Check(python_version.CHECK_ID, "Python version", python_version.run_check),
|
|
26
30
|
Check(venv.CHECK_ID, "Virtual environment", venv.run_check),
|
|
27
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),
|
|
28
36
|
Check(pip_mismatch.CHECK_ID, "pip / python match", pip_mismatch.run_check),
|
|
29
37
|
Check(path_check.CHECK_ID, "python on PATH", path_check.run_check),
|
|
30
38
|
Check(leaks.CHECK_ID, "PYTHONPATH / PYTHONHOME", leaks.run_check),
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Check: Jupyter kernels that point at the wrong (or a missing) Python.
|
|
2
|
+
|
|
3
|
+
The classic notebook problem is "I installed the package, but the notebook can't import
|
|
4
|
+
it": the notebook runs a *kernel*, and each kernel is tied to one Python. This check
|
|
5
|
+
reads the kernel files (kernel.json) on disk. It runs no Jupyter commands.
|
|
6
|
+
|
|
7
|
+
- kernels whose Python no longer exists (deleted or moved venv, upgraded Python),
|
|
8
|
+
- this environment has ipykernel but is not registered as a kernel,
|
|
9
|
+
- several kernels with the same name in the kernel picker.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import importlib.util
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
18
|
+
from typing import Dict, List, Optional, Tuple
|
|
19
|
+
|
|
20
|
+
from pyfirstaid.model import Finding, Options, Status
|
|
21
|
+
from pyfirstaid.util import in_virtualenv, is_within, norm
|
|
22
|
+
|
|
23
|
+
CHECK_ID = "jupyter"
|
|
24
|
+
|
|
25
|
+
Kernel = Tuple[str, str, List[str], str] # (name, folder, argv, display name)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def jupyter_dirs(prefix: Optional[str] = None) -> List[str]:
|
|
29
|
+
"""Folders Jupyter searches for kernels, in Jupyter's own order (user, env, system)."""
|
|
30
|
+
prefix = prefix or sys.prefix
|
|
31
|
+
home = os.path.expanduser("~")
|
|
32
|
+
dirs: List[str] = []
|
|
33
|
+
|
|
34
|
+
data_dir = os.environ.get("JUPYTER_DATA_DIR")
|
|
35
|
+
if data_dir:
|
|
36
|
+
dirs.append(data_dir)
|
|
37
|
+
elif sys.platform == "darwin":
|
|
38
|
+
dirs.append(os.path.join(home, "Library", "Jupyter"))
|
|
39
|
+
elif os.name == "nt":
|
|
40
|
+
appdata = os.environ.get("APPDATA")
|
|
41
|
+
if appdata:
|
|
42
|
+
dirs.append(os.path.join(appdata, "jupyter"))
|
|
43
|
+
else:
|
|
44
|
+
xdg = os.environ.get("XDG_DATA_HOME") or os.path.join(home, ".local", "share")
|
|
45
|
+
dirs.append(os.path.join(xdg, "jupyter"))
|
|
46
|
+
|
|
47
|
+
for extra in (os.environ.get("JUPYTER_PATH") or "").split(os.pathsep):
|
|
48
|
+
if extra:
|
|
49
|
+
dirs.append(extra)
|
|
50
|
+
|
|
51
|
+
dirs.append(os.path.join(prefix, "share", "jupyter"))
|
|
52
|
+
if os.name == "nt":
|
|
53
|
+
programdata = os.environ.get("PROGRAMDATA")
|
|
54
|
+
if programdata:
|
|
55
|
+
dirs.append(os.path.join(programdata, "jupyter"))
|
|
56
|
+
else:
|
|
57
|
+
dirs.extend(["/usr/local/share/jupyter", "/usr/share/jupyter"])
|
|
58
|
+
return [os.path.join(d, "kernels") for d in dirs]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def find_kernels(kernel_dirs: List[str]) -> List[Kernel]:
|
|
62
|
+
"""All kernels, first one wins when the same name is in several folders (like Jupyter)."""
|
|
63
|
+
seen: Dict[str, Kernel] = {}
|
|
64
|
+
for base in kernel_dirs:
|
|
65
|
+
try:
|
|
66
|
+
names = sorted(os.listdir(base))
|
|
67
|
+
except OSError:
|
|
68
|
+
continue
|
|
69
|
+
for name in names:
|
|
70
|
+
folder = os.path.join(base, name)
|
|
71
|
+
spec_file = os.path.join(folder, "kernel.json")
|
|
72
|
+
if name.lower() in seen or not os.path.isfile(spec_file):
|
|
73
|
+
continue
|
|
74
|
+
try:
|
|
75
|
+
with open(spec_file, encoding="utf-8") as fh:
|
|
76
|
+
spec = json.load(fh)
|
|
77
|
+
except (OSError, ValueError):
|
|
78
|
+
continue
|
|
79
|
+
argv = spec.get("argv") if isinstance(spec, dict) else None
|
|
80
|
+
if not isinstance(argv, list) or not argv or not all(isinstance(a, str) for a in argv):
|
|
81
|
+
continue
|
|
82
|
+
display = spec.get("display_name")
|
|
83
|
+
if not isinstance(display, str):
|
|
84
|
+
display = name
|
|
85
|
+
seen[name.lower()] = (name, folder, argv, display)
|
|
86
|
+
return list(seen.values())
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def missing_python(kernel: Kernel) -> bool:
|
|
90
|
+
"""True when the kernel starts a Python by full path and that file is gone.
|
|
91
|
+
|
|
92
|
+
Kernels that use a bare `python` are fine: Jupyter runs them with its own Python.
|
|
93
|
+
"""
|
|
94
|
+
exe = kernel[2][0]
|
|
95
|
+
return os.path.isabs(exe) and not os.path.exists(exe)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def env_name(prefix: str) -> str:
|
|
99
|
+
"""A readable kernel name for an environment: `.venv` folders are named after the project."""
|
|
100
|
+
base = os.path.basename(os.path.normpath(prefix))
|
|
101
|
+
if base.lower() in (".venv", "venv", "env", ".env"):
|
|
102
|
+
base = os.path.basename(os.path.dirname(os.path.normpath(prefix))) or base
|
|
103
|
+
return "".join(c if c.isalnum() or c in "-_." else "-" for c in base).strip("-") or "myenv"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def is_registered(kernels: List[Kernel], prefix: str, executable: str) -> bool:
|
|
107
|
+
for _, _, argv, _ in kernels:
|
|
108
|
+
exe = argv[0]
|
|
109
|
+
if os.path.isabs(exe) and (norm(exe) == norm(executable) or is_within(exe, prefix)):
|
|
110
|
+
return True
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def run_check(opts: Options, kernel_dirs: Optional[List[str]] = None,
|
|
115
|
+
prefix: Optional[str] = None, executable: Optional[str] = None,
|
|
116
|
+
has_ipykernel: Optional[bool] = None, is_env: Optional[bool] = None,
|
|
117
|
+
has_jupyter: Optional[bool] = None) -> List[Finding]:
|
|
118
|
+
prefix = prefix or sys.prefix
|
|
119
|
+
executable = executable or sys.executable
|
|
120
|
+
if has_ipykernel is None:
|
|
121
|
+
has_ipykernel = importlib.util.find_spec("ipykernel") is not None
|
|
122
|
+
if has_jupyter is None:
|
|
123
|
+
has_jupyter = any(importlib.util.find_spec(m) is not None
|
|
124
|
+
for m in ("jupyter_server", "notebook", "jupyterlab"))
|
|
125
|
+
if is_env is None:
|
|
126
|
+
is_env = in_virtualenv() or os.path.isdir(os.path.join(prefix, "conda-meta"))
|
|
127
|
+
kernels = find_kernels(kernel_dirs if kernel_dirs is not None else jupyter_dirs(prefix))
|
|
128
|
+
|
|
129
|
+
if not kernels and not has_ipykernel:
|
|
130
|
+
return [Finding(CHECK_ID, Status.SKIP, "Jupyter not found")]
|
|
131
|
+
|
|
132
|
+
findings: List[Finding] = []
|
|
133
|
+
|
|
134
|
+
broken = [k for k in kernels if missing_python(k)]
|
|
135
|
+
if broken:
|
|
136
|
+
findings.append(Finding(
|
|
137
|
+
CHECK_ID, Status.WARN,
|
|
138
|
+
"%d Jupyter kernel(s) point to a Python that no longer exists" % len(broken),
|
|
139
|
+
detail="These kernels fail to start (often after deleting a venv or upgrading "
|
|
140
|
+
"Python):\n" + "\n".join(
|
|
141
|
+
"%s -> %s" % (k[0], k[2][0]) for k in broken[:8]),
|
|
142
|
+
fix="Remove each one: jupyter kernelspec remove %s\n"
|
|
143
|
+
"Then register the environment again from inside it: "
|
|
144
|
+
"python -m ipykernel install --user --name <name>" % broken[0][0],
|
|
145
|
+
))
|
|
146
|
+
|
|
147
|
+
# Jupyter installed in this same environment already uses this Python.
|
|
148
|
+
if has_ipykernel and is_env and not has_jupyter and not is_registered(
|
|
149
|
+
kernels, prefix, executable):
|
|
150
|
+
name = env_name(prefix)
|
|
151
|
+
findings.append(Finding(
|
|
152
|
+
CHECK_ID, Status.INFO,
|
|
153
|
+
"This environment has ipykernel, but no Jupyter kernel uses it",
|
|
154
|
+
detail="Notebooks can only use this Python (and its packages) through a kernel. "
|
|
155
|
+
"Otherwise `import` in a notebook won't see what you installed here.",
|
|
156
|
+
fix='python -m ipykernel install --user --name %s --display-name "Python (%s)"'
|
|
157
|
+
% (name, name),
|
|
158
|
+
))
|
|
159
|
+
|
|
160
|
+
by_display: Dict[str, List[str]] = {}
|
|
161
|
+
shown: Dict[str, str] = {}
|
|
162
|
+
for name, _, _, display in kernels:
|
|
163
|
+
key = display.strip().lower()
|
|
164
|
+
by_display.setdefault(key, []).append(name)
|
|
165
|
+
shown.setdefault(key, display.strip())
|
|
166
|
+
dupes = {shown[d]: n for d, n in by_display.items() if len(n) > 1}
|
|
167
|
+
if dupes:
|
|
168
|
+
findings.append(Finding(
|
|
169
|
+
CHECK_ID, Status.INFO,
|
|
170
|
+
"Several Jupyter kernels have the same name in the kernel picker",
|
|
171
|
+
detail="\n".join("%s: %s" % (d, ", ".join(n)) for d, n in sorted(dupes.items())),
|
|
172
|
+
fix="List them with `jupyter kernelspec list`, then remove old ones "
|
|
173
|
+
"(`jupyter kernelspec remove <name>`) or re-install with a clear "
|
|
174
|
+
'--display-name "Python (project)".',
|
|
175
|
+
))
|
|
176
|
+
|
|
177
|
+
if not findings:
|
|
178
|
+
findings.append(Finding(
|
|
179
|
+
CHECK_ID, Status.OK,
|
|
180
|
+
"%d Jupyter kernel(s) found, all point to a working Python" % len(kernels)
|
|
181
|
+
if kernels else "ipykernel installed, no kernel problems",
|
|
182
|
+
))
|
|
183
|
+
return findings
|
|
@@ -9,7 +9,7 @@ import sys
|
|
|
9
9
|
from typing import List, Optional, Tuple
|
|
10
10
|
|
|
11
11
|
from pyfirstaid.model import Finding, Options, Status
|
|
12
|
-
from pyfirstaid.util import is_within, run, site_package_dirs
|
|
12
|
+
from pyfirstaid.util import is_within, read_pyvenv_cfg, run, site_package_dirs
|
|
13
13
|
|
|
14
14
|
CHECK_ID = "pip-mismatch"
|
|
15
15
|
|
|
@@ -50,6 +50,13 @@ def run_check(opts: Options) -> List[Finding]:
|
|
|
50
50
|
|
|
51
51
|
code, out, err = run([sys.executable, "-m", "pip", "--version"], opts.timeout)
|
|
52
52
|
if code != 0:
|
|
53
|
+
if "uv" in read_pyvenv_cfg(sys.prefix):
|
|
54
|
+
return [Finding(
|
|
55
|
+
CHECK_ID, Status.INFO,
|
|
56
|
+
"No pip in this environment (normal: it was made by uv)",
|
|
57
|
+
detail="A plain `pip install` would use some other Python's pip.",
|
|
58
|
+
fix="uv pip install <package> (in a uv project: uv add <package>)",
|
|
59
|
+
)]
|
|
53
60
|
return [Finding(
|
|
54
61
|
CHECK_ID, Status.ERROR,
|
|
55
62
|
"pip is not installed for this Python",
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Check: Poetry projects that run the wrong environment (or a broken one).
|
|
2
|
+
|
|
3
|
+
Poetry keeps each project's environment either in the project (`.venv`) or in its own
|
|
4
|
+
cache folder, named `<project>-<hash>-py3.X`. This check finds those folders on disk and
|
|
5
|
+
compares them with the Python that is running. It runs no Poetry commands.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import base64
|
|
11
|
+
import hashlib
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
import sys
|
|
15
|
+
from typing import List, Optional
|
|
16
|
+
|
|
17
|
+
from pyfirstaid.checks.uv import display_path
|
|
18
|
+
from pyfirstaid.model import Finding, Options, Status
|
|
19
|
+
from pyfirstaid.util import (
|
|
20
|
+
broken_venv_base,
|
|
21
|
+
find_upwards,
|
|
22
|
+
in_virtualenv,
|
|
23
|
+
is_within,
|
|
24
|
+
norm,
|
|
25
|
+
read_pyproject,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
CHECK_ID = "poetry"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def poetry_project(cwd: str) -> Optional[str]:
|
|
32
|
+
"""The Poetry project folder for `cwd` (has poetry.lock or [tool.poetry]), if any."""
|
|
33
|
+
lock = find_upwards(cwd, "poetry.lock")
|
|
34
|
+
if lock:
|
|
35
|
+
return os.path.dirname(lock)
|
|
36
|
+
if "poetry" in (read_pyproject(cwd).get("tool") or {}):
|
|
37
|
+
return cwd
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def project_name(root: str) -> Optional[str]:
|
|
42
|
+
data = read_pyproject(root)
|
|
43
|
+
for table in ((data.get("tool") or {}).get("poetry") or {}, data.get("project") or {}):
|
|
44
|
+
name = table.get("name") if isinstance(table, dict) else None
|
|
45
|
+
if isinstance(name, str) and name:
|
|
46
|
+
return name
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def venvs_dir() -> str:
|
|
51
|
+
"""Where Poetry keeps environments that are not inside the project."""
|
|
52
|
+
custom = os.environ.get("POETRY_VIRTUALENVS_PATH")
|
|
53
|
+
if custom:
|
|
54
|
+
return custom
|
|
55
|
+
cache = os.environ.get("POETRY_CACHE_DIR")
|
|
56
|
+
if not cache:
|
|
57
|
+
home = os.path.expanduser("~")
|
|
58
|
+
if sys.platform == "darwin":
|
|
59
|
+
cache = os.path.join(home, "Library", "Caches", "pypoetry")
|
|
60
|
+
elif os.name == "nt":
|
|
61
|
+
local = os.environ.get("LOCALAPPDATA") or os.path.join(home, "AppData", "Local")
|
|
62
|
+
cache = os.path.join(local, "pypoetry", "Cache")
|
|
63
|
+
else:
|
|
64
|
+
xdg = os.environ.get("XDG_CACHE_HOME") or os.path.join(home, ".cache")
|
|
65
|
+
cache = os.path.join(xdg, "pypoetry")
|
|
66
|
+
return os.path.join(cache, "virtualenvs")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def sanitized(name: str) -> str:
|
|
70
|
+
return re.sub(r'[ $`!*@"\\\r\n\t]', "_", name.lower())[:42]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def env_name(name: str, root: str) -> str:
|
|
74
|
+
"""Poetry's environment name: <name>-<first 8 chars of a hash of the project path>."""
|
|
75
|
+
digest = hashlib.sha256(os.path.normcase(os.path.realpath(root)).encode()).digest()
|
|
76
|
+
return "%s-%s" % (sanitized(name), base64.urlsafe_b64encode(digest).decode()[:8])
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def project_envs(root: str, name: Optional[str], cache_dir: str) -> List[str]:
|
|
80
|
+
"""This project's environments: an in-project .venv, plus matching cache folders."""
|
|
81
|
+
envs: List[str] = []
|
|
82
|
+
local = os.path.join(root, ".venv")
|
|
83
|
+
if os.path.isfile(os.path.join(local, "pyvenv.cfg")):
|
|
84
|
+
envs.append(local)
|
|
85
|
+
if name:
|
|
86
|
+
start = env_name(name, root) + "-py"
|
|
87
|
+
try:
|
|
88
|
+
entries = sorted(os.listdir(cache_dir))
|
|
89
|
+
except OSError:
|
|
90
|
+
entries = []
|
|
91
|
+
envs.extend(os.path.join(cache_dir, e) for e in entries if e.startswith(start))
|
|
92
|
+
return envs
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def run_check(opts: Options, prefix: Optional[str] = None, in_venv: Optional[bool] = None,
|
|
96
|
+
cache_dir: Optional[str] = None) -> List[Finding]:
|
|
97
|
+
prefix = prefix or sys.prefix
|
|
98
|
+
in_venv = in_virtualenv() if in_venv is None else in_venv
|
|
99
|
+
cache_dir = cache_dir or venvs_dir()
|
|
100
|
+
cwd = opts.cwd or os.getcwd()
|
|
101
|
+
|
|
102
|
+
root = poetry_project(cwd)
|
|
103
|
+
if not root:
|
|
104
|
+
return [Finding(CHECK_ID, Status.SKIP, "Not a Poetry project")]
|
|
105
|
+
|
|
106
|
+
name = project_name(root)
|
|
107
|
+
envs = project_envs(root, name, cache_dir)
|
|
108
|
+
findings: List[Finding] = []
|
|
109
|
+
|
|
110
|
+
working = []
|
|
111
|
+
for env in envs:
|
|
112
|
+
gone = broken_venv_base(env)
|
|
113
|
+
if not gone:
|
|
114
|
+
working.append(env)
|
|
115
|
+
continue
|
|
116
|
+
if norm(os.path.dirname(env)) == norm(root):
|
|
117
|
+
fix = "Delete the .venv folder, then run `poetry install`."
|
|
118
|
+
else:
|
|
119
|
+
version = env.rsplit("-py", 1)[-1]
|
|
120
|
+
fix = "poetry env remove python%s then: poetry install" % version
|
|
121
|
+
findings.append(Finding(
|
|
122
|
+
CHECK_ID, Status.WARN,
|
|
123
|
+
"A Poetry environment for this project is broken: its base Python is gone",
|
|
124
|
+
detail="%s was built from %s, which no longer exists (often after upgrading "
|
|
125
|
+
"Python)." % (display_path(env, cwd), gone),
|
|
126
|
+
fix=fix,
|
|
127
|
+
))
|
|
128
|
+
|
|
129
|
+
# The running env counts as the project's even if the path hash changed (moved project).
|
|
130
|
+
running_is_project = any(norm(prefix) == norm(e) for e in working) or (
|
|
131
|
+
name is not None and is_within(prefix, cache_dir)
|
|
132
|
+
and os.path.basename(os.path.normpath(prefix)).startswith(sanitized(name) + "-"))
|
|
133
|
+
|
|
134
|
+
if running_is_project:
|
|
135
|
+
findings.append(Finding(CHECK_ID, Status.OK, "Running the Poetry project's environment"))
|
|
136
|
+
elif not envs:
|
|
137
|
+
findings.append(Finding(
|
|
138
|
+
CHECK_ID, Status.INFO,
|
|
139
|
+
"Poetry project, but no environment for it was found",
|
|
140
|
+
detail="project: %s" % root,
|
|
141
|
+
fix="poetry install",
|
|
142
|
+
))
|
|
143
|
+
elif working:
|
|
144
|
+
findings.append(Finding(
|
|
145
|
+
CHECK_ID, Status.WARN if in_venv else Status.INFO,
|
|
146
|
+
"This is a Poetry project, but a DIFFERENT environment is running" if in_venv
|
|
147
|
+
else "This is a Poetry project, but its environment is not the Python running",
|
|
148
|
+
detail="project env: %s\nrunning: %s" % (working[0], prefix),
|
|
149
|
+
fix="Run commands through Poetry: `poetry run python ...`, "
|
|
150
|
+
"or run `poetry env activate` and then the command it prints. "
|
|
151
|
+
"To check it: poetry run python -m pyfirstaid",
|
|
152
|
+
))
|
|
153
|
+
return findings
|