Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(search): fallback to find packages for PyPI #10055

Merged
merged 2 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/poetry/repositories/pypi_repository.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import contextlib
import logging

from typing import TYPE_CHECKING
Expand All @@ -9,9 +10,11 @@
import requests.adapters

from cachecontrol.controller import logger as cache_control_logger
from poetry.core.packages.dependency import Dependency
from poetry.core.packages.package import Package
from poetry.core.packages.utils.link import Link
from poetry.core.version.exceptions import InvalidVersionError
from poetry.core.version.requirements import InvalidRequirementError

from poetry.repositories.exceptions import PackageNotFoundError
from poetry.repositories.http_repository import HTTPRepository
Expand Down Expand Up @@ -76,6 +79,18 @@ def search(self, query: str | list[str]) -> list[Package]:
level="debug",
)

if not results:
# in cases like PyPI search might not be available, we fallback to explicit searches
# to allow for a nicer ux rather than finding nothing at all
# see: https://discuss.python.org/t/fastly-interfering-with-pypi-search/73597/6
#
tokens = query if isinstance(query, list) else [query]
for token in tokens:
with contextlib.suppress(InvalidRequirementError):
results.extend(
self.find_packages(Dependency.create_from_pep_508(token))
)

return results

def get_package_info(self, name: NormalizedName) -> dict[str, Any]:
Expand Down
6 changes: 1 addition & 5 deletions tests/console/commands/test_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import re

from pathlib import Path
from typing import TYPE_CHECKING

import pytest
Expand All @@ -19,10 +18,7 @@
from poetry.repositories.legacy_repository import LegacyRepository
from tests.types import CommandTesterFactory

TESTS_DIRECTORY = Path(__file__).parent.parent.parent
FIXTURES_DIRECTORY = (
TESTS_DIRECTORY / "repositories" / "fixtures" / "pypi.org" / "search"
)

SQLALCHEMY_SEARCH_OUTPUT_PYPI = """\
Package Version Source Description
broadway-sqlalchemy 0.0.1 PyPI A broadway extension wrapping Flask-SQLAlchemy
Expand Down
89 changes: 89 additions & 0 deletions tests/repositories/fixtures/pypi.org/search/search-disallowed.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<!DOCTYPE html>
<html>
<head>
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; img-src 'self' data:; media-src 'self' data:; object-src 'none'; style-src 'self' 'sha256-o4vzfmmUENEg4chMjjRP9EuW9ucGnGIGVdbl8d0SHQQ='; script-src 'self' 'sha256-a9bHdQGvRzDwDVzx8m+Rzw+0FHZad8L0zjtBwkxOIz4=';"
/>
<link
href="/_fs-ch-1T1wmsGaOgGaSxcX/assets/inter-var.woff2"
rel="preload"
as="font"
type="font/woff2"
crossorigin
/>
<link href="/_fs-ch-1T1wmsGaOgGaSxcX/assets/styles.css" rel="stylesheet" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1"
/>
<style>
#loading-error {
font-size: 16px;
font-family: 'Inter', sans-serif;
margin-top: 10px;
margin-left: 10px;
display: none;
}
</style>
</head>
<body>
<noscript>
<div class="noscript-container">
<div class="noscript-content">
<img
src="/_fs-ch-1T1wmsGaOgGaSxcX/assets/errorIcon.svg"
alt="Error Icon"
class="error-icon"
/>
<span class="noscript-span"
>JavaScript is disabled in your browser.</span
>
Please enable JavaScript to proceed.
</div>
</div>
</noscript>
<div id="loading-error">
A required part of this site couldn’t load. This may be due to a browser
extension, network issues, or browser settings. Please check your
connection, disable any ad blockers, or try using a different browser.
</div>
<script>
function loadScript(src) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.onload = resolve;
script.onerror = (event) => {
console.error('Script load error event:', event);
document.getElementById('loading-error').style.display = 'block';
reject(
new Error(
`Failed to load script: ${src}, Please contact the service administrator.`
)
);
};
script.src = src;
document.body.appendChild(script);
});
}

loadScript('/_fs-ch-1T1wmsGaOgGaSxcX/errors.js')
.then(() => {
const script = document.createElement('script');
script.src = '/_fs-ch-1T1wmsGaOgGaSxcX/script.js?reload=true';
script.onerror = (event) => {
console.error('Script load error event:', event);
const errorMsg = new Error(
`Failed to load script: ${script.src}. Please contact the service administrator.`
);
console.error(errorMsg);
handleScriptError();
};
document.body.appendChild(script);
})
.catch((error) => {
console.error(error);
});
</script>
</body>
</html>
19 changes: 19 additions & 0 deletions tests/repositories/fixtures/pypi.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,25 @@ def lookup(name: str) -> Path | None:
return lookup


@pytest.fixture
def with_disallowed_pypi_search_html(
http: type[httpretty], pypi_repository: PyPiRepository
) -> None:
def search_callback(
request: HTTPrettyRequest, uri: str, headers: dict[str, Any]
) -> HTTPrettyResponse:
search_html = FIXTURE_PATH_REPOSITORIES_PYPI.joinpath(
"search", "search-disallowed.html"
)
return 200, headers, search_html.read_bytes()

http.register_uri(
http.GET,
re.compile(r"https://pypi.org/search(\?(.*))?$"),
body=search_callback,
)


@pytest.fixture(autouse=True)
def pypi_repository(
http: type[httpretty],
Expand Down
24 changes: 24 additions & 0 deletions tests/repositories/test_pypi_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,3 +361,27 @@ def test_get_release_info_includes_only_supported_types(

assert len(release_info["files"]) == 1
assert release_info["files"][0]["file"] == "hbmqtt-0.9.6.tar.gz"


@pytest.mark.parametrize(
("query", "count"),
[
("non-existent", 0), # no match
("requests", 6), # exact match
("hbmqtt==0.9.6", 1), # exact dependency match
("requests>=2.18.4", 2), # range dependency match
("request", 0), # partial match
("reques*", 0), # bad token
("reques t", 0), # bad token
(["requests", "hbmqtt"], 7), # list of tokens
],
)
def test_search_fallbacks_to_find_packages(
query: str | list[str],
count: int,
pypi_repository: PyPiRepository,
with_disallowed_pypi_search_html: None,
) -> None:
repo = pypi_repository
packages = repo.search(query)
assert len(packages) == count
Loading