diff --git a/docs/docs/repositories.md b/docs/docs/repositories.md index d281f6ca77a..f1b9572c538 100644 --- a/docs/docs/repositories.md +++ b/docs/docs/repositories.md @@ -120,6 +120,10 @@ a custom certificate authority or client certificates, similarly refer to the ex `certificates` section. Poetry will use these values to authenticate to your private repository when downloading or looking for packages. +401 or 403 responses from secondary repositories will generate a warning +message but are not considered as fatal, as some types of private repository +servers may return a 403 if a request is made for a package not present on the +server. ### Disabling the PyPI repository diff --git a/poetry/repositories/legacy_repository.py b/poetry/repositories/legacy_repository.py index d14d48fe2fe..4652a1172c1 100644 --- a/poetry/repositories/legacy_repository.py +++ b/poetry/repositories/legacy_repository.py @@ -395,6 +395,7 @@ def _get(self, endpoint: str) -> Optional[Page]: response = self.session.get(url) if response.status_code == 404: return + response.raise_for_status() except requests.HTTPError as e: raise RepositoryError(e) diff --git a/poetry/repositories/pool.py b/poetry/repositories/pool.py index 20579b5fdab..6ee1a0f9eb4 100644 --- a/poetry/repositories/pool.py +++ b/poetry/repositories/pool.py @@ -1,3 +1,5 @@ +import logging + from typing import TYPE_CHECKING from typing import Dict from typing import List @@ -5,6 +7,7 @@ from .base_repository import BaseRepository from .exceptions import PackageNotFound +from .exceptions import RepositoryError from .repository import Repository @@ -13,6 +16,9 @@ from poetry.core.packages import Package +logger = logging.getLogger(__name__) + + class Pool(BaseRepository): def __init__( self, @@ -162,8 +168,16 @@ def find_packages(self, dependency: "Dependency") -> List["Package"]: return self.repository(repository).find_packages(dependency) packages = [] - for repo in self._repositories: - packages += repo.find_packages(dependency) + for (idx, repo) in enumerate(self._repositories): + try: + packages += repo.find_packages(dependency) + except RepositoryError: + if idx < self._secondary_start_idx: + raise + self._log( + "error checking secondary repository {}".format(repo.name), + level="warning", + ) return packages @@ -178,3 +192,7 @@ def search(self, query: str) -> List["Package"]: results += repository.search(query) return results + + @classmethod + def _log(cls, msg, level="info"): + getattr(logger, level)("{}: {}".format(cls.__name__, msg)) diff --git a/tests/repositories/test_pool.py b/tests/repositories/test_pool.py index 54a405e555a..1c3815de4a8 100644 --- a/tests/repositories/test_pool.py +++ b/tests/repositories/test_pool.py @@ -1,9 +1,49 @@ +import shutil + +from pathlib import Path +from urllib.parse import urlparse + import pytest +from poetry.factory import Factory from poetry.repositories import Pool from poetry.repositories import Repository from poetry.repositories.exceptions import PackageNotFound +from poetry.repositories.exceptions import RepositoryError from poetry.repositories.legacy_repository import LegacyRepository +from poetry.repositories.legacy_repository import Page + + +class MockRepository(LegacyRepository): + + FIXTURES = Path(__file__).parent / "fixtures" / "legacy" + + def __init__(self): + super(MockRepository, self).__init__( + "legacy", url="http://legacy.foo.bar", disable_cache=True + ) + + def _get(self, endpoint): + parts = endpoint.split("/") + name = parts[1] + + fixture = self.FIXTURES / (name + ".html") + if not fixture.exists(): + return + + with fixture.open(encoding="utf-8") as f: + return Page(self._url + endpoint, f.read(), {}) + + def _download(self, url, dest): + filename = urlparse(url).path.rsplit("/")[-1] + filepath = self.FIXTURES.parent / "pypi.org" / "dists" / filename + + shutil.copyfile(str(filepath), dest) + + +class ErrorRepository(MockRepository): + def _get(self, endpoint): + raise RepositoryError() def test_pool_raises_package_not_found_when_no_package_is_found(): @@ -69,3 +109,26 @@ def test_repository_with_normal_default_and_secondary_repositories(): assert pool.repository("foo") is repo1 assert pool.repository("bar") is repo2 assert pool.has_default() + + +def test_default_repository_failure(): + default = ErrorRepository() + secondary = MockRepository() + pool = Pool() + pool.add_repository(secondary, secondary=True) + pool.add_repository(default, default=True) + + with pytest.raises(RepositoryError): + pool.find_packages(Factory.create_dependency("pyyaml", "*")) + + +def test_secondary_repository_failure(): + secondary = ErrorRepository() + default = MockRepository() + pool = Pool() + pool.add_repository(secondary, secondary=True) + pool.add_repository(default, default=True) + + packages = pool.find_packages(Factory.create_dependency("pyyaml", "*")) + + assert len(packages) == 1