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.
Files changed (35) hide show
  1. {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/CHANGELOG.md +17 -0
  2. {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/PKG-INFO +25 -6
  3. {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/README.md +14 -5
  4. {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/pyproject.toml +10 -0
  5. {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/src/pyfirstaid/__init__.py +1 -1
  6. {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/src/pyfirstaid/checks/__init__.py +2 -0
  7. pyfirstaid-0.3.0/src/pyfirstaid/checks/conda.py +166 -0
  8. {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/src/pyfirstaid/checks/venv.py +2 -1
  9. pyfirstaid-0.3.0/tests/test_conda.py +139 -0
  10. {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/tests/test_more_checks.py +1 -1
  11. {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/.github/workflows/ci.yml +0 -0
  12. {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/.github/workflows/publish.yml +0 -0
  13. {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/.gitignore +0 -0
  14. {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/LICENSE +0 -0
  15. {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/scripts/build_pyz.py +0 -0
  16. {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/src/pyfirstaid/__main__.py +0 -0
  17. {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/src/pyfirstaid/checks/broken_installs.py +0 -0
  18. {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/src/pyfirstaid/checks/compiled.py +0 -0
  19. {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/src/pyfirstaid/checks/dependencies.py +0 -0
  20. {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/src/pyfirstaid/checks/encoding.py +0 -0
  21. {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/src/pyfirstaid/checks/leaks.py +0 -0
  22. {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/src/pyfirstaid/checks/path_check.py +0 -0
  23. {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/src/pyfirstaid/checks/permissions.py +0 -0
  24. {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/src/pyfirstaid/checks/pip_mismatch.py +0 -0
  25. {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/src/pyfirstaid/checks/python_version.py +0 -0
  26. {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/src/pyfirstaid/checks/shadowing.py +0 -0
  27. {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/src/pyfirstaid/checks/ssl_check.py +0 -0
  28. {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/src/pyfirstaid/cli.py +0 -0
  29. {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/src/pyfirstaid/delegate.py +0 -0
  30. {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/src/pyfirstaid/model.py +0 -0
  31. {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/src/pyfirstaid/report.py +0 -0
  32. {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/src/pyfirstaid/util.py +0 -0
  33. {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/tests/test_checks.py +0 -0
  34. {pyfirstaid-0.2.2 → pyfirstaid-0.3.0}/tests/test_cli.py +0 -0
  35. {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.2.2
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
+ [![PyPI](https://img.shields.io/pypi/v/pyfirstaid)](https://pypi.org/project/pyfirstaid/)
36
+ [![Python versions](https://img.shields.io/pypi/pyversions/pyfirstaid)](https://pypi.org/project/pyfirstaid/)
37
+ [![Downloads](https://img.shields.io/pypi/dm/pyfirstaid)](https://pypistats.org/packages/pyfirstaid)
25
38
  [![CI](https://github.com/sai-sakalam/pyfirstaid/actions/workflows/ci.yml/badge.svg)](https://github.com/sai-sakalam/pyfirstaid/actions/workflows/ci.yml)
39
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](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: v0.2.2.** 12 checks, tested on Windows, macOS and Linux (Python 3.9–3.14).
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.0: checking your Python environment
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 cover pip, venv or uv |
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
+ [![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://img.shields.io/pypi/dm/pyfirstaid)](https://pypistats.org/packages/pyfirstaid)
2
5
  [![CI](https://github.com/sai-sakalam/pyfirstaid/actions/workflows/ci.yml/badge.svg)](https://github.com/sai-sakalam/pyfirstaid/actions/workflows/ci.yml)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](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: v0.2.2.** 12 checks, tested on Windows, macOS and Linux (Python 3.9–3.14).
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.0: checking your Python environment
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 cover pip, venv or uv |
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"
@@ -1,3 +1,3 @@
1
1
  """pyfirstaid: first aid for broken Python environments."""
2
2
 
3
- __version__ = "0.2.2"
3
+ __version__ = "0.3.0"
@@ -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 externally managed (PEP 668), "
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)) == 12
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