Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(cache): ensure installation and cache works with any environment #2839

Merged
merged 1 commit into from
Apr 20, 2024
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
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
Loading