10000 bpo-42131: Add PEP 451-related methods to zipimport (GH-23187) · python/cpython@d2e94bb · GitHub
[go: up one dir, main page]

Skip to content

Commit d2e94bb

Browse files
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>
1 parent 9b69342 commit d2e94bb

File tree

6 files changed

+1131
-965
lines changed

6 files changed

+1131
-965
lines changed

Doc/library/zipimport.rst

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,9 @@ doesn't contain :file:`.pyc` files, importing may be rather slow.
4444
follows the specification in :pep:`273`, but uses an implementation written by Just
4545
van Rossum that uses the import hooks described in :pep:`302`.
4646

47-
:pep:`302` - New Import Hooks
48-
The PEP to add the import hooks that help this module work.
47+
:mod:`importlib` - The implementation of the import machinery
48+
Package providing the relevant protocols for all importers to
49+
implement.
4950

5051

5152
This module defines an exception:
@@ -73,14 +74,49 @@ zipimporter Objects
7374
:exc:`ZipImportError` is raised if *archivepath* doesn't point to a valid ZIP
7475
archive.
7576

76-
.. method:: find_module(fullname[, path])
77+
.. method:: create_module(spec)
78+
79+
Implementation of :meth:`importlib.abc.Loader.create_module` that returns
80+
:const:`None` to explicitly request the default semantics.
81+
82+
.. versionadded:: 3.10
83+
84+
85+
.. method:: exec_module(module)
86+
87+
Implementation of :meth:`importlib.abc.Loader.exec_module`.
88+
89+
.. versionadded:: 3.10
90+
91+
92+
.. method:: find_loader(fullname, path=None)
93+
94+
An implementation of :meth:`importlib.abc.PathEntryFinder.find_loader`.
95+
96+
.. deprecated:: 3.10
97+
98+
Use :meth:`find_spec` instead.
99+
100+
101+
.. method:: find_module(fullname, path=None)
77102

78103
Search for a module specified by *fullname*. *fullname* must be the fully
79104
qualified (dotted) module name. It returns the zipimporter instance itself
80105
if the module was found, or :const:`None` if it wasn't. The optional
81106
*path* argument is ignored---it's there for compatibility with the
82107
importer protocol.
83108

109+
.. deprecated:: 3.10
110+
111+
Use :meth:`find_spec` instead.
112+
113+
114+
.. method:: find_spec(fullname, target=None)
115+
116+
An implementation of :meth:`importlib.abc.PathEntryFinder.find_spec`.
117+
118+
.. versionadded:: 3.10
119+
84120

85121
.. method:: get_code(fullname)
86122

@@ -126,6 +162,10 @@ zipimporter Objects
126162
qualified (dotted) module name. It returns the imported module, or raises
127163
:exc:`ZipImportError` if it wasn't found.
128164

165+
.. deprecated:: 3.10
166+
167+
Use :meth:`exec_module` instead.
168+
129169

130170
.. attribute:: archive
131171

Doc/whatsnew/3.10.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,13 @@ Add a :class:`~xml.sax.handler.LexicalHandler` class to the
303303
:mod:`xml.sax.handler` module.
304304
(Contributed by Jonathan Gossage and Zackery Spytz in :issue:`35018`.)
305305

306+
zipimport
307+
---------
308+
Add methods related to :pep:`451`: :meth:`~zipimport.zipimporter.find_spec`,
309+
:meth:`zipimport.zipimporter.create_module`, and
310+
:meth:`zipimport.zipimporter.exec_module`.
311+
(Contributed by Brett Cannon in :issue:`42131`.
312+
306313

307314
Optimizations
308315
=============

Lib/test/test_zipimport.py

Lines changed: 53 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -450,8 +450,9 @@ def testZipImporterMethods(self):
450450

451451
zi = zipimport.zipimporter(TEMP_ZIP)
452452
self.assertEqual(zi.archive, TEMP_ZIP)
453-
self.assertEqual(zi.is_package(TESTPACK), True)
453+
self.assertTrue(zi.is_package(TESTPACK))
454454

455+
# PEP 302
455456
find_mod = zi.find_module('spam')
456457
self.assertIsNotNone(find_mod)
457458
self.assertIsInstance(find_mod, zipimport.zipimporter)
@@ -462,25 +463,39 @@ def testZipImporterMethods(self):
462463
mod = zi.load_module(TESTPACK)
463464
self.assertEqual(zi.get_filename(TESTPACK), mod.__file__)
464465

466+
# PEP 451
467+
spec = zi.find_spec('spam')
468+
self.assertIsNotNone(spec)
469+
self.assertIsInstance(spec.loader, zipimport.zipimporter)
470+
self.assertFalse(spec.loader.is_package('spam'))
471+
exec_mod = importlib.util.module_from_spec(spec)
472+
spec.loader.exec_module(exec_mod)
473+
self.assertEqual(spec.loader.get_filename('spam'), exec_mod.__file__)
474+
475+
spec = zi.find_spec(TESTPACK)
476+
mod = importlib.util.module_from_spec(spec)
477+
spec.loader.exec_module(mod)
478+
self.assertEqual(zi.get_filename(TESTPACK), mod.__file__)
479+
465480
existing_pack_path = importlib.import_module(TESTPACK).__path__[0]
466481
expected_path_path = os.path.join(TEMP_ZIP, TESTPACK)
467482
self.assertEqual(existing_pack_path, expected_path_path)
468483

469-
self.assertEqual(zi.is_package(packdir + '__init__'), False)
470-
self.assertEqual(zi.is_package(packdir + TESTPACK2), True)
471-
self.assertEqual(zi.is_package(packdir2 + TESTMOD), False)
484+
self.assertFalse(zi.is_package(packdir + '__init__'))
485+
self.assertTrue(zi.is_package(packdir + TESTPACK2))
486+
self.assertFalse(zi.is_package(packdir2 + TESTMOD))
472487

473488
mod_path = packdir2 + TESTMOD
474489
mod_name = module_path_to_dotted_name(mod_path)
475490
mod = importlib.import_module(mod_name)
476491
self.assertTrue(mod_name in sys.modules)
477-
self.assertEqual(zi.get_source(TESTPACK), None)
478-
self.assertEqual(zi.get_source(mod_path), None)
492+
self.assertIsNone(zi.get_source(TESTPACK))
493+
self.assertIsNone(zi.get_source(mod_path))
479494
self.assertEqual(zi.get_filename(mod_path), mod.__file__)
480495
# To pass in the module name instead of the path, we must use the
481496
# right importer
482-
loader = mod.__loader__
483-
self.assertEqual(loader.get_source(mod_name), None)
497+
loader = mod.__spec__.loader
498+
self.assertIsNone(loader.get_source(mod_name))
484499
self.assertEqual(loader.get_filename(mod_name), mod.__file__)
485500

486501
# test prefix and archivepath members
@@ -505,17 +520,22 @@ def testZipImporterMethodsInSubDirectory(self):
505520
zi = zipimport.zipimporter(TEMP_ZIP + os.sep + packdir)
506521
self.assertEqual(zi.archive, TEMP_ZIP)
507522
self.assertEqual(zi.prefix, packdir)
508-
self.assertEqual(zi.is_package(TESTPACK2), True)
523+
self.assertTrue(zi.is_package(TESTPACK2))
524+
# PEP 302
509525
mod = zi.load_module(TESTPACK2)
510526
self.assertEqual(zi.get_filename(TESTPACK2), mod.__file__)
527+
# PEP 451
528+
spec = zi.find_spec(TESTPACK2)
529+
mod = importlib.util.module_from_spec(spec)
530+
spec.loader.exec_module(mod)
531+
self.assertEqual(spec.loader.get_filename(TESTPACK2), mod.__file__)
511532

512-
self.assertEqual(
513-
zi.is_package(TESTPACK2 + os.sep + '__init__'), False)
514-
self.assertEqual(
515-
zi.is_package(TESTPACK2 + os.sep + TESTMOD), False)
533+
self.assertFalse(zi.is_package(TESTPACK2 + os.sep + '__init__'))
534+
self.assertFalse(zi.is_package(TESTPACK2 + os.sep + TESTMOD))
516535

517536
pkg_path = TEMP_ZIP + os.sep + packdir + TESTPACK2
518537
zi2 = zipimport.zipimporter(pkg_path)
538+
# PEP 302
519539
find_mod_dotted = zi2.find_module(TESTMOD)
520540
self.assertIsNotNone(find_mod_dotted)
521541
self.assertIsInstance(find_mod_dotted, zipimport.zipimporter)
@@ -524,17 +544,27 @@ def testZipImporterMethodsInSubDirectory(self):
524544
self.assertEqual(
525545
find_mod_dotted.get_filename(TESTMOD), load_mod.__file__)
526546

547+
# PEP 451
548+
spec = zi2.find_spec(TESTMOD)
549+
self.assertIsNotNone(spec)
550+
self.assertIsInstance(spec.loader, zipimport.zipimporter)
551+
self.assertFalse(spec.loader.is_package(TESTMOD))
552+
load_mod = importlib.util.module_from_spec(spec)
553+
spec.loader.exec_module(load_mod)
554+
self.assertEqual(
555+
spec.loader.get_filename(TESTMOD), load_mod.__file__)
556+
527557
mod_path = TESTPACK2 + os.sep + TESTMOD
528558
mod_name = module_path_to_dotted_name(mod_path)
529559
mod = importlib.import_module(mod_name)
530560
self.assertTrue(mod_name in sys.modules)
531-
self.assertEqual(zi.get_source(TESTPACK2), None)
532-
self.assertEqual(zi.get_source(mod_path), None)
561+
self.assertIsNone(zi.get_source(TESTPACK2))
562+
self.assertIsNone(zi.get_source(mod_path))
533563
self.assertEqual(zi.get_filename(mod_path), mod.__file__)
534564
# To pass in the module name instead of the path, we must use the
535565
# right importer.
536566
loader = mod.__loader__
537-
self.assertEqual(loader.get_source(mod_name), None)
567+
self.assertIsNone(loader.get_source(mod_name))
538568
self.assertEqual(loader.get_filename(mod_name), mod.__file__)
539569

540570
def testGetData(self):
@@ -655,7 +685,9 @@ def testUnencodable(self):
655685
zinfo = ZipInfo(TESTMOD + ".py", time.localtime(NOW))
656686
zinfo.compress_type = self.compression
657687
z.writestr(zinfo, test_src)
658-
zipimport.zipimporter(filename).load_module(TESTMOD)
688+
spec = zipimport.zipimporter(filename).find_spec(TESTMOD)
689+
mod = importlib.util.module_from_spec(spec)
690+
spec.loader.exec_module(mod)
659691

660692
def testBytesPath(self):
661693
filename = os_helper.TESTFN + ".zip"
@@ -747,14 +779,17 @@ def _testBogusZipFile(self):
747779

748780
try:
749781
self.assertRaises(TypeError, z.find_module, None)
782+
self.assertRaises(TypeError, z.find_spec, None)
783+
self.assertRaises(TypeError, z.exec_module, None)
750784
self.assertRaises(TypeError, z.load_module, None)
751785
self.assertRaises(TypeError, z.is_package, None)
752786
self.assertRaises(TypeError, z.get_code, None)
753787
self.assertRaises(TypeError, z.get_data, None)
754788
self.assertRaises(TypeError, z.get_source, None)
755789

756790
error = zipimport.ZipImportError
757-
self.assertEqual(z.find_module('abc'), None)
791+
self.assertIsNone(z.find_module('abc'))
792+
self.assertIsNone(z.find_spec('abc'))
758793

759794
self.assertRaises(error, z.load_module, 'abc')
760795
self.assertRaises(error, z.get_code, 'abc')

Lib/zipimport.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ class ZipImportError(ImportError):
4242
STRING_END_ARCHIVE = b'PK\x05\x06'
4343
MAX_COMMENT_LEN = (1 << 16) - 1
4444

45-
class zipimporter:
45+
class zipimporter(_bootstrap_external._LoaderBasics):
4646
"""zipimporter(archivepath) -> zipimporter object
4747
4848
Create a new zipimporter instance. 'archivepath' must be a path to
@@ -115,6 +115,8 @@ def find_loader(self, fullname, path=None):
115115
full path name if it's possibly a portion of a namespace package,
116116
or None otherwise. The optional 'path' argument is ignored -- it's
117117
there for compatibility with the importer protocol.
118+
119+
Deprecated since Python 3.10. Use find_spec() instead.
118120
"""
119121
mi = _get_module_info(self, fullname)
120122
if mi is not None:
@@ -146,9 +148,37 @@ def find_module(self, fullname, path=None):
146148
instance itself if the module was found, or None if it wasn't.
147149
The optional 'path' argument is ignored -- it's there for compatibility
148150
with the importer protocol.
151+
152+
Deprecated since Python 3.10. Use find_spec() instead.
149153
"""
150154
return self.find_loader(fullname, path)[0]
151155

156+
def find_spec(self, fullname, target=None):
157+
"""Create a ModuleSpec for the specified module.
158+
159+
Returns None if the module cannot be found.
160+
"""
161+
module_info = _get_module_info(self, fullname)
162+
if module_info is not None:
163+
return _bootstrap.spec_from_loader(fullname, self, is_package=module_info)
164+
else:
165+
# Not a module or regular package. See if this is a directory, and
166+
# therefore possibly a portion of a namespace package.
167+
168+
# We're only interested in the last path component of fullname
169+
# earlier components are recorded in self.prefix.
170+
modpath = _get_module_path(self, fullname)
171+
if _is_dir(self, modpath):
172+
# This is possibly a portion of a namespace
173+
# package. Return the string representing its path,
174+
# without a trailing separator.
175+
path = f'{self.archive}{path_sep}{modpath}'
176+
spec = _bootstrap.ModuleSpec(name=fullname, loader=None,
177+
is_package=True)
178+
spec.submodule_search_locations.append(path)
179+
return spec
180+
else:
181+
return None
152182

153183
def get_code(self, fullname):
154184
"""get_code(fullname) -> code object.
@@ -237,6 +267,8 @@ def load_module(self, fullname):
237267
Load the module specified by 'fullname'. 'fullname' must be the
238268
fully qualified (dotted) module name. It returns the imported
239269
module, or raises ZipImportError if it wasn't found.
270+
271+
Deprecated since Python 3.10. use exec_module() instead.
240272
"""
241273
code, ispackage, modpath = _get_module_code(self, fullname)
242274
mod = sys.modules.get(fullname)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Implement PEP 451/spec methods on zipimport.zipimporter: find_spec(),
2+
create_module(), and exec_module().
3+
4+
This also allows for the documented deprecation of find_loader(),
5+
find_module(), and load_module().

0 commit comments

Comments
 (0)
0