Skip to content

Commit

Permalink
fix: resolution failure with nested relative path deps (#1712)
Browse files Browse the repository at this point in the history
  • Loading branch information
frostming committed Feb 15, 2023
1 parent 72c82f5 commit 870f712
Show file tree
Hide file tree
Showing 14 changed files with 98 additions and 16 deletions.
1 change: 1 addition & 0 deletions news/1702.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix a resolution failure when the project has cascading relative path dependencies.
25 changes: 19 additions & 6 deletions src/pdm/models/repositories.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import dataclasses
import posixpath
import sys
from functools import lru_cache, wraps
from typing import TYPE_CHECKING, Any, Callable, Iterable, Mapping, TypeVar, cast
Expand All @@ -17,7 +18,13 @@
)
from pdm.models.search import SearchResultParser
from pdm.models.specifiers import PySpecSet
from pdm.utils import cd, normalize_name, url_without_fragments
from pdm.utils import (
cd,
normalize_name,
path_to_url,
url_to_path,
url_without_fragments,
)

if TYPE_CHECKING:
from pdm._types import CandidateInfo, SearchResult, Source
Expand Down Expand Up @@ -392,8 +399,8 @@ def all_candidates(self) -> dict[str, Candidate]:
return {can.req.identify(): can for can in self.packages.values()}

def _read_lockfile(self, lockfile: Mapping[str, Any]) -> None:
with cd(self.environment.project.root):
backend = self.environment.project.backend
root = self.environment.project.root
with cd(root):
for package in lockfile.get("package", []):
version = package.get("version")
if version:
Expand All @@ -406,8 +413,8 @@ def _read_lockfile(self, lockfile: Mapping[str, Any]) -> None:
}
req = Requirement.from_req_dict(package_name, req_dict)
if req.is_file_or_url and req.path and not req.url: # type: ignore
req.url = backend.relative_path_to_url( # type: ignore
req.path.as_posix() # type: ignore
req.url = path_to_url( # type: ignore
posixpath.join(root, req.path) # type: ignore
)
can = make_candidate(req, name=package_name, version=version)
can_id = self._identify_candidate(can)
Expand All @@ -426,10 +433,16 @@ def _read_lockfile(self, lockfile: Mapping[str, Any]) -> None:

def _identify_candidate(self, candidate: Candidate) -> tuple:
url = getattr(candidate.req, "url", None)
if url is not None:
url = url_without_fragments(url)
url = self.environment.project.backend.expand_line(url)
if url.startswith("file://"):
path = posixpath.normpath(url_to_path(url))
url = path_to_url(path)
return (
candidate.identify(),
candidate.version if not url else None,
url_without_fragments(url) if url else None,
url,
candidate.req.editable,
)

Expand Down
12 changes: 10 additions & 2 deletions src/pdm/models/requirements.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import inspect
import json
import os
import posixpath
import re
import secrets
import urllib.parse as urlparse
Expand Down Expand Up @@ -278,8 +279,15 @@ def create(cls: type[T], **kwargs: Any) -> T:
def str_path(self) -> str | None:
if not self.path:
return None
result = self.path.as_posix()
if not self.path.is_absolute() and not result.startswith(("./", "../")):
if self.path.is_absolute():
try:
result = self.path.relative_to(Path.cwd()).as_posix()
except ValueError:
return self.path.as_posix()
else:
result = self.path.as_posix()
result = posixpath.normpath(result)
if not result.startswith(("./", "../")):
result = "./" + result
if result.startswith("./../"):
result = result[2:]
Expand Down
20 changes: 13 additions & 7 deletions src/pdm/resolver/providers.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
from __future__ import annotations

import os
from typing import TYPE_CHECKING, Callable, cast

from packaging.specifiers import InvalidSpecifier, SpecifierSet
from resolvelib import AbstractProvider

from pdm.models.candidates import Candidate, make_candidate
from pdm.models.repositories import LockedRepository
from pdm.models.requirements import parse_requirement, strip_extras
from pdm.models.requirements import FileRequirement, parse_requirement, strip_extras
from pdm.resolver.python import (
PythonCandidate,
PythonRequirement,
Expand Down Expand Up @@ -157,18 +158,23 @@ def matches_gen() -> Iterator[Candidate]:

return matches_gen

def _compare_file_reqs(self, req1: FileRequirement, req2: FileRequirement) -> bool:
backend = self.repository.environment.project.backend
if req1.path and req2.path:
return os.path.normpath(req1.path) == os.path.normpath(req2.path)
left = backend.expand_line(url_without_fragments(req1.get_full_url()))
right = backend.expand_line(url_without_fragments(req2.get_full_url()))
return left == right

def is_satisfied_by(self, requirement: Requirement, candidate: Candidate) -> bool:
if isinstance(requirement, PythonRequirement):
return is_python_satisfied_by(requirement, candidate)
elif candidate.identify() in self.overrides:
return True
if not requirement.is_named:
backend = self.repository.environment.project.backend
return not candidate.req.is_named and backend.expand_line(
url_without_fragments(candidate.req.get_full_url()) # type: ignore
) == backend.expand_line(
url_without_fragments(requirement.get_full_url()) # type: ignore
)
if candidate.req.is_named:
return False
return self._compare_file_reqs(requirement, candidate.req) # type: ignore
version = candidate.version
this_name = self.repository.environment.project.name
if version is None or candidate.name == this_name:
Expand Down
10 changes: 9 additions & 1 deletion tests/cli/test_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from pdm.cli import actions
from pdm.models.requirements import parse_requirement
from pdm.pytest import Distribution
from pdm.utils import cd


@pytest.mark.usefixtures("repository")
Expand Down Expand Up @@ -206,7 +207,6 @@ def test_sync_with_pure_option(project, working_set, invoke):
assert "django" not in working_set


@pytest.mark.usefixtures("repository")
def test_install_referencing_self_package(project, working_set, invoke):
project.add_dependencies({"pytz": parse_requirement("pytz")}, to_group="tz")
project.add_dependencies({"urllib3": parse_requirement("urllib3")}, to_group="web")
Expand All @@ -216,3 +216,11 @@ def test_install_referencing_self_package(project, working_set, invoke):
invoke(["install", "-Gall"], obj=project, strict=True)
assert "pytz" in working_set
assert "urllib3" in working_set


def test_install_monorepo_with_rel_paths(fixture_project, invoke, working_set):
project = fixture_project("test-monorepo")
with cd(project.root):
invoke(["install"], obj=project, strict=True)
for package in ("package-a", "package-b", "core"):
assert package in working_set
1 change: 1 addition & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ def func(project_name):
source = FIXTURES / "projects" / project_name
copytree(source, project_no_init.root)
project_no_init.pyproject.reload()
project_no_init.environment = None
return project_no_init

return func
Expand Down
1 change: 1 addition & 0 deletions tests/fixtures/projects/test-monorepo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# pdm_test
Empty file.
10 changes: 10 additions & 0 deletions tests/fixtures/projects/test-monorepo/core/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[project]
name = "core"
version = "0.0.1"
description = ""
requires-python = ">= 3.7"
dependencies = []

[build-system]
requires = ["pdm-pep517"]
build-backend = "pdm.pep517.api"
Empty file.
12 changes: 12 additions & 0 deletions tests/fixtures/projects/test-monorepo/package_a/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[project]
name = "package_a"
version = "0.0.1"
description = ""
requires-python = ">= 3.7"
dependencies = [
"core @ file:///${PROJECT_ROOT}/../core",
]

[build-system]
requires = ["pdm-pep517"]
build-backend = "pdm.pep517.api"
Empty file.
12 changes: 12 additions & 0 deletions tests/fixtures/projects/test-monorepo/package_b/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[project]
name = "package_b"
version = "0.0.1"
description = ""
requires-python = ">= 3.7"
dependencies = [
"core @ file:///${PROJECT_ROOT}/../core",
]

[build-system]
requires = ["pdm-pep517"]
build-backend = "pdm.pep517.api"
10 changes: 10 additions & 0 deletions tests/fixtures/projects/test-monorepo/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[project]
requires-python = ">= 3.7"
dependencies = [
"package_a @ file:///${PROJECT_ROOT}/package_a",
"package_b @ file:///${PROJECT_ROOT}/package_b",
]

[build-system]
requires = ["pdm-pep517"]
build-backend = "pdm.pep517.api"

0 comments on commit 870f712

Please sign in to comment.