From 6029e086911be873b2ebacb933e3df08c23084e4 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Sat, 25 Jan 2014 15:32:46 -0700 Subject: [PATCH] Issue 19944: Fix importlib.find_spec() so it imports parents as needed. The function is also moved to importlib.util. --- Doc/library/importlib.rst | 34 +++--- Lib/idlelib/EditorWindow.py | 3 +- Lib/importlib/__init__.py | 46 ++------ Lib/importlib/util.py | 72 +++++++++++++ Lib/pkgutil.py | 2 +- Lib/pyclbr.py | 4 +- Lib/runpy.py | 26 +---- Lib/test/test_importlib/test_api.py | 151 +-------------------------- Lib/test/test_importlib/test_util.py | 147 ++++++++++++++++++++++++++ Lib/test/test_importlib/util.py | 34 +++++- Misc/NEWS | 3 + 11 files changed, 292 insertions(+), 230 deletions(-) diff --git a/Doc/library/importlib.rst b/Doc/library/importlib.rst index a3373e0bb8c..e2c826c1531 100644 --- a/Doc/library/importlib.rst +++ b/Doc/library/importlib.rst @@ -89,22 +89,6 @@ Functions .. versionchanged:: 3.3 Parent packages are automatically imported. -.. function:: find_spec(name, path=None) - - Find the :term:`spec ` for a module, optionally within the - specified *path*. If the module is in :attr:`sys.modules`, then - ``sys.modules[name].__spec__`` is returned (unless the spec would be - ``None`` or is not set, in which case :exc:`ValueError` is raised). - Otherwise a search using :attr:`sys.meta_path` is done. ``None`` is - returned if no spec is found. - - A dotted name does not have its parent implicitly imported as that requires - loading them and that may not be desired. To properly import a submodule you - will need to import all parent packages of the submodule and use the correct - argument to *path*. - - .. versionadded:: 3.4 - .. function:: find_loader(name, path=None) Find the loader for a module, optionally within the specified *path*. If the @@ -125,7 +109,7 @@ Functions attribute is set to ``None``. .. deprecated:: 3.4 - Use :func:`find_spec` instead. + Use :func:`importlib.util.find_spec` instead. .. function:: invalidate_caches() @@ -1111,6 +1095,22 @@ an :term:`importer`. .. versionadded:: 3.3 +.. function:: find_spec(name, package=None) + + Find the :term:`spec ` for a module, optionally relative to + the specified **package** name. If the module is in :attr:`sys.modules`, + then ``sys.modules[name].__spec__`` is returned (unless the spec would be + ``None`` or is not set, in which case :exc:`ValueError` is raised). + Otherwise a search using :attr:`sys.meta_path` is done. ``None`` is + returned if no spec is found. + + If **name** is for a submodule (contains a dot), the parent module is + automatically imported. + + **name** and **package** work the same as for :func:`import_module`. + + .. versionadded:: 3.4 + .. decorator:: module_for_loader A :term:`decorator` for :meth:`importlib.abc.Loader.load_module` diff --git a/Lib/idlelib/EditorWindow.py b/Lib/idlelib/EditorWindow.py index 390ffab9b71..d18305783a8 100644 --- a/Lib/idlelib/EditorWindow.py +++ b/Lib/idlelib/EditorWindow.py @@ -1,5 +1,6 @@ import importlib import importlib.abc +import importlib.util import os from platform import python_version import re @@ -660,7 +661,7 @@ class EditorWindow(object): return # XXX Ought to insert current file's directory in front of path try: - spec = importlib.find_spec(name) + spec = importlib.util.find_spec(name) except (ValueError, ImportError) as msg: tkMessageBox.showerror("Import error", str(msg), parent=self.text) return diff --git a/Lib/importlib/__init__.py b/Lib/importlib/__init__.py index c66e46cc935..f6adc5cdcf6 100644 --- a/Lib/importlib/__init__.py +++ b/Lib/importlib/__init__.py @@ -11,8 +11,6 @@ __all__ = ['__import__', 'import_module', 'invalidate_caches', 'reload'] # initialised below if the frozen one is not available). import _imp # Just the builtin component, NOT the full Python module import sys -import types -import warnings try: import _frozen_importlib as _bootstrap @@ -34,6 +32,10 @@ _r_long = _bootstrap._r_long # Fully bootstrapped at this point, import whatever you like, circular # dependencies and startup overhead minimisation permitting :) +import types +import warnings + + # Public API ######################################################### from ._bootstrap import __import__ @@ -47,47 +49,16 @@ def invalidate_caches(): finder.invalidate_caches() -def find_spec(name, path=None): - """Return the spec for the specified module. - - First, sys.modules is checked to see if the module was already imported. If - so, then sys.modules[name].__spec__ is returned. If that happens to be - set to None, then ValueError is raised. If the module is not in - sys.modules, then sys.meta_path is searched for a suitable spec with the - value of 'path' given to the finders. None is returned if no spec could - be found. - - Dotted names do not have their parent packages implicitly imported. You will - most likely need to explicitly import all parent packages in the proper - order for a submodule to get the correct spec. - - """ - if name not in sys.modules: - return _bootstrap._find_spec(name, path) - else: - module = sys.modules[name] - if module is None: - return None - try: - spec = module.__spec__ - except AttributeError: - raise ValueError('{}.__spec__ is not set'.format(name)) - else: - if spec is None: - raise ValueError('{}.__spec__ is None'.format(name)) - return spec - - def find_loader(name, path=None): """Return the loader for the specified module. This is a backward-compatible wrapper around find_spec(). - This function is deprecated in favor of importlib.find_spec(). + This function is deprecated in favor of importlib.util.find_spec(). """ - warnings.warn('Use importlib.find_spec() instead.', DeprecationWarning, - stacklevel=2) + warnings.warn('Use importlib.util.find_spec() instead.', + DeprecationWarning, stacklevel=2) try: loader = sys.modules[name].__loader__ if loader is None: @@ -167,7 +138,8 @@ def reload(module): pkgpath = parent.__path__ else: pkgpath = None - spec = module.__spec__ = _bootstrap._find_spec(name, pkgpath, module) + target = module + spec = module.__spec__ = _bootstrap._find_spec(name, pkgpath, target) methods = _bootstrap._SpecMethods(spec) methods.exec(module) # The module may have replaced itself in sys.modules! diff --git a/Lib/importlib/util.py b/Lib/importlib/util.py index 42fc9eae93f..6d73b1d7b6d 100644 --- a/Lib/importlib/util.py +++ b/Lib/importlib/util.py @@ -7,6 +7,7 @@ from ._bootstrap import source_from_cache from ._bootstrap import spec_from_loader from ._bootstrap import spec_from_file_location from ._bootstrap import _resolve_name +from ._bootstrap import _find_spec from contextlib import contextmanager import functools @@ -29,6 +30,77 @@ def resolve_name(name, package): return _resolve_name(name[level:], package, level) +def _find_spec_from_path(name, path=None): + """Return the spec for the specified module. + + First, sys.modules is checked to see if the module was already imported. If + so, then sys.modules[name].__spec__ is returned. If that happens to be + set to None, then ValueError is raised. If the module is not in + sys.modules, then sys.meta_path is searched for a suitable spec with the + value of 'path' given to the finders. None is returned if no spec could + be found. + + Dotted names do not have their parent packages implicitly imported. You will + most likely need to explicitly import all parent packages in the proper + order for a submodule to get the correct spec. + + """ + if name not in sys.modules: + return _find_spec(name, path) + else: + module = sys.modules[name] + if module is None: + return None + try: + spec = module.__spec__ + except AttributeError: + raise ValueError('{}.__spec__ is not set'.format(name)) + else: + if spec is None: + raise ValueError('{}.__spec__ is None'.format(name)) + return spec + + +def find_spec(name, package=None): + """Return the spec for the specified module. + + First, sys.modules is checked to see if the module was already imported. If + so, then sys.modules[name].__spec__ is returned. If that happens to be + set to None, then ValueError is raised. If the module is not in + sys.modules, then sys.meta_path is searched for a suitable spec with the + value of 'path' given to the finders. None is returned if no spec could + be found. + + If the name is for submodule (contains a dot), the parent module is + automatically imported. + + The name and package arguments work the same as importlib.import_module(). + In other words, relative module names (with leading dots) work. + + """ + fullname = resolve_name(name, package) if name.startswith('.') else name + if fullname not in sys.modules: + parent_name = fullname.rpartition('.')[0] + if parent_name: + # Use builtins.__import__() in case someone replaced it. + parent = __import__(parent_name, fromlist=['__path__']) + return _find_spec(fullname, parent.__path__) + else: + return _find_spec(fullname, None) + else: + module = sys.modules[fullname] + if module is None: + return None + try: + spec = module.__spec__ + except AttributeError: + raise ValueError('{}.__spec__ is not set'.format(name)) + else: + if spec is None: + raise ValueError('{}.__spec__ is None'.format(name)) + return spec + + @contextmanager def _module_to_load(name): is_reload = name in sys.modules diff --git a/Lib/pkgutil.py b/Lib/pkgutil.py index 7beae1203a0..326657aa888 100644 --- a/Lib/pkgutil.py +++ b/Lib/pkgutil.py @@ -611,7 +611,7 @@ def get_data(package, resource): which does not support get_data(), then None is returned. """ - spec = importlib.find_spec(package) + spec = importlib.util.find_spec(package) if spec is None: return None loader = spec.loader diff --git a/Lib/pyclbr.py b/Lib/pyclbr.py index a8d2b1fe560..dd58ada0aa5 100644 --- a/Lib/pyclbr.py +++ b/Lib/pyclbr.py @@ -42,7 +42,7 @@ Instances of this class have the following instance variables: import io import os import sys -import importlib +import importlib.util import tokenize from token import NAME, DEDENT, OP from operator import itemgetter @@ -141,7 +141,7 @@ def _readmodule(module, path, inpackage=None): else: search_path = path + sys.path # XXX This will change once issue19944 lands. - spec = importlib.find_spec(fullmodule, search_path) + spec = importlib.util._find_spec_from_path(fullmodule, search_path) fname = spec.loader.get_filename(fullmodule) _modules[fullmodule] = dict if spec.loader.is_package(fullmodule): diff --git a/Lib/runpy.py b/Lib/runpy.py index dc08f4eae92..577deb29b86 100644 --- a/Lib/runpy.py +++ b/Lib/runpy.py @@ -13,9 +13,8 @@ importers when locating support scripts as well as when importing modules. import os import sys import importlib.machinery # importlib first so we can test #15386 via -m +import importlib.util import types -from importlib import find_spec -from importlib.util import spec_from_loader from pkgutil import read_code, get_importer __all__ = [ @@ -100,33 +99,16 @@ def _run_module_code(code, init_globals=None, # may be cleared when the temporary module goes away return mod_globals.copy() - -def _fixed_find_spec(mod_name): - # find_spec has the same annoying behaviour as find_loader did (it - # fails to work properly for dotted names), so this is a fixed version - # ala pkgutil.get_loader - if mod_name.startswith('.'): - msg = "Relative module name {!r} not supported".format(mod_name) - raise ImportError(msg) - path = None - pkg_name = mod_name.rpartition(".")[0] - if pkg_name: - pkg = importlib.import_module(pkg_name) - path = getattr(pkg, "__path__", None) - if path is None: - return None +# Helper to get the loader, code and filename for a module +def _get_module_details(mod_name): try: - return importlib.find_spec(mod_name, path) + spec = importlib.util.find_spec(mod_name) except (ImportError, AttributeError, TypeError, ValueError) as ex: # This hack fixes an impedance mismatch between pkgutil and # importlib, where the latter raises other errors for cases where # pkgutil previously raised ImportError msg = "Error while finding spec for {!r} ({}: {})" raise ImportError(msg.format(mod_name, type(ex), ex)) from ex - -# Helper to get the loader, code and filename for a module -def _get_module_details(mod_name): - spec = _fixed_find_spec(mod_name) if spec is None: raise ImportError("No module named %s" % mod_name) if spec.submodule_search_locations is not None: diff --git a/Lib/test/test_importlib/test_api.py b/Lib/test/test_importlib/test_api.py index c3c19f4c13e..744001b4c05 100644 --- a/Lib/test/test_importlib/test_api.py +++ b/Lib/test/test_importlib/test_api.py @@ -4,7 +4,6 @@ frozen_init, source_init = util.import_importlib('importlib') frozen_util, source_util = util.import_importlib('importlib.util') frozen_machinery, source_machinery = util.import_importlib('importlib.machinery') -from contextlib import contextmanager import os.path import sys from test import support @@ -13,37 +12,6 @@ import unittest import warnings -@contextmanager -def temp_module(name, content='', *, pkg=False): - conflicts = [n for n in sys.modules if n.partition('.')[0] == name] - with support.temp_cwd(None) as cwd: - with util.uncache(name, *conflicts): - with support.DirsOnSysPath(cwd): - frozen_init.invalidate_caches() - - location = os.path.join(cwd, name) - if pkg: - modpath = os.path.join(location, '__init__.py') - os.mkdir(name) - else: - modpath = location + '.py' - if content is None: - # Make sure the module file gets created. - content = '' - if content is not None: - # not a namespace package - with open(modpath, 'w') as modfile: - modfile.write(content) - yield location - - -def submodule(parent, name, pkg_dir, content=''): - path = os.path.join(pkg_dir, name + '.py') - with open(path, 'w') as subfile: - subfile.write(content) - return '{}.{}'.format(parent, name), path - - class ImportModuleTests: """Test importlib.import_module.""" @@ -210,121 +178,6 @@ class Source_FindLoaderTests(FindLoaderTests, unittest.TestCase): init = source_init -class FindSpecTests: - - class FakeMetaFinder: - @staticmethod - def find_spec(name, path=None, target=None): return name, path, target - - def test_sys_modules(self): - name = 'some_mod' - with util.uncache(name): - module = types.ModuleType(name) - loader = 'a loader!' - spec = self.machinery.ModuleSpec(name, loader) - module.__loader__ = loader - module.__spec__ = spec - sys.modules[name] = module - found = self.init.find_spec(name) - self.assertEqual(found, spec) - - def test_sys_modules_without___loader__(self): - name = 'some_mod' - with util.uncache(name): - module = types.ModuleType(name) - del module.__loader__ - loader = 'a loader!' - spec = self.machinery.ModuleSpec(name, loader) - module.__spec__ = spec - sys.modules[name] = module - found = self.init.find_spec(name) - self.assertEqual(found, spec) - - def test_sys_modules_spec_is_None(self): - name = 'some_mod' - with util.uncache(name): - module = types.ModuleType(name) - module.__spec__ = None - sys.modules[name] = module - with self.assertRaises(ValueError): - self.init.find_spec(name) - - def test_sys_modules_loader_is_None(self): - name = 'some_mod' - with util.uncache(name): - module = types.ModuleType(name) - spec = self.machinery.ModuleSpec(name, None) - module.__spec__ = spec - sys.modules[name] = module - found = self.init.find_spec(name) - self.assertEqual(found, spec) - - def test_sys_modules_spec_is_not_set(self): - name = 'some_mod' - with util.uncache(name): - module = types.ModuleType(name) - try: - del module.__spec__ - except AttributeError: - pass - sys.modules[name] = module - with self.assertRaises(ValueError): - self.init.find_spec(name) - - def test_success(self): - name = 'some_mod' - with util.uncache(name): - with util.import_state(meta_path=[self.FakeMetaFinder]): - self.assertEqual((name, None, None), - self.init.find_spec(name)) - - def test_success_path(self): - # Searching on a path should work. - name = 'some_mod' - path = 'path to some place' - with util.uncache(name): - with util.import_state(meta_path=[self.FakeMetaFinder]): - self.assertEqual((name, path, None), - self.init.find_spec(name, path)) - - def test_nothing(self): - # None is returned upon failure to find a loader. - self.assertIsNone(self.init.find_spec('nevergoingtofindthismodule')) - - def test_find_submodule(self): - name = 'spam' - subname = 'ham' - with temp_module(name, pkg=True) as pkg_dir: - fullname, _ = submodule(name, subname, pkg_dir) - spec = self.init.find_spec(fullname, [pkg_dir]) - self.assertIsNot(spec, None) - self.assertNotIn(name, sorted(sys.modules)) - # Ensure successive calls behave the same. - spec_again = self.init.find_spec(fullname, [pkg_dir]) - self.assertEqual(spec_again, spec) - - def test_find_submodule_missing_path(self): - name = 'spam' - subname = 'ham' - with temp_module(name, pkg=True) as pkg_dir: - fullname, _ = submodule(name, subname, pkg_dir) - spec = self.init.find_spec(fullname) - self.assertIs(spec, None) - self.assertNotIn(name, sorted(sys.modules)) - # Ensure successive calls behave the same. - spec = self.init.find_spec(fullname) - self.assertIs(spec, None) - - -class Frozen_FindSpecTests(FindSpecTests, unittest.TestCase): - init = frozen_init - machinery = frozen_machinery - -class Source_FindSpecTests(FindSpecTests, unittest.TestCase): - init = source_init - machinery = source_machinery - - class ReloadTests: """Test module reloading for builtin and extension modules.""" @@ -484,8 +337,8 @@ class ReloadTests: # See #19851. name = 'spam' subname = 'ham' - with temp_module(name, pkg=True) as pkg_dir: - fullname, _ = submodule(name, subname, pkg_dir) + with util.temp_module(name, pkg=True) as pkg_dir: + fullname, _ = util.submodule(name, subname, pkg_dir) ham = self.init.import_module(fullname) reloaded = self.init.reload(ham) self.assertIs(reloaded, ham) diff --git a/Lib/test/test_importlib/test_util.py b/Lib/test/test_importlib/test_util.py index b6fe86435bb..b2823c6a784 100644 --- a/Lib/test/test_importlib/test_util.py +++ b/Lib/test/test_importlib/test_util.py @@ -1,5 +1,7 @@ from importlib import util from . import util as test_util +frozen_init, source_init = test_util.import_importlib('importlib') +frozen_machinery, source_machinery = test_util.import_importlib('importlib.machinery') frozen_util, source_util = test_util.import_importlib('importlib.util') import os @@ -310,6 +312,151 @@ Frozen_ResolveNameTests, Source_ResolveNameTests = test_util.test_both( util=[frozen_util, source_util]) +class FindSpecTests: + + class FakeMetaFinder: + @staticmethod + def find_spec(name, path=None, target=None): return name, path, target + + def test_sys_modules(self): + name = 'some_mod' + with test_util.uncache(name): + module = types.ModuleType(name) + loader = 'a loader!' + spec = self.machinery.ModuleSpec(name, loader) + module.__loader__ = loader + module.__spec__ = spec + sys.modules[name] = module + found = self.util.find_spec(name) + self.assertEqual(found, spec) + + def test_sys_modules_without___loader__(self): + name = 'some_mod' + with test_util.uncache(name): + module = types.ModuleType(name) + del module.__loader__ + loader = 'a loader!' + spec = self.machinery.ModuleSpec(name, loader) + module.__spec__ = spec + sys.modules[name] = module + found = self.util.find_spec(name) + self.assertEqual(found, spec) + + def test_sys_modules_spec_is_None(self): + name = 'some_mod' + with test_util.uncache(name): + module = types.ModuleType(name) + module.__spec__ = None + sys.modules[name] = module + with self.assertRaises(ValueError): + self.util.find_spec(name) + + def test_sys_modules_loader_is_None(self): + name = 'some_mod' + with test_util.uncache(name): + module = types.ModuleType(name) + spec = self.machinery.ModuleSpec(name, None) + module.__spec__ = spec + sys.modules[name] = module + found = self.util.find_spec(name) + self.assertEqual(found, spec) + + def test_sys_modules_spec_is_not_set(self): + name = 'some_mod' + with test_util.uncache(name): + module = types.ModuleType(name) + try: + del module.__spec__ + except AttributeError: + pass + sys.modules[name] = module + with self.assertRaises(ValueError): + self.util.find_spec(name) + + def test_success(self): + name = 'some_mod' + with test_util.uncache(name): + with test_util.import_state(meta_path=[self.FakeMetaFinder]): + self.assertEqual((name, None, None), + self.util.find_spec(name)) + +# def test_success_path(self): +# # Searching on a path should work. +# name = 'some_mod' +# path = 'path to some place' +# with test_util.uncache(name): +# with test_util.import_state(meta_path=[self.FakeMetaFinder]): +# self.assertEqual((name, path, None), +# self.util.find_spec(name, path)) + + def test_nothing(self): + # None is returned upon failure to find a loader. + self.assertIsNone(self.util.find_spec('nevergoingtofindthismodule')) + + def test_find_submodule(self): + name = 'spam' + subname = 'ham' + with test_util.temp_module(name, pkg=True) as pkg_dir: + fullname, _ = test_util.submodule(name, subname, pkg_dir) + spec = self.util.find_spec(fullname) + self.assertIsNot(spec, None) + self.assertIn(name, sorted(sys.modules)) + self.assertNotIn(fullname, sorted(sys.modules)) + # Ensure successive calls behave the same. + spec_again = self.util.find_spec(fullname) + self.assertEqual(spec_again, spec) + + def test_find_submodule_parent_already_imported(self): + name = 'spam' + subname = 'ham' + with test_util.temp_module(name, pkg=True) as pkg_dir: + self.init.import_module(name) + fullname, _ = test_util.submodule(name, subname, pkg_dir) + spec = self.util.find_spec(fullname) + self.assertIsNot(spec, None) + self.assertIn(name, sorted(sys.modules)) + self.assertNotIn(fullname, sorted(sys.modules)) + # Ensure successive calls behave the same. + spec_again = self.util.find_spec(fullname) + self.assertEqual(spec_again, spec) + + def test_find_relative_module(self): + name = 'spam' + subname = 'ham' + with test_util.temp_module(name, pkg=True) as pkg_dir: + fullname, _ = test_util.submodule(name, subname, pkg_dir) + relname = '.' + subname + spec = self.util.find_spec(relname, name) + self.assertIsNot(spec, None) + self.assertIn(name, sorted(sys.modules)) + self.assertNotIn(fullname, sorted(sys.modules)) + # Ensure successive calls behave the same. + spec_again = self.util.find_spec(fullname) + self.assertEqual(spec_again, spec) + + def test_find_relative_module_missing_package(self): + name = 'spam' + subname = 'ham' + with test_util.temp_module(name, pkg=True) as pkg_dir: + fullname, _ = test_util.submodule(name, subname, pkg_dir) + relname = '.' + subname + with self.assertRaises(ValueError): + self.util.find_spec(relname) + self.assertNotIn(name, sorted(sys.modules)) + self.assertNotIn(fullname, sorted(sys.modules)) + + +class Frozen_FindSpecTests(FindSpecTests, unittest.TestCase): + init = frozen_init + machinery = frozen_machinery + util = frozen_util + +class Source_FindSpecTests(FindSpecTests, unittest.TestCase): + init = source_init + machinery = source_machinery + util = source_util + + class MagicNumberTests: def test_length(self): diff --git a/Lib/test/test_importlib/util.py b/Lib/test/test_importlib/util.py index a0dee6ebbc5..885cec3b291 100644 --- a/Lib/test/test_importlib/util.py +++ b/Lib/test/test_importlib/util.py @@ -1,5 +1,5 @@ from contextlib import contextmanager -from importlib import util +from importlib import util, invalidate_caches import os.path from test import support import unittest @@ -46,6 +46,13 @@ def case_insensitive_tests(test): "requires a case-insensitive filesystem")(test) +def submodule(parent, name, pkg_dir, content=''): + path = os.path.join(pkg_dir, name + '.py') + with open(path, 'w') as subfile: + subfile.write(content) + return '{}.{}'.format(parent, name), path + + @contextmanager def uncache(*names): """Uncache a module from sys.modules. @@ -71,6 +78,31 @@ def uncache(*names): except KeyError: pass + +@contextmanager +def temp_module(name, content='', *, pkg=False): + conflicts = [n for n in sys.modules if n.partition('.')[0] == name] + with support.temp_cwd(None) as cwd: + with uncache(name, *conflicts): + with support.DirsOnSysPath(cwd): + invalidate_caches() + + location = os.path.join(cwd, name) + if pkg: + modpath = os.path.join(location, '__init__.py') + os.mkdir(name) + else: + modpath = location + '.py' + if content is None: + # Make sure the module file gets created. + content = '' + if content is not None: + # not a namespace package + with open(modpath, 'w') as modfile: + modfile.write(content) + yield location + + @contextmanager def import_state(**kwargs): """Context manager to manage the various importers and stored state in the diff --git a/Misc/NEWS b/Misc/NEWS index bd2b6ad26ab..316ffbe6368 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -382,6 +382,9 @@ Library - Issue #15475: Add __sizeof__ implementations for itertools objects. +- Issue #19944: Fix importlib.find_spec() so it imports parents as needed + and move the function to importlib.util. + - Issue #19880: Fix a reference leak in unittest.TestCase. Explicitly break reference cycles between frames and the _Outcome instance.