Skip to content
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
16 changes: 12 additions & 4 deletions pcapkit/foundation/engines/_pcap_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,16 +132,23 @@ def installed_distributions() -> 'tuple[str, ...]':
only way to see the distribution the import did *not* resolve to -- which is
exactly the collision worth reporting.

Only the existence of a ``dist-info`` is asked about, through
:func:`importlib.metadata.distributions` rather than
:func:`importlib.metadata.distribution`. From Python 3.15 the latter parses
every candidate's ``METADATA`` file to prefer one that has metadata, and since
this runs on every ``pcap_ct`` extraction, that parse made the engine about
2.3x slower per packet (:issue:`2110`). Either call finds a distribution
exactly when the other does.

Returns:
The installed subset of :data:`DISTRIBUTIONS`, in that order.

"""
found = [] # type: list[str]
for name in DISTRIBUTIONS:
try:
importlib.metadata.distribution(name)
except importlib.metadata.PackageNotFoundError:
continue
if next(iter(importlib.metadata.distributions(name=name)), None) is None:
continue
except Exception: # pylint: disable=broad-except
# A corrupt or unreadable ``dist-info`` is not a reason to fail an
# extraction. Detection is a courtesy; treat "cannot tell" as "not
Expand Down Expand Up @@ -187,7 +194,8 @@ def probe() -> 'Probe':
"""Import |pcap|_ and report what was found.

Not cached. The cost after a successful first call is a :data:`sys.modules`
lookup, and a cache would make the answer depend on when it was first asked --
lookup plus the ``dist-info`` scan in :func:`installed_distributions`, and a
cache would make the answer depend on when it was first asked --
which the tests, and anything that manipulates :data:`sys.path`, would have to
work around. It is idempotent instead, via :func:`_purge`.

Expand Down
31 changes: 25 additions & 6 deletions tests/foundation/engines/test_pcap_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,12 +159,10 @@ def installed(self, present):

from pcapkit.foundation.engines import _pcap_backend

def distribution(name):
if name in present:
return mock.Mock(version='stub')
raise importlib.metadata.PackageNotFoundError(name)
def distributions(*, name):
return iter([mock.Mock(version='stub')] if name in present else [])

with mock.patch('importlib.metadata.distribution', side_effect=distribution):
with mock.patch('importlib.metadata.distributions', side_effect=distributions):
return _pcap_backend.installed_distributions()

def test_reports_only_what_is_installed_in_a_fixed_order(self) -> None:
Expand All @@ -180,9 +178,30 @@ def test_an_unreadable_dist_info_is_treated_as_absent_not_fatal(self) -> None:

# Detection is a courtesy; a corrupt ``dist-info`` must not be able to
# fail an extraction.
with mock.patch('importlib.metadata.distribution', side_effect=ValueError('corrupt')):
with mock.patch('importlib.metadata.distributions', side_effect=ValueError('corrupt')):
self.assertEqual(_pcap_backend.installed_distributions(), ())

def test_finds_a_dist_info_without_reading_its_metadata(self) -> None:
import importlib.metadata
import os
import sys
import tempfile

from pcapkit.foundation.engines import _pcap_backend

# From Python 3.15 ``importlib.metadata.distribution`` parses every
# candidate's METADATA, which ran on each pcap_ct extraction (#2110).
# Existence is all that is asked, so a METADATA read is now an error.
def unread(self, filename):
raise AssertionError(f'read {filename} of {self}')

with tempfile.TemporaryDirectory() as site:
os.mkdir(os.path.join(site, 'pcap_ct-1.3.0b3.dist-info'))
with mock.patch.object(sys, 'path', [site, *sys.path]), \
mock.patch.object(importlib.metadata.PathDistribution, 'read_text', unread), \
mock.patch('importlib.metadata.distribution', side_effect=AssertionError):
self.assertIn('pcap-ct', _pcap_backend.installed_distributions())


@unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed')
class ReasonTests(unittest.TestCase):
Expand Down
Loading