poetry/tests/repositories/test_legacy_repository.py

630 lines
19 KiB
Python

from __future__ import annotations
import base64
import re
import shutil
from pathlib import Path
from typing import TYPE_CHECKING
import pytest
import requests
from packaging.utils import canonicalize_name
from poetry.core.constraints.version import Version
from poetry.core.packages.dependency import Dependency
from poetry.core.packages.utils.link import Link
from poetry.factory import Factory
from poetry.repositories.exceptions import PackageNotFound
from poetry.repositories.exceptions import RepositoryError
from poetry.repositories.legacy_repository import LegacyRepository
from poetry.repositories.link_sources.html import SimpleRepositoryPage
if TYPE_CHECKING:
import httpretty
from _pytest.monkeypatch import MonkeyPatch
from packaging.utils import NormalizedName
from pytest_mock import MockerFixture
from poetry.config.config import Config
from tests.types import RequestsSessionGet
@pytest.fixture(autouse=True)
def _use_simple_keyring(with_simple_keyring: None) -> None:
pass
class MockRepository(LegacyRepository):
FIXTURES = Path(__file__).parent / "fixtures" / "legacy"
def __init__(self) -> None:
super().__init__("legacy", url="http://legacy.foo.bar", disable_cache=True)
self._lazy_wheel = False
def _get_page(self, name: NormalizedName) -> SimpleRepositoryPage:
fixture = self.FIXTURES / (name + ".html")
if not fixture.exists():
raise PackageNotFound(f"Package [{name}] not found.")
with fixture.open(encoding="utf-8") as f:
return SimpleRepositoryPage(self._url + f"/{name}/", f.read())
def _download(
self, url: str, dest: Path, *, raise_accepts_ranges: bool = False
) -> None:
filename = Link(url).filename
filepath = self.FIXTURES.parent / "pypi.org" / "dists" / filename
shutil.copyfile(str(filepath), dest)
def test_packages_property_returns_empty_list() -> None:
repo = MockRepository()
repo._packages = [repo.package("jupyter", Version.parse("1.0.0"))]
assert repo.packages == []
def test_page_relative_links_path_are_correct() -> None:
repo = MockRepository()
page = repo.get_page("relative")
assert page is not None
for link in page.links:
assert link.netloc == "legacy.foo.bar"
assert link.path.startswith("/relative/poetry")
def test_page_absolute_links_path_are_correct() -> None:
repo = MockRepository()
page = repo.get_page("absolute")
assert page is not None
for link in page.links:
assert link.netloc == "files.pythonhosted.org"
assert link.path.startswith("/packages/")
def test_page_clean_link() -> None:
repo = MockRepository()
page = repo.get_page("relative")
assert page is not None
cleaned = page.clean_link('https://legacy.foo.bar/test /the"/cleaning\0')
assert cleaned == "https://legacy.foo.bar/test%20/the%22/cleaning%00"
def test_page_invalid_version_link() -> None:
repo = MockRepository()
page = repo.get_page("invalid-version")
assert page is not None
links = list(page.links)
assert len(links) == 1
versions = list(page.versions(canonicalize_name("poetry")))
assert len(versions) == 1
assert versions[0].to_string() == "0.1.0"
packages = list(page.packages)
assert len(packages) == 1
assert packages[0].name == "poetry"
assert packages[0].version.to_string() == "0.1.0"
def test_page_filters_out_invalid_package_names() -> None:
class SpecialMockRepository(MockRepository):
def _get_page(self, name: NormalizedName) -> SimpleRepositoryPage:
return super()._get_page(canonicalize_name(f"{name}-with-extra-packages"))
repo = SpecialMockRepository()
packages = repo.find_packages(Factory.create_dependency("pytest", "*"))
assert len(packages) == 1
assert packages[0].name == "pytest"
assert packages[0].version == Version.parse("3.5.0")
package = repo.package("pytest", Version.parse("3.5.0"))
assert package.files == [
{
"file": "pytest-3.5.0-py2.py3-none-any.whl",
"hash": "sha256:6266f87ab64692112e5477eba395cfedda53b1933ccd29478e671e73b420c19c",
},
{
"file": "pytest-3.5.0.tar.gz",
"hash": "sha256:fae491d1874f199537fd5872b5e1f0e74a009b979df9d53d1553fd03da1703e1",
},
]
def test_sdist_format_support() -> None:
repo = MockRepository()
page = repo.get_page("relative")
assert page is not None
bz2_links = list(filter(lambda link: link.ext == ".tar.bz2", page.links))
assert len(bz2_links) == 1
assert bz2_links[0].filename == "poetry-0.1.1.tar.bz2"
def test_missing_version() -> None:
repo = MockRepository()
with pytest.raises(PackageNotFound):
repo._get_release_info(
canonicalize_name("missing_version"), Version.parse("1.1.0")
)
def test_get_package_information_fallback_read_setup() -> None:
repo = MockRepository()
package = repo.package("jupyter", Version.parse("1.0.0"))
assert package.source_type == "legacy"
assert package.source_reference == repo.name
assert package.source_url == repo.url
assert package.name == "jupyter"
assert package.version.text == "1.0.0"
assert (
package.description
== "Jupyter metapackage. Install all the Jupyter components in one go."
)
def test_get_package_information_pep_658(
mocker: MockerFixture, get_metadata_mock: RequestsSessionGet
) -> None:
repo = MockRepository()
isort_package = repo.package("isort", Version.parse("4.3.4"))
mocker.patch.object(repo.session, "get", get_metadata_mock)
spy = mocker.spy(repo, "_get_info_from_metadata")
try:
package = repo.package("isort-metadata", Version.parse("4.3.4"))
except FileNotFoundError:
pytest.fail("Metadata was not successfully retrieved")
else:
assert spy.call_count > 0
assert spy.spy_return is not None
assert package.source_type == isort_package.source_type == "legacy"
assert package.source_reference == isort_package.source_reference == repo.name
assert package.source_url == isort_package.source_url == repo.url
assert package.name == "isort-metadata"
assert package.version.text == isort_package.version.text == "4.3.4"
assert package.description == isort_package.description
assert (
package.requires == isort_package.requires == [Dependency("futures", "*")]
)
assert (
str(package.python_constraint)
== str(isort_package.python_constraint)
== ">=2.7,<3.0.dev0 || >=3.4.dev0"
)
def test_get_package_information_skips_dependencies_with_invalid_constraints() -> None:
repo = MockRepository()
package = repo.package("python-language-server", Version.parse("0.21.2"))
assert package.name == "python-language-server"
assert package.version.text == "0.21.2"
assert (
package.description == "Python Language Server for the Language Server Protocol"
)
assert len(package.requires) == 25
assert sorted(
(r for r in package.requires if not r.is_optional()), key=lambda r: r.name
) == [
Dependency("configparser", "*"),
Dependency("future", ">=0.14.0"),
Dependency("futures", "*"),
Dependency("jedi", ">=0.12"),
Dependency("pluggy", "*"),
Dependency("python-jsonrpc-server", "*"),
]
all_extra = package.extras[canonicalize_name("all")]
# rope>-0.10.5 should be discarded
assert sorted(all_extra, key=lambda r: r.name) == [
Dependency("autopep8", "*"),
Dependency("mccabe", "*"),
Dependency("pycodestyle", "*"),
Dependency("pydocstyle", ">=2.0.0"),
Dependency("pyflakes", ">=1.6.0"),
Dependency("yapf", "*"),
]
def test_package_not_canonicalized() -> None:
repo = MockRepository()
package = repo.package("discord.py", Version.parse("2.0.0"))
assert package.name == "discord-py"
assert package.pretty_name == "discord.py"
def test_find_packages_no_prereleases() -> None:
repo = MockRepository()
packages = repo.find_packages(Factory.create_dependency("pyyaml", "*"))
assert len(packages) == 1
assert packages[0].source_type == "legacy"
assert packages[0].source_reference == repo.name
assert packages[0].source_url == repo.url
@pytest.mark.parametrize(
["constraint", "count"], [("*", 1), (">=1", 1), ("<=18", 0), (">=19.0.0a0", 1)]
)
def test_find_packages_only_prereleases(constraint: str, count: int) -> None:
repo = MockRepository()
packages = repo.find_packages(Factory.create_dependency("black", constraint))
assert len(packages) == count
if count >= 0:
for package in packages:
assert package.source_type == "legacy"
assert package.source_reference == repo.name
assert package.source_url == repo.url
@pytest.mark.parametrize(
["constraint", "expected"],
[
# yanked 21.11b0 is ignored except for pinned version
("*", ["19.10b0"]),
(">=19.0a0", ["19.10b0"]),
(">=20.0a0", []),
(">=21.11b0", []),
("==21.11b0", ["21.11b0"]),
],
)
def test_find_packages_yanked(constraint: str, expected: list[str]) -> None:
repo = MockRepository()
packages = repo.find_packages(Factory.create_dependency("black", constraint))
assert [str(p.version) for p in packages] == expected
def test_get_package_information_chooses_correct_distribution() -> None:
repo = MockRepository()
package = repo.package("isort", Version.parse("4.3.4"))
assert package.name == "isort"
assert package.version.text == "4.3.4"
assert package.requires == [Dependency("futures", "*")]
futures_dep = package.requires[0]
assert futures_dep.python_versions == "~2.7"
def test_get_package_information_includes_python_requires() -> None:
repo = MockRepository()
package = repo.package("futures", Version.parse("3.2.0"))
assert package.name == "futures"
assert package.version.text == "3.2.0"
assert package.python_versions == ">=2.6, <3"
def test_get_package_information_sets_appropriate_python_versions_if_wheels_only() -> (
None
):
repo = MockRepository()
package = repo.package("futures", Version.parse("3.2.0"))
assert package.name == "futures"
assert package.version.text == "3.2.0"
assert package.python_versions == ">=2.6, <3"
def test_get_package_from_both_py2_and_py3_specific_wheels() -> None:
repo = MockRepository()
package = repo.package("ipython", Version.parse("5.7.0"))
assert package.name == "ipython"
assert package.version.text == "5.7.0"
assert package.python_versions == "*"
assert len(package.requires) == 41
expected = [
Dependency("appnope", "*"),
Dependency("backports.shutil-get-terminal-size", "*"),
Dependency("colorama", "*"),
Dependency("decorator", "*"),
Dependency("pathlib2", "*"),
Dependency("pexpect", "*"),
Dependency("pickleshare", "*"),
Dependency("prompt-toolkit", ">=1.0.4,<2.0.0"),
Dependency("pygments", "*"),
Dependency("setuptools", ">=18.5"),
Dependency("simplegeneric", ">0.8"),
Dependency("traitlets", ">=4.2"),
Dependency("win-unicode-console", ">=0.5"),
]
required = [r for r in package.requires if not r.is_optional()]
assert required == expected
assert str(required[1].marker) == 'python_version == "2.7"'
assert (
str(required[12].marker) == 'sys_platform == "win32" and python_version < "3.6"'
)
assert (
str(required[4].marker) == 'python_version == "2.7" or python_version == "3.3"'
)
assert str(required[5].marker) == 'sys_platform != "win32"'
def test_get_package_from_both_py2_and_py3_specific_wheels_python_constraint() -> None:
repo = MockRepository()
package = repo.package("poetry-test-py2-py3-metadata-merge", Version.parse("0.1.0"))
assert package.name == "poetry-test-py2-py3-metadata-merge"
assert package.version.text == "0.1.0"
assert package.python_versions == ">=2.7,<2.8 || >=3.7,<4.0"
def test_get_package_with_dist_and_universal_py3_wheel() -> None:
repo = MockRepository()
package = repo.package("ipython", Version.parse("7.5.0"))
assert package.name == "ipython"
assert package.version.text == "7.5.0"
assert package.python_versions == ">=3.5"
expected = [
Dependency("appnope", "*"),
Dependency("backcall", "*"),
Dependency("colorama", "*"),
Dependency("decorator", "*"),
Dependency("jedi", ">=0.10"),
Dependency("pexpect", "*"),
Dependency("pickleshare", "*"),
Dependency("prompt-toolkit", ">=2.0.0,<2.1.0"),
Dependency("pygments", "*"),
Dependency("setuptools", ">=18.5"),
Dependency("traitlets", ">=4.2"),
Dependency("typing", "*"),
Dependency("win-unicode-console", ">=0.5"),
]
required = [r for r in package.requires if not r.is_optional()]
assert sorted(required, key=lambda dep: dep.name) == expected
def test_get_package_retrieves_non_sha256_hashes() -> None:
repo = MockRepository()
package = repo.package("ipython", Version.parse("7.5.0"))
expected = [
{
"file": "ipython-7.5.0-py3-none-any.whl",
"hash": "sha256:78aea20b7991823f6a32d55f4e963a61590820e43f666ad95ad07c7f0c704efa",
},
{
"file": "ipython-7.5.0.tar.gz",
"hash": "sha256:e840810029224b56cd0d9e7719dc3b39cf84d577f8ac686547c8ba7a06eeab26",
},
]
assert package.files == expected
def test_get_package_retrieves_non_sha256_hashes_mismatching_known_hash() -> None:
repo = MockRepository()
package = repo.package("ipython", Version.parse("5.7.0"))
expected = [
{
"file": "ipython-5.7.0-py2-none-any.whl",
"hash": "md5:a10a802ef98da741cd6f4f6289d47ba7",
},
{
"file": "ipython-5.7.0-py3-none-any.whl",
"hash": "sha256:fc0464e68f9c65cd8c453474b4175432cc29ecb6c83775baedf6dbfcee9275ab",
},
{
"file": "ipython-5.7.0.tar.gz",
"hash": "sha256:8db43a7fb7619037c98626613ff08d03dda9d5d12c84814a4504c78c0da8323c",
},
]
assert package.files == expected
def test_get_package_retrieves_packages_with_no_hashes() -> None:
repo = MockRepository()
package = repo.package("jupyter", Version.parse("1.0.0"))
assert [
{
"file": "jupyter-1.0.0.tar.gz",
"hash": (
"sha256:d9dc4b3318f310e34c82951ea5d6683f67bed7def4b259fafbfe4f1beb1d8e5f"
),
}
] == package.files
@pytest.mark.parametrize(
"package_name, version, yanked, yanked_reason",
[
("black", "19.10b0", False, ""),
("black", "21.11b0", True, "Broken regex dependency. Use 21.11b1 instead."),
],
)
def test_package_yanked(
package_name: str, version: str, yanked: bool, yanked_reason: str
) -> None:
repo = MockRepository()
package = repo.package(package_name, Version.parse(version))
assert package.name == package_name
assert str(package.version) == version
assert package.yanked is yanked
assert package.yanked_reason == yanked_reason
def test_package_partial_yank() -> None:
class SpecialMockRepository(MockRepository):
def _get_page(self, name: NormalizedName) -> SimpleRepositoryPage:
return super()._get_page(canonicalize_name(f"{name}-partial-yank"))
repo = MockRepository()
package = repo.package("futures", Version.parse("3.2.0"))
assert len(package.files) == 2
repo = SpecialMockRepository()
package = repo.package("futures", Version.parse("3.2.0"))
assert len(package.files) == 1
assert package.files[0]["file"].endswith(".tar.gz")
@pytest.mark.parametrize(
"package_name, version, yanked, yanked_reason",
[
("black", "19.10b0", False, ""),
("black", "21.11b0", True, "Broken regex dependency. Use 21.11b1 instead."),
],
)
def test_find_links_for_package_yanked(
package_name: str, version: str, yanked: bool, yanked_reason: str
) -> None:
repo = MockRepository()
package = repo.package(package_name, Version.parse(version))
links = repo.find_links_for_package(package)
assert len(links) == 1
for link in links:
assert link.yanked == yanked
assert link.yanked_reason == yanked_reason
def test_cached_or_downloaded_file_supports_trailing_slash() -> None:
repo = MockRepository()
with repo._cached_or_downloaded_file(
Link("https://foo.bar/pytest-3.5.0-py2.py3-none-any.whl/")
) as filepath:
assert filepath.name == "pytest-3.5.0-py2.py3-none-any.whl"
class MockHttpRepository(LegacyRepository):
def __init__(
self, endpoint_responses: dict[str, int], http: type[httpretty.httpretty]
) -> None:
base_url = "http://legacy.foo.bar"
super().__init__("legacy", url=base_url, disable_cache=True)
for endpoint, response in endpoint_responses.items():
url = base_url + endpoint
http.register_uri(http.GET, url, status=response)
def test_get_200_returns_page(http: type[httpretty.httpretty]) -> None:
repo = MockHttpRepository({"/foo/": 200}, http)
_ = repo.get_page("foo")
@pytest.mark.parametrize("status_code", [401, 403, 404])
def test_get_40x_and_returns_none(
http: type[httpretty.httpretty], status_code: int
) -> None:
repo = MockHttpRepository({"/foo/": status_code}, http)
with pytest.raises(PackageNotFound):
repo.get_page("foo")
def test_get_5xx_raises(http: type[httpretty.httpretty]) -> None:
repo = MockHttpRepository({"/foo/": 500}, http)
with pytest.raises(RepositoryError):
repo.get_page("foo")
def test_get_redirected_response_url(
http: type[httpretty.httpretty], monkeypatch: MonkeyPatch
) -> None:
repo = MockHttpRepository({"/foo/": 200}, http)
redirect_url = "http://legacy.redirect.bar"
def get_mock(
url: str, raise_for_status: bool = True, timeout: int = 5
) -> requests.Response:
response = requests.Response()
response.status_code = 200
response.url = redirect_url + "/foo"
return response
monkeypatch.setattr(repo.session, "get", get_mock)
page = repo.get_page("foo")
assert page is not None
assert page._url == "http://legacy.redirect.bar/foo/"
@pytest.mark.parametrize(
("repositories",),
[
({},),
# ensure path is respected
({"publish": {"url": "https://foo.bar/legacy"}},),
# ensure path length does not give incorrect results
({"publish": {"url": "https://foo.bar/upload/legacy"}},),
],
)
def test_authenticator_with_implicit_repository_configuration(
http: type[httpretty.httpretty],
config: Config,
repositories: dict[str, dict[str, str]],
) -> None:
http.register_uri(
http.GET,
re.compile("^https?://foo.bar/(.+?)$"),
)
config.merge(
{
"repositories": repositories,
"http-basic": {
"source": {"username": "foo", "password": "bar"},
"publish": {"username": "baz", "password": "qux"},
},
}
)
repo = LegacyRepository(name="source", url="https://foo.bar/simple", config=config)
repo.get_page("/foo")
request = http.last_request()
basic_auth = base64.b64encode(b"foo:bar").decode()
assert request.headers["Authorization"] == f"Basic {basic_auth}"