pyimod02_importers.py 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781
  1. #-----------------------------------------------------------------------------
  2. # Copyright (c) 2005-2023, PyInstaller Development Team.
  3. #
  4. # Distributed under the terms of the GNU General Public License (version 2
  5. # or later) with exception for distributing the bootloader.
  6. #
  7. # The full license is in the file COPYING.txt, distributed with this software.
  8. #
  9. # SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
  10. #-----------------------------------------------------------------------------
  11. """
  12. PEP-302 and PEP-451 importers for frozen applications.
  13. """
  14. # **NOTE** This module is used during bootstrap.
  15. # Import *ONLY* builtin modules or modules that are collected into the base_library.zip archive.
  16. # List of built-in modules: sys.builtin_module_names
  17. # List of modules collected into base_library.zip: PyInstaller.compat.PY3_BASE_MODULES
  18. import sys
  19. import os
  20. import io
  21. import _frozen_importlib
  22. import _thread
  23. import pyimod01_archive
  24. if sys.flags.verbose and sys.stderr:
  25. def trace(msg, *a):
  26. sys.stderr.write(msg % a)
  27. sys.stderr.write("\n")
  28. else:
  29. def trace(msg, *a):
  30. pass
  31. def _decode_source(source_bytes):
  32. """
  33. Decode bytes representing source code and return the string. Universal newline support is used in the decoding.
  34. Based on CPython's implementation of the same functionality:
  35. https://github.com/python/cpython/blob/3.9/Lib/importlib/_bootstrap_external.py#L679-L688
  36. """
  37. # Local import to avoid including `tokenize` and its dependencies in `base_library.zip`
  38. from tokenize import detect_encoding
  39. source_bytes_readline = io.BytesIO(source_bytes).readline
  40. encoding = detect_encoding(source_bytes_readline)
  41. newline_decoder = io.IncrementalNewlineDecoder(decoder=None, translate=True)
  42. return newline_decoder.decode(source_bytes.decode(encoding[0]))
  43. # Global instance of PYZ archive reader. Initialized by install().
  44. pyz_archive = None
  45. # Some runtime hooks might need to traverse available frozen package/module hierarchy to simulate filesystem.
  46. # Such traversals can be efficiently implemented using a prefix tree (trie), whose computation we defer until first
  47. # access.
  48. _pyz_tree_lock = _thread.RLock()
  49. _pyz_tree = None
  50. def get_pyz_toc_tree():
  51. global _pyz_tree
  52. with _pyz_tree_lock:
  53. if _pyz_tree is None:
  54. _pyz_tree = _build_pyz_prefix_tree(pyz_archive)
  55. return _pyz_tree
  56. # Populate list of unresolved (original) and resolved paths to top-level directory, used when trying to determine
  57. # relative path.
  58. _TOP_LEVEL_DIRECTORY_PATHS = []
  59. # Original sys._MEIPASS value; ensure separators are normalized (e.g., when using msys2 python).
  60. _TOP_LEVEL_DIRECTORY = os.path.normpath(sys._MEIPASS)
  61. _TOP_LEVEL_DIRECTORY_PATHS.append(_TOP_LEVEL_DIRECTORY)
  62. # Fully resolve sys._MEIPASS in case its location is symlinked at some level; for example, system temporary directory
  63. # (used by onefile builds) is usually a symbolic link under macOS.
  64. _RESOLVED_TOP_LEVEL_DIRECTORY = os.path.realpath(_TOP_LEVEL_DIRECTORY)
  65. if os.path.normcase(_RESOLVED_TOP_LEVEL_DIRECTORY) != os.path.normcase(_TOP_LEVEL_DIRECTORY):
  66. _TOP_LEVEL_DIRECTORY_PATHS.append(_RESOLVED_TOP_LEVEL_DIRECTORY)
  67. # If we are running as macOS .app bundle, compute the alternative top-level directory path as well.
  68. _is_macos_app_bundle = False
  69. if sys.platform == 'darwin' and _TOP_LEVEL_DIRECTORY.endswith("Contents/Frameworks"):
  70. _is_macos_app_bundle = True
  71. _ALTERNATIVE_TOP_LEVEL_DIRECTORY = os.path.join(
  72. os.path.dirname(_TOP_LEVEL_DIRECTORY),
  73. 'Resources',
  74. )
  75. _TOP_LEVEL_DIRECTORY_PATHS.append(_ALTERNATIVE_TOP_LEVEL_DIRECTORY)
  76. _RESOLVED_ALTERNATIVE_TOP_LEVEL_DIRECTORY = os.path.join(
  77. os.path.dirname(_RESOLVED_TOP_LEVEL_DIRECTORY),
  78. 'Resources',
  79. )
  80. if _RESOLVED_ALTERNATIVE_TOP_LEVEL_DIRECTORY != _ALTERNATIVE_TOP_LEVEL_DIRECTORY:
  81. _TOP_LEVEL_DIRECTORY_PATHS.append(_RESOLVED_ALTERNATIVE_TOP_LEVEL_DIRECTORY)
  82. # Helper for computing PYZ prefix tree
  83. def _build_pyz_prefix_tree(pyz_archive):
  84. tree = dict()
  85. for entry_name, entry_data in pyz_archive.toc.items():
  86. name_components = entry_name.split('.')
  87. typecode = entry_data[0]
  88. current = tree
  89. if typecode in {pyimod01_archive.PYZ_ITEM_PKG, pyimod01_archive.PYZ_ITEM_NSPKG}:
  90. # Package; create new dictionary node for its modules
  91. for name_component in name_components:
  92. current = current.setdefault(name_component, {})
  93. else:
  94. # Module; create the leaf node (empty string)
  95. for name_component in name_components[:-1]:
  96. current = current.setdefault(name_component, {})
  97. current[name_components[-1]] = ''
  98. return tree
  99. class PyiFrozenFinder:
  100. """
  101. PyInstaller's frozen path entry finder for specific search path.
  102. Per-path instances allow us to properly translate the given module name ("fullname") into full PYZ entry name.
  103. For example, with search path being `sys._MEIPASS`, the module "mypackage.mod" would translate to "mypackage.mod"
  104. in the PYZ archive. However, if search path was `sys._MEIPASS/myotherpackage/_vendored` (for example, if
  105. `myotherpacakge` added this path to `sys.path`), then "mypackage.mod" would need to translate to
  106. "myotherpackage._vendored.mypackage.mod" in the PYZ archive.
  107. """
  108. def __repr__(self):
  109. return f"{self.__class__.__name__}({self._path})"
  110. @classmethod
  111. def path_hook(cls, path):
  112. trace(f"PyInstaller: running path finder hook for path: {path!r}")
  113. try:
  114. finder = cls(path)
  115. trace("PyInstaller: hook succeeded")
  116. return finder
  117. except Exception as e:
  118. trace(f"PyInstaller: hook failed: {e}")
  119. raise
  120. def __init__(self, path):
  121. self._path = path # Store original path, as given.
  122. self._pyz_archive = pyz_archive
  123. # Compute relative path to the top-level application directory. Do not try to resolve the path itself, because
  124. # it might contain symbolic links in parts other than the prefix that corresponds to the top-level application
  125. # directory. See #8994 for an example (files symlinked from a common directory outside of the top-level
  126. # application directory). Instead, try to compute relative path w.r.t. the original and the resolved top-level
  127. # application directory.
  128. for top_level_path in _TOP_LEVEL_DIRECTORY_PATHS:
  129. try:
  130. relative_path = os.path.relpath(path, top_level_path)
  131. except ValueError:
  132. continue # Failed to compute relative path w.r.t. the given top-level directory path.
  133. if relative_path.startswith('..'):
  134. continue # Relative path points outside of the given top-level directory.
  135. break # Successful match; stop here.
  136. else:
  137. raise ImportError("Failed to determine relative path w.r.t. top-level application directory.")
  138. # Ensure that path does not point to a file on filesystem. Strictly speaking, we should be checking that the
  139. # given path is a valid directory, but that would need to check both PYZ and filesystem. So for now, limit the
  140. # check to catch paths pointing to file, because that breaks `runpy.run_path()`, as per #8767.
  141. if os.path.isfile(path):
  142. raise ImportError("only directories are supported")
  143. if relative_path == '.':
  144. self._pyz_entry_prefix = ''
  145. else:
  146. self._pyz_entry_prefix = '.'.join(relative_path.split(os.path.sep))
  147. def _compute_pyz_entry_name(self, fullname):
  148. """
  149. Convert module fullname into PYZ entry name, subject to the prefix implied by this finder's search path.
  150. """
  151. tail_module = fullname.rpartition('.')[2]
  152. if self._pyz_entry_prefix:
  153. return self._pyz_entry_prefix + "." + tail_module
  154. else:
  155. return tail_module
  156. @property
  157. def fallback_finder(self):
  158. """
  159. Opportunistically create a *fallback finder* using `sys.path_hooks` entries that are located *after* our hook.
  160. The main goal of this exercise is to obtain an instance of python's FileFinder, but in theory any other hook
  161. that comes after ours is eligible to be a fallback.
  162. Having this fallback allows our finder to "cooperate" with python's FileFinder, as if the two were a single
  163. finder, which allows us to work around the python's PathFinder permitting only one finder instance per path
  164. without subclassing FileFinder.
  165. """
  166. if hasattr(self, '_fallback_finder'):
  167. return self._fallback_finder
  168. # Try to instantiate fallback finder
  169. our_hook_found = False
  170. self._fallback_finder = None
  171. for idx, hook in enumerate(sys.path_hooks):
  172. if hook == self.path_hook:
  173. our_hook_found = True
  174. continue # Our hook
  175. if not our_hook_found:
  176. continue # Skip hooks before our hook
  177. try:
  178. self._fallback_finder = hook(self._path)
  179. break
  180. except ImportError:
  181. pass
  182. return self._fallback_finder
  183. def _find_fallback_spec(self, fullname, target):
  184. """
  185. Attempt to find the spec using fallback finder, which is opportunistically created here. Typically, this would
  186. be python's FileFinder, which can discover specs for on-filesystem modules, such as extension modules and
  187. modules that are collected only as source .py files.
  188. Having this fallback allows our finder to "cooperate" with python's FileFinder, as if the two were a single
  189. finder, which allows us to work around the python's PathFinder permitting only one finder instance per path
  190. without subclassing FileFinder.
  191. """
  192. if not hasattr(self, '_fallback_finder'):
  193. self._fallback_finder = self._get_fallback_finder()
  194. if self._fallback_finder is None:
  195. return None
  196. return self._fallback_finder.find_spec(fullname, target)
  197. #-- Core PEP451 finder functionality, modeled after importlib.abc.PathEntryFinder
  198. # https://docs.python.org/3/library/importlib.html#importlib.abc.PathEntryFinder
  199. def invalidate_caches(self):
  200. """
  201. A method which, when called, should invalidate any internal cache used by the finder. Used by
  202. importlib.invalidate_caches() when invalidating the caches of all finders on sys.meta_path.
  203. https://docs.python.org/3/library/importlib.html#importlib.abc.MetaPathFinder.invalidate_caches
  204. """
  205. # We do not use any caches, but if we have created a fallback finder, propagate the function call.
  206. # NOTE: use getattr() with _fallback_finder attribute, in order to avoid unnecessary creation of the
  207. # fallback finder in case when it does not exist yet.
  208. fallback_finder = getattr(self, '_fallback_finder', None)
  209. if fallback_finder is not None:
  210. if hasattr(fallback_finder, 'invalidate_caches'):
  211. fallback_finder.invalidate_caches()
  212. def find_spec(self, fullname, target=None):
  213. """
  214. A method for finding a spec for the specified module. The finder will search for the module only within the
  215. path entry to which it is assigned. If a spec cannot be found, None is returned. When passed in, target is a
  216. module object that the finder may use to make a more educated guess about what spec to return.
  217. https://docs.python.org/3/library/importlib.html#importlib.abc.PathEntryFinder.find_spec
  218. """
  219. trace(f"{self}: find_spec: called with fullname={fullname!r}, target={fullname!r}")
  220. # Convert fullname to PYZ entry name.
  221. pyz_entry_name = self._compute_pyz_entry_name(fullname)
  222. # Try looking up the entry in the PYZ archive
  223. entry_data = self._pyz_archive.toc.get(pyz_entry_name)
  224. if entry_data is None:
  225. # Entry not found - try using fallback finder (for example, python's own FileFinder) to resolve on-disk
  226. # resources, such as extension modules and modules that are collected only as source .py files.
  227. trace(f"{self}: find_spec: {fullname!r} not found in PYZ...")
  228. if self.fallback_finder is not None:
  229. trace(f"{self}: find_spec: attempting resolve using fallback finder {self.fallback_finder!r}.")
  230. fallback_spec = self.fallback_finder.find_spec(fullname, target)
  231. trace(f"{self}: find_spec: fallback finder returned spec: {fallback_spec!r}.")
  232. return fallback_spec
  233. else:
  234. trace(f"{self}: find_spec: fallback finder is not available.")
  235. return None
  236. # Entry found
  237. typecode = entry_data[0]
  238. trace(f"{self}: find_spec: found {fullname!r} in PYZ as {pyz_entry_name!r}, typecode={typecode}")
  239. if typecode == pyimod01_archive.PYZ_ITEM_NSPKG:
  240. # PEP420 namespace package
  241. # We can use regular list for submodule_search_locations; the caller (i.e., python's PathFinder) takes care
  242. # of constructing _NamespacePath from it.
  243. spec = _frozen_importlib.ModuleSpec(fullname, None)
  244. spec.submodule_search_locations = [
  245. # NOTE: since we are using sys._MEIPASS as prefix, we need to construct path from resolved PYZ entry
  246. # name (equivalently, we could combine `self._path` and last part of `fullname`).
  247. os.path.join(sys._MEIPASS, pyz_entry_name.replace('.', os.path.sep)),
  248. ]
  249. return spec
  250. is_package = typecode == pyimod01_archive.PYZ_ITEM_PKG
  251. # Instantiate frozen loader for the module
  252. loader = PyiFrozenLoader(
  253. name=fullname,
  254. pyz_archive=self._pyz_archive,
  255. pyz_entry_name=pyz_entry_name,
  256. is_package=is_package,
  257. )
  258. # Resolve full filename, as if the module/package was located on filesystem. This is done by the loader.
  259. origin = loader.path
  260. # Construct spec for module, using all collected information.
  261. spec = _frozen_importlib.ModuleSpec(
  262. fullname,
  263. loader,
  264. is_package=is_package,
  265. origin=origin,
  266. )
  267. # Make the import machinery set __file__.
  268. # PEP 451 says: "has_location" is true if the module is locatable. In that case the spec's origin is used
  269. # as the location and __file__ is set to spec.origin. If additional location information is required
  270. # (e.g., zipimport), that information may be stored in spec.loader_state.
  271. spec.has_location = True
  272. # Set submodule_search_locations for packages. Seems to be required for importlib_resources from 3.2.0;
  273. # see issue #5395.
  274. if is_package:
  275. spec.submodule_search_locations = [os.path.dirname(origin)]
  276. return spec
  277. # The following methods are part of legacy PEP302 finder interface. They have been deprecated since python 3.4,
  278. # and removed in python 3.12. Provide compatibility shims to accommodate code that might still be using them.
  279. if sys.version_info[:2] < (3, 12):
  280. def find_loader(self, fullname):
  281. """
  282. A legacy method for finding a loader for the specified module. Returns a 2-tuple of (loader, portion) where
  283. portion is a sequence of file system locations contributing to part of a namespace package. The loader may
  284. be None while specifying portion to signify the contribution of the file system locations to a namespace
  285. package. An empty list can be used for portion to signify the loader is not part of a namespace package. If
  286. loader is None and portion is the empty list then no loader or location for a namespace package were found
  287. (i.e. failure to find anything for the module).
  288. Deprecated since python 3.4, removed in 3.12.
  289. """
  290. # Based on:
  291. # https://github.com/python/cpython/blob/v3.11.9/Lib/importlib/_bootstrap_external.py#L1587-L1600
  292. spec = self.find_spec(fullname)
  293. if spec is None:
  294. return None, []
  295. return spec.loader, spec.submodule_search_locations or []
  296. def find_module(self, fullname):
  297. """
  298. A concrete implementation of Finder.find_module() which is equivalent to self.find_loader(fullname)[0].
  299. Deprecated since python 3.4, removed in 3.12.
  300. """
  301. # Based on:
  302. # https://github.com/python/cpython/blob/v3.11.9/Lib/importlib/_bootstrap_external.py#L1585
  303. # https://github.com/python/cpython/blob/v3.11.9/Lib/importlib/_bootstrap_external.py#L622-L639
  304. #
  305. loader, portions = self.find_loader(fullname)
  306. return loader
  307. # Helper for enforcing module name in PyiFrozenLoader methods.
  308. def _check_name(method):
  309. def _check_name_wrapper(self, name, *args, **kwargs):
  310. if self.name != name:
  311. raise ImportError(f'loader for {self.name} cannot handle {name}', name=name)
  312. return method(self, name, *args, **kwargs)
  313. return _check_name_wrapper
  314. class PyiFrozenLoader:
  315. """
  316. PyInstaller's frozen loader for modules in the PYZ archive, which are discovered by PyiFrozenFinder.
  317. Since this loader is instantiated only from PyiFrozenFinder and since each loader instance is tied to a specific
  318. module, the fact that the loader was instantiated serves as the proof that the module exists in the PYZ archive.
  319. Hence, we can avoid any additional validation in the implementation of the loader's methods.
  320. """
  321. def __init__(self, name, pyz_archive, pyz_entry_name, is_package):
  322. # Store the reference to PYZ archive (for code object retrieval), as well as full PYZ entry name
  323. # and typecode, all of which are passed from the PyiFrozenFinder.
  324. self._pyz_archive = pyz_archive
  325. self._pyz_entry_name = pyz_entry_name
  326. self._is_package = is_package
  327. # Compute the module file path, as if module was located on filesystem.
  328. #
  329. # Rather than returning path to the .pyc file, return the path to .py file - which might actually exist, if it
  330. # was explicitly collected into the frozen application). This improves compliance with
  331. # https://docs.python.org/3/library/importlib.html#importlib.abc.ExecutionLoader.get_filename
  332. # as well as general compatibility with 3rd party code that blindly assumes that module's file path points to
  333. # the source .py file.
  334. #
  335. # NOTE: since we are using sys._MEIPASS as prefix, we need to construct path from full PYZ entry name
  336. # (so that a module with `name`=`jaraco.text` and `pyz_entry_name`=`setuptools._vendor.jaraco.text`
  337. # ends up with path set to `sys._MEIPASS/setuptools/_vendor/jaraco/text/__init__.pyc` instead of
  338. # `sys._MEIPASS/jaraco/text/__init__.pyc`).
  339. if is_package:
  340. module_file = os.path.join(sys._MEIPASS, pyz_entry_name.replace('.', os.path.sep), '__init__.py')
  341. else:
  342. module_file = os.path.join(sys._MEIPASS, pyz_entry_name.replace('.', os.path.sep) + '.py')
  343. # These properties are defined as part of importlib.abc.FileLoader. They are used by our implementation
  344. # (e.g., module name validation, get_filename(), get_source(), get_resource_reader()), and might also be used
  345. # by 3rd party code that naively expects to be dealing with a FileLoader instance.
  346. self.name = name # The name of the module the loader can handle.
  347. self.path = module_file # Path to the file of the module
  348. #-- Core PEP451 loader functionality as defined by importlib.abc.Loader
  349. # https://docs.python.org/3/library/importlib.html#importlib.abc.Loader
  350. def create_module(self, spec):
  351. """
  352. A method that returns the module object to use when importing a module. This method may return None, indicating
  353. that default module creation semantics should take place.
  354. https://docs.python.org/3/library/importlib.html#importlib.abc.Loader.create_module
  355. """
  356. return None
  357. def exec_module(self, module):
  358. """
  359. A method that executes the module in its own namespace when a module is imported or reloaded. The module
  360. should already be initialized when exec_module() is called. When this method exists, create_module()
  361. must be defined.
  362. https://docs.python.org/3/library/importlib.html#importlib.abc.Loader.exec_module
  363. """
  364. spec = module.__spec__
  365. bytecode = self.get_code(spec.name) # NOTE: get_code verifies that `spec.name` matches `self.name`!
  366. if bytecode is None:
  367. raise RuntimeError(f"Failed to retrieve bytecode for {spec.name!r}!")
  368. # Set by the import machinery
  369. assert hasattr(module, '__file__')
  370. # If `submodule_search_locations` is not None, this is a package; set __path__.
  371. if spec.submodule_search_locations is not None:
  372. module.__path__ = spec.submodule_search_locations
  373. exec(bytecode, module.__dict__)
  374. # The following method is part of legacy PEP302 loader interface. It has been deprecated since python 3.4, and
  375. # slated for removal in python 3.12, although that has not happened yet. Provide compatibility shim to accommodate
  376. # code that might still be using it.
  377. if True:
  378. @_check_name
  379. def load_module(self, fullname):
  380. """
  381. A legacy method for loading a module. If the module cannot be loaded, ImportError is raised, otherwise the
  382. loaded module is returned.
  383. Deprecated since python 3.4, slated for removal in 3.12 (but still present in python's own FileLoader in
  384. both v3.12.4 and v3.13.0rc1).
  385. """
  386. # Based on:
  387. # https://github.com/python/cpython/blob/v3.11.9/Lib/importlib/_bootstrap_external.py#L942-L945
  388. import importlib._bootstrap as _bootstrap
  389. return _bootstrap._load_module_shim(self, fullname)
  390. #-- PEP302 protocol extensions as defined by importlib.abc.ExecutionLoader
  391. # https://docs.python.org/3/library/importlib.html#importlib.abc.ExecutionLoader
  392. @_check_name
  393. def get_filename(self, fullname):
  394. """
  395. A method that is to return the value of __file__ for the specified module. If no path is available, ImportError
  396. is raised.
  397. If source code is available, then the method should return the path to the source file, regardless of whether a
  398. bytecode was used to load the module.
  399. https://docs.python.org/3/library/importlib.html#importlib.abc.ExecutionLoader.get_filename
  400. """
  401. return self.path
  402. #-- PEP302 protocol extensions as defined by importlib.abc.InspectLoader
  403. # https://docs.python.org/3/library/importlib.html#importlib.abc.InspectLoader
  404. @_check_name
  405. def get_code(self, fullname):
  406. """
  407. Return the code object for a module, or None if the module does not have a code object (as would be the case,
  408. for example, for a built-in module). Raise an ImportError if loader cannot find the requested module.
  409. https://docs.python.org/3/library/importlib.html#importlib.abc.InspectLoader.get_code
  410. """
  411. return self._pyz_archive.extract(self._pyz_entry_name)
  412. @_check_name
  413. def get_source(self, fullname):
  414. """
  415. A method to return the source of a module. It is returned as a text string using universal newlines, translating
  416. all recognized line separators into '\n' characters. Returns None if no source is available (e.g. a built-in
  417. module). Raises ImportError if the loader cannot find the module specified.
  418. https://docs.python.org/3/library/importlib.html#importlib.abc.InspectLoader.get_source
  419. """
  420. # The `path` attribute (which is also returned from `get_filename()`) already points to where the source .py
  421. # file should exist, if it is available.
  422. filename = self.path
  423. try:
  424. # Read in binary mode, then decode
  425. with open(filename, 'rb') as fp:
  426. source_bytes = fp.read()
  427. return _decode_source(source_bytes)
  428. except FileNotFoundError:
  429. pass
  430. # Source code is unavailable.
  431. return None
  432. @_check_name
  433. def is_package(self, fullname):
  434. """
  435. A method to return a true value if the module is a package, a false value otherwise. ImportError is raised if
  436. the loader cannot find the module.
  437. https://docs.python.org/3/library/importlib.html#importlib.abc.InspectLoader.is_package
  438. """
  439. return self._is_package
  440. #-- PEP302 protocol extensions as dfined by importlib.abc.ResourceLoader
  441. # https://docs.python.org/3/library/importlib.html#importlib.abc.ResourceLoader
  442. def get_data(self, path):
  443. """
  444. A method to return the bytes for the data located at path. Loaders that have a file-like storage back-end that
  445. allows storing arbitrary data can implement this abstract method to give direct access to the data stored.
  446. OSError is to be raised if the path cannot be found. The path is expected to be constructed using a module’s
  447. __file__ attribute or an item from a package’s __path__.
  448. https://docs.python.org/3/library/importlib.html#importlib.abc.ResourceLoader.get_data
  449. """
  450. # Try to fetch the data from the filesystem. Since __file__ attribute works properly, just try to open the file
  451. # and read it.
  452. with open(path, 'rb') as fp:
  453. return fp.read()
  454. #-- Support for `importlib.resources`.
  455. @_check_name
  456. def get_resource_reader(self, fullname):
  457. """
  458. Return resource reader compatible with `importlib.resources`.
  459. """
  460. return PyiFrozenResourceReader(self)
  461. class PyiFrozenResourceReader:
  462. """
  463. Resource reader for importlib.resources / importlib_resources support.
  464. Supports only on-disk resources, which should cover the typical use cases, i.e., the access to data files;
  465. PyInstaller collects data files onto filesystem, and as of v6.0.0, the embedded PYZ archive is guaranteed
  466. to contain only .pyc modules.
  467. When listing resources, source .py files will not be listed as they are not collected by default. Similarly,
  468. sub-directories that contained only .py files are not reconstructed on filesystem, so they will not be listed,
  469. either. If access to .py files is required for whatever reason, they need to be explicitly collected as data files
  470. anyway, which will place them on filesystem and make them appear as resources.
  471. For on-disk resources, we *must* return path compatible with pathlib.Path() in order to avoid copy to a temporary
  472. file, which might break under some circumstances, e.g., metpy with importlib_resources back-port, due to:
  473. https://github.com/Unidata/MetPy/blob/a3424de66a44bf3a92b0dcacf4dff82ad7b86712/src/metpy/plots/wx_symbols.py#L24-L25
  474. (importlib_resources tries to use 'fonts/wx_symbols.ttf' as a temporary filename suffix, which fails as it contains
  475. a separator).
  476. Furthermore, some packages expect files() to return either pathlib.Path or zipfile.Path, e.g.,
  477. https://github.com/tensorflow/datasets/blob/master/tensorflow_datasets/core/utils/resource_utils.py#L81-L97
  478. This makes implementation of mixed support for on-disk and embedded resources using importlib.abc.Traversable
  479. protocol rather difficult.
  480. So in order to maximize compatibility with unfrozen behavior, the below implementation is basically equivalent of
  481. importlib.readers.FileReader from python 3.10:
  482. https://github.com/python/cpython/blob/839d7893943782ee803536a47f1d4de160314f85/Lib/importlib/readers.py#L11
  483. and its underlying classes, importlib.abc.TraversableResources and importlib.abc.ResourceReader:
  484. https://github.com/python/cpython/blob/839d7893943782ee803536a47f1d4de160314f85/Lib/importlib/abc.py#L422
  485. https://github.com/python/cpython/blob/839d7893943782ee803536a47f1d4de160314f85/Lib/importlib/abc.py#L312
  486. """
  487. def __init__(self, loader):
  488. # Local import to avoid including `pathlib` and its dependencies in `base_library.zip`
  489. import pathlib
  490. # This covers both modules and (regular) packages. Note that PEP-420 namespace packages are not handled by this
  491. # resource reader (since they are not handled by PyiFrozenLoader, which uses this reader).
  492. self.path = pathlib.Path(loader.path).parent
  493. def open_resource(self, resource):
  494. return self.files().joinpath(resource).open('rb')
  495. def resource_path(self, resource):
  496. return str(self.path.joinpath(resource))
  497. def is_resource(self, path):
  498. return self.files().joinpath(path).is_file()
  499. def contents(self):
  500. return (item.name for item in self.files().iterdir())
  501. def files(self):
  502. return self.path
  503. class PyiFrozenEntryPointLoader:
  504. """
  505. A special loader that enables retrieval of the code-object for the __main__ module.
  506. """
  507. def __repr__(self):
  508. return self.__class__.__name__
  509. def get_code(self, fullname):
  510. if fullname == '__main__':
  511. # Special handling for __main__ module; the bootloader should store code object to _pyi_main_co
  512. # attribute of the module.
  513. return sys.modules['__main__']._pyi_main_co
  514. raise ImportError(f'{self} cannot handle module {fullname!r}')
  515. def install():
  516. """
  517. Install PyInstaller's frozen finders/loaders/importers into python's import machinery.
  518. """
  519. # Setup PYZ archive reader.
  520. #
  521. # The bootloader should store the path to PYZ archive (the path to the PKG archive and the offset within it; for
  522. # executable-embedded archive, this is for example /path/executable_name?117568) into _pyinstaller_pyz
  523. # attribute of the sys module.
  524. global pyz_archive
  525. if not hasattr(sys, '_pyinstaller_pyz'):
  526. raise RuntimeError("Bootloader did not set sys._pyinstaller_pyz!")
  527. try:
  528. pyz_archive = pyimod01_archive.ZlibArchiveReader(sys._pyinstaller_pyz, check_pymagic=True)
  529. except Exception as e:
  530. raise RuntimeError("Failed to setup PYZ archive reader!") from e
  531. delattr(sys, '_pyinstaller_pyz')
  532. # On Windows, there is finder called `_frozen_importlib.WindowsRegistryFinder`, which looks for Python module
  533. # locations in Windows registry. The frozen application should not look for those, so remove this finder
  534. # from `sys.meta_path`.
  535. for entry in sys.meta_path:
  536. if getattr(entry, '__name__', None) == 'WindowsRegistryFinder':
  537. sys.meta_path.remove(entry)
  538. break
  539. # Insert our hook for `PyiFrozenFinder` into `sys.path_hooks`. Place it after `zipimporter`, if available.
  540. for idx, entry in enumerate(sys.path_hooks):
  541. if getattr(entry, '__name__', None) == 'zipimporter':
  542. trace(f"PyInstaller: inserting our finder hook at index {idx + 1} in sys.path_hooks.")
  543. sys.path_hooks.insert(idx + 1, PyiFrozenFinder.path_hook)
  544. break
  545. else:
  546. trace("PyInstaller: zipimporter hook not found in sys.path_hooks! Prepending our finder hook to the list.")
  547. sys.path_hooks.insert(0, PyiFrozenFinder.path_hook)
  548. # Monkey-patch `zipimporter.get_source` to allow loading out-of-zip source .py files for modules that are
  549. # in `base_library.zip`.
  550. _patch_zipimporter_get_source()
  551. # Python might have already created a `FileFinder` for `sys._MEIPASS`. Remove the entry from path importer cache,
  552. # so that next loading attempt creates `PyiFrozenFinder` instead. This could probably be avoided altogether if
  553. # we refrained from adding `sys._MEIPASS` to `sys.path` until our importer hooks is in place.
  554. sys.path_importer_cache.pop(sys._MEIPASS, None)
  555. # Set the PyiFrozenEntryPointLoader as loader for __main__, in order for python to treat __main__ as a module
  556. # instead of a built-in, and to allow its code object to be retrieved.
  557. try:
  558. sys.modules['__main__'].__loader__ = PyiFrozenEntryPointLoader()
  559. except Exception:
  560. pass
  561. # Apply hack for python >= 3.11 and its frozen stdlib modules.
  562. if sys.version_info >= (3, 11):
  563. _fixup_frozen_stdlib()
  564. # A hack for python >= 3.11 and its frozen stdlib modules. Unless `sys._stdlib_dir` is set, these modules end up
  565. # missing __file__ attribute, which causes problems with 3rd party code. At the time of writing, python interpreter
  566. # configuration API does not allow us to influence `sys._stdlib_dir` - it always resets it to `None`. Therefore,
  567. # we manually set the path, and fix __file__ attribute on modules.
  568. def _fixup_frozen_stdlib():
  569. import _imp # built-in
  570. # If sys._stdlib_dir is None or empty, override it with sys._MEIPASS
  571. if not sys._stdlib_dir:
  572. try:
  573. sys._stdlib_dir = sys._MEIPASS
  574. except AttributeError:
  575. pass
  576. # The sys._stdlib_dir set above should affect newly-imported python-frozen modules. However, most of them have
  577. # been already imported during python initialization and our bootstrap, so we need to retroactively fix their
  578. # __file__ attribute.
  579. for module_name, module in sys.modules.items():
  580. if not _imp.is_frozen(module_name):
  581. continue
  582. is_pkg = _imp.is_frozen_package(module_name)
  583. # Determine "real" name from __spec__.loader_state.
  584. loader_state = module.__spec__.loader_state
  585. orig_name = loader_state.origname
  586. if is_pkg:
  587. orig_name += '.__init__'
  588. # We set suffix to .pyc to be consistent with our PyiFrozenLoader.
  589. filename = os.path.join(sys._MEIPASS, *orig_name.split('.')) + '.pyc'
  590. # Fixup the __file__ attribute
  591. if not hasattr(module, '__file__'):
  592. try:
  593. module.__file__ = filename
  594. except AttributeError:
  595. pass
  596. # Fixup the loader_state.filename
  597. # Except for _frozen_importlib (importlib._bootstrap), whose loader_state.filename appears to be left at
  598. # None in python.
  599. if loader_state.filename is None and orig_name != 'importlib._bootstrap':
  600. loader_state.filename = filename
  601. # Monkey-patch the `get_source` implementation of python's `zipimport.zipimporter` with our custom implementation that
  602. # looks up for source files in top-level application directory instead of within the zip file. This allows us to collect
  603. # source .py files for modules that are collected in the `base_library.zip` in the same way as for modules in the PYZ
  604. # archive.
  605. def _patch_zipimporter_get_source():
  606. import zipimport
  607. _orig_get_source = zipimport.zipimporter.get_source
  608. def _get_source(self, fullname):
  609. # Call original implementation first, in case we are dealing with a zip file other than `base_library.zip` (or
  610. # if the source .py file is actually in there, for whatever reason). This also implicitly validates the module
  611. # name, as it raises exception if module does not exist and returns None if module exists but the source code
  612. # is not present in the archive.
  613. source = _orig_get_source(self, fullname)
  614. if source is not None:
  615. return source
  616. # Our override should apply only to `base_library.zip`.
  617. if os.path.basename(self.archive) != 'base_library.zip':
  618. return None
  619. # Translate module/package name into .py filename in the top-level application directory.
  620. if self.is_package(fullname):
  621. filename = os.path.join(*fullname.split('.'), '__init__.py')
  622. else:
  623. filename = os.path.join(*fullname.split('.')) + '.py'
  624. filename = os.path.join(_RESOLVED_TOP_LEVEL_DIRECTORY, filename)
  625. try:
  626. # Read in binary mode, then decode
  627. with open(filename, 'rb') as fp:
  628. source_bytes = fp.read()
  629. return _decode_source(source_bytes)
  630. except FileNotFoundError:
  631. pass
  632. # Source code is unavailable.
  633. return None
  634. zipimport.zipimporter.get_source = _get_source