Skip to content

Commit 3e5f0a6

Browse files
committed
-r pylock: handle relative paths
1 parent 5ba7b8e commit 3e5f0a6

File tree

5 files changed

+203
-42
lines changed

5 files changed

+203
-42
lines changed

src/pip/_internal/req/constructors.py

Lines changed: 24 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@
3131
from pip._internal.utils.filetypes import is_archive_file
3232
from pip._internal.utils.misc import is_installable_dir
3333
from pip._internal.utils.packaging import get_requirement
34+
from pip._internal.utils.pylock import (
35+
package_archive_url,
36+
package_directory_url,
37+
package_sdist_url,
38+
package_vcs_url,
39+
package_wheel_url,
40+
)
3441
from pip._internal.utils.urls import path_to_url
3542
from pip._internal.vcs import is_url, vcs
3643

@@ -572,34 +579,6 @@ def install_req_extend_extras(
572579
return result
573580

574581

575-
def _url_from_path_and_url(path: str | None, url: str | None) -> str:
576-
if path:
577-
return path_to_url(path)
578-
else:
579-
assert url # guaranteed by pylock validation
580-
return url
581-
582-
583-
def _url_from_package_vcs(package_vcs: pylock.PackageVcs) -> str:
584-
url = (
585-
package_vcs.type
586-
+ "+"
587-
+ _url_from_path_and_url(package_vcs.path, package_vcs.url)
588-
+ "@"
589-
+ package_vcs.commit_id
590-
)
591-
if package_vcs.subdirectory:
592-
url += "#" + package_vcs.subdirectory
593-
return url
594-
595-
596-
def _url_from_package_archive(package_archive: pylock.PackageArchive) -> str:
597-
url = _url_from_path_and_url(package_archive.path, package_archive.url)
598-
if package_archive.subdirectory:
599-
url += "#" + package_archive.subdirectory
600-
return url
601-
602-
603582
def _pylock_hashes_to_hash_options(hashes: Mapping[str, str]) -> dict[str, list[str]]:
604583
return {k: [v] for k, v in hashes.items()}
605584

@@ -613,33 +592,37 @@ def install_req_from_pylock_package(
613592
| pylock.PackageSdist
614593
| pylock.PackageWheel
615594
),
616-
comes_from: str | None,
595+
comes_from: str,
617596
) -> InstallRequirement:
618597
pass
619-
# TODO no binary -> use package.sdist
620-
# TODO package.index
621-
# TODO user_supplied
622-
# TODO validate file size?
598+
# TODO: no binary -> use package.sdist
599+
# TODO: package.index
600+
# TODO: user_supplied
601+
# TODO: validate file size?
623602
if isinstance(package_dist, pylock.PackageVcs):
624603
return InstallRequirement(
625-
req=Requirement(f"{package.name} @ {_url_from_package_vcs(package_dist)}"),
604+
req=Requirement(
605+
f"{package.name} @ {package_vcs_url(comes_from, package_dist)}"
606+
),
626607
comes_from=comes_from,
627608
)
628609
elif isinstance(package_dist, pylock.PackageArchive):
629610
return InstallRequirement(
630611
req=Requirement(
631-
f"{package.name} @ {_url_from_package_archive(package_dist)}"
612+
f"{package.name} @ {package_archive_url(comes_from, package_dist)}"
632613
),
633614
comes_from=comes_from,
634615
hash_options=_pylock_hashes_to_hash_options(package_dist.hashes),
635616
)
636617
elif isinstance(package_dist, pylock.PackageDirectory):
637-
# TODO subdirectory
638618
if package_dist.editable:
639-
return install_req_from_editable(package_dist.path, comes_from=comes_from)
619+
return install_req_from_editable(
620+
package_directory_url(comes_from, package_dist), comes_from=comes_from
621+
)
640622
else:
641-
# TODO this may interpret the path as a requirement
642-
return install_req_from_line(package_dist.path, comes_from=comes_from)
623+
return install_req_from_line(
624+
package_directory_url(comes_from, package_dist), comes_from=comes_from
625+
)
643626
elif isinstance(package_dist, pylock.PackageSdist):
644627
if package.version:
645628
version = package.version
@@ -648,7 +631,7 @@ def install_req_from_pylock_package(
648631
ireq = InstallRequirement(
649632
req=Requirement(f"{package.name}=={str(version)}"),
650633
comes_from=comes_from,
651-
link=Link(_url_from_path_and_url(package_dist.path, package_dist.url)),
634+
link=Link(package_sdist_url(comes_from, package_dist)),
652635
hash_options=_pylock_hashes_to_hash_options(package_dist.hashes),
653636
)
654637
ireq.original_link = None # not a direct URL
@@ -661,7 +644,7 @@ def install_req_from_pylock_package(
661644
ireq = InstallRequirement(
662645
req=Requirement(f"{package.name}=={str(version)}"),
663646
comes_from=comes_from,
664-
link=Link(_url_from_path_and_url(package_dist.path, package_dist.url)),
647+
link=Link(package_wheel_url(comes_from, package_dist)),
665648
hash_options=_pylock_hashes_to_hash_options(package_dist.hashes),
666649
)
667650
ireq.original_link = None # not a direct URL

src/pip/_internal/utils/pylock.py

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
from __future__ import annotations
2+
3+
import os
4+
import re
15
from collections.abc import Iterable
26
from pathlib import Path
7+
from urllib.parse import urljoin
38

49
from pip._vendor.packaging.pylock import (
510
Package,
@@ -15,7 +20,7 @@
1520
from pip._internal.models.direct_url import ArchiveInfo, DirInfo, VcsInfo
1621
from pip._internal.models.link import Link
1722
from pip._internal.req.req_install import InstallRequirement
18-
from pip._internal.utils.urls import url_to_path
23+
from pip._internal.utils.urls import path_to_url, url_to_path
1924

2025

2126
def _pylock_package_from_install_requirement(
@@ -114,3 +119,87 @@ def pylock_from_install_requirements(
114119
key=lambda p: p.name,
115120
),
116121
)
122+
123+
124+
_SCHEME_RE = re.compile("^(http|https|file)://", re.IGNORECASE)
125+
126+
127+
def _is_url(s: str) -> bool:
128+
return bool(_SCHEME_RE.match(s))
129+
130+
131+
def _package_dist_url(
132+
pylock_filename_or_url: str,
133+
path: str | None,
134+
url: str | None,
135+
) -> str:
136+
"""Compute an url from a Pylock package path and url.
137+
138+
Give priority to path over url. If path is relative,
139+
compute an url using the pylock file location as base.
140+
"""
141+
if path is not None:
142+
if not os.path.isabs(path):
143+
if _is_url(pylock_filename_or_url):
144+
return urljoin(pylock_filename_or_url, path)
145+
else:
146+
return path_to_url(
147+
os.path.join(os.path.dirname(pylock_filename_or_url), path)
148+
)
149+
else:
150+
return path_to_url(path)
151+
else:
152+
assert url is not None # guaranteed by packaging.pylock validation
153+
return url
154+
155+
156+
def package_vcs_url(pylock_filename_or_url: str, package_vcs: PackageVcs) -> str:
157+
url = (
158+
package_vcs.type
159+
+ "+"
160+
+ _package_dist_url(pylock_filename_or_url, package_vcs.path, package_vcs.url)
161+
+ "@"
162+
+ package_vcs.commit_id
163+
)
164+
if package_vcs.subdirectory:
165+
assert "#" not in url # TODO: prorper exception
166+
url += "#" + package_vcs.subdirectory
167+
return url
168+
169+
170+
def package_archive_url(
171+
pylock_filename_or_url: str, package_archive: PackageArchive
172+
) -> str:
173+
url = _package_dist_url(
174+
pylock_filename_or_url, package_archive.path, package_archive.url
175+
)
176+
if package_archive.subdirectory:
177+
assert "#" not in url # TODO: prorper exception
178+
url += "#" + package_archive.subdirectory
179+
return url
180+
181+
182+
def package_directory_url(
183+
pylock_filename_or_url: str, package_directory: PackageDirectory
184+
) -> str:
185+
url = _package_dist_url(pylock_filename_or_url, package_directory.path, None)
186+
assert url.startswith("file://") # TODO: proper exception
187+
if not url.endswith("/"):
188+
url += "/"
189+
if package_directory.subdirectory:
190+
url += package_directory.subdirectory
191+
if not url.endswith("/"):
192+
url += "/"
193+
return url
194+
195+
196+
def package_sdist_url(pylock_filename_or_url: str, package_sdist: PackageSdist) -> str:
197+
return _package_dist_url(
198+
pylock_filename_or_url, package_sdist.path, package_sdist.url
199+
)
200+
201+
202+
def package_wheel_url(pylock_filename_or_url: str, package_wheel: PackageWheel) -> str:
203+
return _package_dist_url(
204+
pylock_filename_or_url, package_wheel.path, package_wheel.url
205+
)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
lock-version = "1.0"
2+
created-by = "pip"
3+
packages = [
4+
{ name = "simplewheel", directory = { path = "../src/simplewheel-2.0", editable = true } },
5+
{ name = "singlemodule", directory = { path = "../src/singlemodule" } },
6+
]

tests/data/lockfiles/pylock.toml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
lock-version = "1.0"
2+
created-by = "pip"
3+
4+
[[packages]]
5+
name = "simple"
6+
version = "2.0"
7+
8+
[packages.sdist]
9+
name = "simple-2.0.tar.gz"
10+
path = "../packages/simple-2.0.tar.gz"
11+
12+
[packages.sdist.hashes]
13+
sha256 = "3a084929238d13bcd3bb928af04f3bac7ca2357d419e29f01459dc848e2d69a4"
14+
15+
[[packages]]
16+
name = "simple2"
17+
18+
[packages.archive]
19+
path = "../packages/simple2-3.0.tar.gz"
20+
21+
[packages.archive.hashes]
22+
sha256 = "3ad45e1e9aa48b4462af0123f6a7e44a9115db1ef945d4d92c123dfe21815a06"
23+
24+
[[packages]]
25+
name = "simplewheel"
26+
version = "2.0"
27+
28+
[[packages.wheels]]
29+
name = "simplewheel-2.0-1-py2.py3-none-any.whl"
30+
path = "../packages/simplewheel-2.0-1-py2.py3-none-any.whl"
31+
32+
[packages.wheels.hashes]
33+
sha256 = "71e1ca6b16ae3382a698c284013f66504f2581099b2ce4801f60e9536236ceee"

tests/unit/test_utils_pylock.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
from __future__ import annotations
2+
3+
import pytest
4+
5+
from pip._internal.utils.pylock import _package_dist_url
6+
from pip._internal.utils.urls import path_to_url
7+
8+
9+
@pytest.mark.parametrize(
10+
"pylock_filename_or_url,path,url,expected",
11+
[
12+
(
13+
# URL, no path
14+
"pylock.toml",
15+
None,
16+
"https://example.com/foo.tar.gz",
17+
"https://example.com/foo.tar.gz",
18+
),
19+
(
20+
# path over URL, joined with pylock.toml dir
21+
"/base/pylock.toml",
22+
"foo.tar.gz",
23+
"https://example.com/foo.tar.gz",
24+
"file:///base/foo.tar.gz",
25+
),
26+
(
27+
# absolute path over URL, not joined with pylock.toml dir
28+
"/base/pylock.toml",
29+
"/there/foo.tar.gz",
30+
"https://example.com/foo.tar.gz",
31+
"file:///there/foo.tar.gz",
32+
),
33+
(
34+
# relative path joined with pylock.toml http url
35+
"https://example.com/pylock.toml",
36+
"./there/foo.tar.gz",
37+
None,
38+
"https://example.com/there/foo.tar.gz",
39+
),
40+
],
41+
)
42+
def test_package_dist_url(
43+
pylock_filename_or_url: str,
44+
path: str | None,
45+
url: str | None,
46+
expected: str,
47+
) -> None:
48+
if expected.startswith("file:///"):
49+
expected = expected.replace("file:///", path_to_url("/"))
50+
assert _package_dist_url(pylock_filename_or_url, path, url) == expected

0 commit comments

Comments
 (0)