blob: 3344d9dac98a83116bb5812d24bed330a9cfd0e0 [file] [log] [blame]
xf.li86118912025-03-19 20:07:27 -07001"""Utilities to support packages."""
2
3from collections import namedtuple
4from functools import singledispatch as simplegeneric
5import importlib
6import importlib.util
7import importlib.machinery
8import os
9import os.path
10import sys
11from types import ModuleType
12import warnings
13
14__all__ = [
15 'get_importer', 'iter_importers', 'get_loader', 'find_loader',
16 'walk_packages', 'iter_modules', 'get_data',
17 'ImpImporter', 'ImpLoader', 'read_code', 'extend_path',
18 'ModuleInfo',
19]
20
21
22ModuleInfo = namedtuple('ModuleInfo', 'module_finder name ispkg')
23ModuleInfo.__doc__ = 'A namedtuple with minimal info about a module.'
24
25
26def _get_spec(finder, name):
27 """Return the finder-specific module spec."""
28 # Works with legacy finders.
29 try:
30 find_spec = finder.find_spec
31 except AttributeError:
32 loader = finder.find_module(name)
33 if loader is None:
34 return None
35 return importlib.util.spec_from_loader(name, loader)
36 else:
37 return find_spec(name)
38
39
40def read_code(stream):
41 # This helper is needed in order for the PEP 302 emulation to
42 # correctly handle compiled files
43 import marshal
44
45 magic = stream.read(4)
46 if magic != importlib.util.MAGIC_NUMBER:
47 return None
48
49 stream.read(12) # Skip rest of the header
50 return marshal.load(stream)
51
52
53def walk_packages(path=None, prefix='', onerror=None):
54 """Yields ModuleInfo for all modules recursively
55 on path, or, if path is None, all accessible modules.
56
57 'path' should be either None or a list of paths to look for
58 modules in.
59
60 'prefix' is a string to output on the front of every module name
61 on output.
62
63 Note that this function must import all *packages* (NOT all
64 modules!) on the given path, in order to access the __path__
65 attribute to find submodules.
66
67 'onerror' is a function which gets called with one argument (the
68 name of the package which was being imported) if any exception
69 occurs while trying to import a package. If no onerror function is
70 supplied, ImportErrors are caught and ignored, while all other
71 exceptions are propagated, terminating the search.
72
73 Examples:
74
75 # list all modules python can access
76 walk_packages()
77
78 # list all submodules of ctypes
79 walk_packages(ctypes.__path__, ctypes.__name__+'.')
80 """
81
82 def seen(p, m={}):
83 if p in m:
84 return True
85 m[p] = True
86
87 for info in iter_modules(path, prefix):
88 yield info
89
90 if info.ispkg:
91 try:
92 __import__(info.name)
93 except ImportError:
94 if onerror is not None:
95 onerror(info.name)
96 except Exception:
97 if onerror is not None:
98 onerror(info.name)
99 else:
100 raise
101 else:
102 path = getattr(sys.modules[info.name], '__path__', None) or []
103
104 # don't traverse path items we've seen before
105 path = [p for p in path if not seen(p)]
106
107 yield from walk_packages(path, info.name+'.', onerror)
108
109
110def iter_modules(path=None, prefix=''):
111 """Yields ModuleInfo for all submodules on path,
112 or, if path is None, all top-level modules on sys.path.
113
114 'path' should be either None or a list of paths to look for
115 modules in.
116
117 'prefix' is a string to output on the front of every module name
118 on output.
119 """
120 if path is None:
121 importers = iter_importers()
122 elif isinstance(path, str):
123 raise ValueError("path must be None or list of paths to look for "
124 "modules in")
125 else:
126 importers = map(get_importer, path)
127
128 yielded = {}
129 for i in importers:
130 for name, ispkg in iter_importer_modules(i, prefix):
131 if name not in yielded:
132 yielded[name] = 1
133 yield ModuleInfo(i, name, ispkg)
134
135
136@simplegeneric
137def iter_importer_modules(importer, prefix=''):
138 if not hasattr(importer, 'iter_modules'):
139 return []
140 return importer.iter_modules(prefix)
141
142
143# Implement a file walker for the normal importlib path hook
144def _iter_file_finder_modules(importer, prefix=''):
145 if importer.path is None or not os.path.isdir(importer.path):
146 return
147
148 yielded = {}
149 import inspect
150 try:
151 filenames = os.listdir(importer.path)
152 except OSError:
153 # ignore unreadable directories like import does
154 filenames = []
155 filenames.sort() # handle packages before same-named modules
156
157 for fn in filenames:
158 modname = inspect.getmodulename(fn)
159 if modname=='__init__' or modname in yielded:
160 continue
161
162 path = os.path.join(importer.path, fn)
163 ispkg = False
164
165 if not modname and os.path.isdir(path) and '.' not in fn:
166 modname = fn
167 try:
168 dircontents = os.listdir(path)
169 except OSError:
170 # ignore unreadable directories like import does
171 dircontents = []
172 for fn in dircontents:
173 subname = inspect.getmodulename(fn)
174 if subname=='__init__':
175 ispkg = True
176 break
177 else:
178 continue # not a package
179
180 if modname and '.' not in modname:
181 yielded[modname] = 1
182 yield prefix + modname, ispkg
183
184iter_importer_modules.register(
185 importlib.machinery.FileFinder, _iter_file_finder_modules)
186
187
188def _import_imp():
189 global imp
190 with warnings.catch_warnings():
191 warnings.simplefilter('ignore', DeprecationWarning)
192 imp = importlib.import_module('imp')
193
194class ImpImporter:
195 """PEP 302 Finder that wraps Python's "classic" import algorithm
196
197 ImpImporter(dirname) produces a PEP 302 finder that searches that
198 directory. ImpImporter(None) produces a PEP 302 finder that searches
199 the current sys.path, plus any modules that are frozen or built-in.
200
201 Note that ImpImporter does not currently support being used by placement
202 on sys.meta_path.
203 """
204
205 def __init__(self, path=None):
206 global imp
207 warnings.warn("This emulation is deprecated, use 'importlib' instead",
208 DeprecationWarning)
209 _import_imp()
210 self.path = path
211
212 def find_module(self, fullname, path=None):
213 # Note: we ignore 'path' argument since it is only used via meta_path
214 subname = fullname.split(".")[-1]
215 if subname != fullname and self.path is None:
216 return None
217 if self.path is None:
218 path = None
219 else:
220 path = [os.path.realpath(self.path)]
221 try:
222 file, filename, etc = imp.find_module(subname, path)
223 except ImportError:
224 return None
225 return ImpLoader(fullname, file, filename, etc)
226
227 def iter_modules(self, prefix=''):
228 if self.path is None or not os.path.isdir(self.path):
229 return
230
231 yielded = {}
232 import inspect
233 try:
234 filenames = os.listdir(self.path)
235 except OSError:
236 # ignore unreadable directories like import does
237 filenames = []
238 filenames.sort() # handle packages before same-named modules
239
240 for fn in filenames:
241 modname = inspect.getmodulename(fn)
242 if modname=='__init__' or modname in yielded:
243 continue
244
245 path = os.path.join(self.path, fn)
246 ispkg = False
247
248 if not modname and os.path.isdir(path) and '.' not in fn:
249 modname = fn
250 try:
251 dircontents = os.listdir(path)
252 except OSError:
253 # ignore unreadable directories like import does
254 dircontents = []
255 for fn in dircontents:
256 subname = inspect.getmodulename(fn)
257 if subname=='__init__':
258 ispkg = True
259 break
260 else:
261 continue # not a package
262
263 if modname and '.' not in modname:
264 yielded[modname] = 1
265 yield prefix + modname, ispkg
266
267
268class ImpLoader:
269 """PEP 302 Loader that wraps Python's "classic" import algorithm
270 """
271 code = source = None
272
273 def __init__(self, fullname, file, filename, etc):
274 warnings.warn("This emulation is deprecated, use 'importlib' instead",
275 DeprecationWarning)
276 _import_imp()
277 self.file = file
278 self.filename = filename
279 self.fullname = fullname
280 self.etc = etc
281
282 def load_module(self, fullname):
283 self._reopen()
284 try:
285 mod = imp.load_module(fullname, self.file, self.filename, self.etc)
286 finally:
287 if self.file:
288 self.file.close()
289 # Note: we don't set __loader__ because we want the module to look
290 # normal; i.e. this is just a wrapper for standard import machinery
291 return mod
292
293 def get_data(self, pathname):
294 with open(pathname, "rb") as file:
295 return file.read()
296
297 def _reopen(self):
298 if self.file and self.file.closed:
299 mod_type = self.etc[2]
300 if mod_type==imp.PY_SOURCE:
301 self.file = open(self.filename, 'r')
302 elif mod_type in (imp.PY_COMPILED, imp.C_EXTENSION):
303 self.file = open(self.filename, 'rb')
304
305 def _fix_name(self, fullname):
306 if fullname is None:
307 fullname = self.fullname
308 elif fullname != self.fullname:
309 raise ImportError("Loader for module %s cannot handle "
310 "module %s" % (self.fullname, fullname))
311 return fullname
312
313 def is_package(self, fullname):
314 fullname = self._fix_name(fullname)
315 return self.etc[2]==imp.PKG_DIRECTORY
316
317 def get_code(self, fullname=None):
318 fullname = self._fix_name(fullname)
319 if self.code is None:
320 mod_type = self.etc[2]
321 if mod_type==imp.PY_SOURCE:
322 source = self.get_source(fullname)
323 self.code = compile(source, self.filename, 'exec')
324 elif mod_type==imp.PY_COMPILED:
325 self._reopen()
326 try:
327 self.code = read_code(self.file)
328 finally:
329 self.file.close()
330 elif mod_type==imp.PKG_DIRECTORY:
331 self.code = self._get_delegate().get_code()
332 return self.code
333
334 def get_source(self, fullname=None):
335 fullname = self._fix_name(fullname)
336 if self.source is None:
337 mod_type = self.etc[2]
338 if mod_type==imp.PY_SOURCE:
339 self._reopen()
340 try:
341 self.source = self.file.read()
342 finally:
343 self.file.close()
344 elif mod_type==imp.PY_COMPILED:
345 if os.path.exists(self.filename[:-1]):
346 with open(self.filename[:-1], 'r') as f:
347 self.source = f.read()
348 elif mod_type==imp.PKG_DIRECTORY:
349 self.source = self._get_delegate().get_source()
350 return self.source
351
352 def _get_delegate(self):
353 finder = ImpImporter(self.filename)
354 spec = _get_spec(finder, '__init__')
355 return spec.loader
356
357 def get_filename(self, fullname=None):
358 fullname = self._fix_name(fullname)
359 mod_type = self.etc[2]
360 if mod_type==imp.PKG_DIRECTORY:
361 return self._get_delegate().get_filename()
362 elif mod_type in (imp.PY_SOURCE, imp.PY_COMPILED, imp.C_EXTENSION):
363 return self.filename
364 return None
365
366
367try:
368 import zipimport
369 from zipimport import zipimporter
370
371 def iter_zipimport_modules(importer, prefix=''):
372 dirlist = sorted(zipimport._zip_directory_cache[importer.archive])
373 _prefix = importer.prefix
374 plen = len(_prefix)
375 yielded = {}
376 import inspect
377 for fn in dirlist:
378 if not fn.startswith(_prefix):
379 continue
380
381 fn = fn[plen:].split(os.sep)
382
383 if len(fn)==2 and fn[1].startswith('__init__.py'):
384 if fn[0] not in yielded:
385 yielded[fn[0]] = 1
386 yield prefix + fn[0], True
387
388 if len(fn)!=1:
389 continue
390
391 modname = inspect.getmodulename(fn[0])
392 if modname=='__init__':
393 continue
394
395 if modname and '.' not in modname and modname not in yielded:
396 yielded[modname] = 1
397 yield prefix + modname, False
398
399 iter_importer_modules.register(zipimporter, iter_zipimport_modules)
400
401except ImportError:
402 pass
403
404
405def get_importer(path_item):
406 """Retrieve a finder for the given path item
407
408 The returned finder is cached in sys.path_importer_cache
409 if it was newly created by a path hook.
410
411 The cache (or part of it) can be cleared manually if a
412 rescan of sys.path_hooks is necessary.
413 """
414 path_item = os.fsdecode(path_item)
415 try:
416 importer = sys.path_importer_cache[path_item]
417 except KeyError:
418 for path_hook in sys.path_hooks:
419 try:
420 importer = path_hook(path_item)
421 sys.path_importer_cache.setdefault(path_item, importer)
422 break
423 except ImportError:
424 pass
425 else:
426 importer = None
427 return importer
428
429
430def iter_importers(fullname=""):
431 """Yield finders for the given module name
432
433 If fullname contains a '.', the finders will be for the package
434 containing fullname, otherwise they will be all registered top level
435 finders (i.e. those on both sys.meta_path and sys.path_hooks).
436
437 If the named module is in a package, that package is imported as a side
438 effect of invoking this function.
439
440 If no module name is specified, all top level finders are produced.
441 """
442 if fullname.startswith('.'):
443 msg = "Relative module name {!r} not supported".format(fullname)
444 raise ImportError(msg)
445 if '.' in fullname:
446 # Get the containing package's __path__
447 pkg_name = fullname.rpartition(".")[0]
448 pkg = importlib.import_module(pkg_name)
449 path = getattr(pkg, '__path__', None)
450 if path is None:
451 return
452 else:
453 yield from sys.meta_path
454 path = sys.path
455 for item in path:
456 yield get_importer(item)
457
458
459def get_loader(module_or_name):
460 """Get a "loader" object for module_or_name
461
462 Returns None if the module cannot be found or imported.
463 If the named module is not already imported, its containing package
464 (if any) is imported, in order to establish the package __path__.
465 """
466 if module_or_name in sys.modules:
467 module_or_name = sys.modules[module_or_name]
468 if module_or_name is None:
469 return None
470 if isinstance(module_or_name, ModuleType):
471 module = module_or_name
472 loader = getattr(module, '__loader__', None)
473 if loader is not None:
474 return loader
475 if getattr(module, '__spec__', None) is None:
476 return None
477 fullname = module.__name__
478 else:
479 fullname = module_or_name
480 return find_loader(fullname)
481
482
483def find_loader(fullname):
484 """Find a "loader" object for fullname
485
486 This is a backwards compatibility wrapper around
487 importlib.util.find_spec that converts most failures to ImportError
488 and only returns the loader rather than the full spec
489 """
490 if fullname.startswith('.'):
491 msg = "Relative module name {!r} not supported".format(fullname)
492 raise ImportError(msg)
493 try:
494 spec = importlib.util.find_spec(fullname)
495 except (ImportError, AttributeError, TypeError, ValueError) as ex:
496 # This hack fixes an impedance mismatch between pkgutil and
497 # importlib, where the latter raises other errors for cases where
498 # pkgutil previously raised ImportError
499 msg = "Error while finding loader for {!r} ({}: {})"
500 raise ImportError(msg.format(fullname, type(ex), ex)) from ex
501 return spec.loader if spec is not None else None
502
503
504def extend_path(path, name):
505 """Extend a package's path.
506
507 Intended use is to place the following code in a package's __init__.py:
508
509 from pkgutil import extend_path
510 __path__ = extend_path(__path__, __name__)
511
512 This will add to the package's __path__ all subdirectories of
513 directories on sys.path named after the package. This is useful
514 if one wants to distribute different parts of a single logical
515 package as multiple directories.
516
517 It also looks for *.pkg files beginning where * matches the name
518 argument. This feature is similar to *.pth files (see site.py),
519 except that it doesn't special-case lines starting with 'import'.
520 A *.pkg file is trusted at face value: apart from checking for
521 duplicates, all entries found in a *.pkg file are added to the
522 path, regardless of whether they are exist the filesystem. (This
523 is a feature.)
524
525 If the input path is not a list (as is the case for frozen
526 packages) it is returned unchanged. The input path is not
527 modified; an extended copy is returned. Items are only appended
528 to the copy at the end.
529
530 It is assumed that sys.path is a sequence. Items of sys.path that
531 are not (unicode or 8-bit) strings referring to existing
532 directories are ignored. Unicode items of sys.path that cause
533 errors when used as filenames may cause this function to raise an
534 exception (in line with os.path.isdir() behavior).
535 """
536
537 if not isinstance(path, list):
538 # This could happen e.g. when this is called from inside a
539 # frozen package. Return the path unchanged in that case.
540 return path
541
542 sname_pkg = name + ".pkg"
543
544 path = path[:] # Start with a copy of the existing path
545
546 parent_package, _, final_name = name.rpartition('.')
547 if parent_package:
548 try:
549 search_path = sys.modules[parent_package].__path__
550 except (KeyError, AttributeError):
551 # We can't do anything: find_loader() returns None when
552 # passed a dotted name.
553 return path
554 else:
555 search_path = sys.path
556
557 for dir in search_path:
558 if not isinstance(dir, str):
559 continue
560
561 finder = get_importer(dir)
562 if finder is not None:
563 portions = []
564 if hasattr(finder, 'find_spec'):
565 spec = finder.find_spec(final_name)
566 if spec is not None:
567 portions = spec.submodule_search_locations or []
568 # Is this finder PEP 420 compliant?
569 elif hasattr(finder, 'find_loader'):
570 _, portions = finder.find_loader(final_name)
571
572 for portion in portions:
573 # XXX This may still add duplicate entries to path on
574 # case-insensitive filesystems
575 if portion not in path:
576 path.append(portion)
577
578 # XXX Is this the right thing for subpackages like zope.app?
579 # It looks for a file named "zope.app.pkg"
580 pkgfile = os.path.join(dir, sname_pkg)
581 if os.path.isfile(pkgfile):
582 try:
583 f = open(pkgfile)
584 except OSError as msg:
585 sys.stderr.write("Can't open %s: %s\n" %
586 (pkgfile, msg))
587 else:
588 with f:
589 for line in f:
590 line = line.rstrip('\n')
591 if not line or line.startswith('#'):
592 continue
593 path.append(line) # Don't check for existence!
594
595 return path
596
597
598def get_data(package, resource):
599 """Get a resource from a package.
600
601 This is a wrapper round the PEP 302 loader get_data API. The package
602 argument should be the name of a package, in standard module format
603 (foo.bar). The resource argument should be in the form of a relative
604 filename, using '/' as the path separator. The parent directory name '..'
605 is not allowed, and nor is a rooted name (starting with a '/').
606
607 The function returns a binary string, which is the contents of the
608 specified resource.
609
610 For packages located in the filesystem, which have already been imported,
611 this is the rough equivalent of
612
613 d = os.path.dirname(sys.modules[package].__file__)
614 data = open(os.path.join(d, resource), 'rb').read()
615
616 If the package cannot be located or loaded, or it uses a PEP 302 loader
617 which does not support get_data(), then None is returned.
618 """
619
620 spec = importlib.util.find_spec(package)
621 if spec is None:
622 return None
623 loader = spec.loader
624 if loader is None or not hasattr(loader, 'get_data'):
625 return None
626 # XXX needs test
627 mod = (sys.modules.get(package) or
628 importlib._bootstrap._load(spec))
629 if mod is None or not hasattr(mod, '__file__'):
630 return None
631
632 # Modify the resource name to be compatible with the loader.get_data
633 # signature - an os.path format "filename" starting with the dirname of
634 # the package's __file__
635 parts = resource.split('/')
636 parts.insert(0, os.path.dirname(mod.__file__))
637 resource_name = os.path.join(*parts)
638 return loader.get_data(resource_name)