Skip to content

Commit

Permalink
fix(cache): ensure installation and cache works with any environment (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
noirbizarre authored Apr 20, 2024
1 parent b203d17 commit e3784d8
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 2 deletions.
1 change: 1 addition & 0 deletions news/2839.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixes cached packages metadata files (`.referrers`) collisions on `sync` when using a `venv` with `symlink` cache method.
2 changes: 1 addition & 1 deletion src/pdm/installers/installers.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def read_dist_info(self, filename: str) -> str:
def iter_files(self) -> Iterable[Path]:
for root, _, files in os.walk(self.package.path):
for file in files:
if Path(root) == self.package.path and file in (".checksum", ".lock"):
if Path(root) == self.package.path and file in CachedPackage.cache_files:
continue
yield Path(root, file)

Expand Down
5 changes: 4 additions & 1 deletion src/pdm/models/cached_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import shutil
from functools import cached_property
from pathlib import Path
from typing import Any, ContextManager
from typing import Any, ClassVar, ContextManager

from pdm.termui import logger

Expand All @@ -22,6 +22,9 @@ class CachedPackage:
*Only wheel installations will be cached*
"""

cache_files: ClassVar[tuple[str, ...]] = (".lock", ".checksum", ".referrers")
"""List of files storing cache metadata and not being part of the package"""

def __init__(self, path: str | Path, original_wheel: Path | None = None) -> None:
self.path = Path(os.path.normcase(os.path.expanduser(path))).resolve()
self.original_wheel = original_wheel
Expand Down
60 changes: 60 additions & 0 deletions tests/test_installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,23 @@

import logging
import os
import venv
from pathlib import Path
from typing import Callable

import pytest
from unearth import Link

from pdm import utils
from pdm.core import Core
from pdm.environments.base import BaseEnvironment
from pdm.environments.local import PythonLocalEnvironment
from pdm.environments.python import PythonEnvironment
from pdm.installers import InstallManager
from pdm.models.cached_package import CachedPackage
from pdm.models.candidates import Candidate
from pdm.models.requirements import parse_requirement
from pdm.project.core import Project
from tests import FIXTURES

pytestmark = pytest.mark.usefixtures("local_finder")
Expand All @@ -32,6 +39,22 @@ def mocked_support(linker: str) -> bool:
return mocked_support


def _prepare_project_for_env(project: Project, env_cls: type[BaseEnvironment]):
project._saved_python = None
project._python = None
if env_cls is PythonEnvironment:
venv.create(project.root / ".venv", symlinks=True)
project.project_config["python.use_venv"] = True


@pytest.fixture(params=(PythonEnvironment, PythonLocalEnvironment), autouse=True)
def environment(request: pytest.RequestFixture, project: Project) -> type[BaseEnvironment]:
# Run all test against all environments as installation and cache behavior may differ
env_cls: type[BaseEnvironment] = request.param
_prepare_project_for_env(project, env_cls)
return env_cls


def test_install_wheel_with_inconsistent_dist_info(project):
req = parse_requirement("pyfunctional")
candidate = Candidate(
Expand Down Expand Up @@ -134,6 +157,9 @@ def test_install_wheel_with_cache(project, pdm, supports_link):
assert os.path.isfile(os.path.join(lib_path, "future_fstrings.py"))
assert os.path.isfile(os.path.join(lib_path, "aaaaa_future_fstrings.pth"))

for file in CachedPackage.cache_files:
assert not os.path.exists(os.path.join(lib_path, file))

cache_name = "future_fstrings-1.2.0-py2.py3-none-any.whl.cache"
assert any(p.path.name == cache_name for p in project.package_cache.iter_packages())
pdm(["run", "python", "-m", "site"], object=project)
Expand All @@ -152,6 +178,40 @@ def test_install_wheel_with_cache(project, pdm, supports_link):
assert not any(p.path.name == cache_name for p in project.package_cache.iter_packages())


@pytest.mark.parametrize("preferred", ["symlink", "hardlink", None])
def test_can_install_wheel_with_cache_in_multiple_projects(
project: Project, core: Core, supports_link, tmp_path_factory, environment
):
projects = []

for idx in range(3):
path: Path = tmp_path_factory.mktemp(f"project-{idx}")
p = core.create_project(path, global_config=project.global_config.config_file)
_prepare_project_for_env(p, environment)
projects.append(p)

req = parse_requirement("future-fstrings")
candidate = Candidate(
req,
link=Link("http://fixtures.test/artifacts/future_fstrings-1.2.0-py2.py3-none-any.whl"),
)

for p in projects:
installer = InstallManager(p.environment, use_install_cache=True)
installer.install(candidate)

lib_path = p.environment.get_paths()["purelib"]
if supports_link("symlink"):
assert os.path.islink(os.path.join(lib_path, "future_fstrings.py"))
assert os.path.islink(os.path.join(lib_path, "aaaaa_future_fstrings.pth"))
else:
assert os.path.isfile(os.path.join(lib_path, "future_fstrings.py"))
assert os.path.isfile(os.path.join(lib_path, "aaaaa_future_fstrings.pth"))

for file in CachedPackage.cache_files:
assert not os.path.exists(os.path.join(lib_path, file))


def test_url_requirement_is_not_cached(project):
req = parse_requirement(
"future-fstrings @ http://fixtures.test/artifacts/future_fstrings-1.2.0-py2.py3-none-any.whl"
Expand Down

0 comments on commit e3784d8

Please sign in to comment.