diff --git a/pcapkit/foundation/engines/_pcap_backend.py b/pcapkit/foundation/engines/_pcap_backend.py index 0278825545..8708c67042 100644 --- a/pcapkit/foundation/engines/_pcap_backend.py +++ b/pcapkit/foundation/engines/_pcap_backend.py @@ -132,6 +132,14 @@ 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. @@ -139,9 +147,8 @@ def installed_distributions() -> 'tuple[str, ...]': 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 @@ -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`. diff --git a/tests/foundation/engines/test_pcap_backend.py b/tests/foundation/engines/test_pcap_backend.py index f64cde3a4d..2b1bc8466b 100644 --- a/tests/foundation/engines/test_pcap_backend.py +++ b/tests/foundation/engines/test_pcap_backend.py @@ -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: @@ -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):