importer.pxi 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796
  1. import marshal
  2. import sys
  3. from _codecs import utf_8_decode, utf_8_encode
  4. from _frozen_importlib import _call_with_frames_removed, spec_from_loader, BuiltinImporter
  5. from _frozen_importlib_external import (
  6. _os,
  7. _path_isfile,
  8. _path_isabs,
  9. path_sep,
  10. _path_join,
  11. _path_split,
  12. _path_stat,
  13. SourceFileLoader,
  14. cache_from_source,
  15. )
  16. from _io import FileIO
  17. import __res as __resource
  18. _b = lambda x: x if isinstance(x, bytes) else utf_8_encode(x)[0]
  19. _s = lambda x: x if isinstance(x, str) else utf_8_decode(x)[0]
  20. env_source_root = b'Y_PYTHON_SOURCE_ROOT'
  21. cfg_source_root = b'arcadia-source-root'
  22. env_extended_source_search = b'Y_PYTHON_EXTENDED_SOURCE_SEARCH'
  23. res_ya_ide_venv = b'YA_IDE_VENV'
  24. executable = sys.executable or 'Y_PYTHON'
  25. sys.modules['run_import_hook'] = __resource
  26. def _probe(environ_dict, key, default_value=None):
  27. """ Probe bytes and str variants for environ.
  28. This is because in python3:
  29. * _os (nt) on windows returns str,
  30. * _os (posix) on linux return bytes
  31. For more information check:
  32. * https://github.com/python/cpython/blob/main/Lib/importlib/_bootstrap_external.py#L34
  33. * YA-1700
  34. """
  35. keys = [_b(key), _s(key)]
  36. for key in keys:
  37. if key in environ_dict:
  38. return _b(environ_dict[key])
  39. return _b(default_value) if isinstance(default_value, str) else default_value
  40. # This is the prefix in contrib/tools/python3/Lib/ya.make.
  41. py_prefix = b'py/'
  42. py_prefix_len = len(py_prefix)
  43. EXTERNAL_PY_FILES_MODE = __resource.find(b'py/conf/ENABLE_EXTERNAL_PY_FILES') in (b'1', b'yes')
  44. YA_IDE_VENV = __resource.find(res_ya_ide_venv)
  45. Y_PYTHON_EXTENDED_SOURCE_SEARCH = _probe(_os.environ, env_extended_source_search) or YA_IDE_VENV
  46. def _init_venv():
  47. if not _path_isabs(executable):
  48. raise RuntimeError(f'path in sys.executable is not absolute: {executable}')
  49. # Creative copy-paste from site.py
  50. exe_dir, _ = _path_split(executable)
  51. site_prefix, _ = _path_split(exe_dir)
  52. libpath = _path_join(site_prefix, 'lib',
  53. 'python%d.%d' % sys.version_info[:2],
  54. 'site-packages')
  55. sys.path.insert(0, libpath)
  56. # emulate site.venv()
  57. sys.prefix = site_prefix
  58. sys.exec_prefix = site_prefix
  59. conf_basename = 'pyvenv.cfg'
  60. candidate_confs = [
  61. conffile for conffile in (
  62. _path_join(exe_dir, conf_basename),
  63. _path_join(site_prefix, conf_basename)
  64. )
  65. if _path_isfile(conffile)
  66. ]
  67. if not candidate_confs:
  68. raise RuntimeError(f'{conf_basename} not found')
  69. virtual_conf = candidate_confs[0]
  70. with FileIO(virtual_conf, 'r') as f:
  71. for line in f:
  72. if b'=' in line:
  73. key, _, value = line.partition(b'=')
  74. key = key.strip().lower()
  75. value = value.strip()
  76. if key == cfg_source_root:
  77. return value
  78. raise RuntimeError(f'{cfg_source_root} key not found in {virtual_conf}')
  79. def file_bytes(path):
  80. # 'open' is not avaiable yet.
  81. with FileIO(path, 'r') as f:
  82. return f.read()
  83. def _guess_source_root():
  84. path, tail = _os.getcwd(), 'start'
  85. while tail:
  86. guidence_file = _path_join(path, '.root.path')
  87. if _path_isfile(guidence_file):
  88. return file_bytes(guidence_file)
  89. if _path_isfile(_path_join(path, '.arcadia.root')):
  90. return _b(path)
  91. path, tail = _path_split(path)
  92. def _get_source_root():
  93. env_value = _probe(_os.environ, env_source_root)
  94. if env_value:
  95. return env_value
  96. if EXTERNAL_PY_FILES_MODE:
  97. path = _guess_source_root()
  98. if path:
  99. return path
  100. raise RuntimeError(
  101. "Cannot find source root: binary is built in external-py-files mode, but no env.var. Y_PYTHON_SOURCE_ROOT is specified. Current working directory: " + _os.getcwd()
  102. )
  103. if YA_IDE_VENV:
  104. return _init_venv()
  105. return None
  106. def _guess_pycache_prefix():
  107. env_val = _probe(_os.environ, 'PYTHONPYCACHEPREFIX')
  108. if env_val:
  109. return _s(env_val)
  110. path, tail = _os.getcwd(), 'start'
  111. while tail:
  112. guidence_file = _path_join(path, '.pycache.path')
  113. if _path_isfile(guidence_file):
  114. return _s(file_bytes(guidence_file))
  115. path, tail = _path_split(path)
  116. Y_PYTHON_SOURCE_ROOT = _get_source_root()
  117. if EXTERNAL_PY_FILES_MODE:
  118. import _frozen_importlib_external
  119. # Turn relative paths into absolute ones so that the python machinery stores the bytecode files next to the module.
  120. # For more info see data flow in SourceLoader.get_data in contrib/tools/python3/Lib/importlib/_bootstrap_external.py
  121. def patched_cache_from_source(filename):
  122. filename = resfs_resolve(filename, check_existence=False)
  123. return cache_from_source(_s(filename))
  124. setattr(_frozen_importlib_external, 'cache_from_source', patched_cache_from_source)
  125. pycache_prefix = _guess_pycache_prefix()
  126. if pycache_prefix:
  127. sys.pycache_prefix = pycache_prefix
  128. def _print(*xs):
  129. """
  130. This is helpful for debugging, since automatic bytes to str conversion is
  131. not available yet. It is also possible to debug with GDB by breaking on
  132. __Pyx_AddTraceback (with Python GDB pretty printers enabled).
  133. """
  134. parts = []
  135. for s in xs:
  136. if not isinstance(s, (bytes, str)):
  137. s = str(s)
  138. parts.append(_s(s))
  139. sys.stderr.write(' '.join(parts) + '\n')
  140. def iter_keys(prefix):
  141. l = len(prefix)
  142. for idx in range(__resource.count()):
  143. key = __resource.key_by_index(idx)
  144. if key.startswith(prefix):
  145. yield key, key[l:]
  146. def iter_py_modules(with_keys=False):
  147. for key, path in iter_keys(b'resfs/src/resfs/file/' + py_prefix):
  148. if path.endswith(b'.py'): # It may also end with '.pyc'.
  149. mod = _s(path[:-3].replace(b'/', b'.'))
  150. if with_keys:
  151. yield key, mod
  152. else:
  153. yield mod
  154. def py_src_key(filename):
  155. return py_prefix + _b(filename)
  156. def iter_prefixes(s):
  157. i = s.find('.')
  158. while i >= 0:
  159. yield s[:i]
  160. i = s.find('.', i + 1)
  161. def resfs_resolve(path, check_existence=True):
  162. """
  163. Return the absolute path of a root-relative path if it exists.
  164. """
  165. path = _b(path)
  166. if Y_PYTHON_SOURCE_ROOT:
  167. if not path.startswith(Y_PYTHON_SOURCE_ROOT):
  168. path = _b(path_sep).join((Y_PYTHON_SOURCE_ROOT, path))
  169. if not check_existence:
  170. return path
  171. if _path_isfile(path):
  172. return path
  173. def resfs_src(key, resfs_file=False):
  174. """
  175. Return the root-relative file path of a resource key.
  176. """
  177. if resfs_file:
  178. key = b'resfs/file/' + _b(key)
  179. return __resource.find(b'resfs/src/' + _b(key))
  180. def resfs_has(path):
  181. """
  182. Return true if the requested file is embedded in the program
  183. """
  184. return __resource.has(b'resfs/file/' + _b(path))
  185. def resfs_read(path, builtin=None):
  186. """
  187. Return the bytes of the resource file at path, or None.
  188. If builtin is True, do not look for it on the filesystem.
  189. If builtin is False, do not look in the builtin resources.
  190. """
  191. if builtin is not True:
  192. arcpath = resfs_src(path, resfs_file=True)
  193. if arcpath:
  194. fspath = resfs_resolve(arcpath)
  195. if fspath:
  196. return file_bytes(fspath)
  197. if builtin is not False:
  198. return __resource.find(b'resfs/file/' + _b(path))
  199. def resfs_files(prefix=b''):
  200. """
  201. List builtin resource file paths.
  202. """
  203. return [key[11:] for key, _ in iter_keys(b'resfs/file/' + _b(prefix))]
  204. def mod_path(mod):
  205. """
  206. Return the resfs path to the source code of the module with the given name.
  207. """
  208. return py_prefix + _b(mod).replace(b'.', b'/') + b'.py'
  209. class ResourceImporter(SourceFileLoader):
  210. """ A meta_path importer that loads code from built-in resources.
  211. """
  212. def __init__(self, fullname, path):
  213. super().__init__(fullname, path)
  214. self.memory = set(iter_py_modules()) # Set of importable module names.
  215. self.source_map = {} # Map from file names to module names.
  216. self._source_name = {} # Map from original to altered module names.
  217. self._package_prefix = ''
  218. self._before_import_callback = None
  219. self._after_import_callback = None
  220. if Y_PYTHON_SOURCE_ROOT and Y_PYTHON_EXTENDED_SOURCE_SEARCH:
  221. self.arcadia_source_finder = ArcadiaSourceFinder(_s(Y_PYTHON_SOURCE_ROOT))
  222. else:
  223. self.arcadia_source_finder = None
  224. for p in list(self.memory) + list(sys.builtin_module_names):
  225. for pp in iter_prefixes(p):
  226. k = pp + '.__init__'
  227. if k not in self.memory:
  228. self.memory.add(k)
  229. def set_callbacks(self, before_import=None, after_import=None):
  230. """Callable[[module], None]"""
  231. self._before_import_callback= before_import
  232. self._after_import_callback = after_import
  233. def for_package(self, name):
  234. import copy
  235. importer = copy.copy(self)
  236. importer._package_prefix = name + '.'
  237. return importer
  238. def _find_mod_path(self, fullname):
  239. """Find arcadia relative path by module name"""
  240. relpath = resfs_src(mod_path(fullname), resfs_file=True)
  241. if relpath or not self.arcadia_source_finder:
  242. return relpath
  243. return self.arcadia_source_finder.get_module_path(fullname)
  244. def find_spec(self, fullname, path=None, target=None):
  245. # Поддежка переопределения стандартного distutils из пакетом из setuptools
  246. if fullname.startswith("distutils."):
  247. setuptools_path = f"{path_sep}setuptools{path_sep}_distutils"
  248. if path and len(path) > 0 and setuptools_path in path[0]:
  249. import importlib
  250. import importlib.abc
  251. setuptools_name = "setuptools._distutils.{}".format(fullname.removeprefix("distutils."))
  252. is_package = self.is_package(setuptools_name)
  253. if is_package:
  254. source = self.get_source(f"{setuptools_name}.__init__")
  255. relpath = self._find_mod_path(f"{setuptools_name}.__init__")
  256. else:
  257. source = self.get_source(setuptools_name)
  258. relpath = self._find_mod_path(setuptools_name)
  259. class DistutilsLoader(importlib.abc.Loader):
  260. def exec_module(self, module):
  261. code = compile(source, _s(relpath), 'exec', dont_inherit=True)
  262. module.__file__ = code.co_filename
  263. if is_package:
  264. module.__path__= [executable + path_sep + setuptools_name.replace('.', path_sep)]
  265. _call_with_frames_removed(exec, code, module.__dict__)
  266. return spec_from_loader(fullname, DistutilsLoader(), is_package=is_package)
  267. try:
  268. is_package = self.is_package(fullname)
  269. except ImportError:
  270. return None
  271. return spec_from_loader(fullname, self, is_package=is_package)
  272. def find_module(self, fullname, path=None):
  273. """For backward compatibility."""
  274. spec = self.find_spec(fullname, path)
  275. return spec.loader if spec is not None else None
  276. def create_module(self, spec):
  277. """Use default semantics for module creation."""
  278. def exec_module(self, module):
  279. code = self.get_code(module.__name__)
  280. module.__file__ = code.co_filename
  281. if self.is_package(module.__name__):
  282. module.__path__= [executable + path_sep + module.__name__.replace('.', path_sep)]
  283. # exec(code, module.__dict__)
  284. # __name__ and __file__ could be overwritten after execution
  285. # So these two things are needed if wee want to be consistent at some point
  286. initial_modname = module.__name__
  287. initial_filename = module.__file__
  288. if self._before_import_callback:
  289. self._before_import_callback(initial_modname, initial_filename)
  290. # “Zero-cost” exceptions are implemented.
  291. # The cost of try statements is almost eliminated when no exception is raised
  292. try:
  293. _call_with_frames_removed(exec, code, module.__dict__)
  294. finally:
  295. if self._after_import_callback:
  296. self._after_import_callback(initial_modname, initial_filename)
  297. # PEP-302 extension 1 of 3: data loader.
  298. def get_data(self, path):
  299. # XXX
  300. # Python machinery operates on absolute paths and uses this method to load bytecode.
  301. # That's why we don't try to resolve the path, but try to read it right away.
  302. # For more info see get_code() in this file and
  303. # data flow in SourceLoader.get_data in contrib/tools/python3/Lib/importlib/_bootstrap_external.py
  304. if EXTERNAL_PY_FILES_MODE and path.endswith('.pyc'):
  305. return file_bytes(_b(path))
  306. path = _b(path)
  307. abspath = resfs_resolve(path)
  308. if abspath:
  309. return file_bytes(abspath)
  310. path = path.replace(_b('\\'), _b('/'))
  311. data = resfs_read(path, builtin=True)
  312. if data is None:
  313. raise OSError(path) # Y_PYTHON_ENTRY_POINT=:resource_files
  314. return data
  315. # PEP-302 extension 2 of 3: get __file__ without importing.
  316. def get_filename(self, fullname):
  317. modname = fullname
  318. if self.is_package(fullname):
  319. fullname += '.__init__'
  320. relpath = self._find_mod_path(fullname)
  321. if isinstance(relpath, bytes):
  322. relpath = _s(relpath)
  323. return relpath or modname
  324. # PEP-302 extension 3 of 3: packaging introspection.
  325. # Used by `linecache` (while printing tracebacks) unless module filename
  326. # exists on the filesystem.
  327. def get_source(self, fullname):
  328. fullname = self._source_name.get(fullname) or fullname
  329. if self.is_package(fullname):
  330. fullname += '.__init__'
  331. relpath = self.get_filename(fullname)
  332. if relpath:
  333. abspath = resfs_resolve(relpath)
  334. if abspath:
  335. return _s(file_bytes(abspath))
  336. data = resfs_read(mod_path(fullname))
  337. return _s(data) if data else ''
  338. def get_code(self, fullname):
  339. modname = fullname
  340. if self.is_package(fullname):
  341. fullname += '.__init__'
  342. path = mod_path(fullname)
  343. relpath = self._find_mod_path(fullname)
  344. if relpath:
  345. abspath = resfs_resolve(relpath)
  346. if abspath:
  347. if EXTERNAL_PY_FILES_MODE:
  348. if not resfs_has(path):
  349. # 1. This is the case when the requested module is registered in the metadata,
  350. # but the content itself is not embedded in the binary.
  351. # And the application itself is compiled in the external py files mode.
  352. # Thus, we have an abspath to the python file and we need to get its bytecode.
  353. # We transfer control to the standard python machinery,
  354. # which will process the bytecode generation and cache it nearby,
  355. # according to the general rules.
  356. return super().get_code(modname)
  357. else:
  358. # 2. This is the case when the binary is launched in the mode of
  359. # reading python sources from the file system (Y_PYTHON_SOURCE_ROOT),
  360. # and not from the built-in storage.
  361. data = file_bytes(abspath)
  362. return compile(data, _s(abspath), 'exec', dont_inherit=True)
  363. yapyc_path = path + b'.yapyc3'
  364. yapyc_data = resfs_read(yapyc_path, builtin=True)
  365. if yapyc_data:
  366. # 3. This is the basic case - we read the compiled bytecode from the built-in storage.
  367. return marshal.loads(yapyc_data)
  368. else:
  369. py_data = resfs_read(path, builtin=True)
  370. if py_data:
  371. # 4. This is the case when the bytecode for the module is not embedded in the binary (PYBUILD_NO_PYC).
  372. # Read the python file and compile on the fly.
  373. return compile(py_data, _s(relpath), 'exec', dont_inherit=True)
  374. else:
  375. # 5. This covers packages with no __init__.py in resources.
  376. return compile('', modname, 'exec', dont_inherit=True)
  377. def path_stats(self, path):
  378. path = resfs_resolve(path, check_existence=False)
  379. st = _path_stat(path)
  380. return {'mtime': st.st_mtime, 'size': st.st_size}
  381. def is_package(self, fullname):
  382. if fullname in self.memory:
  383. return False
  384. if fullname + '.__init__' in self.memory:
  385. return True
  386. if self.arcadia_source_finder:
  387. return self.arcadia_source_finder.is_package(fullname)
  388. raise ImportError(fullname)
  389. # Extension for contrib/python/coverage.
  390. def file_source(self, filename):
  391. """
  392. Return the key of the module source by its resource path.
  393. """
  394. if not self.source_map:
  395. for key, mod in iter_py_modules(with_keys=True):
  396. path = self.get_filename(mod)
  397. self.source_map[path] = key
  398. if filename in self.source_map:
  399. return self.source_map[filename]
  400. if resfs_has(filename):
  401. return b'resfs/file/' + _b(filename)
  402. return b''
  403. # Extension for pkgutil.iter_modules.
  404. def iter_modules(self, prefix=''):
  405. import re
  406. rx = re.compile(re.escape(self._package_prefix) + r'([^.]+)(\.__init__)?$')
  407. for p in self.memory:
  408. m = rx.match(p)
  409. if m:
  410. yield prefix + m.group(1), m.group(2) is not None
  411. if self.arcadia_source_finder:
  412. for m in self.arcadia_source_finder.iter_modules(self._package_prefix, prefix):
  413. yield m
  414. def get_resource_reader(self, fullname):
  415. import os
  416. path = os.path.dirname(self.get_filename(fullname))
  417. return _ResfsResourceReader(self, path)
  418. @staticmethod
  419. def find_distributions(*args, **kwargs):
  420. """
  421. Find distributions.
  422. Return an iterable of all Distribution instances capable of
  423. loading the metadata for packages matching ``context.name``
  424. (or all names if ``None`` indicated) along the paths in the list
  425. of directories ``context.path``.
  426. """
  427. from sitecustomize import MetadataArcadiaFinder
  428. return MetadataArcadiaFinder.find_distributions(*args, **kwargs)
  429. class _ResfsResourceReader:
  430. def __init__(self, importer, path):
  431. self.importer = importer
  432. self.path = path
  433. def open_resource(self, resource):
  434. path = f'{self.path}/{resource}'
  435. from io import BytesIO
  436. try:
  437. return BytesIO(self.importer.get_data(path))
  438. except OSError:
  439. raise FileNotFoundError(path)
  440. def resource_path(self, resource):
  441. # All resources are in the binary file, so there is no path to the file.
  442. # Raising FileNotFoundError tells the higher level API to extract the
  443. # binary data and create a temporary file.
  444. raise FileNotFoundError
  445. def is_resource(self, name):
  446. path = f'{self.path}/{name}'
  447. try:
  448. self.importer.get_data(path)
  449. except OSError:
  450. return False
  451. return True
  452. def contents(self):
  453. subdirs_seen = set()
  454. len_path = len(self.path) + 1 # path + /
  455. for key in resfs_files(f"{self.path}/"):
  456. relative = key[len_path:]
  457. res_or_subdir, *other = relative.split(b'/')
  458. if not other:
  459. yield _s(res_or_subdir)
  460. elif res_or_subdir not in subdirs_seen:
  461. subdirs_seen.add(res_or_subdir)
  462. yield _s(res_or_subdir)
  463. def files(self):
  464. import sitecustomize
  465. return sitecustomize.ArcadiaResourceContainer(f"resfs/file/{self.path}/")
  466. class BuiltinSubmoduleImporter(BuiltinImporter):
  467. @classmethod
  468. def find_spec(cls, fullname, path=None, target=None):
  469. if path is not None:
  470. return super().find_spec(fullname, None, target)
  471. else:
  472. return None
  473. class ArcadiaSourceFinder:
  474. """
  475. Search modules and packages in arcadia source tree.
  476. See https://wiki.yandex-team.ru/devtools/extended-python-source-search/ for details
  477. """
  478. NAMESPACE_PREFIX = b'py/namespace/'
  479. PY_EXT = '.py'
  480. YA_MAKE = 'ya.make'
  481. S_IFDIR = 0o040000
  482. def __init__(self, source_root):
  483. self.source_root = source_root
  484. self.module_path_cache = {'': set()}
  485. for key, dirty_path in iter_keys(self.NAMESPACE_PREFIX):
  486. # dirty_path contains unique prefix to prevent repeatable keys in the resource storage
  487. path = dirty_path.split(b'/', 1)[1]
  488. namespaces = __resource.find(key).split(b':')
  489. for n in namespaces:
  490. package_name = _s(n.rstrip(b'.'))
  491. self.module_path_cache.setdefault(package_name, set()).add(_s(path))
  492. # Fill parents with default empty path set if parent doesn't exist in the cache yet
  493. while package_name:
  494. package_name = package_name.rpartition('.')[0]
  495. if package_name in self.module_path_cache:
  496. break
  497. self.module_path_cache.setdefault(package_name, set())
  498. for package_name in self.module_path_cache.keys():
  499. self._add_parent_dirs(package_name, visited=set())
  500. def get_module_path(self, fullname):
  501. """
  502. Find file path for module 'fullname'.
  503. For packages caller pass fullname as 'package.__init__'.
  504. Return None if nothing is found.
  505. """
  506. try:
  507. if not self.is_package(fullname):
  508. return _b(self._cache_module_path(fullname))
  509. except ImportError:
  510. pass
  511. def is_package(self, fullname):
  512. """Check if fullname is a package. Raise ImportError if fullname is not found"""
  513. path = self._cache_module_path(fullname)
  514. if isinstance(path, set):
  515. return True
  516. if isinstance(path, str):
  517. return False
  518. raise ImportError(fullname)
  519. def iter_modules(self, package_prefix, prefix):
  520. paths = self._cache_module_path(package_prefix.rstrip('.'))
  521. if paths is not None:
  522. # Note: it's ok to yield duplicates because pkgutil discards them
  523. # Yield from cache
  524. import re
  525. rx = re.compile(re.escape(package_prefix) + r'([^.]+)$')
  526. # Save result to temporary list to prevent 'RuntimeError: dictionary changed size during iteration'
  527. found = []
  528. for mod, path in self.module_path_cache.items():
  529. if path is not None:
  530. m = rx.match(mod)
  531. if m:
  532. found.append((prefix + m.group(1), self.is_package(mod)))
  533. yield from found
  534. # Yield from file system
  535. for path in paths:
  536. abs_path = _path_join(self.source_root, path)
  537. for dir_item in _os.listdir(abs_path):
  538. if self._path_is_simple_dir(_path_join(abs_path, dir_item)):
  539. yield prefix + dir_item, True
  540. elif dir_item.endswith(self.PY_EXT) and _path_isfile(_path_join(abs_path, dir_item)):
  541. yield prefix + dir_item[:-len(self.PY_EXT)], False
  542. def _isdir(self, path):
  543. """ Unlike _path_isdir() this function don't follow symlink """
  544. try:
  545. stat_info = _os.lstat(path)
  546. except OSError:
  547. return False
  548. return (stat_info.st_mode & 0o170000) == self.S_IFDIR
  549. def _path_is_simple_dir(self, abs_path):
  550. """
  551. Check if path is a directory but doesn't contain ya.make file.
  552. We don't want to steal directory from nested project and treat it as a package
  553. """
  554. return self._isdir(abs_path) and not _path_isfile(_path_join(abs_path, self.YA_MAKE))
  555. def _find_module_in_paths(self, find_package_only, paths, module):
  556. """Auxiliary method. See _cache_module_path() for details"""
  557. if paths:
  558. package_paths = set()
  559. for path in paths:
  560. rel_path = _path_join(path, module)
  561. if not find_package_only:
  562. # Check if file_path is a module
  563. module_path = rel_path + self.PY_EXT
  564. if _path_isfile(_path_join(self.source_root, module_path)):
  565. return module_path
  566. # Check if file_path is a package
  567. if self._path_is_simple_dir(_path_join(self.source_root, rel_path)):
  568. package_paths.add(rel_path)
  569. if package_paths:
  570. return package_paths
  571. def _cache_module_path(self, fullname, find_package_only=False):
  572. """
  573. Find module path or package directory paths and save result in the cache
  574. find_package_only=True - don't try to find module
  575. Returns:
  576. List of relative package paths - for a package
  577. Relative module path - for a module
  578. None - module or package is not found
  579. """
  580. if fullname not in self.module_path_cache:
  581. parent, _, tail = fullname.rpartition('.')
  582. parent_paths = self._cache_module_path(parent, find_package_only=True)
  583. self.module_path_cache[fullname] = self._find_module_in_paths(find_package_only, parent_paths, tail)
  584. return self.module_path_cache[fullname]
  585. def _add_parent_dirs(self, package_name, visited):
  586. if not package_name or package_name in visited:
  587. return
  588. visited.add(package_name)
  589. parent, _, tail = package_name.rpartition('.')
  590. self._add_parent_dirs(parent, visited)
  591. paths = self.module_path_cache[package_name]
  592. for parent_path in self.module_path_cache[parent]:
  593. rel_path = _path_join(parent_path, tail)
  594. if self._path_is_simple_dir(_path_join(self.source_root, rel_path)):
  595. paths.add(rel_path)
  596. def excepthook(*args, **kws):
  597. # traceback module cannot be imported at module level, because interpreter
  598. # is not fully initialized yet
  599. import traceback
  600. return traceback.print_exception(*args, **kws)
  601. importer = ResourceImporter(fullname='<resfs>', path='<resfs>')
  602. def executable_path_hook(path):
  603. if path == executable:
  604. return importer
  605. if path.startswith(executable + path_sep):
  606. return importer.for_package(path[len(executable + path_sep):].replace(path_sep, '.'))
  607. raise ImportError(path)
  608. def get_path0():
  609. """
  610. An incomplete and simplified version of _PyPathConfig_ComputeSysPath0.
  611. We need this to somewhat properly emulate the behaviour of a normal python interpreter
  612. when using ya ide venv.
  613. """
  614. if not sys.argv:
  615. return
  616. argv0 = sys.argv[0]
  617. have_module_arg = argv0 == '-m'
  618. if have_module_arg:
  619. return _os.getcwd()
  620. if YA_IDE_VENV:
  621. sys.meta_path.append(importer)
  622. sys.meta_path.append(BuiltinSubmoduleImporter)
  623. if executable not in sys.path:
  624. sys.path.append(executable)
  625. path0 = get_path0()
  626. if path0 is not None:
  627. sys.path.insert(0, path0)
  628. sys.path_hooks.append(executable_path_hook)
  629. else:
  630. sys.meta_path.insert(0, BuiltinSubmoduleImporter)
  631. sys.meta_path.insert(0, importer)
  632. if executable not in sys.path:
  633. sys.path.insert(0, executable)
  634. sys.path_hooks.insert(0, executable_path_hook)
  635. sys.path_importer_cache[executable] = importer
  636. # Indicator that modules and resources are built-in rather than on the file system.
  637. sys.is_standalone_binary = True
  638. sys.frozen = True
  639. # Set of names of importable modules.
  640. sys.extra_modules = importer.memory
  641. # Use custom implementation of traceback printer.
  642. # Built-in printer (PyTraceBack_Print) does not support custom module loaders
  643. sys.excepthook = excepthook