pyfirstaid 0.2.1__tar.gz → 0.2.2__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 (33) hide show
  1. {pyfirstaid-0.2.1 → pyfirstaid-0.2.2}/CHANGELOG.md +10 -0
  2. {pyfirstaid-0.2.1 → pyfirstaid-0.2.2}/PKG-INFO +2 -2
  3. {pyfirstaid-0.2.1 → pyfirstaid-0.2.2}/README.md +1 -1
  4. {pyfirstaid-0.2.1 → pyfirstaid-0.2.2}/src/pyfirstaid/__init__.py +1 -1
  5. {pyfirstaid-0.2.1 → pyfirstaid-0.2.2}/src/pyfirstaid/checks/dependencies.py +20 -1
  6. {pyfirstaid-0.2.1 → pyfirstaid-0.2.2}/src/pyfirstaid/checks/pip_mismatch.py +5 -2
  7. {pyfirstaid-0.2.1 → pyfirstaid-0.2.2}/src/pyfirstaid/util.py +6 -2
  8. {pyfirstaid-0.2.1 → pyfirstaid-0.2.2}/tests/test_more_checks.py +41 -0
  9. {pyfirstaid-0.2.1 → pyfirstaid-0.2.2}/.github/workflows/ci.yml +0 -0
  10. {pyfirstaid-0.2.1 → pyfirstaid-0.2.2}/.github/workflows/publish.yml +0 -0
  11. {pyfirstaid-0.2.1 → pyfirstaid-0.2.2}/.gitignore +0 -0
  12. {pyfirstaid-0.2.1 → pyfirstaid-0.2.2}/LICENSE +0 -0
  13. {pyfirstaid-0.2.1 → pyfirstaid-0.2.2}/pyproject.toml +0 -0
  14. {pyfirstaid-0.2.1 → pyfirstaid-0.2.2}/scripts/build_pyz.py +0 -0
  15. {pyfirstaid-0.2.1 → pyfirstaid-0.2.2}/src/pyfirstaid/__main__.py +0 -0
  16. {pyfirstaid-0.2.1 → pyfirstaid-0.2.2}/src/pyfirstaid/checks/__init__.py +0 -0
  17. {pyfirstaid-0.2.1 → pyfirstaid-0.2.2}/src/pyfirstaid/checks/broken_installs.py +0 -0
  18. {pyfirstaid-0.2.1 → pyfirstaid-0.2.2}/src/pyfirstaid/checks/compiled.py +0 -0
  19. {pyfirstaid-0.2.1 → pyfirstaid-0.2.2}/src/pyfirstaid/checks/encoding.py +0 -0
  20. {pyfirstaid-0.2.1 → pyfirstaid-0.2.2}/src/pyfirstaid/checks/leaks.py +0 -0
  21. {pyfirstaid-0.2.1 → pyfirstaid-0.2.2}/src/pyfirstaid/checks/path_check.py +0 -0
  22. {pyfirstaid-0.2.1 → pyfirstaid-0.2.2}/src/pyfirstaid/checks/permissions.py +0 -0
  23. {pyfirstaid-0.2.1 → pyfirstaid-0.2.2}/src/pyfirstaid/checks/python_version.py +0 -0
  24. {pyfirstaid-0.2.1 → pyfirstaid-0.2.2}/src/pyfirstaid/checks/shadowing.py +0 -0
  25. {pyfirstaid-0.2.1 → pyfirstaid-0.2.2}/src/pyfirstaid/checks/ssl_check.py +0 -0
  26. {pyfirstaid-0.2.1 → pyfirstaid-0.2.2}/src/pyfirstaid/checks/venv.py +0 -0
  27. {pyfirstaid-0.2.1 → pyfirstaid-0.2.2}/src/pyfirstaid/cli.py +0 -0
  28. {pyfirstaid-0.2.1 → pyfirstaid-0.2.2}/src/pyfirstaid/delegate.py +0 -0
  29. {pyfirstaid-0.2.1 → pyfirstaid-0.2.2}/src/pyfirstaid/model.py +0 -0
  30. {pyfirstaid-0.2.1 → pyfirstaid-0.2.2}/src/pyfirstaid/report.py +0 -0
  31. {pyfirstaid-0.2.1 → pyfirstaid-0.2.2}/tests/test_checks.py +0 -0
  32. {pyfirstaid-0.2.1 → pyfirstaid-0.2.2}/tests/test_cli.py +0 -0
  33. {pyfirstaid-0.2.1 → pyfirstaid-0.2.2}/tests/test_delegate.py +0 -0
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.2
4
+
5
+ Fixes found by testing on a real Mac with Homebrew Python:
6
+
7
+ - **Fix: false "pip installs into a DIFFERENT Python" on Homebrew** (and other setups where
8
+ site-packages lives outside Python's own folder). Paths are now also compared with symlinks
9
+ resolved.
10
+ - **Dependency conflicts inside a system / Homebrew Python** are now reported as information
11
+ with safe advice, instead of a `pip install` fix that the system would refuse (PEP 668).
12
+
3
13
  ## 0.2.1
4
14
 
5
15
  - **Fix: correct results when installed with pipx or `uv tool`.** These tools put pyfirstaid in
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pyfirstaid
3
- Version: 0.2.1
3
+ Version: 0.2.2
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: Issues, https://github.com/sai-sakalam/pyfirstaid/issues
@@ -26,7 +26,7 @@ Description-Content-Type: text/markdown
26
26
 
27
27
  **First aid for broken Python environments.** One command tells you *what's broken* and *exactly how to fix it*.
28
28
 
29
- > **Status: v0.2.1.** 12 checks, tested on Windows, macOS and Linux (Python 3.9–3.14).
29
+ > **Status: v0.2.2.** 12 checks, tested on Windows, macOS and Linux (Python 3.9–3.14).
30
30
  > Feedback is very welcome in [Issues](https://github.com/sai-sakalam/pyfirstaid/issues). *Which problems do you hit most?*
31
31
 
32
32
  ---
@@ -3,7 +3,7 @@
3
3
 
4
4
  **First aid for broken Python environments.** One command tells you *what's broken* and *exactly how to fix it*.
5
5
 
6
- > **Status: v0.2.1.** 12 checks, tested on Windows, macOS and Linux (Python 3.9–3.14).
6
+ > **Status: v0.2.2.** 12 checks, tested on Windows, macOS and Linux (Python 3.9–3.14).
7
7
  > Feedback is very welcome in [Issues](https://github.com/sai-sakalam/pyfirstaid/issues). *Which problems do you hit most?*
8
8
 
9
9
  ---
@@ -1,3 +1,3 @@
1
1
  """pyfirstaid: first aid for broken Python environments."""
2
2
 
3
- __version__ = "0.2.1"
3
+ __version__ = "0.2.2"
@@ -7,7 +7,7 @@ import sys
7
7
  from typing import List
8
8
 
9
9
  from pyfirstaid.model import Finding, Options, Status
10
- from pyfirstaid.util import run
10
+ from pyfirstaid.util import in_virtualenv, run
11
11
 
12
12
  CHECK_ID = "dependencies"
13
13
 
@@ -25,6 +25,13 @@ def fix_for(line: str) -> str:
25
25
  return "python -m pip install --upgrade <the package named above>"
26
26
 
27
27
 
28
+ def system_managed() -> bool:
29
+ """A Python that refuses pip installs (PEP 668) and is not a venv."""
30
+ from pyfirstaid.checks.venv import is_externally_managed
31
+
32
+ return is_externally_managed() and not in_virtualenv()
33
+
34
+
28
35
  def run_check(opts: Options) -> List[Finding]:
29
36
  code, out, err = run([sys.executable, "-m", "pip", "check"], timeout=max(opts.timeout, 30))
30
37
  if code in (124, 126, 127) or "No module named pip" in err:
@@ -33,6 +40,18 @@ def run_check(opts: Options) -> List[Finding]:
33
40
  return [Finding(CHECK_ID, Status.OK, "No dependency conflicts (pip check)")]
34
41
 
35
42
  lines = [ln for ln in out.splitlines() if ln.strip()]
43
+ if system_managed():
44
+ # pip install would be refused here (PEP 668), and these packages belong to
45
+ # Homebrew / the OS, so a conflict is informational, not something to fix with pip.
46
+ return [Finding(
47
+ CHECK_ID, Status.INFO,
48
+ "%d dependency conflict(s) inside your system / Homebrew Python (usually harmless)"
49
+ % len(lines),
50
+ detail="\n".join(lines[:8]),
51
+ fix="Don't pip install into this Python. Do your work in a venv "
52
+ "(python -m venv .venv). If it causes real errors, reinstall Python with the "
53
+ "tool that installed it (e.g. brew reinstall python).",
54
+ )]
36
55
  findings = [Finding(CHECK_ID, Status.WARN, "Dependency conflict", detail=ln, fix=fix_for(ln))
37
56
  for ln in lines[:8]]
38
57
  if len(lines) > 8:
@@ -9,7 +9,7 @@ import sys
9
9
  from typing import List, Optional, Tuple
10
10
 
11
11
  from pyfirstaid.model import Finding, Options, Status
12
- from pyfirstaid.util import is_within, run
12
+ from pyfirstaid.util import is_within, run, site_package_dirs
13
13
 
14
14
  CHECK_ID = "pip-mismatch"
15
15
 
@@ -34,7 +34,10 @@ def pip_belongs_here(location: str, pip_python: str, running_python: str,
34
34
 
35
35
 
36
36
  def _allowed_prefixes() -> List[str]:
37
- prefixes = [sys.prefix]
37
+ # Homebrew (and some Linux distros) keep site-packages OUTSIDE sys.prefix,
38
+ # e.g. /opt/homebrew/lib/python3.14/site-packages, so include every
39
+ # site-packages folder this interpreter actually uses.
40
+ prefixes = [sys.prefix] + site_package_dirs()
38
41
  if site.ENABLE_USER_SITE:
39
42
  user_site = site.getusersitepackages()
40
43
  if isinstance(user_site, str):
@@ -31,10 +31,14 @@ def run(cmd: List[str], timeout: float = 15.0) -> Tuple[int, str, str]:
31
31
 
32
32
 
33
33
  def norm(path: Optional[str]) -> str:
34
- """Normalise a path for comparison (absolute, resolved case on Windows)."""
34
+ """Normalise a path for comparison: symlinks resolved, case-folded on Windows.
35
+
36
+ Resolving symlinks matters on macOS/Homebrew, where e.g.
37
+ /opt/homebrew/opt/python@3.14 links into /opt/homebrew/Cellar/...
38
+ """
35
39
  if not path:
36
40
  return ""
37
- return os.path.normcase(os.path.abspath(path))
41
+ return os.path.normcase(os.path.realpath(path))
38
42
 
39
43
 
40
44
  def is_within(path: str, parent: str) -> bool:
@@ -146,3 +146,44 @@ def test_all_checks_registered():
146
146
  ids = [c.id for c in ALL_CHECKS]
147
147
  assert len(ids) == len(set(ids)) == 12
148
148
  assert os.path.basename(__file__) # keeps `os` used
149
+
150
+
151
+ # --- v0.2.2 regressions found on a real Mac (Homebrew) ------------------------
152
+
153
+ def test_pip_in_homebrew_site_packages_outside_prefix(tmp_path):
154
+ # Homebrew: sys.prefix is .../Frameworks/Python.framework/Versions/3.14, but
155
+ # site-packages lives in /opt/homebrew/lib/python3.14/site-packages
156
+ from pyfirstaid.checks import pip_mismatch
157
+
158
+ prefix = str(tmp_path / "Frameworks" / "Versions" / "3.14")
159
+ site_dir = str(tmp_path / "lib" / "python3.14" / "site-packages")
160
+ loc = os.path.join(site_dir, "pip")
161
+ assert not pip_mismatch.pip_belongs_here(loc, "3.14", "3.14", [prefix])
162
+ assert pip_mismatch.pip_belongs_here(loc, "3.14", "3.14", [prefix, site_dir])
163
+
164
+
165
+ def test_norm_resolves_symlinks(tmp_path):
166
+ from pyfirstaid.util import norm
167
+
168
+ real = tmp_path / "Cellar" / "python"
169
+ real.mkdir(parents=True)
170
+ link = tmp_path / "opt-python"
171
+ link.symlink_to(real)
172
+ assert norm(str(link)) == norm(str(real))
173
+
174
+
175
+ def test_dependency_conflicts_in_system_python_are_info(monkeypatch):
176
+ monkeypatch.setattr(dependencies, "system_managed", lambda: True)
177
+ monkeypatch.setattr(dependencies, "run", lambda cmd, timeout=0: (
178
+ 1, "wheel 0.48.0 requires packaging, which is not installed.", ""))
179
+ findings = dependencies.run_check(Options())
180
+ assert len(findings) == 1 and findings[0].status == Status.INFO
181
+ assert "pip install" not in findings[0].fix.split("Don't pip install")[-1].split(".")[0]
182
+
183
+
184
+ def test_dependency_conflicts_in_venv_are_warnings(monkeypatch):
185
+ monkeypatch.setattr(dependencies, "system_managed", lambda: False)
186
+ monkeypatch.setattr(dependencies, "run", lambda cmd, timeout=0: (
187
+ 1, "wheel 0.48.0 requires packaging, which is not installed.", ""))
188
+ findings = dependencies.run_check(Options())
189
+ assert findings[0].status == Status.WARN and 'install "packaging"' in findings[0].fix
File without changes
File without changes
File without changes
File without changes