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: use stdlib url<->path conversion #3364

Merged
merged 3 commits into from
Dec 31, 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/3362.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Use stdlib for URL <-> Path conversions.
12 changes: 6 additions & 6 deletions src/pdm/models/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from pathlib import Path
from typing import TYPE_CHECKING

from pdm.utils import expand_env_vars, path_to_url
from pdm.utils import expand_env_vars

if TYPE_CHECKING:
from typing import TypedDict
Expand All @@ -24,7 +24,7 @@
return line

def relative_path_to_url(self, path: str) -> str:
return path_to_url(os.path.join(self.root, path))
return self.root.joinpath(path).as_uri()

@classmethod
@abc.abstractmethod
Expand Down Expand Up @@ -52,14 +52,14 @@

class PDMBackend(BuildBackend):
def expand_line(self, req: str, expand_env: bool = True) -> str:
line = req.replace("file:///${PROJECT_ROOT}", path_to_url(self.root.as_posix()))
line = req.replace("file:///${PROJECT_ROOT}", self.root.as_uri())
if expand_env:
line = expand_env_vars(line)
return line

def relative_path_to_url(self, path: str) -> str:
if os.path.isabs(path):
return path_to_url(path)
return Path(path).as_uri()

Check warning on line 62 in src/pdm/models/backends.py

View check run for this annotation

Codecov / codecov/patch

src/pdm/models/backends.py#L62

Added line #L62 was not covered by tests
return f"file:///${{PROJECT_ROOT}}/{urllib.parse.quote(path)}"

@classmethod
Expand All @@ -79,7 +79,7 @@
if not __format_spec:
return self.__path.as_posix()
elif __format_spec == "uri":
return path_to_url(self.__path.as_posix())
return self.__path.as_uri()
elif __format_spec == "real":
return self.__path.resolve().as_posix()
raise ValueError(f"Unknown format specifier: {__format_spec}")
Expand Down Expand Up @@ -110,7 +110,7 @@

def relative_path_to_url(self, path: str) -> str:
if os.path.isabs(path):
return path_to_url(path)
return Path(path).as_uri()

Check warning on line 113 in src/pdm/models/backends.py

View check run for this annotation

Codecov / codecov/patch

src/pdm/models/backends.py#L113

Added line #L113 was not covered by tests
return f"{{root:uri}}/{urllib.parse.quote(path)}"

@classmethod
Expand Down
3 changes: 1 addition & 2 deletions src/pdm/models/candidates.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
filtered_sources,
get_rev_from_url,
normalize_name,
path_to_url,
url_without_fragments,
)

Expand Down Expand Up @@ -353,7 +352,7 @@ def direct_url(self) -> dict[str, Any] | None:
assert self._source_dir
return _filter_none(
{
"url": path_to_url(self._source_dir.as_posix()),
"url": self._source_dir.as_uri(),
"dir_info": {"editable": True},
"subdirectory": req.subdirectory,
}
Expand Down
7 changes: 4 additions & 3 deletions src/pdm/models/repositories/lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
import dataclasses
import posixpath
from functools import cached_property
from pathlib import Path
from typing import TYPE_CHECKING, Collection, NamedTuple, cast

from pdm.exceptions import CandidateNotFound, PdmException
from pdm.models.candidates import Candidate
from pdm.models.markers import EnvSpec
from pdm.models.repositories.base import BaseRepository, CandidateMetadata
from pdm.models.requirements import FileRequirement, Requirement, parse_line
from pdm.utils import cd, path_to_url, url_to_path, url_without_fragments
from pdm.utils import cd, url_to_path, url_without_fragments

if TYPE_CHECKING:
from typing import Any, Callable, Iterable, Mapping
Expand Down Expand Up @@ -89,7 +90,7 @@ 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[attr-defined]
req.url = path_to_url(posixpath.join(root, req.path)) # type: ignore[attr-defined]
req.url = root.joinpath(req.path).as_uri() # type: ignore[attr-defined]
can = Candidate(req, name=package_name, version=version)
can.hashes = package.get("files", [])
if not static_urls and any("url" in f for f in can.hashes):
Expand All @@ -112,7 +113,7 @@ def _identify_candidate(self, candidate: Candidate) -> CandidateKey:
url = self.environment.project.backend.expand_line(cast(str, url))
if url.startswith("file://"):
path = posixpath.normpath(url_to_path(url))
url = path_to_url(path)
url = Path(path).as_uri()
return (
candidate.identify(),
candidate.version if not url else None,
Expand Down
39 changes: 23 additions & 16 deletions src/pdm/models/requirements.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@
add_ssh_scheme_to_git_uri,
comparable_version,
normalize_name,
path_to_url,
path_without_fragments,
split_path_fragments,
url_to_path,
url_without_fragments,
)
Expand Down Expand Up @@ -309,17 +308,24 @@
return result

def _parse_url(self) -> None:
if not self.url and self.path and self.path.is_absolute():
self.url = path_to_url(self.path.as_posix())
if not self.path:
path = get_relative_path(self.url)
if path is None:
if self.path:
path, fragments = split_path_fragments(self.path)
if not self.url and path.is_absolute():
self.url = path.as_uri() + fragments
self.path = path
# For relative path, we don't resolve URL now, so the path may still contain fragments,
# it will be handled in `relocate()` method.
else:
url = url_without_fragments(self.url)
relpath = get_relative_path(url)
if relpath is None:
try:
self.path = path_without_fragments(url_to_path(self.url))
except AssertionError:
self.path = Path(url_to_path(url))
except ValueError:
pass
else:
self.path = path_without_fragments(path)
self.path = Path(relpath)

if self.url:
self._parse_name_from_url()

Expand All @@ -328,11 +334,12 @@
if self.path is None or self.path.is_absolute():
return
# self.path is relative
self.path = path_without_fragments(os.path.relpath(self.path, backend.root))
path = self.path.as_posix()
if path == ".":
path = ""
self.url = backend.relative_path_to_url(path)
path, fragments = split_path_fragments(self.path)
self.path = Path(os.path.relpath(path, backend.root))
relpath = self.path.as_posix()
if relpath == ".":
relpath = ""

Check warning on line 341 in src/pdm/models/requirements.py

View check run for this annotation

Codecov / codecov/patch

src/pdm/models/requirements.py#L341

Added line #L341 was not covered by tests
self.url = backend.relative_path_to_url(relpath) + fragments
self._root = backend.root

@property
Expand Down Expand Up @@ -492,7 +499,7 @@
# We replace the {root.uri} temporarily with a dummy URL header
# to make it pass through the packaging.requirement parser
# and then revert it.
root_url = path_to_url(Path().as_posix())
root_url = Path().absolute().as_uri()
replaced = "{root:uri}" in line
if replaced:
line = line.replace("{root:uri}", root_url)
Expand Down
3 changes: 1 addition & 2 deletions src/pdm/project/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
is_conda_base_python,
is_path_relative_to,
normalize_name,
path_to_url,
)

if TYPE_CHECKING:
Expand Down Expand Up @@ -621,7 +620,7 @@ def make_self_candidate(self, editable: bool = True) -> Candidate:

from pdm.models.candidates import Candidate

req = parse_requirement(path_to_url(self.root.as_posix()), editable)
req = parse_requirement(self.root.as_uri(), editable)
assert self.name
req.name = self.name
can = Candidate(req, name=self.name, link=Link.from_path(self.root))
Expand Down
5 changes: 2 additions & 3 deletions src/pdm/pytest.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
from pdm.models.session import PDMPyPIClient
from pdm.project.config import Config
from pdm.project.core import Project
from pdm.utils import find_python_in_path, normalize_name, parse_version, path_to_url
from pdm.utils import find_python_in_path, normalize_name, parse_version

if TYPE_CHECKING:
from typing import Protocol
Expand Down Expand Up @@ -501,12 +501,11 @@ def local_finder_artifacts() -> Path:

@pytest.fixture
def local_finder(project_no_init: Project, local_finder_artifacts: Path) -> None:
artifacts_dir = str(local_finder_artifacts)
project_no_init.pyproject.settings["source"] = [
{
"type": "find_links",
"verify_ssl": False,
"url": path_to_url(artifacts_dir),
"url": local_finder_artifacts.as_uri(),
"name": "pypi",
}
]
Expand Down
27 changes: 6 additions & 21 deletions src/pdm/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,6 @@
from pdm._types import FileHash, RepositoryConfig
from pdm.compat import Distribution

_egg_fragment_re = re.compile(r"(.*)[#&]egg=[^&]*")

try:
_packaging_version = importlib_metadata.version("packaging")
except Exception:
Expand Down Expand Up @@ -189,7 +187,8 @@ def url_to_path(url: str) -> str:

WINDOWS = sys.platform == "win32"

assert url.startswith("file:"), f"You can only turn file: urls into filenames (not {url!r})"
if not url.startswith("file:"):
raise ValueError(f"You can only turn file: urls into filenames (not {url!r})")

_, netloc, path, _, _ = parse.urlsplit(url)

Expand Down Expand Up @@ -220,16 +219,10 @@ def url_to_path(url: str) -> str:
return path


def path_to_url(path: str) -> str:
"""
Convert a path to a file: URL. The path will be made absolute and have
quoted path parts.
"""
from urllib.request import pathname2url

path = os.path.normpath(os.path.abspath(path))
url = parse.urljoin("file:", pathname2url(path))
return url
def split_path_fragments(path: Path) -> tuple[Path, str]:
"""Split a path into fragments"""
left, sep, right = path.as_posix().partition("#egg=")
return Path(left), sep + right


def expand_env_vars(credential: str, quote: bool = False, env: Mapping[str, str] | None = None) -> str:
Expand Down Expand Up @@ -448,14 +441,6 @@ def is_pip_compatible_with_python(python_version: Version | str) -> bool:
return requires_python.contains(python_version, True)


def path_without_fragments(path: str) -> Path:
"""Remove egg fragment from path"""
match = _egg_fragment_re.search(path)
if not match:
return Path(path)
return Path(match.group(1))


def is_in_zipapp() -> bool:
"""Check if the current process is running in a zipapp"""
return not os.path.exists(__file__)
Expand Down
7 changes: 3 additions & 4 deletions tests/cli/test_add.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from pdm.models.markers import EnvSpec
from pdm.models.specifiers import PySpecSet
from pdm.pytest import Distribution
from pdm.utils import path_to_url
from tests import FIXTURES


Expand Down Expand Up @@ -268,9 +267,9 @@ def test_add_with_prerelease(project, working_set, pdm):

def test_add_editable_package_with_extras(project, working_set, pdm):
project.environment.python_requires = PySpecSet(">=3.6")
dep_path = FIXTURES.joinpath("projects/demo").as_posix()
pdm(["add", "-dGdev", "-e", f"{dep_path}[security]"], obj=project, strict=True)
assert f"-e {path_to_url(dep_path)}#egg=demo[security]" in project.use_pyproject_dependencies("dev", True)[0]
dep_path = FIXTURES.joinpath("projects/demo")
pdm(["add", "-dGdev", "-e", f"{dep_path.as_posix()}[security]"], obj=project, strict=True)
assert f"-e {dep_path.as_uri()}#egg=demo[security]" in project.use_pyproject_dependencies("dev", True)[0]
assert "demo" in working_set
assert "requests" in working_set
assert "urllib3" in working_set
Expand Down
5 changes: 3 additions & 2 deletions tests/cli/test_lock.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from pathlib import Path
from unittest.mock import ANY

import pytest
Expand All @@ -8,7 +9,7 @@
from pdm.models.requirements import parse_requirement
from pdm.models.specifiers import PySpecSet
from pdm.project.lockfile import FLAG_CROSS_PLATFORM, Compatibility
from pdm.utils import parse_version, path_to_url
from pdm.utils import parse_version
from tests import FIXTURES


Expand Down Expand Up @@ -418,7 +419,7 @@ def test_lock_for_multiple_targets(project, pdm, repository, nested):


@pytest.mark.usefixtures("repository")
@pytest.mark.parametrize("constraint", [CONSTRAINT_FILE, path_to_url(CONSTRAINT_FILE)])
@pytest.mark.parametrize("constraint", [CONSTRAINT_FILE, Path(CONSTRAINT_FILE).as_uri()])
def test_lock_with_override_file(project, pdm, constraint):
project.add_dependencies(["requests"])
pdm(["lock", "--override", constraint], obj=project, strict=True)
Expand Down
4 changes: 2 additions & 2 deletions tests/cli/test_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from pdm import termui
from pdm.cli import actions
from pdm.cli.utils import get_pep582_path
from pdm.utils import cd, path_to_url
from pdm.utils import cd


@pytest.fixture
Expand Down Expand Up @@ -972,7 +972,7 @@ def test_run_script_with_inline_metadata(project, pdm, local_finder, local_finde
result = pdm(["run", "test_script.py"], obj=project)
assert result.exit_code != 0

local_artifacts_url = path_to_url(str(local_finder_artifacts))
local_artifacts_url = local_finder_artifacts.as_uri()

project.root.joinpath("test_script.py").write_text(
textwrap.dedent(f"""\
Expand Down
4 changes: 1 addition & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@

from pdm.models.auth import keyring
from pdm.project import Project
from pdm.utils import path_to_url
from tests import FIXTURES

if TYPE_CHECKING:
Expand Down Expand Up @@ -118,12 +117,11 @@ def func(project_name):
copytree(source, project_no_init.root)
project_no_init.pyproject.reload()
if "local_finder" in request.fixturenames:
artifacts_dir = str(local_finder_artifacts)
project_no_init.pyproject.settings["source"] = [
{
"type": "find_links",
"verify_ssl": False,
"url": path_to_url(artifacts_dir),
"url": local_finder_artifacts.as_uri(),
"name": "pypi",
}
]
Expand Down
10 changes: 5 additions & 5 deletions tests/models/test_backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from pdm.models.backends import _BACKENDS, get_backend, get_relative_path
from pdm.project import Project
from pdm.utils import cd, path_to_url
from pdm.utils import cd
from tests import FIXTURES


Expand Down Expand Up @@ -36,8 +36,8 @@ def test_project_backend(project, working_set, backend, pdm):
assert "idna" in working_set
assert "demo" in working_set
dep = project.pyproject.metadata["dependencies"][0]
demo_path = project.root.joinpath("demo").as_posix()
demo_url = path_to_url(demo_path)
demo_path = project.root.joinpath("demo")
demo_url = demo_path.as_uri()
if backend == "pdm-backend":
assert dep == "demo @ file:///${PROJECT_ROOT}/demo"
elif backend == "hatchling":
Expand All @@ -54,7 +54,7 @@ def test_project_backend(project, working_set, backend, pdm):

def test_hatch_expand_variables(monkeypatch):
root = Path().absolute()
root_url = path_to_url(root.as_posix())
root_url = root.as_uri()
backend = get_backend("hatchling")(root)
monkeypatch.setenv("BAR", "bar")
assert backend.expand_line("demo @ {root:uri}/demo") == f"demo @ {root_url}/demo"
Expand All @@ -65,7 +65,7 @@ def test_hatch_expand_variables(monkeypatch):

def test_pdm_backend_expand_variables(monkeypatch):
root = Path().absolute()
root_url = path_to_url(root.as_posix())
root_url = root.as_uri()
backend = get_backend("pdm-backend")(root)
monkeypatch.setenv("BAR", "bar")
assert backend.expand_line("demo @ file:///${PROJECT_ROOT}/demo") == f"demo @ {root_url}/demo"
Expand Down
Loading
Loading