pyfirstaid 0.2.2__tar.gz → 0.3.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.2 → pyfirstaid-0.3.0}/CHANGELOG.md +17 -0
- {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/PKG-INFO +25 -6
- {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/README.md +14 -5
- {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/pyproject.toml +10 -0
- {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/src/pyfirstaid/__init__.py +1 -1
- {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/src/pyfirstaid/checks/__init__.py +2 -0
- pyfirstaid-0.3.0/src/pyfirstaid/checks/conda.py +166 -0
- {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/src/pyfirstaid/checks/venv.py +2 -1
- pyfirstaid-0.3.0/tests/test_conda.py +139 -0
- {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/tests/test_more_checks.py +1 -1
- {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/.github/workflows/ci.yml +0 -0
- {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/.github/workflows/publish.yml +0 -0
- {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/.gitignore +0 -0
- {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/LICENSE +0 -0
- {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/scripts/build_pyz.py +0 -0
- {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/src/pyfirstaid/__main__.py +0 -0
- {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/src/pyfirstaid/checks/broken_installs.py +0 -0
- {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/src/pyfirstaid/checks/compiled.py +0 -0
- {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/src/pyfirstaid/checks/dependencies.py +0 -0
- {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/src/pyfirstaid/checks/encoding.py +0 -0
- {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/src/pyfirstaid/checks/leaks.py +0 -0
- {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/src/pyfirstaid/checks/path_check.py +0 -0
- {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/src/pyfirstaid/checks/permissions.py +0 -0
- {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/src/pyfirstaid/checks/pip_mismatch.py +0 -0
- {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/src/pyfirstaid/checks/python_version.py +0 -0
- {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/src/pyfirstaid/checks/shadowing.py +0 -0
- {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/src/pyfirstaid/checks/ssl_check.py +0 -0
- {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/src/pyfirstaid/cli.py +0 -0
- {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/src/pyfirstaid/delegate.py +0 -0
- {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/src/pyfirstaid/model.py +0 -0
- {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/src/pyfirstaid/report.py +0 -0
- {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/src/pyfirstaid/util.py +0 -0
- {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/tests/test_checks.py +0 -0
- {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/tests/test_cli.py +0 -0
- {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/tests/test_delegate.py +0 -0
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
- **New `conda` check** for problems from mixing conda and pip (something `conda doctor`
|
|
6
|
+
doesn't look at):
|
|
7
|
+
- a package conda installed that pip later replaced, so both tools think they own it,
|
|
8
|
+
- pip packages installed into the conda *base* environment,
|
|
9
|
+
- a conda environment that is activated, but isn't the Python actually running.
|
|
10
|
+
It only reads files on disk, runs no conda or pip commands, and stays quiet when unsure.
|
|
11
|
+
- README: up-to-date example output and downloads badge; author link on PyPI.
|
|
12
|
+
|
|
13
|
+
## 0.2.3
|
|
14
|
+
|
|
15
|
+
- Clearer PEP 668 message: it now says the block can come from your OS *or* a package
|
|
16
|
+
manager such as Homebrew.
|
|
17
|
+
- PyPI page: version, Python-versions, downloads and license badges; Source and Changelog
|
|
18
|
+
links; per-version Python classifiers (3.9–3.14).
|
|
19
|
+
|
|
3
20
|
## 0.2.2
|
|
4
21
|
|
|
5
22
|
Fixes found by testing on a real Mac with Homebrew Python:
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pyfirstaid
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.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
|
+
Project-URL: Source, https://github.com/sai-sakalam/pyfirstaid
|
|
6
7
|
Project-URL: Issues, https://github.com/sai-sakalam/pyfirstaid/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/sai-sakalam/pyfirstaid/blob/main/CHANGELOG.md
|
|
9
|
+
Project-URL: Author, https://sai-sakalam.github.io/
|
|
7
10
|
Author: Sai Gavaskar Sakalam
|
|
8
11
|
License-Expression: MIT
|
|
9
12
|
License-File: LICENSE
|
|
@@ -13,6 +16,13 @@ Classifier: Environment :: Console
|
|
|
13
16
|
Classifier: Intended Audience :: Developers
|
|
14
17
|
Classifier: Operating System :: OS Independent
|
|
15
18
|
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
26
|
Classifier: Topic :: Software Development :: Debuggers
|
|
17
27
|
Classifier: Topic :: System :: Installation/Setup
|
|
18
28
|
Requires-Python: >=3.9
|
|
@@ -22,11 +32,15 @@ Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
|
22
32
|
Description-Content-Type: text/markdown
|
|
23
33
|
|
|
24
34
|
# pyfirstaid 🩹
|
|
35
|
+
[](https://pypi.org/project/pyfirstaid/)
|
|
36
|
+
[](https://pypi.org/project/pyfirstaid/)
|
|
37
|
+
[](https://pypistats.org/packages/pyfirstaid)
|
|
25
38
|
[](https://github.com/sai-sakalam/pyfirstaid/actions/workflows/ci.yml)
|
|
39
|
+
[](https://github.com/sai-sakalam/pyfirstaid/blob/main/LICENSE)
|
|
26
40
|
|
|
27
41
|
**First aid for broken Python environments.** One command tells you *what's broken* and *exactly how to fix it*.
|
|
28
42
|
|
|
29
|
-
> **Status:
|
|
43
|
+
> **Status: early, actively developed.** 13 checks, tested on Windows, macOS and Linux (Python 3.9–3.14).
|
|
30
44
|
> Feedback is very welcome in [Issues](https://github.com/sai-sakalam/pyfirstaid/issues). *Which problems do you hit most?*
|
|
31
45
|
|
|
32
46
|
---
|
|
@@ -47,7 +61,7 @@ system Python, and a file called `random.py` is hiding the real `random` module:
|
|
|
47
61
|
```console
|
|
48
62
|
$ python -m pyfirstaid --share
|
|
49
63
|
|
|
50
|
-
pyfirstaid 0.2.
|
|
64
|
+
pyfirstaid 0.2.3: checking your Python environment
|
|
51
65
|
Python 3.13.1 (~/project/.venv/bin/python)
|
|
52
66
|
|
|
53
67
|
✔ Python 3.13.1 (supported until October 2029)
|
|
@@ -191,6 +205,7 @@ Download `pyfirstaid.pyz` from the [latest release](https://github.com/sai-sakal
|
|
|
191
205
|
|---|---|
|
|
192
206
|
| `python-version` | Python past (or close to) end of life, pre-release Pythons, Apple's built-in Command Line Tools Python |
|
|
193
207
|
| `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
|
+
| `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 |
|
|
194
209
|
| `pip-mismatch` | `pip` installs into a different Python than the one you run, pip missing, a broken `pip` command |
|
|
195
210
|
| `path` | Typing `python` / `python3` runs a *different* Python, or the Windows Microsoft Store shortcut |
|
|
196
211
|
| `leaks` | `PYTHONPATH` / `PYTHONHOME` set, venvs that can see system packages |
|
|
@@ -209,7 +224,7 @@ Run one or a few: `python -m pyfirstaid --only shadowing,path`. List them all: `
|
|
|
209
224
|
| Tool | What it does | Gap pyfirstaid fills |
|
|
210
225
|
|---|---|---|
|
|
211
226
|
| `pip check` | Finds dependency conflicts | Only covers one kind of problem |
|
|
212
|
-
| `conda doctor` | Health checks for conda environments | Doesn't
|
|
227
|
+
| `conda doctor` | Health checks for conda environments | Doesn't check what pip did inside conda, or venv / uv |
|
|
213
228
|
| pymedic | Lists environment info (versions, packages) | Reports, but doesn't diagnose or suggest fixes |
|
|
214
229
|
| pyenv-doctor | Early-stage environment checks | Single release so far |
|
|
215
230
|
| env-repair | Repairs conda and pip environments | Changes your environment; pyfirstaid only diagnoses, safely |
|
|
@@ -227,8 +242,8 @@ Run one or a few: `python -m pyfirstaid --only shadowing,path`. List them all: `
|
|
|
227
242
|
|
|
228
243
|
## Scope
|
|
229
244
|
|
|
230
|
-
**In:** pip, venv and uv on Windows, macOS and Linux.
|
|
231
|
-
**Out (for now):** conda (use `conda doctor`), Poetry and pyenv specifics, automatic repair.
|
|
245
|
+
**In:** pip, venv and uv on Windows, macOS and Linux, plus conda / pip mixing problems.
|
|
246
|
+
**Out (for now):** conda's own internals (use `conda doctor`), Poetry and pyenv specifics, automatic repair.
|
|
232
247
|
|
|
233
248
|
## Contributing
|
|
234
249
|
|
|
@@ -236,6 +251,10 @@ Run one or a few: `python -m pyfirstaid --only shadowing,path`. List them all: `
|
|
|
236
251
|
- Development: `python -m pip install -e ".[dev]"`, then `python -m pytest` and `ruff check src tests`.
|
|
237
252
|
- A new check is one file in `src/pyfirstaid/checks/` that returns a list of `Finding`s, registered in `checks/__init__.py`.
|
|
238
253
|
|
|
254
|
+
## Author
|
|
255
|
+
|
|
256
|
+
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.
|
|
257
|
+
|
|
239
258
|
## License
|
|
240
259
|
|
|
241
260
|
MIT
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
# pyfirstaid 🩹
|
|
2
|
+
[](https://pypi.org/project/pyfirstaid/)
|
|
3
|
+
[](https://pypi.org/project/pyfirstaid/)
|
|
4
|
+
[](https://pypistats.org/packages/pyfirstaid)
|
|
2
5
|
[](https://github.com/sai-sakalam/pyfirstaid/actions/workflows/ci.yml)
|
|
6
|
+
[](https://github.com/sai-sakalam/pyfirstaid/blob/main/LICENSE)
|
|
3
7
|
|
|
4
8
|
**First aid for broken Python environments.** One command tells you *what's broken* and *exactly how to fix it*.
|
|
5
9
|
|
|
6
|
-
> **Status:
|
|
10
|
+
> **Status: early, actively developed.** 13 checks, tested on Windows, macOS and Linux (Python 3.9–3.14).
|
|
7
11
|
> Feedback is very welcome in [Issues](https://github.com/sai-sakalam/pyfirstaid/issues). *Which problems do you hit most?*
|
|
8
12
|
|
|
9
13
|
---
|
|
@@ -24,7 +28,7 @@ system Python, and a file called `random.py` is hiding the real `random` module:
|
|
|
24
28
|
```console
|
|
25
29
|
$ python -m pyfirstaid --share
|
|
26
30
|
|
|
27
|
-
pyfirstaid 0.2.
|
|
31
|
+
pyfirstaid 0.2.3: checking your Python environment
|
|
28
32
|
Python 3.13.1 (~/project/.venv/bin/python)
|
|
29
33
|
|
|
30
34
|
✔ Python 3.13.1 (supported until October 2029)
|
|
@@ -168,6 +172,7 @@ Download `pyfirstaid.pyz` from the [latest release](https://github.com/sai-sakal
|
|
|
168
172
|
|---|---|
|
|
169
173
|
| `python-version` | Python past (or close to) end of life, pre-release Pythons, Apple's built-in Command Line Tools Python |
|
|
170
174
|
| `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
|
+
| `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 |
|
|
171
176
|
| `pip-mismatch` | `pip` installs into a different Python than the one you run, pip missing, a broken `pip` command |
|
|
172
177
|
| `path` | Typing `python` / `python3` runs a *different* Python, or the Windows Microsoft Store shortcut |
|
|
173
178
|
| `leaks` | `PYTHONPATH` / `PYTHONHOME` set, venvs that can see system packages |
|
|
@@ -186,7 +191,7 @@ Run one or a few: `python -m pyfirstaid --only shadowing,path`. List them all: `
|
|
|
186
191
|
| Tool | What it does | Gap pyfirstaid fills |
|
|
187
192
|
|---|---|---|
|
|
188
193
|
| `pip check` | Finds dependency conflicts | Only covers one kind of problem |
|
|
189
|
-
| `conda doctor` | Health checks for conda environments | Doesn't
|
|
194
|
+
| `conda doctor` | Health checks for conda environments | Doesn't check what pip did inside conda, or venv / uv |
|
|
190
195
|
| pymedic | Lists environment info (versions, packages) | Reports, but doesn't diagnose or suggest fixes |
|
|
191
196
|
| pyenv-doctor | Early-stage environment checks | Single release so far |
|
|
192
197
|
| env-repair | Repairs conda and pip environments | Changes your environment; pyfirstaid only diagnoses, safely |
|
|
@@ -204,8 +209,8 @@ Run one or a few: `python -m pyfirstaid --only shadowing,path`. List them all: `
|
|
|
204
209
|
|
|
205
210
|
## Scope
|
|
206
211
|
|
|
207
|
-
**In:** pip, venv and uv on Windows, macOS and Linux.
|
|
208
|
-
**Out (for now):** conda (use `conda doctor`), Poetry and pyenv specifics, automatic repair.
|
|
212
|
+
**In:** pip, venv and uv on Windows, macOS and Linux, plus conda / pip mixing problems.
|
|
213
|
+
**Out (for now):** conda's own internals (use `conda doctor`), Poetry and pyenv specifics, automatic repair.
|
|
209
214
|
|
|
210
215
|
## Contributing
|
|
211
216
|
|
|
@@ -213,6 +218,10 @@ Run one or a few: `python -m pyfirstaid --only shadowing,path`. List them all: `
|
|
|
213
218
|
- Development: `python -m pip install -e ".[dev]"`, then `python -m pytest` and `ruff check src tests`.
|
|
214
219
|
- A new check is one file in `src/pyfirstaid/checks/` that returns a list of `Finding`s, registered in `checks/__init__.py`.
|
|
215
220
|
|
|
221
|
+
## Author
|
|
222
|
+
|
|
223
|
+
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.
|
|
224
|
+
|
|
216
225
|
## License
|
|
217
226
|
|
|
218
227
|
MIT
|
|
@@ -18,6 +18,13 @@ classifiers = [
|
|
|
18
18
|
"Intended Audience :: Developers",
|
|
19
19
|
"Operating System :: OS Independent",
|
|
20
20
|
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.9",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Programming Language :: Python :: 3.14",
|
|
27
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
21
28
|
"Topic :: Software Development :: Debuggers",
|
|
22
29
|
"Topic :: System :: Installation/Setup",
|
|
23
30
|
]
|
|
@@ -31,7 +38,10 @@ pyfirstaid = "pyfirstaid.cli:main"
|
|
|
31
38
|
|
|
32
39
|
[project.urls]
|
|
33
40
|
Homepage = "https://github.com/sai-sakalam/pyfirstaid"
|
|
41
|
+
Source = "https://github.com/sai-sakalam/pyfirstaid"
|
|
34
42
|
Issues = "https://github.com/sai-sakalam/pyfirstaid/issues"
|
|
43
|
+
Changelog = "https://github.com/sai-sakalam/pyfirstaid/blob/main/CHANGELOG.md"
|
|
44
|
+
Author = "https://sai-sakalam.github.io/"
|
|
35
45
|
|
|
36
46
|
[tool.hatch.version]
|
|
37
47
|
path = "src/pyfirstaid/__init__.py"
|
|
@@ -7,6 +7,7 @@ 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,
|
|
12
13
|
leaks,
|
|
@@ -23,6 +24,7 @@ from pyfirstaid.model import Check
|
|
|
23
24
|
ALL_CHECKS: List[Check] = [
|
|
24
25
|
Check(python_version.CHECK_ID, "Python version", python_version.run_check),
|
|
25
26
|
Check(venv.CHECK_ID, "Virtual environment", venv.run_check),
|
|
27
|
+
Check(conda.CHECK_ID, "conda / pip mixing", conda.run_check),
|
|
26
28
|
Check(pip_mismatch.CHECK_ID, "pip / python match", pip_mismatch.run_check),
|
|
27
29
|
Check(path_check.CHECK_ID, "python on PATH", path_check.run_check),
|
|
28
30
|
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
|
|
@@ -62,7 +62,8 @@ def run_check(opts: Options) -> List[Finding]:
|
|
|
62
62
|
findings.append(Finding(
|
|
63
63
|
CHECK_ID, Status.WARN,
|
|
64
64
|
"No virtual environment, and this system Python blocks pip installs",
|
|
65
|
-
detail="Your OS marks this Python as
|
|
65
|
+
detail="Your OS or package manager (e.g. Homebrew) marks this Python as "
|
|
66
|
+
"externally managed (PEP 668), "
|
|
66
67
|
"so `pip install` will fail with 'externally-managed-environment'.",
|
|
67
68
|
fix="python -m venv .venv && " + activate_command(".venv"),
|
|
68
69
|
))
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from pyfirstaid.checks import conda
|
|
4
|
+
from pyfirstaid.model import Options, Status
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def make_env(tmp_path, base=False, conda_pkgs=(), dists=()):
|
|
8
|
+
"""Build a fake conda environment.
|
|
9
|
+
|
|
10
|
+
conda_pkgs: (name, version) pairs that conda installed, with their dist-info.
|
|
11
|
+
dists: (dist-info folder name, INSTALLER contents) actually present in site-packages.
|
|
12
|
+
"""
|
|
13
|
+
prefix = tmp_path / "env"
|
|
14
|
+
meta = prefix / "conda-meta"
|
|
15
|
+
meta.mkdir(parents=True)
|
|
16
|
+
if base:
|
|
17
|
+
(prefix / "condabin").mkdir()
|
|
18
|
+
site = prefix / "lib" / "site-packages"
|
|
19
|
+
site.mkdir(parents=True)
|
|
20
|
+
for name, ver in conda_pkgs:
|
|
21
|
+
record = {
|
|
22
|
+
"name": name,
|
|
23
|
+
"version": ver,
|
|
24
|
+
"files": ["lib/site-packages/%s/__init__.py" % name,
|
|
25
|
+
"lib/site-packages/%s-%s.dist-info/METADATA" % (name, ver)],
|
|
26
|
+
}
|
|
27
|
+
(meta / ("%s-%s-0.json" % (name, ver))).write_text(json.dumps(record))
|
|
28
|
+
for folder, installer in dists:
|
|
29
|
+
d = site / folder
|
|
30
|
+
d.mkdir()
|
|
31
|
+
(d / "METADATA").write_text("Name: x\n")
|
|
32
|
+
if installer is not None:
|
|
33
|
+
(d / "INSTALLER").write_text(installer + "\n")
|
|
34
|
+
return str(prefix), [str(site)]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def run(prefix, site_dirs, monkeypatch, active=None):
|
|
38
|
+
if active is None:
|
|
39
|
+
monkeypatch.setenv("CONDA_PREFIX", prefix)
|
|
40
|
+
else:
|
|
41
|
+
monkeypatch.setenv("CONDA_PREFIX", active)
|
|
42
|
+
return conda.run_check(Options(), prefix=prefix, site_dirs=site_dirs)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_not_conda_is_skipped(tmp_path, monkeypatch):
|
|
46
|
+
monkeypatch.delenv("CONDA_PREFIX", raising=False)
|
|
47
|
+
findings = conda.run_check(Options(), prefix=str(tmp_path), site_dirs=[])
|
|
48
|
+
assert [f.status for f in findings] == [Status.SKIP]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_clean_env_is_ok(tmp_path, monkeypatch):
|
|
52
|
+
prefix, sites = make_env(
|
|
53
|
+
tmp_path,
|
|
54
|
+
conda_pkgs=[("numpy", "2.1.0")],
|
|
55
|
+
dists=[("numpy-2.1.0.dist-info", "conda"), ("requests-2.32.0.dist-info", "pip")],
|
|
56
|
+
)
|
|
57
|
+
findings = run(prefix, sites, monkeypatch)
|
|
58
|
+
assert [f.status for f in findings] == [Status.OK]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_pip_replaced_conda_package(tmp_path, monkeypatch):
|
|
62
|
+
prefix, sites = make_env(
|
|
63
|
+
tmp_path,
|
|
64
|
+
conda_pkgs=[("numpy", "2.1.0")],
|
|
65
|
+
dists=[("numpy-2.2.0.dist-info", "pip")], # pip upgraded over conda's numpy
|
|
66
|
+
)
|
|
67
|
+
findings = run(prefix, sites, monkeypatch)
|
|
68
|
+
assert findings[0].status == Status.WARN
|
|
69
|
+
assert "replaced by pip" in findings[0].title
|
|
70
|
+
assert "numpy 2.2.0" in findings[0].detail
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_conda_owned_dist_is_not_flagged_even_if_installer_says_pip(tmp_path, monkeypatch):
|
|
74
|
+
# some conda packages are built with pip; conda-meta lists the dist-info, so it's conda's
|
|
75
|
+
prefix, sites = make_env(
|
|
76
|
+
tmp_path,
|
|
77
|
+
conda_pkgs=[("numpy", "2.1.0")],
|
|
78
|
+
dists=[("numpy-2.1.0.dist-info", "pip")],
|
|
79
|
+
)
|
|
80
|
+
findings = run(prefix, sites, monkeypatch)
|
|
81
|
+
assert [f.status for f in findings] == [Status.OK]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_unknown_installer_is_not_flagged(tmp_path, monkeypatch):
|
|
85
|
+
prefix, sites = make_env(
|
|
86
|
+
tmp_path,
|
|
87
|
+
conda_pkgs=[("numpy", "2.1.0")],
|
|
88
|
+
dists=[("numpy-2.2.0.dist-info", None)], # no INSTALLER file: stay conservative
|
|
89
|
+
)
|
|
90
|
+
findings = run(prefix, sites, monkeypatch)
|
|
91
|
+
assert [f.status for f in findings] == [Status.OK]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_pip_packages_in_base_env(tmp_path, monkeypatch):
|
|
95
|
+
prefix, sites = make_env(
|
|
96
|
+
tmp_path, base=True,
|
|
97
|
+
dists=[("requests-2.32.0.dist-info", "pip")],
|
|
98
|
+
)
|
|
99
|
+
findings = run(prefix, sites, monkeypatch)
|
|
100
|
+
assert findings[0].status == Status.WARN
|
|
101
|
+
assert "base environment" in findings[0].title
|
|
102
|
+
assert "requests" in findings[0].detail
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_pip_packages_in_normal_env_are_fine(tmp_path, monkeypatch):
|
|
106
|
+
prefix, sites = make_env(tmp_path, dists=[("requests-2.32.0.dist-info", "pip")])
|
|
107
|
+
findings = run(prefix, sites, monkeypatch)
|
|
108
|
+
assert [f.status for f in findings] == [Status.OK]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_different_conda_env_activated(tmp_path, monkeypatch):
|
|
112
|
+
prefix, sites = make_env(tmp_path)
|
|
113
|
+
other = tmp_path / "other"
|
|
114
|
+
(other / "conda-meta").mkdir(parents=True)
|
|
115
|
+
findings = run(prefix, sites, monkeypatch, active=str(other))
|
|
116
|
+
assert findings[0].status == Status.WARN
|
|
117
|
+
assert "DIFFERENT conda environment" in findings[0].title
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_conda_activated_but_python_is_elsewhere(tmp_path, monkeypatch):
|
|
121
|
+
active = tmp_path / "myenv"
|
|
122
|
+
(active / "conda-meta").mkdir(parents=True)
|
|
123
|
+
monkeypatch.setenv("CONDA_PREFIX", str(active))
|
|
124
|
+
findings = conda.run_check(Options(), prefix=str(tmp_path / "not-conda"), site_dirs=[])
|
|
125
|
+
assert findings[0].status == Status.WARN
|
|
126
|
+
assert "not from it" in findings[0].title
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_conda_records_ignores_bad_json(tmp_path):
|
|
130
|
+
meta = tmp_path / "conda-meta"
|
|
131
|
+
meta.mkdir()
|
|
132
|
+
(meta / "broken.json").write_text("{not json")
|
|
133
|
+
(meta / "ok-1.0-0.json").write_text(json.dumps({"name": "Some_Pkg", "files": []}))
|
|
134
|
+
assert conda.conda_records(str(tmp_path)) == {"some-pkg": set()}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_real_environment_does_not_crash():
|
|
138
|
+
findings = conda.run_check(Options())
|
|
139
|
+
assert findings and all(f.check == "conda" for f in findings)
|
|
@@ -144,7 +144,7 @@ def test_all_checks_registered():
|
|
|
144
144
|
from pyfirstaid.checks import ALL_CHECKS
|
|
145
145
|
|
|
146
146
|
ids = [c.id for c in ALL_CHECKS]
|
|
147
|
-
assert len(ids) == len(set(ids)) ==
|
|
147
|
+
assert len(ids) == len(set(ids)) == 13
|
|
148
148
|
assert os.path.basename(__file__) # keeps `os` used
|
|
149
149
|
|
|
150
150
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|