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: Fix the support for relative paths from outside the project #1432

Merged
merged 3 commits into from
Oct 13, 2022
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/1220.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix the issue that relative paths don't work well with `--project` argument.
1 change: 1 addition & 0 deletions news/1381.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix the support for relative paths from outside the project.
27 changes: 15 additions & 12 deletions src/pdm/cli/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
from pdm.models.specifiers import get_specifier
from pdm.project import Project
from pdm.resolver import resolve
from pdm.utils import normalize_name
from pdm.utils import cd, normalize_name

PEP582_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "pep582")

Expand Down Expand Up @@ -259,6 +259,8 @@ def do_add(
for r in [parse_requirement(line, True) for line in editables] + [
parse_requirement(line) for line in packages
]:
if r.is_file_or_url:
r.relocate(project.root) # type: ignore
key = r.identify()
r.prerelease = prerelease
tracked_names.add(key)
Expand Down Expand Up @@ -438,18 +440,19 @@ def do_remove(
f"{'dev-' if dev else ''}dependencies: "
+ ", ".join(f"[bold green]{name}[/]" for name in packages)
)
for name in packages:
req = parse_requirement(name)
matched_indexes = sorted(
(i for i, r in enumerate(deps) if req.matches(r)), reverse=True
)
if not matched_indexes:
raise ProjectError(
f"[bold green]{name}[/] does not exist in "
f"[bold cyan]{group}[/] dependencies."
with cd(project.root):
for name in packages:
req = parse_requirement(name)
matched_indexes = sorted(
(i for i, r in enumerate(deps) if req.matches(r)), reverse=True
)
for i in matched_indexes:
del deps[i]
if not matched_indexes:
raise ProjectError(
f"[bold green]{name}[/] does not exist in "
f"[bold cyan]{group}[/] dependencies."
)
for i in matched_indexes:
del deps[i]
cast(Array, deps).multiline(True)

if not dry_run:
Expand Down
31 changes: 13 additions & 18 deletions src/pdm/models/candidates.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from pdm.project.metadata import MutableMetadata, SetupDistribution
from pdm.utils import (
cached_property,
cd,
convert_hashes,
create_tracked_tempdir,
expand_env_vars_in_auth,
Expand Down Expand Up @@ -236,14 +237,15 @@ def as_lockfile_entry(self, project_root: Path) -> dict[str, Any]:
if not self.req.editable:
result.update(revision=self.get_revision())
elif not self.req.is_named:
if self.req.is_file_or_url and self.req.is_local_dir:
result.update(path=path_replace(root_path, ".", self.req.str_path))
else:
result.update(
url=path_replace(
root_path.lstrip("/"), "${PROJECT_ROOT}", self.req.url
with cd(project_root):
if self.req.is_file_or_url and self.req.is_local_dir:
result.update(path=path_replace(root_path, ".", self.req.str_path))
else:
result.update(
url=path_replace(
root_path.lstrip("/"), "${PROJECT_ROOT}", self.req.url
)
)
)
return {k: v for k, v in result.items() if v}

def format(self) -> str:
Expand Down Expand Up @@ -329,30 +331,23 @@ def direct_url(self) -> dict[str, Any] | None:
}
)
elif isinstance(req, FileRequirement):
if req.is_local_dir:
assert self.link is not None
if self.link.is_file and self.link.file_path.is_dir():
return _filter_none(
{
"url": url_without_fragments(req.url),
"dir_info": _filter_none({"editable": req.editable or None}),
"subdirectory": req.subdirectory,
}
)
url = expand_env_vars_in_auth(
req.url.replace(
"${PROJECT_ROOT}",
self.environment.project.root.as_posix().lstrip( # type: ignore
"/"
),
)
)
with self.environment.get_finder() as finder:
hash_cache = self.environment.project.make_hash_cache()
return _filter_none(
{
"url": url_without_fragments(req.url),
"url": self.link.url_without_fragment,
"archive_info": {
"hash": hash_cache.get_hash(
Link(url), finder.session
self.link, finder.session
).replace(":", "=")
},
"subdirectory": req.subdirectory,
Expand Down
43 changes: 22 additions & 21 deletions src/pdm/models/repositories.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
)
from pdm.models.search import SearchResultParser
from pdm.models.specifiers import PySpecSet
from pdm.utils import normalize_name, url_without_fragments
from pdm.utils import cd, normalize_name, url_without_fragments

if TYPE_CHECKING:
from pdm._types import CandidateInfo, SearchResult, Source
Expand Down Expand Up @@ -397,26 +397,27 @@ 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:
for package in lockfile.get("package", []):
version = package.get("version")
if version:
package["version"] = f"=={version}"
package_name = package.pop("name")
req_dict = {
k: v
for k, v in package.items()
if k not in ("dependencies", "requires_python", "summary")
}
req = Requirement.from_req_dict(package_name, req_dict)
can = make_candidate(req, name=package_name, version=version)
can_id = self._identify_candidate(can)
self.packages[can_id] = can
candidate_info: CandidateInfo = (
package.get("dependencies", []),
package.get("requires_python", ""),
package.get("summary", ""),
)
self.candidate_info[can_id] = candidate_info
with cd(self.environment.project.root):
for package in lockfile.get("package", []):
version = package.get("version")
if version:
package["version"] = f"=={version}"
package_name = package.pop("name")
req_dict = {
k: v
for k, v in package.items()
if k not in ("dependencies", "requires_python", "summary")
}
req = Requirement.from_req_dict(package_name, req_dict)
can = make_candidate(req, name=package_name, version=version)
can_id = self._identify_candidate(can)
self.packages[can_id] = can
candidate_info: CandidateInfo = (
package.get("dependencies", []),
package.get("requires_python", ""),
package.get("summary", ""),
)
self.candidate_info[can_id] = candidate_info

for key, hashes in lockfile.get("metadata", {}).get("files", {}).items():
self.file_hashes[tuple(key.split(None, 1))] = { # type: ignore
Expand Down
28 changes: 19 additions & 9 deletions src/pdm/models/requirements.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ def __post_init__(self) -> None:
super().__post_init__()
self._parse_url()
if self.path and not self.path.exists():
raise RequirementError(f"The local path {self.path} does not exist.")
raise RequirementError(f"The local path '{self.path}' does not exist.")
if self.is_local_dir:
self._check_installable()

Expand All @@ -272,21 +272,22 @@ 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("./")
and not result.startswith("../")
):
if not self.path.is_absolute() and not result.startswith(("./", "../")):
result = "./" + result
if result.startswith("./../"):
result = result[2:]
return result

def _parse_url(self) -> None:
if not self.url:
if self.path:
self.url = path_to_url(self.path.absolute().as_posix())
if not self.path.is_absolute():
project_root = Path(".").absolute().as_posix().lstrip("/")
self.url = self.url.replace(project_root, "${PROJECT_ROOT}")
str_path = cast(str, self.str_path)
if str_path.startswith("./"):
str_path = str_path[2:]
self.url = f"file:///${{PROJECT_ROOT}}/{str_path}"
else:
self.url = path_to_url(self.path.absolute().as_posix())
else:
try:
self.path = Path(
Expand All @@ -301,6 +302,15 @@ def _parse_url(self) -> None:
pass
self._parse_name_from_url()

def relocate(self, root: str | Path) -> None:
"""Change the project root to the given path"""
if self.path is None or self.path.is_absolute():
return
# self.path is relative
self.path = Path(os.path.relpath(self.path, root))
self.url = ""
self._parse_url()

@property
def is_local(self) -> bool:
return self.path and self.path.exists() or False
Expand Down
2 changes: 1 addition & 1 deletion src/pdm/project/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def __init__(
err=True,
)

self.root = Path(root_path or "").absolute()
self.root: Path = Path(root_path or "").absolute()
self.is_global = is_global
self.init_global_project()
self._lockfile_file = self.root / "pdm.lock"
Expand Down
24 changes: 23 additions & 1 deletion tests/models/test_requirements.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import os
import shutil

import pytest

from pdm.models.requirements import RequirementError, parse_requirement
from pdm.utils import path_to_url
from pdm.utils import cd, path_to_url
from tests import FIXTURES

FILE_PREFIX = "file:///" if os.name == "nt" else "file://"
Expand Down Expand Up @@ -86,3 +87,24 @@ def test_not_supported_editable_requirement(line):
RequirementError, match="Editable requirement is only supported"
):
parse_requirement(line, True)


def test_convert_req_with_relative_path_from_outside(tmp_path):
shutil.copytree(FIXTURES / "projects/demo", tmp_path / "demo")
tmp_path.joinpath("project").mkdir()
with cd(tmp_path / "project"):
r = parse_requirement("../demo")
assert r.path.resolve() == tmp_path / "demo"
assert r.url == "file:///${PROJECT_ROOT}/../demo"


def test_file_req_relocate(tmp_path):
shutil.copytree(FIXTURES / "projects/demo", tmp_path / "demo")
tmp_path.joinpath("project").mkdir()
with cd(tmp_path):
r = parse_requirement("./demo")
assert r.path.resolve() == tmp_path / "demo"
assert r.url == "file:///${PROJECT_ROOT}/demo"
r.relocate(tmp_path / "project")
assert r.str_path == "../demo"
assert r.url == "file:///${PROJECT_ROOT}/../demo"