bpo-42131: Add PEP 451-related methods to zipimport (GH-23187)

Specifically, find_spec(), create_module(), and exec_module().

Co-authored-by: Nick Coghlan <ncoghlan@gmail.com>
This commit is contained in:
Brett Cannon 2020-11-13 15:14:58 -08:00 committed by GitHub
parent 9b6934230c
commit d2e94bb084
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 1131 additions and 965 deletions

View File

@ -44,8 +44,9 @@ doesn't contain :file:`.pyc` files, importing may be rather slow.
follows the specification in :pep:`273`, but uses an implementation written by Just follows the specification in :pep:`273`, but uses an implementation written by Just
van Rossum that uses the import hooks described in :pep:`302`. van Rossum that uses the import hooks described in :pep:`302`.
:pep:`302` - New Import Hooks :mod:`importlib` - The implementation of the import machinery
The PEP to add the import hooks that help this module work. Package providing the relevant protocols for all importers to
implement.
This module defines an exception: This module defines an exception:
@ -73,7 +74,31 @@ zipimporter Objects
:exc:`ZipImportError` is raised if *archivepath* doesn't point to a valid ZIP :exc:`ZipImportError` is raised if *archivepath* doesn't point to a valid ZIP
archive. archive.
.. method:: find_module(fullname[, path]) .. method:: create_module(spec)
Implementation of :meth:`importlib.abc.Loader.create_module` that returns
:const:`None` to explicitly request the default semantics.
.. versionadded:: 3.10
.. method:: exec_module(module)
Implementation of :meth:`importlib.abc.Loader.exec_module`.
.. versionadded:: 3.10
.. method:: find_loader(fullname, path=None)
An implementation of :meth:`importlib.abc.PathEntryFinder.find_loader`.
.. deprecated:: 3.10
Use :meth:`find_spec` instead.
.. method:: find_module(fullname, path=None)
Search for a module specified by *fullname*. *fullname* must be the fully Search for a module specified by *fullname*. *fullname* must be the fully
qualified (dotted) module name. It returns the zipimporter instance itself qualified (dotted) module name. It returns the zipimporter instance itself
@ -81,6 +106,17 @@ zipimporter Objects
*path* argument is ignored---it's there for compatibility with the *path* argument is ignored---it's there for compatibility with the
importer protocol. importer protocol.
.. deprecated:: 3.10
Use :meth:`find_spec` instead.
.. method:: find_spec(fullname, target=None)
An implementation of :meth:`importlib.abc.PathEntryFinder.find_spec`.
.. versionadded:: 3.10
.. method:: get_code(fullname) .. method:: get_code(fullname)
@ -126,6 +162,10 @@ zipimporter Objects
qualified (dotted) module name. It returns the imported module, or raises qualified (dotted) module name. It returns the imported module, or raises
:exc:`ZipImportError` if it wasn't found. :exc:`ZipImportError` if it wasn't found.
.. deprecated:: 3.10
Use :meth:`exec_module` instead.
.. attribute:: archive .. attribute:: archive

View File

@ -303,6 +303,13 @@ Add a :class:`~xml.sax.handler.LexicalHandler` class to the
:mod:`xml.sax.handler` module. :mod:`xml.sax.handler` module.
(Contributed by Jonathan Gossage and Zackery Spytz in :issue:`35018`.) (Contributed by Jonathan Gossage and Zackery Spytz in :issue:`35018`.)
zipimport
---------
Add methods related to :pep:`451`: :meth:`~zipimport.zipimporter.find_spec`,
:meth:`zipimport.zipimporter.create_module`, and
:meth:`zipimport.zipimporter.exec_module`.
(Contributed by Brett Cannon in :issue:`42131`.
Optimizations Optimizations
============= =============

View File

@ -450,8 +450,9 @@ class UncompressedZipImportTestCase(ImportHooksBaseTestCase):
zi = zipimport.zipimporter(TEMP_ZIP) zi = zipimport.zipimporter(TEMP_ZIP)
self.assertEqual(zi.archive, TEMP_ZIP) self.assertEqual(zi.archive, TEMP_ZIP)
self.assertEqual(zi.is_package(TESTPACK), True) self.assertTrue(zi.is_package(TESTPACK))
# PEP 302
find_mod = zi.find_module('spam') find_mod = zi.find_module('spam')
self.assertIsNotNone(find_mod) self.assertIsNotNone(find_mod)
self.assertIsInstance(find_mod, zipimport.zipimporter) self.assertIsInstance(find_mod, zipimport.zipimporter)
@ -462,25 +463,39 @@ class UncompressedZipImportTestCase(ImportHooksBaseTestCase):
mod = zi.load_module(TESTPACK) mod = zi.load_module(TESTPACK)
self.assertEqual(zi.get_filename(TESTPACK), mod.__file__) self.assertEqual(zi.get_filename(TESTPACK), mod.__file__)
# PEP 451
spec = zi.find_spec('spam')
self.assertIsNotNone(spec)
self.assertIsInstance(spec.loader, zipimport.zipimporter)
self.assertFalse(spec.loader.is_package('spam'))
exec_mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(exec_mod)
self.assertEqual(spec.loader.get_filename('spam'), exec_mod.__file__)
spec = zi.find_spec(TESTPACK)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
self.assertEqual(zi.get_filename(TESTPACK), mod.__file__)
existing_pack_path = importlib.import_module(TESTPACK).__path__[0] existing_pack_path = importlib.import_module(TESTPACK).__path__[0]
expected_path_path = os.path.join(TEMP_ZIP, TESTPACK) expected_path_path = os.path.join(TEMP_ZIP, TESTPACK)
self.assertEqual(existing_pack_path, expected_path_path) self.assertEqual(existing_pack_path, expected_path_path)
self.assertEqual(zi.is_package(packdir + '__init__'), False) self.assertFalse(zi.is_package(packdir + '__init__'))
self.assertEqual(zi.is_package(packdir + TESTPACK2), True) self.assertTrue(zi.is_package(packdir + TESTPACK2))
self.assertEqual(zi.is_package(packdir2 + TESTMOD), False) self.assertFalse(zi.is_package(packdir2 + TESTMOD))
mod_path = packdir2 + TESTMOD mod_path = packdir2 + TESTMOD
mod_name = module_path_to_dotted_name(mod_path) mod_name = module_path_to_dotted_name(mod_path)
mod = importlib.import_module(mod_name) mod = importlib.import_module(mod_name)
self.assertTrue(mod_name in sys.modules) self.assertTrue(mod_name in sys.modules)
self.assertEqual(zi.get_source(TESTPACK), None) self.assertIsNone(zi.get_source(TESTPACK))
self.assertEqual(zi.get_source(mod_path), None) self.assertIsNone(zi.get_source(mod_path))
self.assertEqual(zi.get_filename(mod_path), mod.__file__) self.assertEqual(zi.get_filename(mod_path), mod.__file__)
# To pass in the module name instead of the path, we must use the # To pass in the module name instead of the path, we must use the
# right importer # right importer
loader = mod.__loader__ loader = mod.__spec__.loader
self.assertEqual(loader.get_source(mod_name), None) self.assertIsNone(loader.get_source(mod_name))
self.assertEqual(loader.get_filename(mod_name), mod.__file__) self.assertEqual(loader.get_filename(mod_name), mod.__file__)
# test prefix and archivepath members # test prefix and archivepath members
@ -505,17 +520,22 @@ class UncompressedZipImportTestCase(ImportHooksBaseTestCase):
zi = zipimport.zipimporter(TEMP_ZIP + os.sep + packdir) zi = zipimport.zipimporter(TEMP_ZIP + os.sep + packdir)
self.assertEqual(zi.archive, TEMP_ZIP) self.assertEqual(zi.archive, TEMP_ZIP)
self.assertEqual(zi.prefix, packdir) self.assertEqual(zi.prefix, packdir)
self.assertEqual(zi.is_package(TESTPACK2), True) self.assertTrue(zi.is_package(TESTPACK2))
# PEP 302
mod = zi.load_module(TESTPACK2) mod = zi.load_module(TESTPACK2)
self.assertEqual(zi.get_filename(TESTPACK2), mod.__file__) self.assertEqual(zi.get_filename(TESTPACK2), mod.__file__)
# PEP 451
spec = zi.find_spec(TESTPACK2)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
self.assertEqual(spec.loader.get_filename(TESTPACK2), mod.__file__)
self.assertEqual( self.assertFalse(zi.is_package(TESTPACK2 + os.sep + '__init__'))
zi.is_package(TESTPACK2 + os.sep + '__init__'), False) self.assertFalse(zi.is_package(TESTPACK2 + os.sep + TESTMOD))
self.assertEqual(
zi.is_package(TESTPACK2 + os.sep + TESTMOD), False)
pkg_path = TEMP_ZIP + os.sep + packdir + TESTPACK2 pkg_path = TEMP_ZIP + os.sep + packdir + TESTPACK2
zi2 = zipimport.zipimporter(pkg_path) zi2 = zipimport.zipimporter(pkg_path)
# PEP 302
find_mod_dotted = zi2.find_module(TESTMOD) find_mod_dotted = zi2.find_module(TESTMOD)
self.assertIsNotNone(find_mod_dotted) self.assertIsNotNone(find_mod_dotted)
self.assertIsInstance(find_mod_dotted, zipimport.zipimporter) self.assertIsInstance(find_mod_dotted, zipimport.zipimporter)
@ -524,17 +544,27 @@ class UncompressedZipImportTestCase(ImportHooksBaseTestCase):
self.assertEqual( self.assertEqual(
find_mod_dotted.get_filename(TESTMOD), load_mod.__file__) find_mod_dotted.get_filename(TESTMOD), load_mod.__file__)
# PEP 451
spec = zi2.find_spec(TESTMOD)
self.assertIsNotNone(spec)
self.assertIsInstance(spec.loader, zipimport.zipimporter)
self.assertFalse(spec.loader.is_package(TESTMOD))
load_mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(load_mod)
self.assertEqual(
spec.loader.get_filename(TESTMOD), load_mod.__file__)
mod_path = TESTPACK2 + os.sep + TESTMOD mod_path = TESTPACK2 + os.sep + TESTMOD
mod_name = module_path_to_dotted_name(mod_path) mod_name = module_path_to_dotted_name(mod_path)
mod = importlib.import_module(mod_name) mod = importlib.import_module(mod_name)
self.assertTrue(mod_name in sys.modules) self.assertTrue(mod_name in sys.modules)
self.assertEqual(zi.get_source(TESTPACK2), None) self.assertIsNone(zi.get_source(TESTPACK2))
self.assertEqual(zi.get_source(mod_path), None) self.assertIsNone(zi.get_source(mod_path))
self.assertEqual(zi.get_filename(mod_path), mod.__file__) self.assertEqual(zi.get_filename(mod_path), mod.__file__)
# To pass in the module name instead of the path, we must use the # To pass in the module name instead of the path, we must use the
# right importer. # right importer.
loader = mod.__loader__ loader = mod.__loader__
self.assertEqual(loader.get_source(mod_name), None) self.assertIsNone(loader.get_source(mod_name))
self.assertEqual(loader.get_filename(mod_name), mod.__file__) self.assertEqual(loader.get_filename(mod_name), mod.__file__)
def testGetData(self): def testGetData(self):
@ -655,7 +685,9 @@ class UncompressedZipImportTestCase(ImportHooksBaseTestCase):
zinfo = ZipInfo(TESTMOD + ".py", time.localtime(NOW)) zinfo = ZipInfo(TESTMOD + ".py", time.localtime(NOW))
zinfo.compress_type = self.compression zinfo.compress_type = self.compression
z.writestr(zinfo, test_src) z.writestr(zinfo, test_src)
zipimport.zipimporter(filename).load_module(TESTMOD) spec = zipimport.zipimporter(filename).find_spec(TESTMOD)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
def testBytesPath(self): def testBytesPath(self):
filename = os_helper.TESTFN + ".zip" filename = os_helper.TESTFN + ".zip"
@ -747,6 +779,8 @@ class BadFileZipImportTestCase(unittest.TestCase):
try: try:
self.assertRaises(TypeError, z.find_module, None) self.assertRaises(TypeError, z.find_module, None)
self.assertRaises(TypeError, z.find_spec, None)
self.assertRaises(TypeError, z.exec_module, None)
self.assertRaises(TypeError, z.load_module, None) self.assertRaises(TypeError, z.load_module, None)
self.assertRaises(TypeError, z.is_package, None) self.assertRaises(TypeError, z.is_package, None)
self.assertRaises(TypeError, z.get_code, None) self.assertRaises(TypeError, z.get_code, None)
@ -754,7 +788,8 @@ class BadFileZipImportTestCase(unittest.TestCase):
self.assertRaises(TypeError, z.get_source, None) self.assertRaises(TypeError, z.get_source, None)
error = zipimport.ZipImportError error = zipimport.ZipImportError
self.assertEqual(z.find_module('abc'), None) self.assertIsNone(z.find_module('abc'))
self.assertIsNone(z.find_spec('abc'))
self.assertRaises(error, z.load_module, 'abc') self.assertRaises(error, z.load_module, 'abc')
self.assertRaises(error, z.get_code, 'abc') self.assertRaises(error, z.get_code, 'abc')

View File

@ -42,7 +42,7 @@ END_CENTRAL_DIR_SIZE = 22
STRING_END_ARCHIVE = b'PK\x05\x06' STRING_END_ARCHIVE = b'PK\x05\x06'
MAX_COMMENT_LEN = (1 << 16) - 1 MAX_COMMENT_LEN = (1 << 16) - 1
class zipimporter: class zipimporter(_bootstrap_external._LoaderBasics):
"""zipimporter(archivepath) -> zipimporter object """zipimporter(archivepath) -> zipimporter object
Create a new zipimporter instance. 'archivepath' must be a path to Create a new zipimporter instance. 'archivepath' must be a path to
@ -115,6 +115,8 @@ class zipimporter:
full path name if it's possibly a portion of a namespace package, full path name if it's possibly a portion of a namespace package,
or None otherwise. The optional 'path' argument is ignored -- it's or None otherwise. The optional 'path' argument is ignored -- it's
there for compatibility with the importer protocol. there for compatibility with the importer protocol.
Deprecated since Python 3.10. Use find_spec() instead.
""" """
mi = _get_module_info(self, fullname) mi = _get_module_info(self, fullname)
if mi is not None: if mi is not None:
@ -146,9 +148,37 @@ class zipimporter:
instance itself if the module was found, or None if it wasn't. instance itself if the module was found, or None if it wasn't.
The optional 'path' argument is ignored -- it's there for compatibility The optional 'path' argument is ignored -- it's there for compatibility
with the importer protocol. with the importer protocol.
Deprecated since Python 3.10. Use find_spec() instead.
""" """
return self.find_loader(fullname, path)[0] return self.find_loader(fullname, path)[0]
def find_spec(self, fullname, target=None):
"""Create a ModuleSpec for the specified module.
Returns None if the module cannot be found.
"""
module_info = _get_module_info(self, fullname)
if module_info is not None:
return _bootstrap.spec_from_loader(fullname, self, is_package=module_info)
else:
# Not a module or regular package. See if this is a directory, and
# therefore possibly a portion of a namespace package.
# We're only interested in the last path component of fullname
# earlier components are recorded in self.prefix.
modpath = _get_module_path(self, fullname)
if _is_dir(self, modpath):
# This is possibly a portion of a namespace
# package. Return the string representing its path,
# without a trailing separator.
path = f'{self.archive}{path_sep}{modpath}'
spec = _bootstrap.ModuleSpec(name=fullname, loader=None,
is_package=True)
spec.submodule_search_locations.append(path)
return spec
else:
return None
def get_code(self, fullname): def get_code(self, fullname):
"""get_code(fullname) -> code object. """get_code(fullname) -> code object.
@ -237,6 +267,8 @@ class zipimporter:
Load the module specified by 'fullname'. 'fullname' must be the Load the module specified by 'fullname'. 'fullname' must be the
fully qualified (dotted) module name. It returns the imported fully qualified (dotted) module name. It returns the imported
module, or raises ZipImportError if it wasn't found. module, or raises ZipImportError if it wasn't found.
Deprecated since Python 3.10. use exec_module() instead.
""" """
code, ispackage, modpath = _get_module_code(self, fullname) code, ispackage, modpath = _get_module_code(self, fullname)
mod = sys.modules.get(fullname) mod = sys.modules.get(fullname)

View File

@ -0,0 +1,5 @@
Implement PEP 451/spec methods on zipimport.zipimporter: find_spec(),
create_module(), and exec_module().
This also allows for the documented deprecation of find_loader(),
find_module(), and load_module().

File diff suppressed because it is too large Load Diff