Skip to content

Commit 3f185e6

Browse files
committed
Avoid the deprecated JSON API
1 parent ec610a3 commit 3f185e6

Some content is hidden

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

42 files changed

+3764
-6825
lines changed

src/poetry/repositories/legacy_repository.py

+37-34
Original file line numberDiff line numberDiff line change
@@ -34,59 +34,62 @@ def __init__(
3434
super().__init__(name, url.rstrip("/"), config, disable_cache)
3535

3636
def find_packages(self, dependency: Dependency) -> list[Package]:
37+
"""
38+
Find packages on the remote server.
39+
"""
3740
packages = []
3841
constraint, allow_prereleases = self._get_constraints_from_dependency(
3942
dependency
4043
)
44+
ignored_pre_release_packages = []
45+
46+
versions: list[Version]
4147

4248
key = dependency.name
4349
if not constraint.is_any():
4450
key = f"{key}:{constraint!s}"
4551

46-
ignored_pre_release_versions = []
47-
4852
if self._cache.store("matches").has(key):
4953
versions = self._cache.store("matches").get(key)
5054
else:
51-
page = self._get_page(f"/{dependency.name.replace('.', '-')}/")
55+
page = self._get_page(f"/{dependency.name}/")
5256
if page is None:
57+
self._log(
58+
f"No packages found for {dependency.name}",
59+
level="debug",
60+
)
5361
return []
5462

55-
versions = []
56-
for version in page.versions(dependency.name):
57-
if version.is_unstable() and not allow_prereleases:
58-
if constraint.is_any():
59-
# we need this when all versions of the package are pre-releases
60-
ignored_pre_release_versions.append(version)
61-
continue
62-
63-
if constraint.allows(version):
64-
versions.append(version)
65-
63+
versions = [
64+
version
65+
for version in page.versions(dependency.name)
66+
if constraint.allows(version)
67+
]
6668
self._cache.store("matches").put(key, versions, 5)
6769

68-
for package_versions in (versions, ignored_pre_release_versions):
69-
for version in package_versions:
70-
package = Package(
71-
dependency.name,
72-
version,
73-
source_type="legacy",
74-
source_reference=self.name,
75-
source_url=self._url,
76-
)
70+
for version in versions:
71+
package = Package(
72+
dependency.name,
73+
version,
74+
source_type="legacy",
75+
source_reference=self.name,
76+
source_url=self._url,
77+
)
7778

78-
packages.append(package)
79+
if package.is_prerelease() and not allow_prereleases:
80+
if constraint.is_any():
81+
# we need this when all versions of the package are pre-releases
82+
ignored_pre_release_packages.append(package)
83+
continue
7984

80-
self._log(
81-
f"{len(packages)} packages found for {dependency.name} {constraint!s}",
82-
level="debug",
83-
)
85+
packages.append(package)
8486

85-
if packages or not constraint.is_any():
86-
# we have matching packages, or constraint is not (*)
87-
break
87+
self._log(
88+
f"{len(packages)} packages found for {dependency.name} {constraint!s}",
89+
level="debug",
90+
)
8891

89-
return packages
92+
return packages or ignored_pre_release_packages
9093

9194
def package(
9295
self, name: str, version: str, extras: list[str] | None = None
@@ -115,14 +118,14 @@ def package(
115118
return package
116119

117120
def find_links_for_package(self, package: Package) -> list[Link]:
118-
page = self._get_page(f"/{package.name.replace('.', '-')}/")
121+
page = self._get_page(f"/{package.name}/")
119122
if page is None:
120123
return []
121124

122125
return list(page.links_for_version(package.name, package.version))
123126

124127
def _get_release_info(self, name: str, version: str) -> dict[str, Any]:
125-
page = self._get_page(f"/{canonicalize_name(name).replace('.', '-')}/")
128+
page = self._get_page(f"/{canonicalize_name(name)}/")
126129
if page is None:
127130
raise PackageNotFound(f'No package named "{name}"')
128131

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING
4+
from typing import Any
5+
6+
from poetry.core.packages.utils.link import Link
7+
8+
from poetry.repositories.link_sources.base import LinkSource
9+
10+
11+
if TYPE_CHECKING:
12+
from collections.abc import Iterator
13+
14+
15+
class SimpleJsonPage(LinkSource):
16+
"""Links as returned by PEP 691 compatible JSON-based Simple API."""
17+
18+
def __init__(self, url: str, content: dict[str, Any]) -> None:
19+
super().__init__(url=url)
20+
self.content = content
21+
22+
@property
23+
def links(self) -> Iterator[Link]:
24+
for file in self.content["files"]:
25+
url = file["url"]
26+
requires_python = file.get("requires-python")
27+
link = Link(url, requires_python=requires_python)
28+
if link.ext not in self.SUPPORTED_FORMATS:
29+
continue
30+
31+
yield link

src/poetry/repositories/pypi_repository.py

+41-30
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
from poetry.repositories.exceptions import PackageNotFound
1818
from poetry.repositories.http import HTTPRepository
19+
from poetry.repositories.link_sources.json import SimpleJsonPage
1920
from poetry.utils._compat import to_str
2021
from poetry.utils.constants import REQUESTS_TIMEOUT
2122

@@ -27,6 +28,7 @@
2728

2829
if TYPE_CHECKING:
2930
from poetry.core.packages.dependency import Dependency
31+
from poetry.core.semver.version import Version
3032

3133

3234
class PyPiRepository(HTTPRepository):
@@ -47,50 +49,49 @@ def find_packages(self, dependency: Dependency) -> list[Package]:
4749
"""
4850
Find packages on the remote server.
4951
"""
50-
constraint, allow_prereleases = self._get_constraints_from_dependency(
51-
dependency
52-
)
53-
5452
try:
55-
info = self.get_package_info(dependency.name)
53+
json_page = self.get_json_page(dependency.name)
5654
except PackageNotFound:
5755
self._log(
58-
f"No packages found for {dependency.name} {constraint!s}",
56+
f"No packages found for {dependency.name}",
5957
level="debug",
6058
)
6159
return []
6260

61+
pretty_name = json_page.content["name"]
62+
6363
packages = []
64+
constraint, allow_prereleases = self._get_constraints_from_dependency(
65+
dependency
66+
)
6467
ignored_pre_release_packages = []
6568

66-
for version, release in info["releases"].items():
67-
if not release:
68-
# Bad release
69-
self._log(
70-
f"No release information found for {dependency.name}-{version},"
71-
" skipping",
72-
level="debug",
73-
)
74-
continue
69+
versions: list[Version]
7570

76-
try:
77-
package = Package(info["info"]["name"], version)
78-
except InvalidVersion:
79-
self._log(
80-
f'Unable to parse version "{version}" for the'
81-
f" {dependency.name} package, skipping",
82-
level="debug",
83-
)
84-
continue
71+
key = dependency.name
72+
if not constraint.is_any():
73+
key = f"{key}:{constraint!s}"
74+
75+
if self._cache.store("matches").has(key):
76+
versions = self._cache.store("matches").get(key)
77+
else:
78+
versions = [
79+
version
80+
for version in json_page.versions(dependency.name)
81+
if constraint.allows(version)
82+
]
83+
self._cache.store("matches").put(key, versions, 5)
84+
85+
for version in versions:
86+
package = Package(pretty_name, version)
8587

8688
if package.is_prerelease() and not allow_prereleases:
8789
if constraint.is_any():
8890
# we need this when all versions of the package are pre-releases
8991
ignored_pre_release_packages.append(package)
9092
continue
9193

92-
if constraint.allows(package.version):
93-
packages.append(package)
94+
packages.append(package)
9495

9596
self._log(
9697
f"{len(packages)} packages found for {dependency.name} {constraint!s}",
@@ -161,11 +162,12 @@ def get_package_info(self, name: str) -> dict[str, Any]:
161162
return package_info
162163

163164
def _get_package_info(self, name: str) -> dict[str, Any]:
164-
data = self._get(f"pypi/{name}/json")
165-
if data is None:
165+
headers = {"Accept": "application/vnd.pypi.simple.v1+json"}
166+
info = self._get(f"simple/{name}/", headers=headers)
167+
if info is None:
166168
raise PackageNotFound(f"Package [{name}] not found.")
167169

168-
return data
170+
return info
169171

170172
def find_links_for_package(self, package: Package) -> list[Link]:
171173
json_data = self._get(f"pypi/{package.name}/{package.version}/json")
@@ -244,12 +246,20 @@ def _get_release_info(
244246

245247
return data.asdict()
246248

247-
def _get(self, endpoint: str) -> dict[str, Any] | None:
249+
def get_json_page(self, name: str) -> SimpleJsonPage:
250+
source = self._base_url + f"simple/{name}/"
251+
info = self.get_package_info(name)
252+
return SimpleJsonPage(source, info)
253+
254+
def _get(
255+
self, endpoint: str, headers: dict[str, str] | None = None
256+
) -> dict[str, Any] | None:
248257
try:
249258
json_response = self.session.get(
250259
self._base_url + endpoint,
251260
raise_for_status=False,
252261
timeout=REQUESTS_TIMEOUT,
262+
headers=headers,
253263
)
254264
except requests.exceptions.TooManyRedirects:
255265
# Cache control redirect loop.
@@ -259,6 +269,7 @@ def _get(self, endpoint: str) -> dict[str, Any] | None:
259269
self._base_url + endpoint,
260270
raise_for_status=False,
261271
timeout=REQUESTS_TIMEOUT,
272+
headers=headers,
262273
)
263274

264275
if json_response.status_code != 200:

src/poetry/utils/authenticator.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,8 @@ def authenticated_url(self, url: str) -> str:
191191
def request(
192192
self, method: str, url: str, raise_for_status: bool = True, **kwargs: Any
193193
) -> requests.Response:
194-
request = requests.Request(method, url)
194+
headers = kwargs.get("headers")
195+
request = requests.Request(method, url, headers=headers)
195196
credential = self.get_credentials_for_url(url)
196197

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

tests/installation/test_executor.py

+9-9
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ def test_execute_executes_a_batch_of_operations(
146146

147147
return_code = executor.execute(
148148
[
149-
Install(Package("pytest", "3.5.2")),
149+
Install(Package("pytest", "3.5.1")),
150150
Uninstall(Package("attrs", "17.4.0")),
151151
Update(Package("requests", "2.18.3"), Package("requests", "2.18.4")),
152152
Uninstall(Package("clikit", "0.2.3")).skip("Not currently installed"),
@@ -159,7 +159,7 @@ def test_execute_executes_a_batch_of_operations(
159159
expected = f"""
160160
Package operations: 4 installs, 1 update, 1 removal
161161
162-
• Installing pytest (3.5.2)
162+
• Installing pytest (3.5.1)
163163
• Removing attrs (17.4.0)
164164
• Updating requests (2.18.3 -> 2.18.4)
165165
• Installing demo (0.1.0 {file_package.source_url})
@@ -243,18 +243,18 @@ def test_execute_works_with_ansi_output(
243243
mocker.patch.object(env, "_run", return_value=install_output)
244244
return_code = executor.execute(
245245
[
246-
Install(Package("pytest", "3.5.2")),
246+
Install(Package("pytest", "3.5.1")),
247247
]
248248
)
249249
env._run.assert_called_once()
250250

251251
# fmt: off
252252
expected = [
253253
"\x1b[39;1mPackage operations\x1b[39;22m: \x1b[34m1\x1b[39m install, \x1b[34m0\x1b[39m updates, \x1b[34m0\x1b[39m removals", # noqa: E501
254-
"\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
255-
"\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
256-
"\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
257-
"\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
254+
"\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
255+
"\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
256+
"\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
257+
"\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
258258
]
259259
# fmt: on
260260

@@ -285,15 +285,15 @@ def test_execute_works_with_no_ansi_output(
285285
mocker.patch.object(env, "_run", return_value=install_output)
286286
return_code = executor.execute(
287287
[
288-
Install(Package("pytest", "3.5.2")),
288+
Install(Package("pytest", "3.5.1")),
289289
]
290290
)
291291
env._run.assert_called_once()
292292

293293
expected = """
294294
Package operations: 1 install, 0 updates, 0 removals
295295
296-
• Installing pytest (3.5.2)
296+
• Installing pytest (3.5.1)
297297
"""
298298
expected = set(expected.splitlines())
299299
output = set(io_not_decorated.fetch_output().splitlines())

0 commit comments

Comments
 (0)