Skip to content

Commit

Permalink
fix: don't validate requirements that are not used (#3377)
Browse files Browse the repository at this point in the history
  • Loading branch information
frostming authored Jan 13, 2025
1 parent f0ad0c0 commit 4821fe5
Show file tree
Hide file tree
Showing 6 changed files with 29 additions and 22 deletions.
1 change: 1 addition & 0 deletions news/3376.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Don't validate local file requirements that are not used.
4 changes: 2 additions & 2 deletions src/pdm/formats/poetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def fix_req_path(req: Requirement) -> Requirement:
for req in req_dict:
yield from _convert_req(name, req)
elif isinstance(req_dict, str):
pdm_req = fix_req_path(Requirement.from_req_dict(name, _convert_specifier(req_dict), check_installable=False))
pdm_req = fix_req_path(Requirement.from_req_dict(name, _convert_specifier(req_dict)))
yield pdm_req.as_line()
else:
assert isinstance(req_dict, dict)
Expand All @@ -100,7 +100,7 @@ def fix_req_path(req: Requirement) -> Requirement:
"rev",
req_dict.pop("tag", req_dict.pop("branch", None)), # type: ignore[arg-type]
)
pdm_req = fix_req_path(Requirement.from_req_dict(name, req_dict, check_installable=False))
pdm_req = fix_req_path(Requirement.from_req_dict(name, req_dict))
yield pdm_req.as_line()


Expand Down
3 changes: 3 additions & 0 deletions src/pdm/models/candidates.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ def __init__(
:param link: the file link of the candidate.
"""
self.req = req
if isinstance(req, FileRequirement):
req.check_installable()
self.name = name or self.req.project_name
self.version = version
if link is None and not req.is_named:
Expand Down Expand Up @@ -277,6 +279,7 @@ def as_lockfile_entry(self, project_root: Path) -> dict[str, Any]:
result.update(revision=self.get_revision())
elif not self.req.is_named:
if self.req.is_file_or_url and self.req.is_local:
self.req._root = project_root
result.update(path=self.req.str_path)
else:
result.update(url=self.req.url)
Expand Down
35 changes: 18 additions & 17 deletions src/pdm/models/requirements.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ def create(cls: type[T], **kwargs: Any) -> T:
try:
kwargs["specifier"] = get_specifier(version)
except InvalidSpecifier as e:
raise RequirementError(f'Invalid specifier for {kwargs.get("name")}: {version}: {e}') from None
raise RequirementError(f"Invalid specifier for {kwargs.get('name')}: {version}: {e}") from None
return cls(**{k: v for k, v in kwargs.items() if k in inspect.signature(cls).parameters})

@classmethod
Expand All @@ -171,7 +171,7 @@ def from_dist(cls, dist: Distribution) -> Requirement:
return NamedRequirement.create(name=dist.metadata["Name"], version=f"=={dist.version}")

@classmethod
def from_req_dict(cls, name: str, req_dict: RequirementDict, check_installable: bool = True) -> Requirement:
def from_req_dict(cls, name: str, req_dict: RequirementDict) -> Requirement:
if isinstance(req_dict, str): # Version specifier only.
return NamedRequirement.create(name=name, version=req_dict)
for vcs in VCS_SCHEMA:
Expand All @@ -180,7 +180,7 @@ def from_req_dict(cls, name: str, req_dict: RequirementDict, check_installable:
url = f"{vcs}+{repo}"
return VcsRequirement.create(name=name, vcs=vcs, url=url, **req_dict)
if "path" in req_dict or "url" in req_dict:
return FileRequirement.create(name=name, **req_dict, check_installable=check_installable)
return FileRequirement.create(name=name, **req_dict)
return NamedRequirement.create(name=name, **req_dict)

@property
Expand Down Expand Up @@ -242,14 +242,11 @@ class FileRequirement(Requirement):
url: str = ""
path: Path | None = None
subdirectory: str | None = None
check_installable: bool = True
_root: Path = dataclasses.field(default_factory=Path.cwd, repr=False)

def __post_init__(self) -> None:
super().__post_init__()
self._parse_url()
if self.is_local_dir and self.check_installable:
self._check_installable()

def _hash_key(self) -> tuple:
return (*super()._hash_key(), self.get_full_url(), self.editable)
Expand Down Expand Up @@ -397,15 +394,20 @@ def _parse_name_from_url(self) -> None:
if not self.name and not self.is_vcs:
self.name = self.guess_name()

def _check_installable(self) -> None:
assert self.path
if not self.path.exists():
return
if not (self.path.joinpath("setup.py").exists() or self.path.joinpath("pyproject.toml").exists()):
raise RequirementError(f"The local path '{self.path}' is not installable.")
result = Setup.from_directory(self.path.absolute())
if result.name:
self.name = result.name
def check_installable(self) -> None:
if path := self.absolute_path:
if not path.exists():
raise RequirementError(f"The local path '{self.path}' does not exist.")
if path.is_dir():
if not path.joinpath("setup.py").exists() and not path.joinpath("pyproject.toml").exists():
raise RequirementError(f"The local path '{self.path}' is not installable.")
result = Setup.from_directory(path)
if result.name:
self.name = result.name
elif self.editable:
raise RequirementError("Local file requirement must not be editable.")
elif self.editable and not self.is_vcs:
raise RequirementError("Non-VCS remote file requirement must not be editable.")


@dataclasses.dataclass(eq=False)
Expand Down Expand Up @@ -521,8 +523,7 @@ def parse_requirement(line: str, editable: bool = False) -> Requirement:
r.path = Path(get_relative_path(r.url) or "")

if editable:
if r.is_vcs or (r.is_file_or_url and r.is_local_dir): # type: ignore[attr-defined]
assert isinstance(r, FileRequirement)
if isinstance(r, FileRequirement) and (r.is_vcs or not r.url or r.url.startswith("file://")):
r.editable = True
else:
raise RequirementError(f"{line}: Editable requirement is only supported for VCS link or local directory.")
Expand Down
6 changes: 3 additions & 3 deletions tests/models/test_candidates.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import pytest
from unearth import Link

from pdm.exceptions import RequirementError
from pdm.models.candidates import Candidate
from pdm.models.requirements import Requirement, parse_requirement
from pdm.models.specifiers import PySpecSet
Expand Down Expand Up @@ -313,6 +314,5 @@ def test_parse_metadata_with_dynamic_fields(project, local_finder):

def test_get_metadata_for_non_existing_path(project):
req = parse_requirement("file:///${PROJECT_ROOT}/non-existing-path")
candidate = Candidate(req)
with pytest.raises(FileNotFoundError, match="No such file or directory"):
candidate.prepare(project.environment).metadata
with pytest.raises(RequirementError, match="The local path '.+' does not exist"):
Candidate(req)
2 changes: 2 additions & 0 deletions tests/models/test_requirements.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ def filter_requirements_to_lines(
@pytest.mark.parametrize("req, result", REQUIREMENTS)
def test_convert_req_dict_to_req_line(req, result):
r = parse_requirement(req)
if hasattr(r, "check_installable"):
r.check_installable()
result = result or req
assert r.as_line() == result

Expand Down

0 comments on commit 4821fe5

Please sign in to comment.