pyfirstaid 0.2.3__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.3 → pyfirstaid-0.3.0}/CHANGELOG.md +10 -0
  2. {pyfirstaid-0.2.3 → pyfirstaid-0.3.0}/PKG-INFO +13 -7
  3. {pyfirstaid-0.2.3 → pyfirstaid-0.3.0}/README.md +11 -6
  4. {pyfirstaid-0.2.3 → pyfirstaid-0.3.0}/pyproject.toml +1 -0
  5. {pyfirstaid-0.2.3 → pyfirstaid-0.3.0}/src/pyfirstaid/__init__.py +1 -1
  6. {pyfirstaid-0.2.3 → 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.3.0/tests/test_conda.py +139 -0
  9. {pyfirstaid-0.2.3 → pyfirstaid-0.3.0}/tests/test_more_checks.py +1 -1
  10. {pyfirstaid-0.2.3 → pyfirstaid-0.3.0}/.github/workflows/ci.yml +0 -0
  11. {pyfirstaid-0.2.3 → pyfirstaid-0.3.0}/.github/workflows/publish.yml +0 -0
  12. {pyfirstaid-0.2.3 → pyfirstaid-0.3.0}/.gitignore +0 -0
  13. {pyfirstaid-0.2.3 → pyfirstaid-0.3.0}/LICENSE +0 -0
  14. {pyfirstaid-0.2.3 → pyfirstaid-0.3.0}/scripts/build_pyz.py +0 -0
  15. {pyfirstaid-0.2.3 → pyfirstaid-0.3.0}/src/pyfirstaid/__main__.py +0 -0
  16. {pyfirstaid-0.2.3 → pyfirstaid-0.3.0}/src/pyfirstaid/checks/broken_installs.py +0 -0
  17. {pyfirstaid-0.2.3 → pyfirstaid-0.3.0}/src/pyfirstaid/checks/compiled.py +0 -0
  18. {pyfirstaid-0.2.3 → pyfirstaid-0.3.0}/src/pyfirstaid/checks/dependencies.py +0 -0
  19. {pyfirstaid-0.2.3 → pyfirstaid-0.3.0}/src/pyfirstaid/checks/encoding.py +0 -0
  20. {pyfirstaid-0.2.3 → pyfirstaid-0.3.0}/src/pyfirstaid/checks/leaks.py +0 -0
  21. {pyfirstaid-0.2.3 → pyfirstaid-0.3.0}/src/pyfirstaid/checks/path_check.py +0 -0
  22. {pyfirstaid-0.2.3 → pyfirstaid-0.3.0}/src/pyfirstaid/checks/permissions.py +0 -0
  23. {pyfirstaid-0.2.3 → pyfirstaid-0.3.0}/src/pyfirstaid/checks/pip_mismatch.py +0 -0
  24. {pyfirstaid-0.2.3 → pyfirstaid-0.3.0}/src/pyfirstaid/checks/python_version.py +0 -0
  25. {pyfirstaid-0.2.3 → pyfirstaid-0.3.0}/src/pyfirstaid/checks/shadowing.py +0 -0
  26. {pyfirstaid-0.2.3 → pyfirstaid-0.3.0}/src/pyfirstaid/checks/ssl_check.py +0 -0
  27. {pyfirstaid-0.2.3 → pyfirstaid-0.3.0}/src/pyfirstaid/checks/venv.py +0 -0
  28. {pyfirstaid-0.2.3 → pyfirstaid-0.3.0}/src/pyfirstaid/cli.py +0 -0
  29. {pyfirstaid-0.2.3 → pyfirstaid-0.3.0}/src/pyfirstaid/delegate.py +0 -0
  30. {pyfirstaid-0.2.3 → pyfirstaid-0.3.0}/src/pyfirstaid/model.py +0 -0
  31. {pyfirstaid-0.2.3 → pyfirstaid-0.3.0}/src/pyfirstaid/report.py +0 -0
  32. {pyfirstaid-0.2.3 → pyfirstaid-0.3.0}/src/pyfirstaid/util.py +0 -0
  33. {pyfirstaid-0.2.3 → pyfirstaid-0.3.0}/tests/test_checks.py +0 -0
  34. {pyfirstaid-0.2.3 → pyfirstaid-0.3.0}/tests/test_cli.py +0 -0
  35. {pyfirstaid-0.2.3 → pyfirstaid-0.3.0}/tests/test_delegate.py +0 -0
@@ -1,5 +1,15 @@
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
+
3
13
  ## 0.2.3
4
14
 
5
15
  - Clearer PEP 668 message: it now says the block can come from your OS *or* a package
@@ -1,11 +1,12 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pyfirstaid
3
- Version: 0.2.3
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
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
@@ -33,13 +34,13 @@ Description-Content-Type: text/markdown
33
34
  # pyfirstaid 🩹
34
35
  [![PyPI](https://img.shields.io/pypi/v/pyfirstaid)](https://pypi.org/project/pyfirstaid/)
35
36
  [![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)
37
+ [![Downloads](https://img.shields.io/pypi/dm/pyfirstaid)](https://pypistats.org/packages/pyfirstaid)
37
38
  [![CI](https://github.com/sai-sakalam/pyfirstaid/actions/workflows/ci.yml/badge.svg)](https://github.com/sai-sakalam/pyfirstaid/actions/workflows/ci.yml)
38
39
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](https://github.com/sai-sakalam/pyfirstaid/blob/main/LICENSE)
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.** 13 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.2.3: 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)
@@ -204,6 +205,7 @@ Download `pyfirstaid.pyz` from the [latest release](https://github.com/sai-sakal
204
205
  |---|---|
205
206
  | `python-version` | Python past (or close to) end of life, pre-release Pythons, Apple's built-in Command Line Tools Python |
206
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 |
207
209
  | `pip-mismatch` | `pip` installs into a different Python than the one you run, pip missing, a broken `pip` command |
208
210
  | `path` | Typing `python` / `python3` runs a *different* Python, or the Windows Microsoft Store shortcut |
209
211
  | `leaks` | `PYTHONPATH` / `PYTHONHOME` set, venvs that can see system packages |
@@ -222,7 +224,7 @@ Run one or a few: `python -m pyfirstaid --only shadowing,path`. List them all: `
222
224
  | Tool | What it does | Gap pyfirstaid fills |
223
225
  |---|---|---|
224
226
  | `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 |
227
+ | `conda doctor` | Health checks for conda environments | Doesn't check what pip did inside conda, or venv / uv |
226
228
  | pymedic | Lists environment info (versions, packages) | Reports, but doesn't diagnose or suggest fixes |
227
229
  | pyenv-doctor | Early-stage environment checks | Single release so far |
228
230
  | env-repair | Repairs conda and pip environments | Changes your environment; pyfirstaid only diagnoses, safely |
@@ -240,8 +242,8 @@ Run one or a few: `python -m pyfirstaid --only shadowing,path`. List them all: `
240
242
 
241
243
  ## Scope
242
244
 
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.
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.
245
247
 
246
248
  ## Contributing
247
249
 
@@ -249,6 +251,10 @@ Run one or a few: `python -m pyfirstaid --only shadowing,path`. List them all: `
249
251
  - Development: `python -m pip install -e ".[dev]"`, then `python -m pytest` and `ruff check src tests`.
250
252
  - A new check is one file in `src/pyfirstaid/checks/` that returns a list of `Finding`s, registered in `checks/__init__.py`.
251
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
+
252
258
  ## License
253
259
 
254
260
  MIT
@@ -1,13 +1,13 @@
1
1
  # pyfirstaid 🩹
2
2
  [![PyPI](https://img.shields.io/pypi/v/pyfirstaid)](https://pypi.org/project/pyfirstaid/)
3
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)
4
+ [![Downloads](https://img.shields.io/pypi/dm/pyfirstaid)](https://pypistats.org/packages/pyfirstaid)
5
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
6
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](https://github.com/sai-sakalam/pyfirstaid/blob/main/LICENSE)
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.** 13 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.2.3: 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)
@@ -172,6 +172,7 @@ Download `pyfirstaid.pyz` from the [latest release](https://github.com/sai-sakal
172
172
  |---|---|
173
173
  | `python-version` | Python past (or close to) end of life, pre-release Pythons, Apple's built-in Command Line Tools Python |
174
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 |
175
176
  | `pip-mismatch` | `pip` installs into a different Python than the one you run, pip missing, a broken `pip` command |
176
177
  | `path` | Typing `python` / `python3` runs a *different* Python, or the Windows Microsoft Store shortcut |
177
178
  | `leaks` | `PYTHONPATH` / `PYTHONHOME` set, venvs that can see system packages |
@@ -190,7 +191,7 @@ Run one or a few: `python -m pyfirstaid --only shadowing,path`. List them all: `
190
191
  | Tool | What it does | Gap pyfirstaid fills |
191
192
  |---|---|---|
192
193
  | `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 |
194
+ | `conda doctor` | Health checks for conda environments | Doesn't check what pip did inside conda, or venv / uv |
194
195
  | pymedic | Lists environment info (versions, packages) | Reports, but doesn't diagnose or suggest fixes |
195
196
  | pyenv-doctor | Early-stage environment checks | Single release so far |
196
197
  | env-repair | Repairs conda and pip environments | Changes your environment; pyfirstaid only diagnoses, safely |
@@ -208,8 +209,8 @@ Run one or a few: `python -m pyfirstaid --only shadowing,path`. List them all: `
208
209
 
209
210
  ## Scope
210
211
 
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.
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.
213
214
 
214
215
  ## Contributing
215
216
 
@@ -217,6 +218,10 @@ Run one or a few: `python -m pyfirstaid --only shadowing,path`. List them all: `
217
218
  - Development: `python -m pip install -e ".[dev]"`, then `python -m pytest` and `ruff check src tests`.
218
219
  - A new check is one file in `src/pyfirstaid/checks/` that returns a list of `Finding`s, registered in `checks/__init__.py`.
219
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
+
220
225
  ## License
221
226
 
222
227
  MIT
@@ -41,6 +41,7 @@ Homepage = "https://github.com/sai-sakalam/pyfirstaid"
41
41
  Source = "https://github.com/sai-sakalam/pyfirstaid"
42
42
  Issues = "https://github.com/sai-sakalam/pyfirstaid/issues"
43
43
  Changelog = "https://github.com/sai-sakalam/pyfirstaid/blob/main/CHANGELOG.md"
44
+ Author = "https://sai-sakalam.github.io/"
44
45
 
45
46
  [tool.hatch.version]
46
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.3"
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
@@ -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