Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions ruff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ select = [
ignore = ["COM812", "D203", "D213", "EM101", "EM102", "TRY003", "RUF012"]
per-file-ignores = {"tests/*.py" = ["D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107", "UP", "ANN", "S101", "S603"]}

[lint.mccabe]
max-complexity = 12

[format]
quote-style = "double"
indent-style = "space"
Expand Down
30 changes: 21 additions & 9 deletions src/picopip.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@
log = logging.getLogger(__name__)


def get_site_package_paths(venv_path: str) -> List[Path]:
def get_site_package_paths(
venv_path: str, *, include_system_packages: bool = True
) -> List[Path]:
"""Return all directories where packages might be installed for the given venv."""
for version_dir in (Path(venv_path) / "lib").iterdir():
if version_dir.name.startswith("python"):
Expand Down Expand Up @@ -59,20 +61,30 @@ def get_site_package_paths(venv_path: str) -> List[Path]:
)
continue

# Append system packages at the end, so that venv site-packages take precedence
for system_path in _find_system_packages(venv_path):
if system_path not in seen:
scan_paths.append(system_path)
seen.add(system_path)
if include_system_packages:
# Append system packages at the end, so that venv site-packages take precedence
for system_path in _find_system_packages(venv_path):
if system_path not in seen:
scan_paths.append(system_path)
seen.add(system_path)

return scan_paths


def get_packages_from_env(venv_path: str) -> List[Tuple[str, str]]:
def get_packages_from_env(
venv_path: str, *, ignore_system_packages: bool = False
) -> List[Tuple[str, str]]:
"""Return a list of (name, version) for all installed packages in the given venv."""

def _canonical_name(name: str) -> str:
"""PEP 503 normalization plus dashes as underscores."""
return re.sub(r"[-_.]+", "-", name).lower().replace("-", "_")

seen = set()
packages = []
for path in get_site_package_paths(venv_path):
for path in get_site_package_paths(
venv_path, include_system_packages=not ignore_system_packages
):
log.debug(f"Scanning {path} for installed packages...")
for dist_info in itertools.chain(
path.glob("*.dist-info"), path.glob("*.egg-info")
Expand All @@ -88,7 +100,7 @@ def get_packages_from_env(venv_path: str) -> List[Tuple[str, str]]:
dist_info,
)
continue
name = raw_name.lower()
name = _canonical_name(raw_name)
if name not in seen:
seen.add(name)
packages.append((raw_name, version))
Expand Down
32 changes: 32 additions & 0 deletions tests/test_packages_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,18 @@ def test_get_packages_from_env_egg_info(fake_venv):
assert ("legacy-pkg", "2.1.0") in pkgs


def test_get_packages_from_env_canonical_dedup(fake_venv):
"""Packages differing only by dashes/underscores should be deduped."""
venv, site = fake_venv
make_dist_info(site, "pure-eval", "0.2.2")
extra = (site / "../dupe_extra").absolute()
extra.mkdir(parents=True)
make_dist_info(extra, "pure_eval", "0.2.3")
(site / "dupe_extra.pth").write_text("../dupe_extra\n")
pkgs = get_packages_from_env(str(venv))
assert pkgs == [("pure-eval", "0.2.2")]


def test_get_packages_from_env_mixed_formats(fake_venv):
"""Test that both dist-info and egg-info packages are discovered together."""
venv, site = fake_venv
Expand All @@ -176,6 +188,26 @@ def test_get_packages_from_env_egg_info_with_pth(fake_venv):
assert ("external-legacy", "0.9.5") in pkgs


def test_get_packages_from_env_ignore_system_packages(tmp_path, monkeypatch):
"""System packages should be omitted when ignore_system_packages is True."""
venv_dir = tmp_path / "venv"
site_dir = venv_dir / "lib" / "python3.11" / "site-packages"
site_dir.mkdir(parents=True)
(venv_dir / "pyvenv.cfg").write_text("include-system-site-packages = true\n")

system_site = tmp_path / "system_site"
system_site.mkdir()
make_dist_info(system_site, "system-pkg", "0.1.0")

monkeypatch.setattr("picopip.site.getsitepackages", lambda: [str(system_site)])

pkgs = get_packages_from_env(str(venv_dir))
assert ("system-pkg", "0.1.0") in pkgs

pkgs = get_packages_from_env(str(venv_dir), ignore_system_packages=True)
assert ("system-pkg", "0.1.0") not in pkgs


def test_e2e_readme_example():
with tempfile.TemporaryDirectory() as tmpdir:
venv.create(tmpdir, with_pip=True)
Expand Down