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.
Files changed (42) hide show
  1. {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/.github/workflows/ci.yml +8 -0
  2. {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/.github/workflows/publish.yml +1 -1
  3. {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/CHANGELOG.md +30 -0
  4. {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/PKG-INFO +39 -10
  5. {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/README.md +36 -8
  6. {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/pyproject.toml +3 -1
  7. {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/src/pyfirstaid/__init__.py +1 -1
  8. {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/src/pyfirstaid/checks/__init__.py +10 -0
  9. pyfirstaid-0.4.0/src/pyfirstaid/checks/conda.py +166 -0
  10. pyfirstaid-0.4.0/src/pyfirstaid/checks/jupyter.py +183 -0
  11. {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/src/pyfirstaid/checks/pip_mismatch.py +8 -1
  12. pyfirstaid-0.4.0/src/pyfirstaid/checks/poetry.py +153 -0
  13. pyfirstaid-0.4.0/src/pyfirstaid/checks/pyenv.py +125 -0
  14. pyfirstaid-0.4.0/src/pyfirstaid/checks/uv.py +100 -0
  15. {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/src/pyfirstaid/report.py +15 -1
  16. pyfirstaid-0.4.0/src/pyfirstaid/util.py +162 -0
  17. {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/tests/test_cli.py +16 -0
  18. pyfirstaid-0.4.0/tests/test_conda.py +139 -0
  19. pyfirstaid-0.4.0/tests/test_jupyter.py +115 -0
  20. {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/tests/test_more_checks.py +1 -1
  21. pyfirstaid-0.4.0/tests/test_project_tools.py +290 -0
  22. pyfirstaid-0.2.3/src/pyfirstaid/util.py +0 -81
  23. {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/.gitignore +0 -0
  24. {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/LICENSE +0 -0
  25. {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/scripts/build_pyz.py +0 -0
  26. {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/src/pyfirstaid/__main__.py +0 -0
  27. {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/src/pyfirstaid/checks/broken_installs.py +0 -0
  28. {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/src/pyfirstaid/checks/compiled.py +0 -0
  29. {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/src/pyfirstaid/checks/dependencies.py +0 -0
  30. {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/src/pyfirstaid/checks/encoding.py +0 -0
  31. {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/src/pyfirstaid/checks/leaks.py +0 -0
  32. {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/src/pyfirstaid/checks/path_check.py +0 -0
  33. {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/src/pyfirstaid/checks/permissions.py +0 -0
  34. {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/src/pyfirstaid/checks/python_version.py +0 -0
  35. {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/src/pyfirstaid/checks/shadowing.py +0 -0
  36. {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/src/pyfirstaid/checks/ssl_check.py +0 -0
  37. {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/src/pyfirstaid/checks/venv.py +0 -0
  38. {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/src/pyfirstaid/cli.py +0 -0
  39. {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/src/pyfirstaid/delegate.py +0 -0
  40. {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/src/pyfirstaid/model.py +0 -0
  41. {pyfirstaid-0.2.3 → pyfirstaid-0.4.0}/tests/test_checks.py +0 -0
  42. {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
@@ -8,7 +8,7 @@ on:
8
8
  tag:
9
9
  description: "Release tag to attach pyfirstaid.pyz to"
10
10
  required: true
11
- default: "v0.1.0"
11
+ default: "v0.4.0"
12
12
 
13
13
  jobs:
14
14
  publish:
@@ -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.2.3
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
  [![PyPI](https://img.shields.io/pypi/v/pyfirstaid)](https://pypi.org/project/pyfirstaid/)
35
- [![Python versions](https://img.shields.io/pypi/pyversions/pyfirstaid)](https://pypi.org/project/pyfirstaid/)
36
- [![Downloads](https://static.pepy.tech/badge/pyfirstaid/month)](https://pepy.tech/projects/pyfirstaid)
36
+ [![Python versions](https://img.shields.io/badge/python-3.9%20%7C%203.10%20%7C%203.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue)](https://pypi.org/project/pyfirstaid/)
37
37
  [![CI](https://github.com/sai-sakalam/pyfirstaid/actions/workflows/ci.yml/badge.svg)](https://github.com/sai-sakalam/pyfirstaid/actions/workflows/ci.yml)
38
38
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](https://github.com/sai-sakalam/pyfirstaid/blob/main/LICENSE)
39
+ [![XERJ Corpus Hub contributor](https://hub.xerj.org/contributors/sai-sakalam/badge.svg)](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.** 12 checks, tested on Windows, macOS and Linux (Python 3.9–3.14).
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.2.0: checking your Python environment
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
- | `pip-mismatch` | `pip` installs into a different Python than the one you run, pip missing, a broken `pip` command |
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 cover pip, venv or uv |
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
  [![PyPI](https://img.shields.io/pypi/v/pyfirstaid)](https://pypi.org/project/pyfirstaid/)
3
- [![Python versions](https://img.shields.io/pypi/pyversions/pyfirstaid)](https://pypi.org/project/pyfirstaid/)
4
- [![Downloads](https://static.pepy.tech/badge/pyfirstaid/month)](https://pepy.tech/projects/pyfirstaid)
3
+ [![Python versions](https://img.shields.io/badge/python-3.9%20%7C%203.10%20%7C%203.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue)](https://pypi.org/project/pyfirstaid/)
5
4
  [![CI](https://github.com/sai-sakalam/pyfirstaid/actions/workflows/ci.yml/badge.svg)](https://github.com/sai-sakalam/pyfirstaid/actions/workflows/ci.yml)
6
5
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](https://github.com/sai-sakalam/pyfirstaid/blob/main/LICENSE)
6
+ [![XERJ Corpus Hub contributor](https://hub.xerj.org/contributors/sai-sakalam/badge.svg)](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.** 12 checks, tested on Windows, macOS and Linux (Python 3.9–3.14).
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.2.0: checking your Python environment
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
- | `pip-mismatch` | `pip` installs into a different Python than the one you run, pip missing, a broken `pip` command |
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 cover pip, venv or uv |
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"
@@ -1,3 +1,3 @@
1
1
  """pyfirstaid: first aid for broken Python environments."""
2
2
 
3
- __version__ = "0.2.3"
3
+ __version__ = "0.4.0"
@@ -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