diff --git a/src/poetry/inspection/info.py b/src/poetry/inspection/info.py index 9afb4612b37..362cb6a7370 100644 --- a/src/poetry/inspection/info.py +++ b/src/poetry/inspection/info.py @@ -30,10 +30,9 @@ if TYPE_CHECKING: from collections.abc import Iterator + from packaging.metadata import RawMetadata from poetry.core.packages.project_package import ProjectPackage - from poetry.inspection.lazy_wheel import MemoryWheel - logger = logging.getLogger(__name__) @@ -231,7 +230,9 @@ def to_package( return package @classmethod - def _from_distribution(cls, dist: pkginfo.Distribution) -> PackageInfo: + def _from_distribution( + cls, dist: pkginfo.BDist | pkginfo.SDist | pkginfo.Wheel + ) -> PackageInfo: """ Helper method to parse package information from a `pkginfo.Distribution` instance. @@ -242,7 +243,7 @@ def _from_distribution(cls, dist: pkginfo.Distribution) -> PackageInfo: if dist.requires_dist: requirements = list(dist.requires_dist) - elif isinstance(dist, (pkginfo.BDist, pkginfo.SDist, pkginfo.Wheel)): + else: requires = Path(dist.filename) / "requires.txt" if requires.exists(): text = requires.read_text(encoding="utf-8") @@ -256,9 +257,8 @@ def _from_distribution(cls, dist: pkginfo.Distribution) -> PackageInfo: requires_python=dist.requires_python, ) - if isinstance(dist, (pkginfo.BDist, pkginfo.SDist, pkginfo.Wheel)): - info._source_type = "file" - info._source_url = Path(dist.filename).resolve().as_posix() + info._source_type = "file" + info._source_url = Path(dist.filename).resolve().as_posix() return info @@ -522,13 +522,19 @@ def from_wheel(cls, path: Path) -> PackageInfo: return PackageInfo() @classmethod - def from_memory_wheel(cls, wheel: MemoryWheel) -> PackageInfo: + def from_wheel_metadata(cls, metadata: RawMetadata) -> PackageInfo: """ - Gather package information from a partial fetched wheel kept in memory. + Gather package information from metadata of a remote wheel. - :param path: Path to wheel. + :param metadata: metadata of the wheel. """ - return cls._from_distribution(wheel) + return cls( + name=metadata.get("name"), + version=metadata.get("version"), + summary=metadata.get("summary"), + requires_dist=metadata.get("requires_dist"), + requires_python=metadata.get("requires_python"), + ) @classmethod def from_bdist(cls, path: Path) -> PackageInfo: diff --git a/src/poetry/inspection/lazy_wheel.py b/src/poetry/inspection/lazy_wheel.py index 7542ed3abe0..0361d788594 100644 --- a/src/poetry/inspection/lazy_wheel.py +++ b/src/poetry/inspection/lazy_wheel.py @@ -20,7 +20,7 @@ from zipfile import BadZipFile from zipfile import ZipFile -from pkginfo import Distribution +from packaging.metadata import parse_email from requests.models import CONTENT_CHUNK_SIZE from requests.models import HTTPError from requests.models import Response @@ -31,6 +31,7 @@ from collections.abc import Iterable from collections.abc import Iterator + from packaging.metadata import RawMetadata from requests import Session from poetry.utils.authenticator import Authenticator @@ -58,29 +59,10 @@ def __str__(self) -> str: return f"Wheel '{self.name}' located at {self.location} is invalid." -class MemoryWheel(Distribution): - def __init__(self, lazy_file: LazyWheelOverHTTP) -> None: - self.lazy_file = lazy_file - self.extractMetadata() - - def read(self) -> bytes: - with ZipFile(self.lazy_file) as archive: - tuples = [x.split("/") for x in archive.namelist() if "METADATA" in x] - schwarz = sorted([(len(x), x) for x in tuples]) - for path in [x[1] for x in schwarz]: - candidate = "/".join(path) - logger.debug(f"read {candidate}") - data = archive.read(candidate) - if b"Metadata-Version" in data: - return data - else: - raise ValueError(f"No METADATA in archive: {self.lazy_file.name}") - - -def memory_wheel_from_url( +def metadata_from_wheel_url( name: str, url: str, session: Session | Authenticator -) -> MemoryWheel: - """Return a MemoryWheel (compatible to pkginfo.Wheel) from the given wheel URL. +) -> RawMetadata: + """Fetch metadata from the given wheel URL. This uses HTTP range requests to only fetch the portion of the wheel containing metadata, just enough for the object to be constructed. @@ -92,10 +74,10 @@ def memory_wheel_from_url( # After context manager exit, wheel.name will point to a deleted file path. # Add `delete_backing_file=False` to disable this for debugging. with LazyWheelOverHTTP(url, session) as lazy_file: - # prefetch metadata to reduce the number of range requests - # (we know that METADATA is the only file from the wheel we need) - lazy_file.prefetch_metadata(name) - return MemoryWheel(lazy_file) + metadata_bytes = lazy_file.read_metadata(name) + + metadata, _ = parse_email(metadata_bytes) + return metadata except (BadZipFile, UnsupportedWheel): # We assume that these errors have occurred because the wheel contents @@ -720,7 +702,7 @@ def _extract_content_length( self._domains_without_negative_range.add(domain) return file_length, tail - def prefetch_metadata(self, name: str) -> None: + def _prefetch_metadata(self, name: str) -> str: """Locate the *.dist-info/METADATA entry from a temporary ``ZipFile`` wrapper, and download it. @@ -729,7 +711,7 @@ def prefetch_metadata(self, name: str) -> None: can be downloaded in a single ranged GET request.""" logger.debug("begin prefetching METADATA for %s", name) - dist_info_prefix = re.compile(r"^[^/]*\.dist-info/METADATA") + metadata_regex = re.compile(r"^[^/]*\.dist-info/METADATA$") start: int | None = None end: int | None = None @@ -738,25 +720,36 @@ def prefetch_metadata(self, name: str) -> None: # should be set large enough to avoid this). zf = ZipFile(self) + filename = "" for info in zf.infolist(): if start is None: - if dist_info_prefix.search(info.filename): + if metadata_regex.search(info.filename): + filename = info.filename start = info.header_offset continue else: # The last .dist-info/ entry may be before the end of the file if the # wheel's entries are sorted lexicographically (which is unusual). - if not dist_info_prefix.search(info.filename): + if not metadata_regex.search(info.filename): end = info.header_offset break if start is None: raise UnsupportedWheel( - f"no {dist_info_prefix!r} found for {name} in {self.name}" + f"no {metadata_regex!r} found for {name} in {self.name}" ) # If it is the last entry of the zip, then give us everything # until the start of the central directory. if end is None: end = zf.start_dir - logger.debug("fetch METADATA") + logger.debug(f"fetch {filename}") self.ensure_downloaded(start, end) logger.debug("done prefetching METADATA for %s", name) + + return filename + + def read_metadata(self, name: str) -> bytes: + """Download and read the METADATA file from the remote wheel.""" + with ZipFile(self) as zf: + # prefetch metadata to reduce the number of range requests + filename = self._prefetch_metadata(name) + return zf.read(filename) diff --git a/src/poetry/repositories/http_repository.py b/src/poetry/repositories/http_repository.py index 546dfc4dcdc..9c89ec83ee1 100644 --- a/src/poetry/repositories/http_repository.py +++ b/src/poetry/repositories/http_repository.py @@ -21,7 +21,7 @@ from poetry.config.config import Config from poetry.inspection.lazy_wheel import HTTPRangeRequestUnsupported -from poetry.inspection.lazy_wheel import memory_wheel_from_url +from poetry.inspection.lazy_wheel import metadata_from_wheel_url from poetry.repositories.cached_repository import CachedRepository from poetry.repositories.exceptions import PackageNotFound from poetry.repositories.exceptions import RepositoryError @@ -118,8 +118,8 @@ def _get_info_from_wheel(self, url: str) -> PackageInfo: # or we don't know yet, we try range requests. if self._lazy_wheel and self._supports_range_requests.get(netloc, True): try: - package_info = PackageInfo.from_memory_wheel( - memory_wheel_from_url(link.filename, link.url, self.session) + package_info = PackageInfo.from_wheel_metadata( + metadata_from_wheel_url(link.filename, link.url, self.session) ) except HTTPRangeRequestUnsupported: # Do not set to False if we already know that the domain supports diff --git a/tests/inspection/conftest.py b/tests/inspection/conftest.py deleted file mode 100644 index 87edc940adf..00000000000 --- a/tests/inspection/conftest.py +++ /dev/null @@ -1,148 +0,0 @@ -from __future__ import annotations - -from pathlib import Path -from typing import TYPE_CHECKING -from typing import Any -from typing import Dict -from typing import Protocol -from typing import Tuple -from urllib.parse import urlparse - -import pytest - -from requests import codes - - -if TYPE_CHECKING: - from collections.abc import Callable - - from httpretty.core import HTTPrettyRequest - - from tests.types import FixtureDirGetter - - HttPrettyResponse = Tuple[int, Dict[str, Any], bytes] # status code, headers, body - HttPrettyRequestCallback = Callable[ - [HTTPrettyRequest, str, Dict[str, Any]], HttPrettyResponse - ] - - class RequestCallbackFactory(Protocol): - def __call__( - self, - *, - accept_ranges: str | None = "bytes", - negative_offset_error: tuple[int, bytes] | None = None, - ) -> HttPrettyRequestCallback: ... - - -NEGATIVE_OFFSET_AS_POSITIVE = -1 - - -def build_head_response( - accept_ranges: str | None, content_length: int, response_headers: dict[str, Any] -) -> HttPrettyResponse: - response_headers["Content-Length"] = content_length - if accept_ranges: - response_headers["Accept-Ranges"] = accept_ranges - return 200, response_headers, b"" - - -def build_partial_response( - rng: str, - wheel_bytes: bytes, - response_headers: dict[str, Any], - *, - negative_offset_as_positive: bool = False, -) -> HttPrettyResponse: - status_code = 206 - response_headers["Accept-Ranges"] = "bytes" - total_length = len(wheel_bytes) - if rng.startswith("-"): - # negative offset - offset = int(rng) - if negative_offset_as_positive: - # some servers interpret a negative offset like "-10" as "0-10" - start = 0 - end = min(-offset, total_length - 1) - body = wheel_bytes[start : end + 1] - else: - start = total_length + offset - if start < 0: - # wheel is smaller than initial chunk size - return 200, response_headers, wheel_bytes - end = total_length - 1 - body = wheel_bytes[offset:] - else: - # range with start and end - rng_parts = rng.split("-") - start = int(rng_parts[0]) - end = int(rng_parts[1]) - body = wheel_bytes[start : end + 1] - response_headers["Content-Range"] = f"bytes {start}-{end}/{total_length}" - return status_code, response_headers, body - - -@pytest.fixture -def handle_request_factory(fixture_dir: FixtureDirGetter) -> RequestCallbackFactory: - def _factory( - *, - accept_ranges: str | None = "bytes", - negative_offset_error: tuple[int, bytes] | None = None, - ) -> HttPrettyRequestCallback: - def handle_request( - request: HTTPrettyRequest, uri: str, response_headers: dict[str, Any] - ) -> HttPrettyResponse: - name = Path(urlparse(uri).path).name - - wheel = Path(__file__).parent.parent.joinpath( - "repositories/fixtures/pypi.org/dists/" + name - ) - - if not wheel.exists(): - wheel = fixture_dir("distributions") / name - - if not wheel.exists(): - wheel = ( - fixture_dir("distributions") / "demo-0.1.0-py2.py3-none-any.whl" - ) - - wheel_bytes = wheel.read_bytes() - - del response_headers["status"] - - if request.method == "HEAD": - return build_head_response( - accept_ranges, len(wheel_bytes), response_headers - ) - - rng = request.headers.get("Range", "") - if rng: - rng = rng.split("=")[1] - - negative_offset_as_positive = False - if negative_offset_error and rng.startswith("-"): - if negative_offset_error[0] == codes.requested_range_not_satisfiable: - response_headers["Content-Range"] = f"bytes */{len(wheel_bytes)}" - if negative_offset_error[0] == NEGATIVE_OFFSET_AS_POSITIVE: - negative_offset_as_positive = True - else: - return ( - negative_offset_error[0], - response_headers, - negative_offset_error[1], - ) - if accept_ranges == "bytes" and rng: - return build_partial_response( - rng, - wheel_bytes, - response_headers, - negative_offset_as_positive=negative_offset_as_positive, - ) - - status_code = 200 - body = wheel_bytes - - return status_code, response_headers, body - - return handle_request - - return _factory diff --git a/tests/inspection/test_info.py b/tests/inspection/test_info.py index 3e59c7e518b..fed8efcf444 100644 --- a/tests/inspection/test_info.py +++ b/tests/inspection/test_info.py @@ -4,14 +4,14 @@ from subprocess import CalledProcessError from typing import TYPE_CHECKING +from zipfile import ZipFile import pytest -import requests + +from packaging.metadata import parse_email from poetry.inspection.info import PackageInfo from poetry.inspection.info import PackageInfoError -from poetry.inspection.lazy_wheel import MemoryWheel -from poetry.inspection.lazy_wheel import memory_wheel_from_url from poetry.utils.env import EnvCommandError from poetry.utils.env import VirtualEnv @@ -19,10 +19,9 @@ if TYPE_CHECKING: from pathlib import Path - from httpretty import httpretty + from packaging.metadata import RawMetadata from pytest_mock import MockerFixture - from tests.inspection.conftest import RequestCallbackFactory from tests.types import FixtureDirGetter @@ -42,15 +41,10 @@ def demo_wheel(fixture_dir: FixtureDirGetter) -> Path: @pytest.fixture -def demo_memory_wheel( - http: type[httpretty], - handle_request_factory: RequestCallbackFactory, -) -> MemoryWheel: - url = "https://foo.com/demo-0.1.0-py2.py3-none-any.whl" - request_callback = handle_request_factory() - http.register_uri(http.GET, url, body=request_callback) - - return memory_wheel_from_url("demo", url, requests.Session()) +def demo_wheel_metadata(demo_wheel: Path) -> RawMetadata: + with ZipFile(demo_wheel) as zf: + metadata, _ = parse_email(zf.read("demo-0.1.0.dist-info/METADATA")) + return metadata @pytest.fixture @@ -179,13 +173,28 @@ def test_info_from_wheel(demo_wheel: Path) -> None: assert info._source_url == demo_wheel.resolve().as_posix() -def test_info_from_memory_wheel(demo_memory_wheel: MemoryWheel) -> None: - info = PackageInfo.from_memory_wheel(demo_memory_wheel) +def test_info_from_wheel_metadata(demo_wheel_metadata: RawMetadata) -> None: + info = PackageInfo.from_wheel_metadata(demo_wheel_metadata) demo_check_info(info) + assert info.requires_python == ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" assert info._source_type is None assert info._source_url is None +def test_info_from_wheel_metadata_incomplete() -> None: + """ + To avoid differences in cached metadata, + it is important that the representation of missing fields does not change! + """ + metadata, _ = parse_email(b"Metadata-Version: 2.1\nName: demo\nVersion: 0.1.0\n") + info = PackageInfo.from_wheel_metadata(metadata) + assert info.name == "demo" + assert info.version == "0.1.0" + assert info.summary is None + assert info.requires_dist is None + assert info.requires_python is None + + def test_info_from_bdist(demo_wheel: Path) -> None: info = PackageInfo.from_bdist(demo_wheel) demo_check_info(info) diff --git a/tests/inspection/test_lazy_wheel.py b/tests/inspection/test_lazy_wheel.py index 6c21b86a71a..c3bf310e773 100644 --- a/tests/inspection/test_lazy_wheel.py +++ b/tests/inspection/test_lazy_wheel.py @@ -2,7 +2,13 @@ import re +from pathlib import Path from typing import TYPE_CHECKING +from typing import Any +from typing import Dict +from typing import Protocol +from typing import Tuple +from urllib.parse import urlparse import httpretty import pytest @@ -12,12 +18,142 @@ from poetry.inspection.lazy_wheel import HTTPRangeRequestUnsupported from poetry.inspection.lazy_wheel import InvalidWheel -from poetry.inspection.lazy_wheel import memory_wheel_from_url -from tests.inspection.conftest import NEGATIVE_OFFSET_AS_POSITIVE +from poetry.inspection.lazy_wheel import metadata_from_wheel_url if TYPE_CHECKING: - from tests.inspection.conftest import RequestCallbackFactory + from collections.abc import Callable + + from httpretty.core import HTTPrettyRequest + + from tests.types import FixtureDirGetter + + HttPrettyResponse = Tuple[int, Dict[str, Any], bytes] # status code, headers, body + HttPrettyRequestCallback = Callable[ + [HTTPrettyRequest, str, Dict[str, Any]], HttPrettyResponse + ] + + class RequestCallbackFactory(Protocol): + def __call__( + self, + *, + accept_ranges: str | None = "bytes", + negative_offset_error: tuple[int, bytes] | None = None, + ) -> HttPrettyRequestCallback: ... + + +NEGATIVE_OFFSET_AS_POSITIVE = -1 + + +def build_head_response( + accept_ranges: str | None, content_length: int, response_headers: dict[str, Any] +) -> HttPrettyResponse: + response_headers["Content-Length"] = content_length + if accept_ranges: + response_headers["Accept-Ranges"] = accept_ranges + return 200, response_headers, b"" + + +def build_partial_response( + rng: str, + wheel_bytes: bytes, + response_headers: dict[str, Any], + *, + negative_offset_as_positive: bool = False, +) -> HttPrettyResponse: + status_code = 206 + response_headers["Accept-Ranges"] = "bytes" + total_length = len(wheel_bytes) + if rng.startswith("-"): + # negative offset + offset = int(rng) + if negative_offset_as_positive: + # some servers interpret a negative offset like "-10" as "0-10" + start = 0 + end = min(-offset, total_length - 1) + body = wheel_bytes[start : end + 1] + else: + start = total_length + offset + if start < 0: + # wheel is smaller than initial chunk size + return 200, response_headers, wheel_bytes + end = total_length - 1 + body = wheel_bytes[offset:] + else: + # range with start and end + rng_parts = rng.split("-") + start = int(rng_parts[0]) + end = int(rng_parts[1]) + body = wheel_bytes[start : end + 1] + response_headers["Content-Range"] = f"bytes {start}-{end}/{total_length}" + return status_code, response_headers, body + + +@pytest.fixture +def handle_request_factory(fixture_dir: FixtureDirGetter) -> RequestCallbackFactory: + def _factory( + *, + accept_ranges: str | None = "bytes", + negative_offset_error: tuple[int, bytes] | None = None, + ) -> HttPrettyRequestCallback: + def handle_request( + request: HTTPrettyRequest, uri: str, response_headers: dict[str, Any] + ) -> HttPrettyResponse: + name = Path(urlparse(uri).path).name + + wheel = Path(__file__).parent.parent.joinpath( + "repositories/fixtures/pypi.org/dists/" + name + ) + + if not wheel.exists(): + wheel = fixture_dir("distributions") / name + + if not wheel.exists(): + wheel = ( + fixture_dir("distributions") / "demo-0.1.0-py2.py3-none-any.whl" + ) + + wheel_bytes = wheel.read_bytes() + + del response_headers["status"] + + if request.method == "HEAD": + return build_head_response( + accept_ranges, len(wheel_bytes), response_headers + ) + + rng = request.headers.get("Range", "") + if rng: + rng = rng.split("=")[1] + + negative_offset_as_positive = False + if negative_offset_error and rng.startswith("-"): + if negative_offset_error[0] == codes.requested_range_not_satisfiable: + response_headers["Content-Range"] = f"bytes */{len(wheel_bytes)}" + if negative_offset_error[0] == NEGATIVE_OFFSET_AS_POSITIVE: + negative_offset_as_positive = True + else: + return ( + negative_offset_error[0], + response_headers, + negative_offset_error[1], + ) + if accept_ranges == "bytes" and rng: + return build_partial_response( + rng, + wheel_bytes, + response_headers, + negative_offset_as_positive=negative_offset_as_positive, + ) + + status_code = 200 + body = wheel_bytes + + return status_code, response_headers, body + + return handle_request + + return _factory @pytest.mark.parametrize( @@ -30,7 +166,7 @@ (NEGATIVE_OFFSET_AS_POSITIVE, b"handle negative offset as positive"), ], ) -def test_memory_wheel_from_url( +def test_metadata_from_wheel_url( http: type[httpretty.httpretty], handle_request_factory: RequestCallbackFactory, negative_offset_error: tuple[int, bytes] | None, @@ -47,12 +183,12 @@ def test_memory_wheel_from_url( url = f"https://{domain}/poetry_core-1.5.0-py3-none-any.whl" - wheel = memory_wheel_from_url("poetry-core", url, requests.Session()) + metadata = metadata_from_wheel_url("poetry-core", url, requests.Session()) - assert wheel.name == "poetry-core" - assert wheel.version == "1.5.0" - assert wheel.author == "Sébastien Eustace" - assert wheel.requires_dist == [ + assert metadata["name"] == "poetry-core" + assert metadata["version"] == "1.5.0" + assert metadata["author"] == "Sébastien Eustace" + assert metadata["requires_dist"] == [ 'importlib-metadata (>=1.7.0) ; python_version < "3.8"' ] @@ -78,14 +214,14 @@ def test_memory_wheel_from_url( # second wheel -> one less request if negative offsets are not supported latest_requests.clear() - memory_wheel_from_url("poetry-core", url, requests.Session()) + metadata_from_wheel_url("poetry-core", url, requests.Session()) expected_requests = min(expected_requests, 4) latest_requests = httpretty.latest_requests() assert len(latest_requests) == expected_requests @pytest.mark.parametrize("negative_offset_as_positive", [False, True]) -def test_memory_wheel_from_url_smaller_than_initial_chunk_size( +def test_metadata_from_wheel_url_smaller_than_initial_chunk_size( http: type[httpretty.httpretty], handle_request_factory: RequestCallbackFactory, negative_offset_as_positive: bool, @@ -102,12 +238,12 @@ def test_memory_wheel_from_url_smaller_than_initial_chunk_size( url = f"https://{domain}/zipp-3.5.0-py3-none-any.whl" - wheel = memory_wheel_from_url("zipp", url, requests.Session()) + metadata = metadata_from_wheel_url("zipp", url, requests.Session()) - assert wheel.name == "zipp" - assert wheel.version == "3.5.0" - assert wheel.author == "Jason R. Coombs" - assert len(wheel.requires_dist) == 12 + assert metadata["name"] == "zipp" + assert metadata["version"] == "3.5.0" + assert metadata["author"] == "Jason R. Coombs" + assert len(metadata["requires_dist"]) == 12 # only one request because server gives a normal response with the entire wheel # except for when server interprets negative offset as positive @@ -116,7 +252,7 @@ def test_memory_wheel_from_url_smaller_than_initial_chunk_size( @pytest.mark.parametrize("accept_ranges", [None, "none"]) -def test_memory_wheel_from_url_range_requests_not_supported_one_request( +def test_metadata_from_wheel_url_range_requests_not_supported_one_request( http: type[httpretty.httpretty], handle_request_factory: RequestCallbackFactory, accept_ranges: str | None, @@ -130,7 +266,7 @@ def test_memory_wheel_from_url_range_requests_not_supported_one_request( url = f"https://{domain}/poetry_core-1.5.0-py3-none-any.whl" with pytest.raises(HTTPRangeRequestUnsupported): - memory_wheel_from_url("poetry-core", url, requests.Session()) + metadata_from_wheel_url("poetry-core", url, requests.Session()) latest_requests = http.latest_requests() assert len(latest_requests) == 1 @@ -144,7 +280,7 @@ def test_memory_wheel_from_url_range_requests_not_supported_one_request( (codes.not_implemented, b"Unsupported client range"), ], ) -def test_memory_wheel_from_url_range_requests_not_supported_two_requests( +def test_metadata_from_wheel_url_range_requests_not_supported_two_requests( http: type[httpretty.httpretty], handle_request_factory: RequestCallbackFactory, negative_offset_error: tuple[int, bytes], @@ -160,7 +296,7 @@ def test_memory_wheel_from_url_range_requests_not_supported_two_requests( url = f"https://{domain}/poetry_core-1.5.0-py3-none-any.whl" with pytest.raises(HTTPRangeRequestUnsupported): - memory_wheel_from_url("poetry-core", url, requests.Session()) + metadata_from_wheel_url("poetry-core", url, requests.Session()) latest_requests = http.latest_requests() assert len(latest_requests) == 2 @@ -168,7 +304,7 @@ def test_memory_wheel_from_url_range_requests_not_supported_two_requests( assert latest_requests[1].method == "HEAD" -def test_memory_wheel_from_url_invalid_wheel( +def test_metadata_from_wheel_url_invalid_wheel( http: type[httpretty.httpretty], handle_request_factory: RequestCallbackFactory, ) -> None: @@ -181,7 +317,7 @@ def test_memory_wheel_from_url_invalid_wheel( url = f"https://{domain}/demo_missing_dist_info-0.1.0-py2.py3-none-any.whl" with pytest.raises(InvalidWheel): - memory_wheel_from_url("demo-missing-dist-info", url, requests.Session()) + metadata_from_wheel_url("demo-missing-dist-info", url, requests.Session()) latest_requests = http.latest_requests() assert len(latest_requests) == 1 diff --git a/tests/repositories/test_http_repository.py b/tests/repositories/test_http_repository.py index 0eb0e9046e0..88d63171f86 100644 --- a/tests/repositories/test_http_repository.py +++ b/tests/repositories/test_http_repository.py @@ -5,11 +5,13 @@ from pathlib import Path from typing import TYPE_CHECKING from typing import Any +from zipfile import ZipFile import pytest +from packaging.metadata import parse_email + from poetry.inspection.lazy_wheel import HTTPRangeRequestUnsupported -from poetry.inspection.lazy_wheel import MemoryWheel from poetry.repositories.http_repository import HTTPRepository from poetry.utils.helpers import HTTPRangeRequestSupported @@ -40,10 +42,12 @@ def test_get_info_from_wheel( ) -> None: filename = "poetry_core-1.5.0-py3-none-any.whl" filepath = MockRepository.DIST_FIXTURES / filename + with ZipFile(filepath) as zf: + metadata, _ = parse_email(zf.read("poetry_core-1.5.0.dist-info/METADATA")) - mock_memory_wheel_from_url = mocker.patch( - "poetry.repositories.http_repository.memory_wheel_from_url", - return_value=MemoryWheel(filepath), # type: ignore[arg-type] + mock_metadata_from_wheel_url = mocker.patch( + "poetry.repositories.http_repository.metadata_from_wheel_url", + return_value=metadata, ) mock_download = mocker.patch( "poetry.repositories.http_repository.download_file", @@ -65,11 +69,13 @@ def test_get_info_from_wheel( ] if lazy_wheel and (supports_range_requests or supports_range_requests is None): - mock_memory_wheel_from_url.assert_called_once_with(filename, url, repo.session) + mock_metadata_from_wheel_url.assert_called_once_with( + filename, url, repo.session + ) mock_download.assert_not_called() assert repo._supports_range_requests[domain] is True else: - mock_memory_wheel_from_url.assert_not_called() + mock_metadata_from_wheel_url.assert_not_called() mock_download.assert_called_once_with( url, mocker.ANY, session=repo.session, raise_accepts_ranges=lazy_wheel ) @@ -97,8 +103,8 @@ def test_get_info_from_wheel_state_sequence(mocker: MockerFixture) -> None: 6. Range requests are supported for some files: We try range requests (success). """ - mock_memory_wheel_from_url = mocker.patch( - "poetry.repositories.http_repository.memory_wheel_from_url" + mock_metadata_from_wheel_url = mocker.patch( + "poetry.repositories.http_repository.metadata_from_wheel_url" ) mock_download = mocker.patch("poetry.repositories.http_repository.download_file") @@ -108,37 +114,37 @@ def test_get_info_from_wheel_state_sequence(mocker: MockerFixture) -> None: repo = MockRepository() # 1. range request and download - mock_memory_wheel_from_url.side_effect = HTTPRangeRequestUnsupported + mock_metadata_from_wheel_url.side_effect = HTTPRangeRequestUnsupported repo._get_info_from_wheel(url) - assert mock_memory_wheel_from_url.call_count == 1 + assert mock_metadata_from_wheel_url.call_count == 1 assert mock_download.call_count == 1 # 2. only download repo._get_info_from_wheel(url) - assert mock_memory_wheel_from_url.call_count == 1 + assert mock_metadata_from_wheel_url.call_count == 1 assert mock_download.call_count == 2 # 3. range request and download - mock_memory_wheel_from_url.side_effect = None + mock_metadata_from_wheel_url.side_effect = None mock_download.side_effect = HTTPRangeRequestSupported repo._get_info_from_wheel(url) - assert mock_memory_wheel_from_url.call_count == 2 + assert mock_metadata_from_wheel_url.call_count == 2 assert mock_download.call_count == 3 # 4. only range request repo._get_info_from_wheel(url) - assert mock_memory_wheel_from_url.call_count == 3 + assert mock_metadata_from_wheel_url.call_count == 3 assert mock_download.call_count == 3 # 5. range request and download - mock_memory_wheel_from_url.side_effect = HTTPRangeRequestUnsupported + mock_metadata_from_wheel_url.side_effect = HTTPRangeRequestUnsupported mock_download.side_effect = None repo._get_info_from_wheel(url) - assert mock_memory_wheel_from_url.call_count == 4 + assert mock_metadata_from_wheel_url.call_count == 4 assert mock_download.call_count == 4 # 6. only range request - mock_memory_wheel_from_url.side_effect = None + mock_metadata_from_wheel_url.side_effect = None repo._get_info_from_wheel(url) - assert mock_memory_wheel_from_url.call_count == 5 + assert mock_metadata_from_wheel_url.call_count == 5 assert mock_download.call_count == 4