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
26 changes: 26 additions & 0 deletions tests/_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import sys
import time
import types
import typing
import unittest
import warnings
from typing import Callable, Iterable, Iterator, Optional, TypeVar
Expand Down Expand Up @@ -601,6 +602,29 @@ class objects, and their creation churns the C-level ``_abc_impl`` caches
reset(obj)


def _clear_typing_caches() -> None:
"""Empty the :mod:`typing` subscription caches.

Each import of :mod:`pcapkit` subscribes generics with its own classes --
``Protocol[...]`` at class creation, ``Optional[...]`` when
:func:`typing.get_type_hints` resolves an ``Info`` annotation -- and
:mod:`typing` caches those aliases where no purge reaches. An entry naming a
purged class keeps its generation reachable, and through that generation's
``_previous_excepthook`` the generation whose hook it saved. So in one
plain-unittest process the live generations grow from module to module, and
with them the heap and the cost of every negative
:class:`collections.abc.Mapping` subclass check, which walks the subclasses of
them all (#2750).

``typing._cleanups`` holds each cache's ``cache_clear``, which CPython's own
test runner calls before each test module. A runtime without it skips the
clear rather than erroring, as :func:`_reset_abc_caches` does.

"""
for clear in getattr(typing, '_cleanups', ()):
clear()


#: Default :data:`sys.modules` prefixes the isolation helpers below cover, and
#: the ones :func:`tests.conftest.restore_module_table` puts back after every
#: test. Only the package under test: a test that imports a third-party module
Expand Down Expand Up @@ -689,6 +713,7 @@ def restore_modules(snapshot: 'dict[str, types.ModuleType]',
# subclasses and churned the shared ABC caches, and those stale answers
# outlive the modules that caused them.
_reset_abc_caches()
_clear_typing_caches()


def _rebind_parent_attributes(prefixes: 'tuple[str, ...]') -> None:
Expand Down Expand Up @@ -1066,6 +1091,7 @@ def purge_modules(prefixes: Iterable[str]) -> None:
if _under_prefix(name, tuple(prefixes)):
sys.modules.pop(name, None)
_reset_abc_caches()
_clear_typing_caches()


#: Class attribute :func:`reimport_once_per_class` keeps a class's own import in.
Expand Down
3 changes: 3 additions & 0 deletions tests/protocols/misc/test_pcapng_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -3962,6 +3962,9 @@ class PCAPNGTruncatedFileTests(unittest.TestCase):
def setUpClass(cls) -> None:
super().setUpClass()
cls._swept = None
# The failures keep their tracebacks, and with them this class's pcapkit
# import, so the sweep goes once the class is done (#2750).
cls.addClassCleanup(setattr, cls, '_swept', None)

def setUp(self) -> None:
reimport_once_per_class(self)
Expand Down
45 changes: 45 additions & 0 deletions tests/test_support_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
from __future__ import annotations

import contextlib
import gc
import hashlib
import importlib
import importlib.util
Expand All @@ -70,8 +71,10 @@
import threading
import time
import types
import typing
import unittest
import urllib.error
import weakref
from typing import TYPE_CHECKING
from unittest import mock

Expand Down Expand Up @@ -806,6 +809,48 @@ def test_a_name_the_parent_defines_for_itself_is_left_alone(self) -> None:
self.assertIs(self.parent.child, own)


@unittest.skipUnless(hasattr(typing, '_cleanups'), 'this typing lists no cache clears')
class TypingCachesReleasedTests(unittest.TestCase):
"""A purge and a restore let go of the classes :mod:`typing` has subscribed.

:mod:`typing` caches an alias such as ``Optional[C]``, so without a clear a
purged generation of :mod:`pcapkit` stays reachable through any cached alias
naming one of its classes, and the live generations grow from module to module
of a plain-unittest leg (#2750). A throwaway class stands in for one, and the
prefix matches nothing, so no real module is purged.

"""

PREFIX = 'pcapkit.__support_probe_typing'

def _subscribe(self) -> 'weakref.ref[type]':
"""A weak reference to a class that only :mod:`typing`'s cache holds."""
probe = type('Probe', (), {})
alias = typing.Optional[probe]
del alias
ref = weakref.ref(probe)
del probe
gc.collect()
self.assertIsNotNone(ref(), 'typing did not cache the alias, so this case proves nothing')
return ref

def test_a_purge_releases_a_subscribed_class(self) -> None:
ref = self._subscribe()

purge_modules([self.PREFIX])
gc.collect()

self.assertIsNone(ref())

def test_a_restore_releases_a_subscribed_class(self) -> None:
ref = self._subscribe()

restore_modules(snapshot_modules([self.PREFIX]), [self.PREFIX])
gc.collect()

self.assertIsNone(ref())


class StandInsDoNotOutliveTheirTestTests(unittest.TestCase):
"""A test that installs a stand-in leaves :data:`sys.modules` as it found it.

Expand Down
Loading