Skip to content

Commit f1f64b1

Browse files
abnSecrus
authored andcommitted
repositories: add support for PEP 658
This change allows Poetry to make use of PEP 503 "simple" API repositories that implement PEP 658 for core metadata. Co-authored-by: Bartosz Sokorski <[email protected]>
1 parent e12ca03 commit f1f64b1

File tree

5 files changed

+151
-13
lines changed

5 files changed

+151
-13
lines changed

src/poetry/repositories/http_repository.py

+72-12
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from typing import Any
1111
from typing import Iterator
1212

13+
import pkginfo
1314
import requests
1415
import requests.adapters
1516

@@ -19,6 +20,7 @@
1920
from poetry.core.utils.helpers import temporary_directory
2021
from poetry.core.version.markers import parse_marker
2122

23+
from poetry.inspection.info import PackageInfo
2224
from poetry.repositories.cached_repository import CachedRepository
2325
from poetry.repositories.exceptions import PackageNotFound
2426
from poetry.repositories.exceptions import RepositoryError
@@ -33,7 +35,6 @@
3335
from packaging.utils import NormalizedName
3436

3537
from poetry.config.config import Config
36-
from poetry.inspection.info import PackageInfo
3738
from poetry.repositories.link_sources.base import LinkSource
3839
from poetry.utils.authenticator import RepositoryCertificateConfig
3940

@@ -97,10 +98,29 @@ def _get_info_from_sdist(self, url: str) -> PackageInfo:
9798
with self._cached_or_downloaded_file(Link(url)) as filepath:
9899
return PackageInfo.from_sdist(filepath)
99100

100-
def _get_info_from_urls(self, urls: dict[str, list[str]]) -> PackageInfo:
101+
@staticmethod
102+
def _get_info_from_metadata(
103+
url: str, metadata: dict[str, pkginfo.Distribution]
104+
) -> PackageInfo | None:
105+
if url in metadata:
106+
dist = metadata[url]
107+
return PackageInfo(
108+
name=dist.name,
109+
version=dist.version,
110+
summary=dist.summary,
111+
requires_dist=list(dist.requires_dist),
112+
requires_python=dist.requires_python,
113+
)
114+
return None
115+
116+
def _get_info_from_urls(
117+
self,
118+
urls: dict[str, list[str]],
119+
metadata: dict[str, pkginfo.Distribution] | None = None,
120+
) -> PackageInfo:
121+
metadata = metadata or {}
101122
# Prefer to read data from wheels: this is faster and more reliable
102-
wheels = urls.get("bdist_wheel")
103-
if wheels:
123+
if wheels := urls.get("bdist_wheel"):
104124
# We ought just to be able to look at any of the available wheels to read
105125
# metadata, they all should give the same answer.
106126
#
@@ -136,13 +156,19 @@ def _get_info_from_urls(self, urls: dict[str, list[str]]) -> PackageInfo:
136156
platform_specific_wheels.append(wheel)
137157

138158
if universal_wheel is not None:
139-
return self._get_info_from_wheel(universal_wheel)
159+
return self._get_info_from_metadata(
160+
universal_wheel, metadata
161+
) or self._get_info_from_wheel(universal_wheel)
140162

141163
info = None
142164
if universal_python2_wheel and universal_python3_wheel:
143-
info = self._get_info_from_wheel(universal_python2_wheel)
165+
info = self._get_info_from_metadata(
166+
universal_python2_wheel, metadata
167+
) or self._get_info_from_wheel(universal_python2_wheel)
144168

145-
py3_info = self._get_info_from_wheel(universal_python3_wheel)
169+
py3_info = self._get_info_from_metadata(
170+
universal_python3_wheel, metadata
171+
) or self._get_info_from_wheel(universal_python3_wheel)
146172

147173
if info.requires_python or py3_info.requires_python:
148174
info.requires_python = str(
@@ -192,16 +218,24 @@ def _get_info_from_urls(self, urls: dict[str, list[str]]) -> PackageInfo:
192218

193219
# Prefer non platform specific wheels
194220
if universal_python3_wheel:
195-
return self._get_info_from_wheel(universal_python3_wheel)
221+
return self._get_info_from_metadata(
222+
universal_python3_wheel, metadata
223+
) or self._get_info_from_wheel(universal_python3_wheel)
196224

197225
if universal_python2_wheel:
198-
return self._get_info_from_wheel(universal_python2_wheel)
226+
return self._get_info_from_metadata(
227+
universal_python2_wheel, metadata
228+
) or self._get_info_from_wheel(universal_python2_wheel)
199229

200230
if platform_specific_wheels:
201231
first_wheel = platform_specific_wheels[0]
202-
return self._get_info_from_wheel(first_wheel)
232+
return self._get_info_from_metadata(
233+
first_wheel, metadata
234+
) or self._get_info_from_wheel(first_wheel)
203235

204-
return self._get_info_from_sdist(urls["sdist"][0])
236+
return self._get_info_from_metadata(
237+
urls["sdist"][0], metadata
238+
) or self._get_info_from_sdist(urls["sdist"][0])
205239

206240
def _links_to_data(self, links: list[Link], data: PackageInfo) -> dict[str, Any]:
207241
if not links:
@@ -210,11 +244,37 @@ def _links_to_data(self, links: list[Link], data: PackageInfo) -> dict[str, Any]
210244
f' "{data.version}"'
211245
)
212246
urls = defaultdict(list)
247+
metadata = {}
213248
files: list[dict[str, Any]] = []
214249
for link in links:
215250
if link.yanked and not data.yanked:
216251
# drop yanked files unless the entire release is yanked
217252
continue
253+
if link.has_metadata:
254+
try:
255+
assert link.metadata_url is not None
256+
response = self.session.get(link.metadata_url)
257+
distribution = pkginfo.Distribution()
258+
assert link.metadata_hash_name is not None
259+
metadata_hash = getattr(hashlib, link.metadata_hash_name)(
260+
response.text.encode()
261+
).hexdigest()
262+
263+
if metadata_hash != link.metadata_hash:
264+
self._log(
265+
f"Metadata file hash ({metadata_hash}) does not match"
266+
f" expected hash ({link.metadata_hash}).",
267+
level="warning",
268+
)
269+
270+
distribution.parse(response.content)
271+
metadata[link.url] = distribution
272+
except requests.HTTPError:
273+
self._log(
274+
f"Failed to retrieve metadata at {link.metadata_url}",
275+
level="debug",
276+
)
277+
218278
if link.is_wheel:
219279
urls["bdist_wheel"].append(link.url)
220280
elif link.filename.endswith(
@@ -235,7 +295,7 @@ def _links_to_data(self, links: list[Link], data: PackageInfo) -> dict[str, Any]
235295

236296
data.files = files
237297

238-
info = self._get_info_from_urls(urls)
298+
info = self._get_info_from_urls(urls, metadata)
239299

240300
data.summary = info.summary
241301
data.requires_dist = info.requires_dist

src/poetry/repositories/link_sources/html.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@ def _link_cache(self) -> LinkCache:
4242
yanked = unescape(yanked_value)
4343
else:
4444
yanked = "data-yanked" in anchor
45-
link = Link(url, requires_python=pyrequire, yanked=yanked)
45+
metadata = anchor.get("data-dist-info-metadata")
46+
link = Link(
47+
url, requires_python=pyrequire, yanked=yanked, metadata=metadata
48+
)
4649

4750
if link.ext not in self.SUPPORTED_FORMATS:
4851
continue
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>Links for isort</title>
5+
</head>
6+
<body>
7+
<h1>Links for isort</h1>
8+
<a href="https://files.pythonhosted.org/packages/1f/2c/non-existant/isort-metadata-4.3.4-py3-none-any.whl#sha256=1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af"
9+
data-dist-info-metadata="sha256=e360bf0ed8a06390513d50dd5b7e9d635c789853a93b84163f9de4ae0647580c">isort-metadata-4.3.4-py3-none-any.whl</a><br/>
10+
</body>
11+
</html>
12+
<!--SERIAL 3575149-->
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"

tests/repositories/test_legacy_repository.py

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

33
import base64
4+
import posixpath
45
import re
56
import shutil
67

@@ -23,10 +24,13 @@
2324

2425

2526
if TYPE_CHECKING:
27+
from typing import Any
28+
2629
import httpretty
2730

2831
from _pytest.monkeypatch import MonkeyPatch
2932
from packaging.utils import NormalizedName
33+
from pytest_mock import MockerFixture
3034

3135
from poetry.config.config import Config
3236

@@ -173,6 +177,37 @@ def test_get_package_information_fallback_read_setup() -> None:
173177
)
174178

175179

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:
192+
repo = MockRepository()
193+
194+
isort_package = repo.package("isort", Version.parse("4.3.4"))
195+
196+
mocker.patch.object(repo.session, "get", _get_mock)
197+
198+
try:
199+
package = repo.package("isort-metadata", Version.parse("4.3.4"))
200+
except FileNotFoundError:
201+
pytest.fail("Metadata was not successfully retrieved")
202+
else:
203+
assert package.source_type == isort_package.source_type == "legacy"
204+
assert package.source_reference == isort_package.source_reference == repo.name
205+
assert package.source_url == isort_package.source_url == repo.url
206+
assert package.name == "isort-metadata"
207+
assert package.version.text == isort_package.version.text == "4.3.4"
208+
assert package.description == isort_package.description
209+
210+
176211
def test_get_package_information_skips_dependencies_with_invalid_constraints() -> None:
177212
repo = MockRepository()
178213

0 commit comments

Comments
 (0)