Skip to content

Commit 5956617

Browse files
committed
repositories: add support for PEP 691 as fallback for PyPI
1 parent 9531082 commit 5956617

File tree

12 files changed

+384
-43
lines changed

12 files changed

+384
-43
lines changed

src/poetry/repositories/http_repository.py

+24-9
Original file line numberDiff line numberDiff line change
@@ -237,17 +237,16 @@ def _get_info_from_urls(
237237
urls["sdist"][0], metadata
238238
) or self._get_info_from_sdist(urls["sdist"][0])
239239

240-
def _links_to_data(self, links: list[Link], data: PackageInfo) -> dict[str, Any]:
241-
if not links:
242-
raise PackageNotFound(
243-
f'No valid distribution links found for package: "{data.name}" version:'
244-
f' "{data.version}"'
245-
)
240+
def _get_info_from_links(
241+
self,
242+
links: list[Link],
243+
*,
244+
ignore_yanked: bool = True,
245+
) -> PackageInfo:
246246
urls = defaultdict(list)
247247
metadata: dict[str, pkginfo.Distribution] = {}
248-
files: list[dict[str, Any]] = []
249248
for link in links:
250-
if link.yanked and not data.yanked:
249+
if link.yanked and ignore_yanked:
251250
# drop yanked files unless the entire release is yanked
252251
continue
253252
if link.has_metadata:
@@ -284,6 +283,21 @@ def _links_to_data(self, links: list[Link], data: PackageInfo) -> dict[str, Any]
284283
):
285284
urls["sdist"].append(link.url)
286285

286+
return self._get_info_from_urls(urls, metadata)
287+
288+
def _links_to_data(self, links: list[Link], data: PackageInfo) -> dict[str, Any]:
289+
if not links:
290+
raise PackageNotFound(
291+
f'No valid distribution links found for package: "{data.name}" version:'
292+
f' "{data.version}"'
293+
)
294+
295+
files: list[dict[str, Any]] = []
296+
for link in links:
297+
if link.yanked and not data.yanked:
298+
# drop yanked files unless the entire release is yanked
299+
continue
300+
287301
file_hash = f"{link.hash_name}:{link.hash}" if link.hash else None
288302

289303
if not link.hash or (
@@ -297,7 +311,8 @@ def _links_to_data(self, links: list[Link], data: PackageInfo) -> dict[str, Any]
297311

298312
data.files = files
299313

300-
info = self._get_info_from_urls(urls, metadata)
314+
# drop yanked files unless the entire release is yanked
315+
info = self._get_info_from_links(links, ignore_yanked=not data.yanked)
301316

302317
data.summary = info.summary
303318
data.requires_dist = info.requires_dist

src/poetry/repositories/link_sources/json.py

+22-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,28 @@ def _link_cache(self) -> LinkCache:
2828
url = file["url"]
2929
requires_python = file.get("requires-python")
3030
yanked = file.get("yanked", False)
31-
link = Link(url, requires_python=requires_python, yanked=yanked)
31+
32+
# see https://peps.python.org/pep-0714/#clients
33+
# and https://peps.python.org/pep-0691/#project-detail
34+
metadata: str | bool = False
35+
for metadata_key in ("core-metadata", "dist-info-metadata"):
36+
if metadata_key in file:
37+
metadata_value = file[metadata_key]
38+
if metadata_value and isinstance(metadata_value, dict):
39+
# The interface of poetry.core.packages.utils.link.Link
40+
# is currently limited to strings with one hash.
41+
if sha256 := metadata_value.get("sha256"):
42+
metadata = f"sha256={sha256}"
43+
else:
44+
key, value = next(iter(metadata_value.items()))
45+
metadata = f"{key}={value}"
46+
else:
47+
metadata = bool(metadata_value)
48+
break
49+
50+
link = Link(
51+
url, requires_python=requires_python, yanked=yanked, metadata=metadata
52+
)
3253

3354
if link.ext not in self.SUPPORTED_FORMATS:
3455
continue

src/poetry/repositories/pypi_repository.py

+9-17
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import logging
44

5-
from collections import defaultdict
65
from typing import TYPE_CHECKING
76
from typing import Any
87

@@ -161,25 +160,18 @@ def _get_release_info(
161160
})
162161

163162
if self._fallback and data.requires_dist is None:
164-
self._log("No dependencies found, downloading archives", level="debug")
163+
self._log(
164+
"No dependencies found, downloading metadata and/or archives",
165+
level="debug",
166+
)
165167
# No dependencies set (along with other information)
166168
# This might be due to actually no dependencies
167-
# or badly set metadata when uploading
169+
# or badly set metadata when uploading.
168170
# So, we need to make sure there is actually no
169-
# dependencies by introspecting packages
170-
urls = defaultdict(list)
171-
for url in json_data["urls"]:
172-
# Only get sdist and wheels if they exist
173-
dist_type = url["packagetype"]
174-
if dist_type not in SUPPORTED_PACKAGE_TYPES:
175-
continue
176-
177-
urls[dist_type].append(url["url"])
178-
179-
if not urls:
180-
return data.asdict()
181-
182-
info = self._get_info_from_urls(urls)
171+
# dependencies by introspecting packages.
172+
page = self.get_page(name)
173+
links = list(page.links_for_version(name, version))
174+
info = self._get_info_from_links(links)
183175

184176
data.requires_dist = info.requires_dist
185177

tests/repositories/conftest.py

+24
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
from __future__ import annotations
22

3+
import posixpath
4+
5+
from pathlib import Path
36
from typing import TYPE_CHECKING
7+
from typing import Any
48

59
import pytest
10+
import requests
611

712

813
if TYPE_CHECKING:
914
from tests.types import HTMLPageGetter
15+
from tests.types import RequestsSessionGet
1016

1117

1218
@pytest.fixture
@@ -29,3 +35,21 @@ def _fixture(content: str, base_url: str | None = None) -> str:
2935
"""
3036

3137
return _fixture
38+
39+
40+
@pytest.fixture
41+
def get_metadata_mock() -> RequestsSessionGet:
42+
def metadata_mock(url: str, **__: Any) -> requests.Response:
43+
if url.endswith(".metadata"):
44+
response = requests.Response()
45+
response.encoding = "application/text"
46+
response._content = (
47+
Path(__file__).parent
48+
/ "fixtures"
49+
/ "metadata"
50+
/ posixpath.basename(url)
51+
).read_bytes()
52+
return response
53+
raise requests.HTTPError()
54+
55+
return metadata_mock
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
Metadata-Version: 2.0
2+
Name: isort-metadata
3+
Version: 4.3.4
4+
Summary: A Python utility / library to sort Python imports.
5+
Home-page: https://github.com/timothycrosley/isort
6+
Author: Timothy Crosley
7+
Author-email: [email protected]
8+
License: MIT
9+
Keywords: Refactor,Python,Python2,Python3,Refactoring,Imports,Sort,Clean
10+
Platform: UNKNOWN
11+
Classifier: Development Status :: 6 - Mature
12+
Classifier: Intended Audience :: Developers
13+
Classifier: Natural Language :: English
14+
Classifier: Environment :: Console
15+
Classifier: License :: OSI Approved :: MIT License
16+
Classifier: Programming Language :: Python
17+
Classifier: Programming Language :: Python :: 2
18+
Classifier: Programming Language :: Python :: 2.7
19+
Classifier: Programming Language :: Python :: 3
20+
Classifier: Programming Language :: Python :: 3.4
21+
Classifier: Programming Language :: Python :: 3.5
22+
Classifier: Programming Language :: Python :: 3.6
23+
Classifier: Programming Language :: Python :: Implementation :: CPython
24+
Classifier: Programming Language :: Python :: Implementation :: PyPy
25+
Classifier: Topic :: Software Development :: Libraries
26+
Classifier: Topic :: Utilities
27+
Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*
28+
Requires-Dist: futures; python_version=="2.7"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"name": "isort-metadata",
3+
"files": [
4+
{
5+
"filename": "isort-metadata-4.3.4-py2-none-any.whl",
6+
"url": "https://files.pythonhosted.org/packages/41/d8/a945da414f2adc1d9e2f7d6e7445b27f2be42766879062a2e63616ad4199/isort-metadata-4.3.4-py2-none-any.whl",
7+
"core-metadata": true,
8+
"hashes": {
9+
"md5": "f0ad7704b6dc947073398ba290c3517f",
10+
"sha256": "ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497"
11+
}
12+
},
13+
{
14+
"filename": "isort-metadata-4.3.4-py3-none-any.whl",
15+
"url": "https://files.pythonhosted.org/packages/1f/2c/22eee714d7199ae0464beda6ad5fedec8fee6a2f7ffd1e8f1840928fe318/isort-metadata-4.3.4-py3-none-any.whl",
16+
"core-metadata": true,
17+
"hashes": {
18+
"md5": "fbaac4cd669ac21ea9e21ab1ea3180db",
19+
"sha256": "1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af"
20+
}
21+
},
22+
{
23+
"filename": "isort-metadata-4.3.4.tar.gz",
24+
"url": "https://files.pythonhosted.org/packages/b1/de/a628d16fdba0d38cafb3d7e34d4830f2c9cb3881384ce5c08c44762e1846/isort-metadata-4.3.4.tar.gz",
25+
"hashes": {
26+
"md5": "fb554e9c8f9aa76e333a03d470a5cf52",
27+
"sha256": "b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8"
28+
}
29+
}
30+
],
31+
"meta": {
32+
"api-version": "1.0",
33+
"_last-serial": 3575149
34+
}
35+
}

tests/repositories/fixtures/pypi.org/json/isort-metadata/4.3.4.json

+117
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
from __future__ import annotations
2+
3+
import pytest
4+
5+
from poetry.repositories.link_sources.json import SimpleJsonPage
6+
7+
8+
@pytest.mark.parametrize(
9+
"metadata, expected_has_metadata, expected_hash_name, expected_hash",
10+
[
11+
({}, False, None, None),
12+
# new
13+
({"core-metadata": False}, False, None, None),
14+
({"core-metadata": True}, True, None, None),
15+
({"core-metadata": {"sha1": "1234", "sha256": "abcd"}}, True, "sha256", "abcd"),
16+
({"core-metadata": {}}, False, None, None),
17+
(
18+
{"core-metadata": {"sha1": "1234", "sha256": "abcd"}},
19+
True,
20+
"sha256",
21+
"abcd",
22+
),
23+
# old
24+
({"dist-info-metadata": False}, False, None, None),
25+
({"dist-info-metadata": True}, True, None, None),
26+
({"dist-info-metadata": {"sha256": "abcd"}}, True, "sha256", "abcd"),
27+
({"dist-info-metadata": {}}, False, None, None),
28+
(
29+
{"dist-info-metadata": {"sha1": "1234", "sha256": "abcd"}},
30+
True,
31+
"sha256",
32+
"abcd",
33+
),
34+
# conflicting (new wins)
35+
({"core-metadata": False, "dist-info-metadata": True}, False, None, None),
36+
(
37+
{"core-metadata": False, "dist-info-metadata": {"sha256": "abcd"}},
38+
False,
39+
None,
40+
None,
41+
),
42+
({"core-metadata": True, "dist-info-metadata": False}, True, None, None),
43+
(
44+
{"core-metadata": True, "dist-info-metadata": {"sha256": "abcd"}},
45+
True,
46+
None,
47+
None,
48+
),
49+
(
50+
{"core-metadata": {"sha256": "abcd"}, "dist-info-metadata": False},
51+
True,
52+
"sha256",
53+
"abcd",
54+
),
55+
(
56+
{"core-metadata": {"sha256": "abcd"}, "dist-info-metadata": True},
57+
True,
58+
"sha256",
59+
"abcd",
60+
),
61+
(
62+
{
63+
"core-metadata": {"sha256": "abcd"},
64+
"dist-info-metadata": {"sha256": "1234"},
65+
},
66+
True,
67+
"sha256",
68+
"abcd",
69+
),
70+
],
71+
)
72+
def test_metadata(
73+
metadata: dict[str, bool | dict[str, str]],
74+
expected_has_metadata: bool,
75+
expected_hash_name: str | None,
76+
expected_hash: str | None,
77+
) -> None:
78+
content = {"files": [{"url": "https://example.org/demo-0.1.whl", **metadata}]}
79+
page = SimpleJsonPage("https://example.org", content)
80+
81+
link = next(page.links)
82+
assert link.has_metadata is expected_has_metadata
83+
assert link.metadata_hash_name == expected_hash_name
84+
assert link.metadata_hash == expected_hash

tests/repositories/test_legacy_repository.py

+9-16
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from __future__ import annotations
22

33
import base64
4-
import posixpath
54
import re
65
import shutil
76

@@ -24,15 +23,14 @@
2423

2524

2625
if TYPE_CHECKING:
27-
from typing import Any
28-
2926
import httpretty
3027

3128
from _pytest.monkeypatch import MonkeyPatch
3229
from packaging.utils import NormalizedName
3330
from pytest_mock import MockerFixture
3431

3532
from poetry.config.config import Config
33+
from tests.types import RequestsSessionGet
3634

3735

3836
@pytest.fixture(autouse=True)
@@ -177,29 +175,24 @@ def test_get_package_information_fallback_read_setup() -> None:
177175
)
178176

179177

180-
def _get_mock(url: str, **__: Any) -> requests.Response:
181-
if url.endswith(".metadata"):
182-
response = requests.Response()
183-
response.encoding = "application/text"
184-
response._content = MockRepository.FIXTURES.joinpath(
185-
"metadata", posixpath.basename(url)
186-
).read_bytes()
187-
return response
188-
raise requests.HTTPError()
189-
190-
191-
def test_get_package_information_pep_658(mocker: MockerFixture) -> None:
178+
def test_get_package_information_pep_658(
179+
mocker: MockerFixture, get_metadata_mock: RequestsSessionGet
180+
) -> None:
192181
repo = MockRepository()
193182

194183
isort_package = repo.package("isort", Version.parse("4.3.4"))
195184

196-
mocker.patch.object(repo.session, "get", _get_mock)
185+
mocker.patch.object(repo.session, "get", get_metadata_mock)
186+
spy = mocker.spy(repo, "_get_info_from_metadata")
197187

198188
try:
199189
package = repo.package("isort-metadata", Version.parse("4.3.4"))
200190
except FileNotFoundError:
201191
pytest.fail("Metadata was not successfully retrieved")
202192
else:
193+
assert spy.call_count > 0
194+
assert spy.spy_return is not None
195+
203196
assert package.source_type == isort_package.source_type == "legacy"
204197
assert package.source_reference == isort_package.source_reference == repo.name
205198
assert package.source_url == isort_package.source_url == repo.url

0 commit comments

Comments
 (0)