Skip to content

Commit 773068c

Browse files
committed
Avoid the deprecated JSON API
1 parent e803c3d commit 773068c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+4275
-7717
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from __future__ import annotations
2+
3+
from collections import defaultdict
4+
from typing import TYPE_CHECKING
5+
from typing import Any
6+
7+
from poetry.core.packages.utils.link import Link
8+
9+
from poetry.repositories.link_sources.base import LinkSource
10+
11+
12+
if TYPE_CHECKING:
13+
from poetry.repositories.link_sources.base import LinkCache
14+
15+
16+
class SimpleJsonPage(LinkSource):
17+
"""Links as returned by PEP 691 compatible JSON-based Simple API."""
18+
19+
def __init__(self, url: str, content: dict[str, Any]) -> None:
20+
super().__init__(url=url)
21+
self.content = content
22+
23+
@property
24+
def _link_cache(self) -> LinkCache:
25+
links: LinkCache = defaultdict(lambda: defaultdict(list))
26+
for file in self.content["files"]:
27+
url = file["url"]
28+
requires_python = file.get("requires-python")
29+
yanked = file.get("yanked", False)
30+
link = Link(url, requires_python=requires_python, yanked=yanked)
31+
32+
if link.ext not in self.SUPPORTED_FORMATS:
33+
continue
34+
35+
pkg = self.link_package_data(link)
36+
if pkg:
37+
links[pkg.name][pkg.version].append(link)
38+
39+
return links

src/poetry/repositories/pypi_repository.py

+36-32
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@
1212
from html5lib.html5parser import parse
1313
from poetry.core.packages.package import Package
1414
from poetry.core.packages.utils.link import Link
15-
from poetry.core.semver.version import Version
1615
from poetry.core.version.exceptions import InvalidVersion
1716

1817
from poetry.repositories.exceptions import PackageNotFound
1918
from poetry.repositories.http import HTTPRepository
19+
from poetry.repositories.link_sources.json import SimpleJsonPage
2020
from poetry.utils._compat import to_str
2121
from poetry.utils.constants import REQUESTS_TIMEOUT
2222

@@ -27,6 +27,7 @@
2727

2828
if TYPE_CHECKING:
2929
from packaging.utils import NormalizedName
30+
from poetry.core.semver.version import Version
3031
from poetry.core.semver.version_constraint import VersionConstraint
3132

3233
SUPPORTED_PACKAGE_TYPES = {"sdist", "bdist_wheel"}
@@ -114,50 +115,44 @@ def _find_packages(
114115
Find packages on the remote server.
115116
"""
116117
try:
117-
info = self.get_package_info(name)
118+
json_page = self.get_json_page(name)
118119
except PackageNotFound:
119120
self._log(
120-
f"No packages found for {name} {constraint!s}",
121+
f"No packages found for {name}",
121122
level="debug",
122123
)
123124
return []
124125

125-
packages = []
126+
versions: list[tuple[Version, str | bool]]
126127

127-
for version_string, release in info["releases"].items():
128-
if not release:
129-
# Bad release
130-
self._log(
131-
f"No release information found for {name}-{version_string},"
132-
" skipping",
133-
level="debug",
134-
)
135-
continue
128+
key: str = name
129+
if not constraint.is_any():
130+
key = f"{key}:{constraint!s}"
136131

137-
try:
138-
version = Version.parse(version_string)
139-
except InvalidVersion:
140-
self._log(
141-
f'Unable to parse version "{version_string}" for the'
142-
f" {name} package, skipping",
143-
level="debug",
144-
)
145-
continue
132+
if self._cache.store("matches").has(key):
133+
versions = self._cache.store("matches").get(key)
134+
else:
135+
versions = [
136+
(version, json_page.yanked(name, version))
137+
for version in json_page.versions(name)
138+
if constraint.allows(version)
139+
]
140+
self._cache.store("matches").put(key, versions, 5)
146141

147-
if constraint.allows(version):
148-
# PEP 592: PyPI always yanks entire releases, not individual files,
149-
# so we just have to look for the first file
150-
yanked = self._get_yanked(release[0])
151-
packages.append(Package(info["info"]["name"], version, yanked=yanked))
142+
pretty_name = json_page.content["name"]
143+
packages = [
144+
Package(pretty_name, version, yanked=yanked) for version, yanked in versions
145+
]
152146

153147
return packages
154148

155-
def _get_package_info(self, name: NormalizedName) -> dict[str, Any]:
156-
data = self._get(f"pypi/{name}/json")
157-
if data is None:
149+
def _get_package_info(self, name: str) -> dict[str, Any]:
150+
headers = {"Accept": "application/vnd.pypi.simple.v1+json"}
151+
info = self._get(f"simple/{name}/", headers=headers)
152+
if info is None:
158153
raise PackageNotFound(f"Package [{name}] not found.")
159154

160-
return data
155+
return info
161156

162157
def find_links_for_package(self, package: Package) -> list[Link]:
163158
json_data = self._get(f"pypi/{package.name}/{package.version}/json")
@@ -239,12 +234,20 @@ def _get_release_info(
239234

240235
return data.asdict()
241236

242-
def _get(self, endpoint: str) -> dict[str, Any] | None:
237+
def get_json_page(self, name: NormalizedName) -> SimpleJsonPage:
238+
source = self._base_url + f"simple/{name}/"
239+
info = self.get_package_info(name)
240+
return SimpleJsonPage(source, info)
241+
242+
def _get(
243+
self, endpoint: str, headers: dict[str, str] | None = None
244+
) -> dict[str, Any] | None:
243245
try:
244246
json_response = self.session.get(
245247
self._base_url + endpoint,
246248
raise_for_status=False,
247249
timeout=REQUESTS_TIMEOUT,
250+
headers=headers,
248251
)
249252
except requests.exceptions.TooManyRedirects:
250253
# Cache control redirect loop.
@@ -254,6 +257,7 @@ def _get(self, endpoint: str) -> dict[str, Any] | None:
254257
self._base_url + endpoint,
255258
raise_for_status=False,
256259
timeout=REQUESTS_TIMEOUT,
260+
headers=headers,
257261
)
258262

259263
if json_response.status_code != 200:

src/poetry/utils/authenticator.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,8 @@ def authenticated_url(self, url: str) -> str:
183183
def request(
184184
self, method: str, url: str, raise_for_status: bool = True, **kwargs: Any
185185
) -> requests.Response:
186-
request = requests.Request(method, url)
186+
headers = kwargs.get("headers")
187+
request = requests.Request(method, url, headers=headers)
187188
credential = self.get_credentials_for_url(url)
188189

189190
if credential.username is not None or credential.password is not None:

tests/installation/test_executor.py

+12-12
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ def test_execute_executes_a_batch_of_operations(
147147

148148
return_code = executor.execute(
149149
[
150-
Install(Package("pytest", "3.5.2")),
150+
Install(Package("pytest", "3.5.1")),
151151
Uninstall(Package("attrs", "17.4.0")),
152152
Update(Package("requests", "2.18.3"), Package("requests", "2.18.4")),
153153
Uninstall(Package("clikit", "0.2.3")).skip("Not currently installed"),
@@ -160,7 +160,7 @@ def test_execute_executes_a_batch_of_operations(
160160
expected = f"""
161161
Package operations: 4 installs, 1 update, 1 removal
162162
163-
• Installing pytest (3.5.2)
163+
• Installing pytest (3.5.1)
164164
• Removing attrs (17.4.0)
165165
• Updating requests (2.18.3 -> 2.18.4)
166166
• Installing demo (0.1.0 {file_package.source_url})
@@ -182,20 +182,20 @@ def test_execute_executes_a_batch_of_operations(
182182
"operations, has_warning",
183183
[
184184
(
185-
[Install(Package("black", "21.11b0")), Install(Package("pytest", "3.5.2"))],
185+
[Install(Package("black", "21.11b0")), Install(Package("pytest", "3.5.1"))],
186186
True,
187187
),
188188
(
189189
[
190190
Uninstall(Package("black", "21.11b0")),
191-
Uninstall(Package("pytest", "3.5.2")),
191+
Uninstall(Package("pytest", "3.5.1")),
192192
],
193193
False,
194194
),
195195
(
196196
[
197197
Update(Package("black", "19.10b0"), Package("black", "21.11b0")),
198-
Update(Package("pytest", "3.5.1"), Package("pytest", "3.5.2")),
198+
Update(Package("pytest", "3.5.0"), Package("pytest", "3.5.1")),
199199
],
200200
True,
201201
),
@@ -299,18 +299,18 @@ def test_execute_works_with_ansi_output(
299299
mocker.patch.object(env, "_run", return_value=install_output)
300300
return_code = executor.execute(
301301
[
302-
Install(Package("pytest", "3.5.2")),
302+
Install(Package("pytest", "3.5.1")),
303303
]
304304
)
305305
env._run.assert_called_once()
306306

307307
# fmt: off
308308
expected = [
309309
"\x1b[39;1mPackage operations\x1b[39;22m: \x1b[34m1\x1b[39m install, \x1b[34m0\x1b[39m updates, \x1b[34m0\x1b[39m removals", # noqa: E501
310-
"\x1b[34;1m•\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mpytest\x1b[39m\x1b[39m (\x1b[39m\x1b[39;1m3.5.2\x1b[39;22m\x1b[39m)\x1b[39m: \x1b[34mPending...\x1b[39m", # noqa: E501
311-
"\x1b[34;1m•\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mpytest\x1b[39m\x1b[39m (\x1b[39m\x1b[39;1m3.5.2\x1b[39;22m\x1b[39m)\x1b[39m: \x1b[34mDownloading...\x1b[39m", # noqa: E501
312-
"\x1b[34;1m•\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mpytest\x1b[39m\x1b[39m (\x1b[39m\x1b[39;1m3.5.2\x1b[39;22m\x1b[39m)\x1b[39m: \x1b[34mInstalling...\x1b[39m", # noqa: E501
313-
"\x1b[32;1m•\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mpytest\x1b[39m\x1b[39m (\x1b[39m\x1b[32m3.5.2\x1b[39m\x1b[39m)\x1b[39m", # finished # noqa: E501
310+
"\x1b[34;1m•\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mpytest\x1b[39m\x1b[39m (\x1b[39m\x1b[39;1m3.5.1\x1b[39;22m\x1b[39m)\x1b[39m: \x1b[34mPending...\x1b[39m", # noqa: E501
311+
"\x1b[34;1m•\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mpytest\x1b[39m\x1b[39m (\x1b[39m\x1b[39;1m3.5.1\x1b[39;22m\x1b[39m)\x1b[39m: \x1b[34mDownloading...\x1b[39m", # noqa: E501
312+
"\x1b[34;1m•\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mpytest\x1b[39m\x1b[39m (\x1b[39m\x1b[39;1m3.5.1\x1b[39;22m\x1b[39m)\x1b[39m: \x1b[34mInstalling...\x1b[39m", # noqa: E501
313+
"\x1b[32;1m•\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mpytest\x1b[39m\x1b[39m (\x1b[39m\x1b[32m3.5.1\x1b[39m\x1b[39m)\x1b[39m", # finished # noqa: E501
314314
]
315315
# fmt: on
316316

@@ -341,15 +341,15 @@ def test_execute_works_with_no_ansi_output(
341341
mocker.patch.object(env, "_run", return_value=install_output)
342342
return_code = executor.execute(
343343
[
344-
Install(Package("pytest", "3.5.2")),
344+
Install(Package("pytest", "3.5.1")),
345345
]
346346
)
347347
env._run.assert_called_once()
348348

349349
expected = """
350350
Package operations: 1 install, 0 updates, 0 removals
351351
352-
• Installing pytest (3.5.2)
352+
• Installing pytest (3.5.1)
353353
"""
354354
expected = set(expected.splitlines())
355355
output = set(io_not_decorated.fetch_output().splitlines())

0 commit comments

Comments
 (0)