Skip to content

fix: close ZipFile in _prefetch_metadata with context manager#10800

Merged
radoering merged 3 commits intopython-poetry:mainfrom
dimbleby:fix/lazy-wheel-zipfile-close
Apr 3, 2026
Merged

fix: close ZipFile in _prefetch_metadata with context manager#10800
radoering merged 3 commits intopython-poetry:mainfrom
dimbleby:fix/lazy-wheel-zipfile-close

Conversation

@dimbleby
Copy link
Copy Markdown
Contributor

@dimbleby dimbleby commented Mar 30, 2026

ZipFile opened in _prefetch_metadata was never closed. If an UnsupportedWheelError was raised, the handle leaked. Changed bare ZipFile() assignment to a with statement for proper cleanup.

github and whitespace do not make a pretty diff, nothing has changed in the main body of what happens

Summary by Sourcery

Ensure wheel metadata prefetching closes ZipFile handles even when errors occur.

Bug Fixes:

  • Prevent ZipFile handle leaks in _prefetch_metadata by using a context manager for opening wheels.

Tests:

  • Add a regression test verifying that ZipFile is closed when _prefetch_metadata raises an error.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai bot commented Mar 30, 2026

Reviewer's Guide

Refactors LazyWheelOverHTTP._prefetch_metadata to open ZipFile with a context manager so it is always closed, and adds a regression test to assert the ZipFile is closed even when metadata discovery fails with an error.

Sequence diagram for _prefetch_metadata with ZipFile context manager

sequenceDiagram
    participant caller as LazyWheelOverHTTP_caller
    participant wheel as LazyWheelOverHTTP
    participant zlib as ZipFile

    caller->>wheel: _prefetch_metadata(name)
    activate wheel
    wheel->>zlib: __enter__()
    activate zlib

    loop iterate_zip_entries
        wheel->>zlib: infolist()
        zlib-->>wheel: list[ZipInfo]
        wheel->>wheel: search for metadata filename
    end

    alt metadata_found
        wheel->>wheel: determine start and end offsets
        wheel->>wheel: _ensure_downloaded(start, end)
        wheel-->>caller: return metadata_path
    else metadata_not_found
        wheel->>wheel: raise UnsupportedWheelError
    end

    zlib-->>wheel: __exit__()
    deactivate zlib
    deactivate wheel
Loading

Class diagram for LazyWheelOverHTTP and supporting types in _prefetch_metadata

classDiagram
    class LazyWheelOverHTTP {
        +str name
        +Pattern _metadata_regex
        +str _prefetch_metadata(str name)
        +_ensure_downloaded(int start, int end)
    }

    class ZipFile {
        +infolist() list~ZipInfo~
        +start_dir int
        +close() void
    }

    class ZipInfo {
        +str filename
        +int header_offset
    }

    class UnsupportedWheelError {
        +UnsupportedWheelError(str message)
    }

    LazyWheelOverHTTP ..> ZipFile : uses
    LazyWheelOverHTTP ..> ZipInfo : iterates
    LazyWheelOverHTTP ..> UnsupportedWheelError : raises
Loading

File-Level Changes

Change Details Files
Ensure ZipFile opened in _prefetch_metadata is always closed via a context manager.
  • Wrap ZipFile(self) usage in a with statement and indent the metadata scanning logic into the context manager body
  • Keep existing control flow for locating .dist-info/METADATA entries and computing start/end offsets unchanged aside from indentation
  • Use zf.start_dir inside the context manager when computing the end offset for the last entry
src/poetry/inspection/lazy_wheel.py
Add regression test verifying ZipFile is closed on error during metadata prefetch.
  • Instantiate a LazyWheelOverHTTP object without running init and configure its _metadata_regex and _file.name manually
  • Patch poetry.inspection.lazy_wheel.ZipFile to return a MagicMock ZipFile that yields only non-matching entries from infolist
  • Assert that _prefetch_metadata raises an UnsupportedWheelError and that the mocked ZipFile.exit is called exactly once
tests/inspection/test_lazy_wheel.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In test_prefetch_metadata_closes_zipfile_on_error, consider asserting pytest.raises(UnsupportedWheelError, ...) instead of catching a generic Exception to keep the test focused on the expected failure mode.
  • The new test heavily mocks ZipFile; you might simplify and make it more robust by constructing a small in-memory wheel/zip that lacks a matching .dist-info/METADATA entry instead of patching ZipFile and its methods.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `test_prefetch_metadata_closes_zipfile_on_error`, consider asserting `pytest.raises(UnsupportedWheelError, ...)` instead of catching a generic `Exception` to keep the test focused on the expected failure mode.
- The new test heavily mocks `ZipFile`; you might simplify and make it more robust by constructing a small in-memory wheel/zip that lacks a matching `.dist-info/METADATA` entry instead of patching `ZipFile` and its methods.

## Individual Comments

### Comment 1
<location path="tests/inspection/test_lazy_wheel.py" line_range="496-497" />
<code_context>
+    mock_zf.__exit__ = MagicMock(return_value=False)
+
+    with (
+        patch("poetry.inspection.lazy_wheel.ZipFile", return_value=mock_zf),
+        pytest.raises(Exception, match=r"no .* found for"),
+    ):
+        lazy._prefetch_metadata("test-pkg")
</code_context>
<issue_to_address>
**suggestion (testing):** Assert the specific `UnsupportedWheelError` instead of a generic `Exception`

This test is asserting that `_prefetch_metadata` raises `UnsupportedWheelError` when no matching `.dist-info/METADATA` is found, but `pytest.raises(Exception, ...)` is too broad and would still pass if a different exception were raised. Please import and assert the specific exception:

```python
from poetry.inspection.lazy_wheel import LazyWheelOverHTTP, UnsupportedWheelError
...
with (
    patch("poetry.inspection.lazy_wheel.ZipFile", return_value=mock_zf),
    pytest.raises(UnsupportedWheelError, match=r"no .* found for"),
):
    lazy._prefetch_metadata("test-pkg")
```

This keeps the test aligned with the intended contract and will fail if the exception type changes unexpectedly.

Suggested implementation:

```python
from poetry.inspection.lazy_wheel import LazyWheelOverHTTP, UnsupportedWheelError

```

```python
    with (
        patch("poetry.inspection.lazy_wheel.ZipFile", return_value=mock_zf),
        pytest.raises(UnsupportedWheelError, match=r"no .* found for"),
    ):
        lazy._prefetch_metadata("test-pkg")

```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

dimbleby and others added 2 commits April 3, 2026 06:52
ZipFile opened in _prefetch_metadata was never closed. If an
UnsupportedWheelError was raised, the handle leaked. Changed bare
ZipFile() assignment to a with statement for proper cleanup.

Co-authored-by: Copilot <[email protected]>
@radoering radoering force-pushed the fix/lazy-wheel-zipfile-close branch from 17bda58 to 6628f9d Compare April 3, 2026 04:52
radoering
radoering previously approved these changes Apr 3, 2026
@radoering radoering merged commit 4bc7ae1 into python-poetry:main Apr 3, 2026
54 checks passed
@dimbleby dimbleby deleted the fix/lazy-wheel-zipfile-close branch April 3, 2026 08:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants