aboutsummaryrefslogtreecommitdiffstats
path: root/library/python
diff options
context:
space:
mode:
authornkozlovskiy <nmk@ydb.tech>2023-09-29 12:24:06 +0300
committernkozlovskiy <nmk@ydb.tech>2023-09-29 12:41:34 +0300
commite0e3e1717e3d33762ce61950504f9637a6e669ed (patch)
treebca3ff6939b10ed60c3d5c12439963a1146b9711 /library/python
parent38f2c5852db84c7b4d83adfcb009eb61541d1ccd (diff)
downloadydb-e0e3e1717e3d33762ce61950504f9637a6e669ed.tar.gz
add ydb deps
Diffstat (limited to 'library/python')
-rw-r--r--library/python/runtime/__res.pyx31
-rw-r--r--library/python/runtime/entry_points.py64
-rw-r--r--library/python/runtime/importer.pxi297
-rw-r--r--library/python/runtime/main/main.c46
-rw-r--r--library/python/runtime/main/ya.make15
-rw-r--r--library/python/runtime/sitecustomize.pyx1
-rw-r--r--library/python/runtime/ya.make32
-rw-r--r--library/python/runtime_py3/__res.pyx36
-rw-r--r--library/python/runtime_py3/entry_points.py59
-rw-r--r--library/python/runtime_py3/importer.pxi609
-rw-r--r--library/python/runtime_py3/main/get_py_main.cpp8
-rw-r--r--library/python/runtime_py3/main/main.c209
-rw-r--r--library/python/runtime_py3/main/venv.cpp6
-rw-r--r--library/python/runtime_py3/main/ya.make22
-rw-r--r--library/python/runtime_py3/sitecustomize.pyx74
-rw-r--r--library/python/runtime_py3/test/.dist-info/METADATA14
-rw-r--r--library/python/runtime_py3/test/.dist-info/RECORD1
-rw-r--r--library/python/runtime_py3/test/.dist-info/entry_points.txt2
-rw-r--r--library/python/runtime_py3/test/.dist-info/top_level.txt1
-rw-r--r--library/python/runtime_py3/test/canondata/result.json50
-rw-r--r--library/python/runtime_py3/test/canondata/test_traceback.test_traceback_custom-default_/stderr.txt14
-rw-r--r--library/python/runtime_py3/test/canondata/test_traceback.test_traceback_custom-default_/stdout.txt2
-rw-r--r--library/python/runtime_py3/test/canondata/test_traceback.test_traceback_custom-ultratb_color_/stderr.txt13
-rw-r--r--library/python/runtime_py3/test/canondata/test_traceback.test_traceback_custom-ultratb_color_/stdout.txt2
-rw-r--r--library/python/runtime_py3/test/canondata/test_traceback.test_traceback_custom-ultratb_verbose_/stderr.txt27
-rw-r--r--library/python/runtime_py3/test/canondata/test_traceback.test_traceback_custom-ultratb_verbose_/stdout.txt2
-rw-r--r--library/python/runtime_py3/test/canondata/test_traceback.test_traceback_main-default_/stderr.txt14
-rw-r--r--library/python/runtime_py3/test/canondata/test_traceback.test_traceback_main-default_/stdout.txt2
-rw-r--r--library/python/runtime_py3/test/canondata/test_traceback.test_traceback_main-ultratb_color_/stderr.txt13
-rw-r--r--library/python/runtime_py3/test/canondata/test_traceback.test_traceback_main-ultratb_color_/stdout.txt2
-rw-r--r--library/python/runtime_py3/test/canondata/test_traceback.test_traceback_main-ultratb_verbose_/stderr.txt27
-rw-r--r--library/python/runtime_py3/test/canondata/test_traceback.test_traceback_main-ultratb_verbose_/stdout.txt2
-rw-r--r--library/python/runtime_py3/test/resources/__init__.py0
-rw-r--r--library/python/runtime_py3/test/resources/foo.txt1
-rw-r--r--library/python/runtime_py3/test/resources/submodule/__init__.py0
-rw-r--r--library/python/runtime_py3/test/resources/submodule/bar.txt1
-rw-r--r--library/python/runtime_py3/test/test_arcadia_source_finder.py389
-rw-r--r--library/python/runtime_py3/test/test_metadata.py44
-rw-r--r--library/python/runtime_py3/test/test_resources.py73
-rw-r--r--library/python/runtime_py3/test/test_traceback.py65
-rw-r--r--library/python/runtime_py3/test/traceback/__main__.py4
-rw-r--r--library/python/runtime_py3/test/traceback/crash.py42
-rw-r--r--library/python/runtime_py3/test/traceback/mod/__init__.py3
-rw-r--r--library/python/runtime_py3/test/traceback/ya.make16
-rw-r--r--library/python/runtime_py3/test/ya.make37
-rw-r--r--library/python/runtime_py3/ya.make43
46 files changed, 2415 insertions, 0 deletions
diff --git a/library/python/runtime/__res.pyx b/library/python/runtime/__res.pyx
new file mode 100644
index 0000000000..5e2e72abf3
--- /dev/null
+++ b/library/python/runtime/__res.pyx
@@ -0,0 +1,31 @@
+from libcpp cimport bool
+
+from util.generic.string cimport TString, TStringBuf
+
+
+cdef extern from "library/cpp/resource/resource.h" namespace "NResource":
+ cdef size_t Count() except +
+ cdef TStringBuf KeyByIndex(size_t idx) except +
+ cdef bool FindExact(const TStringBuf key, TString* result) nogil except +
+
+
+def count():
+ return Count()
+
+
+def key_by_index(idx):
+ cdef TStringBuf ret = KeyByIndex(idx)
+
+ return ret.Data()[:ret.Size()]
+
+
+def find(s):
+ cdef TString res
+
+ if FindExact(TStringBuf(s, len(s)), &res):
+ return res.c_str()[:res.length()]
+
+ return None
+
+
+include "importer.pxi"
diff --git a/library/python/runtime/entry_points.py b/library/python/runtime/entry_points.py
new file mode 100644
index 0000000000..ca5c38a26a
--- /dev/null
+++ b/library/python/runtime/entry_points.py
@@ -0,0 +1,64 @@
+import sys
+
+import __res
+
+
+def repl():
+ user_ns = {}
+ py_main = __res.find('PY_MAIN')
+
+ if py_main:
+ py_main_split = py_main.split(':', 1)
+ if len(py_main_split) == 2:
+ mod_name, func_name = py_main_split
+ else:
+ mod_name, func_name = py_main_split[0], 'main'
+
+ if not mod_name:
+ mod_name = 'library.python.runtime.entry_points'
+
+ try:
+ import importlib
+ mod = importlib.import_module(mod_name)
+ user_ns = mod.__dict__
+ except: # noqa E722
+ import traceback
+ traceback.print_exc()
+
+ if '__main__' not in user_ns:
+ def run(args):
+ if isinstance(args, basestring):
+ import shlex
+ args = shlex.split(args)
+
+ import sys
+ sys.argv = [sys.argv[0]] + args
+ getattr(mod, func_name)()
+
+ user_ns['__main__'] = run
+ else:
+ try:
+ mod = __res.importer.load_module('__main__', fix_name='__main_real')
+ user_ns = mod.__dict__
+ except ImportError:
+ pass
+
+ try:
+ import IPython
+ except ImportError:
+ import code
+ code.interact(local=user_ns)
+ else:
+ IPython.start_ipython(user_ns=user_ns)
+
+
+def resource_files():
+ sys.stdout.write('\n'.join(sorted(__res.resfs_files()) + ['']))
+
+
+def run_constructors():
+ for key, module_name in __res.iter_keys('py/constructors/'):
+ import importlib
+ module = importlib.import_module(module_name)
+ init_func = getattr(module, __res.find(key))
+ init_func()
diff --git a/library/python/runtime/importer.pxi b/library/python/runtime/importer.pxi
new file mode 100644
index 0000000000..e27eaa0549
--- /dev/null
+++ b/library/python/runtime/importer.pxi
@@ -0,0 +1,297 @@
+import copy
+import imp
+import importlib
+import marshal
+import os
+import re
+import sys
+import traceback
+from os.path import sep as path_sep
+
+import __res as __resource
+
+env_entry_point = 'Y_PYTHON_ENTRY_POINT'
+env_source_root = 'Y_PYTHON_SOURCE_ROOT'
+executable = sys.executable or 'Y_PYTHON'
+sys.modules['run_import_hook'] = __resource
+
+find_py_module = lambda mod: __resource.find('/py_modules/' + mod)
+find_py_code = lambda mod: __resource.find('/py_code/' + mod)
+
+Y_PYTHON_SOURCE_ROOT = os.environ.get(env_source_root)
+if Y_PYTHON_SOURCE_ROOT is not None:
+ Y_PYTHON_SOURCE_ROOT = os.path.abspath(os.path.expanduser(Y_PYTHON_SOURCE_ROOT))
+ os.environ[env_source_root] = Y_PYTHON_SOURCE_ROOT
+
+
+def file_bytes(path):
+ with open(path, 'rb') as f:
+ return f.read()
+
+
+def iter_keys(prefix):
+ l = len(prefix)
+ for idx in xrange(__resource.count()):
+ key = __resource.key_by_index(idx)
+ if key.startswith(prefix):
+ yield key, key[l:]
+
+
+def iter_py_modules(with_keys=False):
+ for key, mod in iter_keys('/py_modules/'):
+ if '/' in mod:
+ raise Exception('Incorrect py_modules resource: ' + repr(key))
+ if with_keys:
+ yield key, mod
+ else:
+ yield mod
+
+
+def iter_prefixes(s):
+ i = s.find('.')
+ while i >= 0:
+ yield s[:i]
+ i = s.find('.', i + 1)
+
+
+def resfs_resolve(path):
+ """
+ Return the absolute path of a root-relative path if it exists.
+ """
+ if Y_PYTHON_SOURCE_ROOT:
+ abspath = os.path.join(Y_PYTHON_SOURCE_ROOT, path)
+ if os.path.exists(abspath):
+ return abspath
+
+
+def resfs_src(key, resfs_file=False):
+ """
+ Return the root-relative file path of a resource key.
+ """
+ if resfs_file:
+ key = 'resfs/file/' + key
+ return __resource.find('resfs/src/' + key)
+
+
+def resfs_read(path, builtin=None):
+ """
+ Return the bytes of the resource file at path, or None.
+ If builtin is True, do not look for it on the filesystem.
+ If builtin is False, do not look in the builtin resources.
+ """
+ if builtin is not True:
+ arcpath = resfs_src(path, resfs_file=True)
+ if arcpath:
+ fspath = resfs_resolve(arcpath)
+ if fspath:
+ return file_bytes(fspath)
+
+ if builtin is not False:
+ return __resource.find('resfs/file/' + path)
+
+
+def resfs_files(prefix=''):
+ """
+ List builtin resource file paths.
+ """
+ return [key[11:] for key, _ in iter_keys('resfs/file/' + prefix)]
+
+
+class ResourceImporter(object):
+
+ """ A meta_path importer that loads code from built-in resources.
+ """
+
+ def __init__(self):
+ self.memory = set(iter_py_modules()) # Set of importable module names.
+ self.source_map = {} # Map from file names to module names.
+ self._source_name = {} # Map from original to altered module names.
+ self._package_prefix = ''
+
+ for p in list(self.memory) + list(sys.builtin_module_names):
+ for pp in iter_prefixes(p):
+ k = pp + '.__init__'
+ if k not in self.memory:
+ self.memory.add(k)
+
+ def for_package(self, name):
+ importer = copy.copy(self)
+ importer._package_prefix = name + '.'
+ return importer
+
+ # PEP-302 finder.
+ def find_module(self, fullname, path=None):
+ try:
+ self.is_package(fullname)
+ except ImportError:
+ return None
+ return self
+
+ # PEP-302 extension 1 of 3: data loader.
+ def get_data(self, path):
+ abspath = resfs_resolve(path)
+ if abspath:
+ return file_bytes(abspath)
+ path = path.replace('\\', '/')
+ data = resfs_read(path, builtin=True)
+ if data is None:
+ raise IOError(path) # Y_PYTHON_ENTRY_POINT=:resource_files
+ return data
+
+ # PEP-302 extension 2 of 3: get __file__ without importing.
+ def get_filename(self, fullname):
+ modname = fullname
+ if self.is_package(fullname):
+ fullname += '.__init__'
+ return resfs_src('/py_modules/' + fullname) or modname
+
+ # PEP-302 extension 3 of 3: packaging introspection.
+ # Used by `linecache` (while printing tracebacks) unless module filename
+ # exists on the filesystem.
+ def get_source(self, fullname):
+ fullname = self._source_name.get(fullname, fullname)
+ if self.is_package(fullname):
+ fullname += '.__init__'
+
+ abspath = resfs_resolve(self.get_filename(fullname))
+ if abspath:
+ return file_bytes(abspath)
+ return find_py_module(fullname)
+
+ def get_code(self, fullname):
+ modname = fullname
+ if self.is_package(fullname):
+ fullname += '.__init__'
+
+ abspath = resfs_resolve(self.get_filename(fullname))
+ if abspath:
+ data = file_bytes(abspath)
+ return compile(data, abspath, 'exec', dont_inherit=True)
+
+ pyc = find_py_code(fullname)
+ if pyc:
+ return marshal.loads(pyc)
+ else:
+ # This covers packages with no __init__.py in resources.
+ return compile('', modname, 'exec', dont_inherit=True)
+
+ def is_package(self, fullname):
+ if fullname in self.memory:
+ return False
+
+ if fullname + '.__init__' in self.memory:
+ return True
+
+ raise ImportError(fullname)
+
+ # Extension for contrib/python/coverage.
+ def file_source(self, filename):
+ """
+ Return the key of the module source by its resource path.
+ """
+ if not self.source_map:
+ for key, mod in iter_py_modules(with_keys=True):
+ path = self.get_filename(mod)
+ self.source_map[path] = key
+
+ if filename in self.source_map:
+ return self.source_map[filename]
+
+ if resfs_read(filename, builtin=True) is not None:
+ return 'resfs/file/' + filename
+
+ return ''
+
+ # Extension for pkgutil.iter_modules.
+ def iter_modules(self, prefix=''):
+ rx = re.compile(re.escape(self._package_prefix) + r'([^.]+)(\.__init__)?$')
+ for p in self.memory:
+ m = rx.match(p)
+ if m:
+ yield prefix + m.group(1), m.group(2) is not None
+
+ # PEP-302 loader.
+ def load_module(self, mod_name, fix_name=None):
+ code = self.get_code(mod_name)
+ is_package = self.is_package(mod_name)
+ source_name = self._source_name
+
+ mod = imp.new_module(mod_name)
+ mod.__loader__ = self
+ mod.__file__ = code.co_filename
+
+ if is_package:
+ mod.__path__ = [executable + path_sep + mod_name.replace('.', path_sep)]
+ mod.__package__ = mod_name
+ else:
+ mod.__package__ = mod_name.rpartition('.')[0]
+
+ if fix_name:
+ mod.__name__ = fix_name
+ self._source_name = dict(source_name, **{fix_name: mod_name})
+
+ old_mod = sys.modules.get(mod_name, None)
+ sys.modules[mod_name] = mod
+
+ try:
+ exec code in mod.__dict__
+ old_mod = sys.modules[mod_name]
+ finally:
+ sys.modules[mod_name] = old_mod
+
+ # Some hacky modules (e.g. pygments.lexers) replace themselves in
+ # `sys.modules` with proxies.
+ return sys.modules[mod_name]
+
+ def run_main(self):
+ entry_point = os.environ.pop(env_entry_point, None)
+
+ if entry_point is None:
+ entry_point = __resource.find('PY_MAIN')
+
+ if entry_point is None:
+ entry_point = '__main__'
+
+ if ':' in entry_point:
+ mod_name, func_name = entry_point.split(':', 1)
+ if mod_name == '':
+ mod_name = 'library.python.runtime.entry_points'
+ mod = importlib.import_module(mod_name)
+ func = getattr(mod, func_name)
+ return func()
+
+ if entry_point not in self.memory:
+ raise Exception(entry_point + ' not found')
+
+ self.load_module(entry_point, fix_name='__main__')
+
+
+importer = ResourceImporter()
+sys.meta_path.insert(0, importer)
+
+
+def executable_path_hook(path):
+ if path == executable:
+ return importer
+
+ if path.startswith(executable + path_sep):
+ return importer.for_package(path[len(executable + path_sep):].replace(path_sep, '.'))
+
+ raise ImportError(path)
+
+
+if executable not in sys.path:
+ sys.path.insert(0, executable)
+sys.path_hooks.insert(0, executable_path_hook)
+sys.path_importer_cache[executable] = importer
+
+# Indicator that modules and resources are built-in rather than on the file system.
+sys.is_standalone_binary = True
+sys.frozen = True
+
+# Set of names of importable modules.
+sys.extra_modules = importer.memory
+
+# Use pure-python implementation of traceback printer.
+# Built-in printer (PyTraceBack_Print) does not support custom module loaders
+sys.excepthook = traceback.print_exception
diff --git a/library/python/runtime/main/main.c b/library/python/runtime/main/main.c
new file mode 100644
index 0000000000..69dd80e558
--- /dev/null
+++ b/library/python/runtime/main/main.c
@@ -0,0 +1,46 @@
+#include <Python.h>
+
+#include <stdlib.h>
+#include <string.h>
+
+void Py_InitArgcArgv(int argc, char** argv);
+
+static const char* env_entry_point = "Y_PYTHON_ENTRY_POINT";
+
+#ifdef _MSC_VER
+extern char** environ;
+
+void unsetenv(const char* name) {
+ const int n = strlen(name);
+ char** dst = environ;
+ for (char** src = environ; *src; src++)
+ if (strncmp(*src, name, n) || (*src)[n] != '=')
+ *dst++ = *src;
+ *dst = NULL;
+}
+#endif
+
+static int pymain(int argc, char** argv) {
+ const char* entry_point = getenv(env_entry_point);
+ if (entry_point && !strcmp(entry_point, ":main")) {
+ unsetenv(env_entry_point);
+ return Py_Main(argc, argv);
+ }
+ Py_InitArgcArgv(argc, argv);
+ Py_SetProgramName(argv[0]);
+ Py_Initialize();
+ PySys_SetArgv(argc, argv);
+ int rc = PyRun_SimpleString(
+ "from library.python.runtime import entry_points\n"
+ "entry_points.run_constructors()\n"
+ "import __res\n"
+ "__res.importer.run_main()\n");
+ Py_Finalize();
+ return rc == 0 ? 0 : 1;
+}
+
+int (*mainptr)(int argc, char** argv) = pymain;
+
+int main(int argc, char** argv) {
+ return mainptr(argc, argv);
+}
diff --git a/library/python/runtime/main/ya.make b/library/python/runtime/main/ya.make
new file mode 100644
index 0000000000..245ffe680d
--- /dev/null
+++ b/library/python/runtime/main/ya.make
@@ -0,0 +1,15 @@
+LIBRARY()
+
+PEERDIR(
+ contrib/tools/python/base
+)
+
+ADDINCL(
+ contrib/tools/python/src/Include
+)
+
+SRCS(
+ main.c
+)
+
+END()
diff --git a/library/python/runtime/sitecustomize.pyx b/library/python/runtime/sitecustomize.pyx
new file mode 100644
index 0000000000..03fdf36085
--- /dev/null
+++ b/library/python/runtime/sitecustomize.pyx
@@ -0,0 +1 @@
+import __res
diff --git a/library/python/runtime/ya.make b/library/python/runtime/ya.make
new file mode 100644
index 0000000000..30147d241d
--- /dev/null
+++ b/library/python/runtime/ya.make
@@ -0,0 +1,32 @@
+PY2_LIBRARY()
+
+NO_WSHADOW()
+
+PEERDIR(
+ contrib/tools/python/lib
+ library/cpp/resource
+)
+
+CFLAGS(-DCYTHON_REGISTER_ABCS=0)
+
+NO_PYTHON_INCLUDES()
+
+PY_SRCS(
+ entry_points.py
+ TOP_LEVEL
+ __res.pyx
+ sitecustomize.pyx
+)
+
+IF (CYTHON_COVERAGE)
+ # Let covarage support add all needed files to resources
+ELSE()
+ RESOURCE_FILES(
+ PREFIX ${MODDIR}/
+ __res.pyx
+ importer.pxi
+ sitecustomize.pyx
+ )
+ENDIF()
+
+END()
diff --git a/library/python/runtime_py3/__res.pyx b/library/python/runtime_py3/__res.pyx
new file mode 100644
index 0000000000..97190d9f29
--- /dev/null
+++ b/library/python/runtime_py3/__res.pyx
@@ -0,0 +1,36 @@
+from _codecs import utf_8_decode, utf_8_encode
+
+from libcpp cimport bool
+
+from util.generic.string cimport TString, TStringBuf
+
+
+cdef extern from "library/cpp/resource/resource.h" namespace "NResource":
+ cdef size_t Count() except +
+ cdef TStringBuf KeyByIndex(size_t idx) except +
+ cdef bool FindExact(const TStringBuf key, TString* result) nogil except +
+
+
+def count():
+ return Count()
+
+
+def key_by_index(idx):
+ cdef TStringBuf ret = KeyByIndex(idx)
+
+ return ret.Data()[:ret.Size()]
+
+
+def find(s):
+ cdef TString res
+
+ if isinstance(s, str):
+ s = utf_8_encode(s)[0]
+
+ if FindExact(TStringBuf(s, len(s)), &res):
+ return res.c_str()[:res.length()]
+
+ return None
+
+
+include "importer.pxi"
diff --git a/library/python/runtime_py3/entry_points.py b/library/python/runtime_py3/entry_points.py
new file mode 100644
index 0000000000..37d9af864a
--- /dev/null
+++ b/library/python/runtime_py3/entry_points.py
@@ -0,0 +1,59 @@
+import sys
+
+import __res
+
+
+def repl():
+ user_ns = {}
+ py_main = __res.find("PY_MAIN")
+
+ if py_main:
+ mod_name, func_name = (py_main.split(b":", 1) + [None])[:2]
+ try:
+ import importlib
+
+ mod = importlib.import_module(mod_name.decode("UTF-8"))
+ user_ns = mod.__dict__
+ except ModuleNotFoundError:
+ import traceback
+
+ traceback.print_exc()
+
+ if func_name and "__main__" not in user_ns:
+
+ def run(args):
+ if isinstance(args, str):
+ import shlex
+
+ args = shlex.split(args)
+
+ import sys
+
+ sys.argv = [sys.argv[0]] + args
+ getattr(mod, func_name)()
+
+ user_ns["__main__"] = run
+
+ try:
+ import IPython
+ except ModuleNotFoundError:
+ pass
+ else:
+ return IPython.start_ipython(user_ns=user_ns)
+
+ import code
+
+ code.interact(local=user_ns)
+
+
+def resource_files():
+ sys.stdout.buffer.write(b"\n".join(sorted(__res.resfs_files()) + [b""]))
+
+
+def run_constructors():
+ for key, module_name in __res.iter_keys(b"py/constructors/"):
+ import importlib
+
+ module = importlib.import_module(module_name.decode())
+ init_func = getattr(module, __res.find(key).decode())
+ init_func()
diff --git a/library/python/runtime_py3/importer.pxi b/library/python/runtime_py3/importer.pxi
new file mode 100644
index 0000000000..6b3c5889a5
--- /dev/null
+++ b/library/python/runtime_py3/importer.pxi
@@ -0,0 +1,609 @@
+import marshal
+import sys
+from _codecs import utf_8_decode, utf_8_encode
+from _frozen_importlib import _call_with_frames_removed, spec_from_loader, BuiltinImporter
+from _frozen_importlib_external import _os, _path_isfile, _path_isabs, path_sep, _path_join, _path_split
+from _io import FileIO
+
+import __res as __resource
+
+_b = lambda x: x if isinstance(x, bytes) else utf_8_encode(x)[0]
+_s = lambda x: x if isinstance(x, str) else utf_8_decode(x)[0]
+env_entry_point = b'Y_PYTHON_ENTRY_POINT'
+env_source_root = b'Y_PYTHON_SOURCE_ROOT'
+cfg_source_root = b'arcadia-source-root'
+env_extended_source_search = b'Y_PYTHON_EXTENDED_SOURCE_SEARCH'
+res_ya_ide_venv = b'YA_IDE_VENV'
+executable = sys.executable or 'Y_PYTHON'
+sys.modules['run_import_hook'] = __resource
+
+# This is the prefix in contrib/tools/python3/src/Lib/ya.make.
+py_prefix = b'py/'
+py_prefix_len = len(py_prefix)
+
+YA_IDE_VENV = __resource.find(res_ya_ide_venv)
+Y_PYTHON_EXTENDED_SOURCE_SEARCH = _os.environ.get(env_extended_source_search) or YA_IDE_VENV
+
+
+def _init_venv():
+ if not _path_isabs(executable):
+ raise RuntimeError('path in sys.executable is not absolute: {}'.format(executable))
+
+ # Creative copy-paste from site.py
+ exe_dir, _ = _path_split(executable)
+ site_prefix, _ = _path_split(exe_dir)
+ libpath = _path_join(site_prefix, 'lib',
+ 'python%d.%d' % sys.version_info[:2],
+ 'site-packages')
+ sys.path.insert(0, libpath)
+
+ # emulate site.venv()
+ sys.prefix = site_prefix
+ sys.exec_prefix = site_prefix
+
+ conf_basename = 'pyvenv.cfg'
+ candidate_confs = [
+ conffile for conffile in (
+ _path_join(exe_dir, conf_basename),
+ _path_join(site_prefix, conf_basename)
+ )
+ if _path_isfile(conffile)
+ ]
+ if not candidate_confs:
+ raise RuntimeError('{} not found'.format(conf_basename))
+ virtual_conf = candidate_confs[0]
+ with FileIO(virtual_conf, 'r') as f:
+ for line in f:
+ if b'=' in line:
+ key, _, value = line.partition(b'=')
+ key = key.strip().lower()
+ value = value.strip()
+ if key == cfg_source_root:
+ return value
+ raise RuntimeError('{} key not found in {}'.format(cfg_source_root, virtual_conf))
+
+
+def _get_source_root():
+ env_value = _os.environ.get(env_source_root)
+ if env_value or not YA_IDE_VENV:
+ return env_value
+
+ return _init_venv()
+
+
+Y_PYTHON_SOURCE_ROOT = _get_source_root()
+
+
+def _print(*xs):
+ """
+ This is helpful for debugging, since automatic bytes to str conversion is
+ not available yet. It is also possible to debug with GDB by breaking on
+ __Pyx_AddTraceback (with Python GDB pretty printers enabled).
+ """
+ parts = []
+ for s in xs:
+ if not isinstance(s, (bytes, str)):
+ s = str(s)
+ parts.append(_s(s))
+ sys.stderr.write(' '.join(parts) + '\n')
+
+
+def file_bytes(path):
+ # 'open' is not avaiable yet.
+ with FileIO(path, 'r') as f:
+ return f.read()
+
+
+def iter_keys(prefix):
+ l = len(prefix)
+ for idx in range(__resource.count()):
+ key = __resource.key_by_index(idx)
+ if key.startswith(prefix):
+ yield key, key[l:]
+
+
+def iter_py_modules(with_keys=False):
+ for key, path in iter_keys(b'resfs/file/' + py_prefix):
+ if path.endswith(b'.py'): # It may also end with '.pyc'.
+ mod = _s(path[:-3].replace(b'/', b'.'))
+ if with_keys:
+ yield key, mod
+ else:
+ yield mod
+
+
+def py_src_key(filename):
+ return py_prefix + _b(filename)
+
+
+def iter_prefixes(s):
+ i = s.find('.')
+ while i >= 0:
+ yield s[:i]
+ i = s.find('.', i + 1)
+
+
+def resfs_resolve(path):
+ """
+ Return the absolute path of a root-relative path if it exists.
+ """
+ path = _b(path)
+ if Y_PYTHON_SOURCE_ROOT:
+ if not path.startswith(Y_PYTHON_SOURCE_ROOT):
+ path = _b(path_sep).join((Y_PYTHON_SOURCE_ROOT, path))
+ if _path_isfile(path):
+ return path
+
+
+def resfs_src(key, resfs_file=False):
+ """
+ Return the root-relative file path of a resource key.
+ """
+ if resfs_file:
+ key = b'resfs/file/' + _b(key)
+ return __resource.find(b'resfs/src/' + _b(key))
+
+
+def resfs_read(path, builtin=None):
+ """
+ Return the bytes of the resource file at path, or None.
+ If builtin is True, do not look for it on the filesystem.
+ If builtin is False, do not look in the builtin resources.
+ """
+ if builtin is not True:
+ arcpath = resfs_src(path, resfs_file=True)
+ if arcpath:
+ fspath = resfs_resolve(arcpath)
+ if fspath:
+ return file_bytes(fspath)
+
+ if builtin is not False:
+ return __resource.find(b'resfs/file/' + _b(path))
+
+
+def resfs_files(prefix=b''):
+ """
+ List builtin resource file paths.
+ """
+ return [key[11:] for key, _ in iter_keys(b'resfs/file/' + _b(prefix))]
+
+
+def mod_path(mod):
+ """
+ Return the resfs path to the source code of the module with the given name.
+ """
+ return py_prefix + _b(mod).replace(b'.', b'/') + b'.py'
+
+
+class ResourceImporter(object):
+
+ """ A meta_path importer that loads code from built-in resources.
+ """
+
+ def __init__(self):
+ self.memory = set(iter_py_modules()) # Set of importable module names.
+ self.source_map = {} # Map from file names to module names.
+ self._source_name = {} # Map from original to altered module names.
+ self._package_prefix = ''
+ if Y_PYTHON_SOURCE_ROOT and Y_PYTHON_EXTENDED_SOURCE_SEARCH:
+ self.arcadia_source_finder = ArcadiaSourceFinder(_s(Y_PYTHON_SOURCE_ROOT))
+ else:
+ self.arcadia_source_finder = None
+
+ for p in list(self.memory) + list(sys.builtin_module_names):
+ for pp in iter_prefixes(p):
+ k = pp + '.__init__'
+ if k not in self.memory:
+ self.memory.add(k)
+
+ def for_package(self, name):
+ import copy
+ importer = copy.copy(self)
+ importer._package_prefix = name + '.'
+ return importer
+
+ def _find_mod_path(self, fullname):
+ """Find arcadia relative path by module name"""
+ relpath = resfs_src(mod_path(fullname), resfs_file=True)
+ if relpath or not self.arcadia_source_finder:
+ return relpath
+ return self.arcadia_source_finder.get_module_path(fullname)
+
+ def find_spec(self, fullname, path=None, target=None):
+ try:
+ is_package = self.is_package(fullname)
+ except ImportError:
+ return None
+ return spec_from_loader(fullname, self, is_package=is_package)
+
+ def find_module(self, fullname, path=None):
+ """For backward compatibility."""
+ spec = self.find_spec(fullname, path)
+ return spec.loader if spec is not None else None
+
+ def create_module(self, spec):
+ """Use default semantics for module creation."""
+
+ def exec_module(self, module):
+ code = self.get_code(module.__name__)
+ module.__file__ = code.co_filename
+ if self.is_package(module.__name__):
+ module.__path__= [executable + path_sep + module.__name__.replace('.', path_sep)]
+ # exec(code, module.__dict__)
+ _call_with_frames_removed(exec, code, module.__dict__)
+
+ # PEP-302 extension 1 of 3: data loader.
+ def get_data(self, path):
+ path = _b(path)
+ abspath = resfs_resolve(path)
+ if abspath:
+ return file_bytes(abspath)
+ path = path.replace(_b('\\'), _b('/'))
+ data = resfs_read(path, builtin=True)
+ if data is None:
+ raise IOError(path) # Y_PYTHON_ENTRY_POINT=:resource_files
+ return data
+
+ # PEP-302 extension 2 of 3: get __file__ without importing.
+ def get_filename(self, fullname):
+ modname = fullname
+ if self.is_package(fullname):
+ fullname += '.__init__'
+ relpath = self._find_mod_path(fullname)
+ if isinstance(relpath, bytes):
+ relpath = _s(relpath)
+ return relpath or modname
+
+ # PEP-302 extension 3 of 3: packaging introspection.
+ # Used by `linecache` (while printing tracebacks) unless module filename
+ # exists on the filesystem.
+ def get_source(self, fullname):
+ fullname = self._source_name.get(fullname) or fullname
+ if self.is_package(fullname):
+ fullname += '.__init__'
+
+ relpath = self.get_filename(fullname)
+ if relpath:
+ abspath = resfs_resolve(relpath)
+ if abspath:
+ return _s(file_bytes(abspath))
+ data = resfs_read(mod_path(fullname))
+ return _s(data) if data else ''
+
+ def get_code(self, fullname):
+ modname = fullname
+ if self.is_package(fullname):
+ fullname += '.__init__'
+
+ path = mod_path(fullname)
+ relpath = self._find_mod_path(fullname)
+ if relpath:
+ abspath = resfs_resolve(relpath)
+ if abspath:
+ data = file_bytes(abspath)
+ return compile(data, _s(abspath), 'exec', dont_inherit=True)
+
+ yapyc_path = path + b'.yapyc3'
+ yapyc_data = resfs_read(yapyc_path, builtin=True)
+ if yapyc_data:
+ return marshal.loads(yapyc_data)
+ else:
+ py_data = resfs_read(path, builtin=True)
+ if py_data:
+ return compile(py_data, _s(relpath), 'exec', dont_inherit=True)
+ else:
+ # This covers packages with no __init__.py in resources.
+ return compile('', modname, 'exec', dont_inherit=True)
+
+ def is_package(self, fullname):
+ if fullname in self.memory:
+ return False
+
+ if fullname + '.__init__' in self.memory:
+ return True
+
+ if self.arcadia_source_finder:
+ return self.arcadia_source_finder.is_package(fullname)
+
+ raise ImportError(fullname)
+
+ # Extension for contrib/python/coverage.
+ def file_source(self, filename):
+ """
+ Return the key of the module source by its resource path.
+ """
+ if not self.source_map:
+ for key, mod in iter_py_modules(with_keys=True):
+ path = self.get_filename(mod)
+ self.source_map[path] = key
+
+ if filename in self.source_map:
+ return self.source_map[filename]
+
+ if resfs_read(filename, builtin=True) is not None:
+ return b'resfs/file/' + _b(filename)
+
+ return b''
+
+ # Extension for pkgutil.iter_modules.
+ def iter_modules(self, prefix=''):
+ import re
+ rx = re.compile(re.escape(self._package_prefix) + r'([^.]+)(\.__init__)?$')
+ for p in self.memory:
+ m = rx.match(p)
+ if m:
+ yield prefix + m.group(1), m.group(2) is not None
+ if self.arcadia_source_finder:
+ for m in self.arcadia_source_finder.iter_modules(self._package_prefix, prefix):
+ yield m
+
+ def get_resource_reader(self, fullname):
+ try:
+ if not self.is_package(fullname):
+ return None
+ except ImportError:
+ return None
+ return _ResfsResourceReader(self, fullname)
+
+
+class _ResfsResourceReader:
+
+ def __init__(self, importer, fullname):
+ self.importer = importer
+ self.fullname = fullname
+
+ import os
+ self.prefix = "{}/".format(os.path.dirname(self.importer.get_filename(self.fullname)))
+
+ def open_resource(self, resource):
+ path = f'{self.prefix}{resource}'
+ from io import BytesIO
+ try:
+ return BytesIO(self.importer.get_data(path))
+ except OSError:
+ raise FileNotFoundError(path)
+
+ def resource_path(self, resource):
+ # All resources are in the binary file, so there is no path to the file.
+ # Raising FileNotFoundError tells the higher level API to extract the
+ # binary data and create a temporary file.
+ raise FileNotFoundError
+
+ def is_resource(self, name):
+ path = f'{self.prefix}{name}'
+ try:
+ self.importer.get_data(path)
+ except OSError:
+ return False
+ return True
+
+ def contents(self):
+ subdirs_seen = set()
+ for key in resfs_files(self.prefix):
+ relative = key[len(self.prefix):]
+ res_or_subdir, *other = relative.split(b'/')
+ if not other:
+ yield _s(res_or_subdir)
+ elif res_or_subdir not in subdirs_seen:
+ subdirs_seen.add(res_or_subdir)
+ yield _s(res_or_subdir)
+
+
+class BuiltinSubmoduleImporter(BuiltinImporter):
+ @classmethod
+ def find_spec(cls, fullname, path=None, target=None):
+ if path is not None:
+ return super().find_spec(fullname, None, target)
+ else:
+ return None
+
+
+class ArcadiaSourceFinder:
+ """
+ Search modules and packages in arcadia source tree.
+ See https://wiki.yandex-team.ru/devtools/extended-python-source-search/ for details
+ """
+ NAMESPACE_PREFIX = b'py/namespace/'
+ PY_EXT = '.py'
+ YA_MAKE = 'ya.make'
+ S_IFDIR = 0o040000
+
+ def __init__(self, source_root):
+ self.source_root = source_root
+ self.module_path_cache = {'': set()}
+ for key, dirty_path in iter_keys(self.NAMESPACE_PREFIX):
+ # dirty_path contains unique prefix to prevent repeatable keys in the resource storage
+ path = dirty_path.split(b'/', 1)[1]
+ namespaces = __resource.find(key).split(b':')
+ for n in namespaces:
+ package_name = _s(n.rstrip(b'.'))
+ self.module_path_cache.setdefault(package_name, set()).add(_s(path))
+ # Fill parents with default empty path set if parent doesn't exist in the cache yet
+ while package_name:
+ package_name = package_name.rpartition('.')[0]
+ if package_name in self.module_path_cache:
+ break
+ self.module_path_cache.setdefault(package_name, set())
+ for package_name in self.module_path_cache.keys():
+ self._add_parent_dirs(package_name, visited=set())
+
+ def get_module_path(self, fullname):
+ """
+ Find file path for module 'fullname'.
+ For packages caller pass fullname as 'package.__init__'.
+ Return None if nothing is found.
+ """
+ try:
+ if not self.is_package(fullname):
+ return _b(self._cache_module_path(fullname))
+ except ImportError:
+ pass
+
+ def is_package(self, fullname):
+ """Check if fullname is a package. Raise ImportError if fullname is not found"""
+ path = self._cache_module_path(fullname)
+ if isinstance(path, set):
+ return True
+ if isinstance(path, str):
+ return False
+ raise ImportError(fullname)
+
+ def iter_modules(self, package_prefix, prefix):
+ paths = self._cache_module_path(package_prefix.rstrip('.'))
+ if paths is not None:
+ # Note: it's ok to yield duplicates because pkgutil discards them
+
+ # Yield from cache
+ import re
+ rx = re.compile(re.escape(package_prefix) + r'([^.]+)$')
+ # Save result to temporary list to prevent 'RuntimeError: dictionary changed size during iteration'
+ found = []
+ for mod, path in self.module_path_cache.items():
+ if path is not None:
+ m = rx.match(mod)
+ if m:
+ found.append((prefix + m.group(1), self.is_package(mod)))
+ for cm in found:
+ yield cm
+
+ # Yield from file system
+ for path in paths:
+ abs_path = _path_join(self.source_root, path)
+ for dir_item in _os.listdir(abs_path):
+ if self._path_is_simple_dir(_path_join(abs_path, dir_item)):
+ yield prefix + dir_item, True
+ elif dir_item.endswith(self.PY_EXT) and _path_isfile(_path_join(abs_path, dir_item)):
+ yield prefix + dir_item[:-len(self.PY_EXT)], False
+
+ def _isdir(self, path):
+ """ Unlike _path_isdir() this function don't follow symlink """
+ try:
+ stat_info = _os.lstat(path)
+ except OSError:
+ return False
+ return (stat_info.st_mode & 0o170000) == self.S_IFDIR
+
+ def _path_is_simple_dir(self, abs_path):
+ """
+ Check if path is a directory but doesn't contain ya.make file.
+ We don't want to steal directory from nested project and treat it as a package
+ """
+ return self._isdir(abs_path) and not _path_isfile(_path_join(abs_path, self.YA_MAKE))
+
+ def _find_module_in_paths(self, find_package_only, paths, module):
+ """Auxiliary method. See _cache_module_path() for details"""
+ if paths:
+ package_paths = set()
+ for path in paths:
+ rel_path = _path_join(path, module)
+ if not find_package_only:
+ # Check if file_path is a module
+ module_path = rel_path + self.PY_EXT
+ if _path_isfile(_path_join(self.source_root, module_path)):
+ return module_path
+ # Check if file_path is a package
+ if self._path_is_simple_dir(_path_join(self.source_root, rel_path)):
+ package_paths.add(rel_path)
+ if package_paths:
+ return package_paths
+
+ def _cache_module_path(self, fullname, find_package_only=False):
+ """
+ Find module path or package directory paths and save result in the cache
+
+ find_package_only=True - don't try to find module
+
+ Returns:
+ List of relative package paths - for a package
+ Relative module path - for a module
+ None - module or package is not found
+ """
+ if fullname not in self.module_path_cache:
+ parent, _, tail = fullname.rpartition('.')
+ parent_paths = self._cache_module_path(parent, find_package_only=True)
+ self.module_path_cache[fullname] = self._find_module_in_paths(find_package_only, parent_paths, tail)
+ return self.module_path_cache[fullname]
+
+ def _add_parent_dirs(self, package_name, visited):
+ if not package_name or package_name in visited:
+ return
+ visited.add(package_name)
+
+ parent, _, tail = package_name.rpartition('.')
+ self._add_parent_dirs(parent, visited)
+
+ paths = self.module_path_cache[package_name]
+ for parent_path in self.module_path_cache[parent]:
+ rel_path = _path_join(parent_path, tail)
+ if self._path_is_simple_dir(_path_join(self.source_root, rel_path)):
+ paths.add(rel_path)
+
+
+def excepthook(*args, **kws):
+ # traceback module cannot be imported at module level, because interpreter
+ # is not fully initialized yet
+
+ import traceback
+
+ return traceback.print_exception(*args, **kws)
+
+
+importer = ResourceImporter()
+
+
+def executable_path_hook(path):
+ if path == executable:
+ return importer
+
+ if path.startswith(executable + path_sep):
+ return importer.for_package(path[len(executable + path_sep):].replace(path_sep, '.'))
+
+ raise ImportError(path)
+
+
+def get_path0():
+ """
+ An incomplete and simplified version of _PyPathConfig_ComputeSysPath0.
+ We need this to somewhat properly emulate the behaviour of a normal python interpreter
+ when using ya ide venv.
+
+ """
+ if not sys.argv:
+ return
+ argv0 = sys.argv[0]
+
+ have_module_arg = argv0 == '-m'
+
+ if have_module_arg:
+ return _os.getcwd()
+
+
+if YA_IDE_VENV:
+ sys.meta_path.append(importer)
+ sys.meta_path.append(BuiltinSubmoduleImporter)
+ if executable not in sys.path:
+ sys.path.append(executable)
+ path0 = get_path0()
+ if path0 is not None:
+ sys.path.insert(0, path0)
+
+ sys.path_hooks.append(executable_path_hook)
+else:
+ sys.meta_path.insert(0, BuiltinSubmoduleImporter)
+ sys.meta_path.insert(0, importer)
+ if executable not in sys.path:
+ sys.path.insert(0, executable)
+ sys.path_hooks.insert(0, executable_path_hook)
+
+sys.path_importer_cache[executable] = importer
+
+# Indicator that modules and resources are built-in rather than on the file system.
+sys.is_standalone_binary = True
+sys.frozen = True
+
+# Set of names of importable modules.
+sys.extra_modules = importer.memory
+
+# Use custom implementation of traceback printer.
+# Built-in printer (PyTraceBack_Print) does not support custom module loaders
+sys.excepthook = excepthook
diff --git a/library/python/runtime_py3/main/get_py_main.cpp b/library/python/runtime_py3/main/get_py_main.cpp
new file mode 100644
index 0000000000..67c400d4f4
--- /dev/null
+++ b/library/python/runtime_py3/main/get_py_main.cpp
@@ -0,0 +1,8 @@
+#include <library/cpp/resource/resource.h>
+
+#include <stdlib.h>
+
+extern "C" char* GetPyMain() {
+ TString res = NResource::Find("PY_MAIN");
+ return strdup(res.c_str());
+}
diff --git a/library/python/runtime_py3/main/main.c b/library/python/runtime_py3/main/main.c
new file mode 100644
index 0000000000..bf80678d26
--- /dev/null
+++ b/library/python/runtime_py3/main/main.c
@@ -0,0 +1,209 @@
+#include <Python.h>
+#include <contrib/tools/python3/src/Include/internal/pycore_runtime.h> // _PyRuntime_Initialize()
+
+#include <stdlib.h>
+#include <string.h>
+#include <locale.h>
+
+char* GetPyMain();
+int IsYaIdeVenv();
+
+static const char* env_entry_point = "Y_PYTHON_ENTRY_POINT";
+static const char* main_entry_point = ":main";
+static const char* env_bytes_warning = "Y_PYTHON_BYTES_WARNING";
+
+#ifdef _MSC_VER
+extern char** environ;
+
+void unsetenv(const char* name) {
+ const int n = strlen(name);
+ char** dst = environ;
+ for (char** src = environ; *src; src++)
+ if (strncmp(*src, name, n) || (*src)[n] != '=')
+ *dst++ = *src;
+ *dst = NULL;
+}
+#endif
+
+static int RunModule(const char* modname)
+{
+ PyObject *module, *runpy, *runmodule, *runargs, *result;
+ runpy = PyImport_ImportModule("runpy");
+ if (runpy == NULL) {
+ fprintf(stderr, "Could not import runpy module\n");
+ PyErr_Print();
+ return -1;
+ }
+ runmodule = PyObject_GetAttrString(runpy, "_run_module_as_main");
+ if (runmodule == NULL) {
+ fprintf(stderr, "Could not access runpy._run_module_as_main\n");
+ PyErr_Print();
+ Py_DECREF(runpy);
+ return -1;
+ }
+ module = PyUnicode_FromString(modname);
+ if (module == NULL) {
+ fprintf(stderr, "Could not convert module name to unicode\n");
+ PyErr_Print();
+ Py_DECREF(runpy);
+ Py_DECREF(runmodule);
+ return -1;
+ }
+ runargs = Py_BuildValue("(Oi)", module, 0);
+ if (runargs == NULL) {
+ fprintf(stderr,
+ "Could not create arguments for runpy._run_module_as_main\n");
+ PyErr_Print();
+ Py_DECREF(runpy);
+ Py_DECREF(runmodule);
+ Py_DECREF(module);
+ return -1;
+ }
+ result = PyObject_Call(runmodule, runargs, NULL);
+ if (result == NULL) {
+ PyErr_Print();
+ }
+ Py_DECREF(runpy);
+ Py_DECREF(runmodule);
+ Py_DECREF(module);
+ Py_DECREF(runargs);
+ if (result == NULL) {
+ return -1;
+ }
+ Py_DECREF(result);
+ return 0;
+}
+
+static int pymain(int argc, char** argv) {
+ if (IsYaIdeVenv()) {
+ return Py_BytesMain(argc, argv);
+ }
+
+ PyStatus status = _PyRuntime_Initialize();
+ if (PyStatus_Exception(status)) {
+ Py_ExitStatusException(status);
+ }
+
+ int sts = 1;
+ char* entry_point_copy = NULL;
+
+ PyConfig config;
+ PyConfig_InitPythonConfig(&config);
+ // Suppress errors from getpath.c
+ config.pathconfig_warnings = 0;
+ // Disable parsing command line arguments
+ config.parse_argv = 0;
+
+ const char* bytes_warning = getenv(env_bytes_warning);
+ if (bytes_warning) {
+ config.bytes_warning = atoi(bytes_warning);
+ }
+
+ if (argc > 0 && argv) {
+ status = PyConfig_SetBytesString(&config, &config.program_name, argv[0]);
+ if (PyStatus_Exception(status)) {
+ goto error;
+ }
+
+ status = PyConfig_SetBytesArgv(&config, argc, argv);
+ if (PyStatus_Exception(status)) {
+ goto error;
+ }
+ }
+
+ const char* entry_point = getenv(env_entry_point);
+ if (entry_point) {
+ entry_point_copy = strdup(entry_point);
+ if (!entry_point_copy) {
+ fprintf(stderr, "out of memory\n");
+ goto error;
+ }
+ } else {
+ entry_point_copy = GetPyMain();
+ }
+
+ if (entry_point_copy == NULL) {
+ fprintf(stderr, "No entry point, did you forget PY_MAIN?\n");
+ goto error;
+ }
+
+ if (entry_point_copy && !strcmp(entry_point_copy, main_entry_point)) {
+ unsetenv(env_entry_point);
+ // Py_InitializeFromConfig freeze environ, so we need to finish all manipulations with environ before
+ }
+
+ status = Py_InitializeFromConfig(&config);
+
+ PyConfig_Clear(&config);
+ if (PyStatus_Exception(status)) {
+ Py_ExitStatusException(status);
+ }
+
+ if (entry_point_copy && !strcmp(entry_point_copy, main_entry_point)) {
+ sts = Py_BytesMain(argc, argv);
+ free(entry_point_copy);
+ return sts;
+ }
+
+ {
+ PyObject* module = PyImport_ImportModule("library.python.runtime_py3.entry_points");
+ if (module == NULL) {
+ PyErr_Print();
+ } else {
+ PyObject* res = PyObject_CallMethod(module, "run_constructors", NULL);
+ if (res == NULL) {
+ PyErr_Print();
+ } else {
+ Py_DECREF(res);
+ }
+ Py_DECREF(module);
+ }
+ }
+
+ const char* module_name = entry_point_copy;
+ const char* func_name = NULL;
+
+ char* colon = strchr(entry_point_copy, ':');
+ if (colon != NULL) {
+ colon[0] = '\0';
+ func_name = colon + 1;
+ }
+ if (module_name[0] == '\0') {
+ module_name = "library.python.runtime_py3.entry_points";
+ }
+
+ if (!func_name) {
+ sts = RunModule(module_name);
+ } else {
+ PyObject* module = PyImport_ImportModule(module_name);
+
+ if (module == NULL) {
+ PyErr_Print();
+ } else {
+ PyObject* value = PyObject_CallMethod(module, func_name, NULL);
+
+ if (value == NULL) {
+ PyErr_Print();
+ } else {
+ Py_DECREF(value);
+ sts = 0;
+ }
+
+ Py_DECREF(module);
+ }
+ }
+
+ if (Py_FinalizeEx() < 0) {
+ sts = 120;
+ }
+
+error:
+ free(entry_point_copy);
+ return sts;
+}
+
+int (*mainptr)(int argc, char** argv) = pymain;
+
+int main(int argc, char** argv) {
+ return mainptr(argc, argv);
+}
diff --git a/library/python/runtime_py3/main/venv.cpp b/library/python/runtime_py3/main/venv.cpp
new file mode 100644
index 0000000000..b26611b918
--- /dev/null
+++ b/library/python/runtime_py3/main/venv.cpp
@@ -0,0 +1,6 @@
+#include <library/cpp/resource/resource.h>
+
+extern "C" int IsYaIdeVenv() {
+ TString dummy;
+ return NResource::FindExact("YA_IDE_VENV", &dummy);
+}
diff --git a/library/python/runtime_py3/main/ya.make b/library/python/runtime_py3/main/ya.make
new file mode 100644
index 0000000000..bfd0955b0f
--- /dev/null
+++ b/library/python/runtime_py3/main/ya.make
@@ -0,0 +1,22 @@
+LIBRARY()
+
+PEERDIR(
+ contrib/tools/python3/src
+ library/cpp/resource
+)
+
+ADDINCL(
+ contrib/tools/python3/src/Include
+)
+
+CFLAGS(
+ -DPy_BUILD_CORE
+)
+
+SRCS(
+ main.c
+ get_py_main.cpp
+ venv.cpp
+)
+
+END()
diff --git a/library/python/runtime_py3/sitecustomize.pyx b/library/python/runtime_py3/sitecustomize.pyx
new file mode 100644
index 0000000000..f1076b87d4
--- /dev/null
+++ b/library/python/runtime_py3/sitecustomize.pyx
@@ -0,0 +1,74 @@
+import pathlib
+import re
+import sys
+
+import __res
+
+from importlib.abc import ResourceReader
+from importlib.metadata import Distribution, DistributionFinder, PackageNotFoundError, Prepared
+
+ResourceReader.register(__res._ResfsResourceReader)
+
+METADATA_NAME = re.compile('^Name: (.*)$', re.MULTILINE)
+
+
+class ArcadiaDistribution(Distribution):
+
+ def __init__(self, prefix):
+ self.prefix = prefix
+
+ @property
+ def _path(self):
+ return pathlib.Path(self.prefix)
+
+ def read_text(self, filename):
+ data = __res.resfs_read(f'{self.prefix}{filename}')
+ if data:
+ return data.decode('utf-8')
+ read_text.__doc__ = Distribution.read_text.__doc__
+
+ def locate_file(self, path):
+ return f'{self.prefix}{path}'
+
+
+class ArcadiaMetadataFinder(DistributionFinder):
+
+ prefixes = {}
+
+ @classmethod
+ def find_distributions(cls, context=DistributionFinder.Context()):
+ found = cls._search_prefixes(context.name)
+ return map(ArcadiaDistribution, found)
+
+ @classmethod
+ def _init_prefixes(cls):
+ cls.prefixes.clear()
+
+ for resource in __res.resfs_files():
+ resource = resource.decode('utf-8')
+ if not resource.endswith('METADATA'):
+ continue
+ data = __res.resfs_read(resource).decode('utf-8')
+ metadata_name = METADATA_NAME.search(data)
+ if metadata_name:
+ metadata_name = Prepared(metadata_name.group(1))
+ cls.prefixes[metadata_name.normalized] = resource[:-len('METADATA')]
+
+ @classmethod
+ def _search_prefixes(cls, name):
+ if not cls.prefixes:
+ cls._init_prefixes()
+
+ if name:
+ try:
+ yield cls.prefixes[Prepared(name).normalized]
+ except KeyError:
+ raise PackageNotFoundError(name)
+ else:
+ for prefix in sorted(cls.prefixes.values()):
+ yield prefix
+
+
+# monkeypatch standart library
+import importlib.metadata
+importlib.metadata.MetadataPathFinder = ArcadiaMetadataFinder
diff --git a/library/python/runtime_py3/test/.dist-info/METADATA b/library/python/runtime_py3/test/.dist-info/METADATA
new file mode 100644
index 0000000000..bb36162199
--- /dev/null
+++ b/library/python/runtime_py3/test/.dist-info/METADATA
@@ -0,0 +1,14 @@
+Metadata-Version: 2.1
+Name: foo-bar
+Version: 1.2.3
+Summary:
+Home-page: https://foo.org/
+Author: Foo
+Author-email: foo@ya.com
+License: UNKNOWN
+Platform: any
+Classifier: Development Status :: 4 - Beta
+Classifier: Programming Language :: Python :: 3
+Requires-Python: >=3.8
+Requires-Dist: Werkzeug (>=0.15)
+Requires-Dist: Jinja2 (>=2.10.1)
diff --git a/library/python/runtime_py3/test/.dist-info/RECORD b/library/python/runtime_py3/test/.dist-info/RECORD
new file mode 100644
index 0000000000..dabbbff80d
--- /dev/null
+++ b/library/python/runtime_py3/test/.dist-info/RECORD
@@ -0,0 +1 @@
+foo_bar.py,sha256=0000000000000000000000000000000000000000000,20
diff --git a/library/python/runtime_py3/test/.dist-info/entry_points.txt b/library/python/runtime_py3/test/.dist-info/entry_points.txt
new file mode 100644
index 0000000000..f5e2fd2657
--- /dev/null
+++ b/library/python/runtime_py3/test/.dist-info/entry_points.txt
@@ -0,0 +1,2 @@
+[console_scripts]
+foo_cli = foo_bar:cli
diff --git a/library/python/runtime_py3/test/.dist-info/top_level.txt b/library/python/runtime_py3/test/.dist-info/top_level.txt
new file mode 100644
index 0000000000..d2c068bc6b
--- /dev/null
+++ b/library/python/runtime_py3/test/.dist-info/top_level.txt
@@ -0,0 +1 @@
+foo_bar
diff --git a/library/python/runtime_py3/test/canondata/result.json b/library/python/runtime_py3/test/canondata/result.json
new file mode 100644
index 0000000000..8e684deb43
--- /dev/null
+++ b/library/python/runtime_py3/test/canondata/result.json
@@ -0,0 +1,50 @@
+{
+ "test_traceback.test_traceback[custom-default]": {
+ "stderr": {
+ "uri": "file://test_traceback.test_traceback_custom-default_/stderr.txt"
+ },
+ "stdout": {
+ "uri": "file://test_traceback.test_traceback_custom-default_/stdout.txt"
+ }
+ },
+ "test_traceback.test_traceback[custom-ultratb_color]": {
+ "stderr": {
+ "uri": "file://test_traceback.test_traceback_custom-ultratb_color_/stderr.txt"
+ },
+ "stdout": {
+ "uri": "file://test_traceback.test_traceback_custom-ultratb_color_/stdout.txt"
+ }
+ },
+ "test_traceback.test_traceback[custom-ultratb_verbose]": {
+ "stderr": {
+ "uri": "file://test_traceback.test_traceback_custom-ultratb_verbose_/stderr.txt"
+ },
+ "stdout": {
+ "uri": "file://test_traceback.test_traceback_custom-ultratb_verbose_/stdout.txt"
+ }
+ },
+ "test_traceback.test_traceback[main-default]": {
+ "stderr": {
+ "uri": "file://test_traceback.test_traceback_main-default_/stderr.txt"
+ },
+ "stdout": {
+ "uri": "file://test_traceback.test_traceback_main-default_/stdout.txt"
+ }
+ },
+ "test_traceback.test_traceback[main-ultratb_color]": {
+ "stderr": {
+ "uri": "file://test_traceback.test_traceback_main-ultratb_color_/stderr.txt"
+ },
+ "stdout": {
+ "uri": "file://test_traceback.test_traceback_main-ultratb_color_/stdout.txt"
+ }
+ },
+ "test_traceback.test_traceback[main-ultratb_verbose]": {
+ "stderr": {
+ "uri": "file://test_traceback.test_traceback_main-ultratb_verbose_/stderr.txt"
+ },
+ "stdout": {
+ "uri": "file://test_traceback.test_traceback_main-ultratb_verbose_/stdout.txt"
+ }
+ }
+}
diff --git a/library/python/runtime_py3/test/canondata/test_traceback.test_traceback_custom-default_/stderr.txt b/library/python/runtime_py3/test/canondata/test_traceback.test_traceback_custom-default_/stderr.txt
new file mode 100644
index 0000000000..a18678b50e
--- /dev/null
+++ b/library/python/runtime_py3/test/canondata/test_traceback.test_traceback_custom-default_/stderr.txt
@@ -0,0 +1,14 @@
+Traceback (most recent call last):
+ File "library/python/runtime_py3/test/traceback/crash.py", line 42, in main
+ one()
+ File "library/python/runtime_py3/test/traceback/crash.py", line 11, in one
+ modfunc(two) # aaa
+ ^^^^^^^^^^^^
+ File "library/python/runtime_py3/test/traceback/mod/__init__.py", line 3, in modfunc
+ f() # call back to caller
+ ^^^
+ File "library/python/runtime_py3/test/traceback/crash.py", line 15, in two
+ three(42)
+ File "library/python/runtime_py3/test/traceback/crash.py", line 19, in three
+ raise RuntimeError(f"Kaboom! I'm dead: {x}")
+RuntimeError: Kaboom! I'm dead: 42
diff --git a/library/python/runtime_py3/test/canondata/test_traceback.test_traceback_custom-default_/stdout.txt b/library/python/runtime_py3/test/canondata/test_traceback.test_traceback_custom-default_/stdout.txt
new file mode 100644
index 0000000000..2c9793eb14
--- /dev/null
+++ b/library/python/runtime_py3/test/canondata/test_traceback.test_traceback_custom-default_/stdout.txt
@@ -0,0 +1,2 @@
+__name__ = library.python.runtime_py3.test.traceback.crash
+__file__ = library/python/runtime_py3/test/traceback/crash.py
diff --git a/library/python/runtime_py3/test/canondata/test_traceback.test_traceback_custom-ultratb_color_/stderr.txt b/library/python/runtime_py3/test/canondata/test_traceback.test_traceback_custom-ultratb_color_/stderr.txt
new file mode 100644
index 0000000000..a28234f11c
--- /dev/null
+++ b/library/python/runtime_py3/test/canondata/test_traceback.test_traceback_custom-ultratb_color_/stderr.txt
@@ -0,0 +1,13 @@
+Traceback (most recent call last):
+ File library/python/runtime_py3/test/traceback/crash.py:42 in main
+ one()
+ File library/python/runtime_py3/test/traceback/crash.py:11 in one
+ modfunc(two) # aaa
+ File library/python/runtime_py3/test/traceback/mod/__init__.py:3 in modfunc
+ f() # call back to caller
+ File library/python/runtime_py3/test/traceback/crash.py:15 in two
+ three(42)
+ File library/python/runtime_py3/test/traceback/crash.py:19 in three
+ raise RuntimeError(f"Kaboom! I'm dead: {x}")
+RuntimeError: Kaboom! I'm dead: 42
+
diff --git a/library/python/runtime_py3/test/canondata/test_traceback.test_traceback_custom-ultratb_color_/stdout.txt b/library/python/runtime_py3/test/canondata/test_traceback.test_traceback_custom-ultratb_color_/stdout.txt
new file mode 100644
index 0000000000..2c9793eb14
--- /dev/null
+++ b/library/python/runtime_py3/test/canondata/test_traceback.test_traceback_custom-ultratb_color_/stdout.txt
@@ -0,0 +1,2 @@
+__name__ = library.python.runtime_py3.test.traceback.crash
+__file__ = library/python/runtime_py3/test/traceback/crash.py
diff --git a/library/python/runtime_py3/test/canondata/test_traceback.test_traceback_custom-ultratb_verbose_/stderr.txt b/library/python/runtime_py3/test/canondata/test_traceback.test_traceback_custom-ultratb_verbose_/stderr.txt
new file mode 100644
index 0000000000..f5545c6976
--- /dev/null
+++ b/library/python/runtime_py3/test/canondata/test_traceback.test_traceback_custom-ultratb_verbose_/stderr.txt
@@ -0,0 +1,27 @@
+---------------------------------------------------------------------------
+RuntimeError Traceback (most recent call last)
+File library/python/runtime_py3/test/traceback/crash.py:42, in main()
+ 39 time.time = lambda: 1531996624.0 # Freeze time
+ 40 sys.executable = "<traceback test>"
+---> 42 one()
+
+File library/python/runtime_py3/test/traceback/crash.py:11, in one()
+ 10 def one():
+---> 11 modfunc(two)
+
+File library/python/runtime_py3/test/traceback/mod/__init__.py:3, in modfunc(f=<function two>)
+ 1 def modfunc(f):
+ 2 # lalala
+----> 3 f()
+ f = <function two>
+
+File library/python/runtime_py3/test/traceback/crash.py:15, in two()
+ 14 def two():
+---> 15 three(42)
+
+File library/python/runtime_py3/test/traceback/crash.py:19, in three(x=42)
+ 18 def three(x):
+---> 19 raise RuntimeError(f"Kaboom! I'm dead: {x}")
+ x = 42
+
+RuntimeError: Kaboom! I'm dead: 42
diff --git a/library/python/runtime_py3/test/canondata/test_traceback.test_traceback_custom-ultratb_verbose_/stdout.txt b/library/python/runtime_py3/test/canondata/test_traceback.test_traceback_custom-ultratb_verbose_/stdout.txt
new file mode 100644
index 0000000000..2c9793eb14
--- /dev/null
+++ b/library/python/runtime_py3/test/canondata/test_traceback.test_traceback_custom-ultratb_verbose_/stdout.txt
@@ -0,0 +1,2 @@
+__name__ = library.python.runtime_py3.test.traceback.crash
+__file__ = library/python/runtime_py3/test/traceback/crash.py
diff --git a/library/python/runtime_py3/test/canondata/test_traceback.test_traceback_main-default_/stderr.txt b/library/python/runtime_py3/test/canondata/test_traceback.test_traceback_main-default_/stderr.txt
new file mode 100644
index 0000000000..a18678b50e
--- /dev/null
+++ b/library/python/runtime_py3/test/canondata/test_traceback.test_traceback_main-default_/stderr.txt
@@ -0,0 +1,14 @@
+Traceback (most recent call last):
+ File "library/python/runtime_py3/test/traceback/crash.py", line 42, in main
+ one()
+ File "library/python/runtime_py3/test/traceback/crash.py", line 11, in one
+ modfunc(two) # aaa
+ ^^^^^^^^^^^^
+ File "library/python/runtime_py3/test/traceback/mod/__init__.py", line 3, in modfunc
+ f() # call back to caller
+ ^^^
+ File "library/python/runtime_py3/test/traceback/crash.py", line 15, in two
+ three(42)
+ File "library/python/runtime_py3/test/traceback/crash.py", line 19, in three
+ raise RuntimeError(f"Kaboom! I'm dead: {x}")
+RuntimeError: Kaboom! I'm dead: 42
diff --git a/library/python/runtime_py3/test/canondata/test_traceback.test_traceback_main-default_/stdout.txt b/library/python/runtime_py3/test/canondata/test_traceback.test_traceback_main-default_/stdout.txt
new file mode 100644
index 0000000000..2c9793eb14
--- /dev/null
+++ b/library/python/runtime_py3/test/canondata/test_traceback.test_traceback_main-default_/stdout.txt
@@ -0,0 +1,2 @@
+__name__ = library.python.runtime_py3.test.traceback.crash
+__file__ = library/python/runtime_py3/test/traceback/crash.py
diff --git a/library/python/runtime_py3/test/canondata/test_traceback.test_traceback_main-ultratb_color_/stderr.txt b/library/python/runtime_py3/test/canondata/test_traceback.test_traceback_main-ultratb_color_/stderr.txt
new file mode 100644
index 0000000000..a28234f11c
--- /dev/null
+++ b/library/python/runtime_py3/test/canondata/test_traceback.test_traceback_main-ultratb_color_/stderr.txt
@@ -0,0 +1,13 @@
+Traceback (most recent call last):
+ File library/python/runtime_py3/test/traceback/crash.py:42 in main
+ one()
+ File library/python/runtime_py3/test/traceback/crash.py:11 in one
+ modfunc(two) # aaa
+ File library/python/runtime_py3/test/traceback/mod/__init__.py:3 in modfunc
+ f() # call back to caller
+ File library/python/runtime_py3/test/traceback/crash.py:15 in two
+ three(42)
+ File library/python/runtime_py3/test/traceback/crash.py:19 in three
+ raise RuntimeError(f"Kaboom! I'm dead: {x}")
+RuntimeError: Kaboom! I'm dead: 42
+
diff --git a/library/python/runtime_py3/test/canondata/test_traceback.test_traceback_main-ultratb_color_/stdout.txt b/library/python/runtime_py3/test/canondata/test_traceback.test_traceback_main-ultratb_color_/stdout.txt
new file mode 100644
index 0000000000..2c9793eb14
--- /dev/null
+++ b/library/python/runtime_py3/test/canondata/test_traceback.test_traceback_main-ultratb_color_/stdout.txt
@@ -0,0 +1,2 @@
+__name__ = library.python.runtime_py3.test.traceback.crash
+__file__ = library/python/runtime_py3/test/traceback/crash.py
diff --git a/library/python/runtime_py3/test/canondata/test_traceback.test_traceback_main-ultratb_verbose_/stderr.txt b/library/python/runtime_py3/test/canondata/test_traceback.test_traceback_main-ultratb_verbose_/stderr.txt
new file mode 100644
index 0000000000..f5545c6976
--- /dev/null
+++ b/library/python/runtime_py3/test/canondata/test_traceback.test_traceback_main-ultratb_verbose_/stderr.txt
@@ -0,0 +1,27 @@
+---------------------------------------------------------------------------
+RuntimeError Traceback (most recent call last)
+File library/python/runtime_py3/test/traceback/crash.py:42, in main()
+ 39 time.time = lambda: 1531996624.0 # Freeze time
+ 40 sys.executable = "<traceback test>"
+---> 42 one()
+
+File library/python/runtime_py3/test/traceback/crash.py:11, in one()
+ 10 def one():
+---> 11 modfunc(two)
+
+File library/python/runtime_py3/test/traceback/mod/__init__.py:3, in modfunc(f=<function two>)
+ 1 def modfunc(f):
+ 2 # lalala
+----> 3 f()
+ f = <function two>
+
+File library/python/runtime_py3/test/traceback/crash.py:15, in two()
+ 14 def two():
+---> 15 three(42)
+
+File library/python/runtime_py3/test/traceback/crash.py:19, in three(x=42)
+ 18 def three(x):
+---> 19 raise RuntimeError(f"Kaboom! I'm dead: {x}")
+ x = 42
+
+RuntimeError: Kaboom! I'm dead: 42
diff --git a/library/python/runtime_py3/test/canondata/test_traceback.test_traceback_main-ultratb_verbose_/stdout.txt b/library/python/runtime_py3/test/canondata/test_traceback.test_traceback_main-ultratb_verbose_/stdout.txt
new file mode 100644
index 0000000000..2c9793eb14
--- /dev/null
+++ b/library/python/runtime_py3/test/canondata/test_traceback.test_traceback_main-ultratb_verbose_/stdout.txt
@@ -0,0 +1,2 @@
+__name__ = library.python.runtime_py3.test.traceback.crash
+__file__ = library/python/runtime_py3/test/traceback/crash.py
diff --git a/library/python/runtime_py3/test/resources/__init__.py b/library/python/runtime_py3/test/resources/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/library/python/runtime_py3/test/resources/__init__.py
diff --git a/library/python/runtime_py3/test/resources/foo.txt b/library/python/runtime_py3/test/resources/foo.txt
new file mode 100644
index 0000000000..ba0e162e1c
--- /dev/null
+++ b/library/python/runtime_py3/test/resources/foo.txt
@@ -0,0 +1 @@
+bar \ No newline at end of file
diff --git a/library/python/runtime_py3/test/resources/submodule/__init__.py b/library/python/runtime_py3/test/resources/submodule/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/library/python/runtime_py3/test/resources/submodule/__init__.py
diff --git a/library/python/runtime_py3/test/resources/submodule/bar.txt b/library/python/runtime_py3/test/resources/submodule/bar.txt
new file mode 100644
index 0000000000..1910281566
--- /dev/null
+++ b/library/python/runtime_py3/test/resources/submodule/bar.txt
@@ -0,0 +1 @@
+foo \ No newline at end of file
diff --git a/library/python/runtime_py3/test/test_arcadia_source_finder.py b/library/python/runtime_py3/test/test_arcadia_source_finder.py
new file mode 100644
index 0000000000..5110abc412
--- /dev/null
+++ b/library/python/runtime_py3/test/test_arcadia_source_finder.py
@@ -0,0 +1,389 @@
+import stat
+import unittest
+import yaml
+from collections import namedtuple
+from unittest.mock import patch
+from parameterized import parameterized
+
+import __res as res
+
+
+NAMESPACE_PREFIX = b"py/namespace/"
+TEST_SOURCE_ROOT = "/home/arcadia"
+
+
+class ImporterMocks:
+ def __init__(self, mock_fs, mock_resources):
+ self._mock_fs = mock_fs
+ self._mock_resources = mock_resources
+ self._patchers = [
+ patch("__res.iter_keys", wraps=self._iter_keys),
+ patch("__res.__resource.find", wraps=self._resource_find),
+ patch("__res._path_isfile", wraps=self._path_isfile),
+ patch("__res._os.listdir", wraps=self._os_listdir),
+ patch("__res._os.lstat", wraps=self._os_lstat),
+ ]
+ for patcher in self._patchers:
+ patcher.start()
+
+ def stop(self):
+ for patcher in self._patchers:
+ patcher.stop()
+
+ def _iter_keys(self, prefix):
+ assert prefix == NAMESPACE_PREFIX
+ for k in self._mock_resources.keys():
+ yield k, k.removeprefix(prefix)
+
+ def _resource_find(self, key):
+ return self._mock_resources.get(key)
+
+ def _lookup_mock_fs(self, filename):
+ path = filename.lstrip("/").split("/")
+ curdir = self._mock_fs
+ for item in path:
+ if item in curdir:
+ curdir = curdir[item]
+ else:
+ return None
+ return curdir
+
+ def _path_isfile(self, filename):
+ f = self._lookup_mock_fs(filename)
+ return isinstance(f, str)
+
+ def _os_lstat(self, filename):
+ f = self._lookup_mock_fs(filename)
+ mode = stat.S_IFDIR if isinstance(f, dict) else stat.S_IFREG
+ return namedtuple("fake_stat_type", "st_mode")(st_mode=mode)
+
+ def _os_listdir(self, dirname):
+ f = self._lookup_mock_fs(dirname)
+ if isinstance(f, dict):
+ return f.keys()
+ else:
+ return []
+
+
+class ArcadiaSourceFinderTestCase(unittest.TestCase):
+ def setUp(self):
+ self.import_mock = ImporterMocks(yaml.safe_load(self._get_mock_fs()), self._get_mock_resources())
+ self.arcadia_source_finder = res.ArcadiaSourceFinder(TEST_SOURCE_ROOT)
+
+ def tearDown(self):
+ self.import_mock.stop()
+
+ def _get_mock_fs(self):
+ raise NotImplementedError()
+
+ def _get_mock_resources(self):
+ raise NotImplementedError()
+
+
+class TestLibraryWithoutNamespace(ArcadiaSourceFinderTestCase):
+ def _get_mock_fs(self):
+ return """
+ home:
+ arcadia:
+ project:
+ lib:
+ mod1.py: ""
+ package1:
+ mod2.py: ""
+ """
+
+ def _get_mock_resources(self):
+ return {
+ b"py/namespace/unique_prefix1/project/lib": b"project.lib.",
+ }
+
+ @parameterized.expand(
+ [
+ ("project.lib.mod1", b"project/lib/mod1.py"),
+ ("project.lib.package1.mod2", b"project/lib/package1/mod2.py"),
+ ("project.lib.unknown_module", None),
+ ("project.lib", None), # package
+ ]
+ )
+ def test_get_module_path(self, module, path):
+ assert path == self.arcadia_source_finder.get_module_path(module)
+
+ @parameterized.expand(
+ [
+ ("project.lib.mod1", False),
+ ("project.lib.package1.mod2", False),
+ ("project", True),
+ ("project.lib", True),
+ ("project.lib.package1", True),
+ ]
+ )
+ def test_is_packages(self, module, is_package):
+ assert is_package == self.arcadia_source_finder.is_package(module)
+
+ def test_is_package_for_unknown_module(self):
+ self.assertRaises(
+ ImportError,
+ lambda: self.arcadia_source_finder.is_package("project.lib.package2"),
+ )
+
+ @parameterized.expand(
+ [
+ (
+ "",
+ {
+ ("PFX.project", True),
+ },
+ ),
+ (
+ "project.",
+ {
+ ("PFX.lib", True),
+ },
+ ),
+ (
+ "project.lib.",
+ {
+ ("PFX.mod1", False),
+ ("PFX.package1", True),
+ },
+ ),
+ (
+ "project.lib.package1.",
+ {
+ ("PFX.mod2", False),
+ },
+ ),
+ ]
+ )
+ def test_iter_modules(self, package_prefix, expected):
+ got = self.arcadia_source_finder.iter_modules(package_prefix, "PFX.")
+ assert expected == set(got)
+
+ # Check iter_modules() don't crash and return correct result after not existing module was requested
+ def test_iter_modules_after_unknown_module_import(self):
+ self.arcadia_source_finder.get_module_path("project.unknown_module")
+ assert {("lib", True)} == set(self.arcadia_source_finder.iter_modules("project.", ""))
+
+
+class TestLibraryExtendedFromAnotherLibrary(ArcadiaSourceFinderTestCase):
+ def _get_mock_fs(self):
+ return """
+ home:
+ arcadia:
+ project:
+ lib:
+ mod1.py: ''
+ lib_extension:
+ mod2.py: ''
+ """
+
+ def _get_mock_resources(self):
+ return {
+ b"py/namespace/unique_prefix1/project/lib": b"project.lib.",
+ b"py/namespace/unique_prefix2/project/lib_extension": b"project.lib.",
+ }
+
+ @parameterized.expand(
+ [
+ ("project.lib.mod1", b"project/lib/mod1.py"),
+ ("project.lib.mod2", b"project/lib_extension/mod2.py"),
+ ]
+ )
+ def test_get_module_path(self, module, path):
+ assert path == self.arcadia_source_finder.get_module_path(module)
+
+ @parameterized.expand(
+ [
+ (
+ "project.lib.",
+ {
+ ("PFX.mod1", False),
+ ("PFX.mod2", False),
+ },
+ ),
+ ]
+ )
+ def test_iter_modules(self, package_prefix, expected):
+ got = self.arcadia_source_finder.iter_modules(package_prefix, "PFX.")
+ assert expected == set(got)
+
+
+class TestNamespaceAndTopLevelLibraries(ArcadiaSourceFinderTestCase):
+ def _get_mock_fs(self):
+ return """
+ home:
+ arcadia:
+ project:
+ ns_lib:
+ mod1.py: ''
+ top_level_lib:
+ mod2.py: ''
+ """
+
+ def _get_mock_resources(self):
+ return {
+ b"py/namespace/unique_prefix1/project/ns_lib": b"ns.",
+ b"py/namespace/unique_prefix2/project/top_level_lib": b".",
+ }
+
+ @parameterized.expand(
+ [
+ ("ns.mod1", b"project/ns_lib/mod1.py"),
+ ("mod2", b"project/top_level_lib/mod2.py"),
+ ]
+ )
+ def test_get_module_path(self, module, path):
+ assert path == self.arcadia_source_finder.get_module_path(module)
+
+ @parameterized.expand(
+ [
+ ("ns", True),
+ ("ns.mod1", False),
+ ("mod2", False),
+ ]
+ )
+ def test_is_packages(self, module, is_package):
+ assert is_package == self.arcadia_source_finder.is_package(module)
+
+ @parameterized.expand(
+ [
+ "project",
+ "project.ns_lib",
+ "project.top_level_lib",
+ ]
+ )
+ def test_is_package_for_unknown_modules(self, module):
+ self.assertRaises(ImportError, lambda: self.arcadia_source_finder.is_package(module))
+
+ @parameterized.expand(
+ [
+ (
+ "",
+ {
+ ("PFX.ns", True),
+ ("PFX.mod2", False),
+ },
+ ),
+ (
+ "ns.",
+ {
+ ("PFX.mod1", False),
+ },
+ ),
+ ]
+ )
+ def test_iter_modules(self, package_prefix, expected):
+ got = self.arcadia_source_finder.iter_modules(package_prefix, "PFX.")
+ assert expected == set(got)
+
+
+class TestIgnoreDirectoriesWithYaMakeFile(ArcadiaSourceFinderTestCase):
+ """Packages and modules from tests should not be part of pylib namespace"""
+
+ def _get_mock_fs(self):
+ return """
+ home:
+ arcadia:
+ contrib:
+ python:
+ pylib:
+ mod1.py: ""
+ tests:
+ conftest.py: ""
+ ya.make: ""
+ """
+
+ def _get_mock_resources(self):
+ return {
+ b"py/namespace/unique_prefix1/contrib/python/pylib": b"pylib.",
+ }
+
+ def test_get_module_path_for_lib(self):
+ assert b"contrib/python/pylib/mod1.py" == self.arcadia_source_finder.get_module_path("pylib.mod1")
+
+ def test_get_module_for_tests(self):
+ assert self.arcadia_source_finder.get_module_path("pylib.tests.conftest") is None
+
+ def test_is_package_for_tests(self):
+ self.assertRaises(ImportError, lambda: self.arcadia_source_finder.is_package("pylib.tests"))
+
+
+class TestMergingNamespaceAndDirectoryPackages(ArcadiaSourceFinderTestCase):
+ """Merge parent package (top level in this test) dirs with namespace dirs (DEVTOOLS-8979)"""
+
+ def _get_mock_fs(self):
+ return """
+ home:
+ arcadia:
+ contrib:
+ python:
+ pylint:
+ ya.make: ""
+ pylint:
+ __init__.py: ""
+ patcher:
+ patch.py: ""
+ ya.make: ""
+ """
+
+ def _get_mock_resources(self):
+ return {
+ b"py/namespace/unique_prefix1/contrib/python/pylint": b".",
+ b"py/namespace/unique_prefix1/contrib/python/pylint/patcher": b"pylint.",
+ }
+
+ @parameterized.expand(
+ [
+ ("pylint.__init__", b"contrib/python/pylint/pylint/__init__.py"),
+ ("pylint.patch", b"contrib/python/pylint/patcher/patch.py"),
+ ]
+ )
+ def test_get_module_path(self, module, path):
+ assert path == self.arcadia_source_finder.get_module_path(module)
+
+
+class TestEmptyResources(ArcadiaSourceFinderTestCase):
+ def _get_mock_fs(self):
+ return """
+ home:
+ arcadia:
+ project:
+ lib:
+ mod1.py: ''
+ """
+
+ def _get_mock_resources(self):
+ return {}
+
+ def test_get_module_path(self):
+ assert self.arcadia_source_finder.get_module_path("project.lib.mod1") is None
+
+ def test_is_package(self):
+ self.assertRaises(ImportError, lambda: self.arcadia_source_finder.is_package("project"))
+
+ def test_iter_modules(self):
+ assert [] == list(self.arcadia_source_finder.iter_modules("", "PFX."))
+
+
+class TestDictionaryChangedSizeDuringIteration(ArcadiaSourceFinderTestCase):
+ def _get_mock_fs(self):
+ return """
+ home:
+ arcadia:
+ project:
+ lib1:
+ mod1.py: ''
+ lib2:
+ mod2.py: ''
+ """
+
+ def _get_mock_resources(self):
+ return {
+ b"py/namespace/unique_prefix1/project/lib1": b"project.lib1.",
+ b"py/namespace/unique_prefix1/project/lib2": b"project.lib2.",
+ }
+
+ def test_no_crash_on_recusive_iter_modules(self):
+ for package in self.arcadia_source_finder.iter_modules("project.", ""):
+ for _ in self.arcadia_source_finder.iter_modules(package[0], ""):
+ pass
diff --git a/library/python/runtime_py3/test/test_metadata.py b/library/python/runtime_py3/test/test_metadata.py
new file mode 100644
index 0000000000..686c176468
--- /dev/null
+++ b/library/python/runtime_py3/test/test_metadata.py
@@ -0,0 +1,44 @@
+import importlib.metadata as im
+
+import pytest
+
+
+@pytest.mark.parametrize("name", ("foo-bar", "foo_bar", "Foo-Bar"))
+def test_distribution(name):
+ assert im.distribution(name) is not None
+
+
+def test_unknown_package():
+ with pytest.raises(im.PackageNotFoundError):
+ im.distribution("bar")
+
+
+def test_version():
+ assert im.version("foo-bar") == "1.2.3"
+
+
+def test_metadata():
+ assert im.metadata("foo-bar") is not None
+
+
+def test_files():
+ files = im.files("foo-bar")
+ assert len(files) == 1
+ assert files[0].name == "foo_bar.py"
+ assert files[0].size == 20
+
+
+def test_requires():
+ assert im.requires("foo-bar") == ["Werkzeug (>=0.15)", "Jinja2 (>=2.10.1)"]
+
+
+def test_entry_points():
+ entry_points = im.entry_points()
+ assert "console_scripts" in entry_points
+
+ flg_found = False
+ for entry_point in entry_points["console_scripts"]:
+ if entry_point.name == "foo_cli" and entry_point.value == "foo_bar:cli":
+ flg_found = True
+
+ assert flg_found
diff --git a/library/python/runtime_py3/test/test_resources.py b/library/python/runtime_py3/test/test_resources.py
new file mode 100644
index 0000000000..059cc039e6
--- /dev/null
+++ b/library/python/runtime_py3/test/test_resources.py
@@ -0,0 +1,73 @@
+import importlib.resources as ir
+
+import pytest
+
+
+@pytest.mark.parametrize(
+ "package, resource",
+ (
+ ("resources", "foo.txt"),
+ ("resources.submodule", "bar.txt"),
+ ),
+)
+def test_is_resource_good_path(package, resource):
+ assert ir.is_resource(package, resource)
+
+
+@pytest.mark.parametrize(
+ "package, resource",
+ (
+ ("resources", "111.txt"),
+ ("resources.submodule", "222.txt"),
+ ),
+)
+def test_is_resource_missing(package, resource):
+ assert not ir.is_resource(package, resource)
+
+
+def test_is_resource_subresource_directory():
+ # Directories are not resources.
+ assert not ir.is_resource("resources", "submodule")
+
+
+@pytest.mark.parametrize(
+ "package, resource, expected",
+ (
+ ("resources", "foo.txt", b"bar"),
+ ("resources.submodule", "bar.txt", b"foo"),
+ ),
+)
+def test_read_binary_good_path(package, resource, expected):
+ assert ir.read_binary(package, resource) == expected
+
+
+def test_read_binary_missing():
+ with pytest.raises(FileNotFoundError):
+ ir.read_binary("resources", "111.txt")
+
+
+@pytest.mark.parametrize(
+ "package, resource, expected",
+ (
+ ("resources", "foo.txt", "bar"),
+ ("resources.submodule", "bar.txt", "foo"),
+ ),
+)
+def test_read_text_good_path(package, resource, expected):
+ assert ir.read_text(package, resource) == expected
+
+
+def test_read_text_missing():
+ with pytest.raises(FileNotFoundError):
+ ir.read_text("resources", "111.txt")
+
+
+@pytest.mark.parametrize(
+ "package, expected",
+ (
+ ("resources", ["submodule", "foo.txt"]),
+ ("resources.submodule", ["bar.txt"]),
+ ),
+)
+def test_contents_good_path(package, expected):
+ assert sorted(ir.contents(package)) == sorted(expected)
diff --git a/library/python/runtime_py3/test/test_traceback.py b/library/python/runtime_py3/test/test_traceback.py
new file mode 100644
index 0000000000..08aed1a510
--- /dev/null
+++ b/library/python/runtime_py3/test/test_traceback.py
@@ -0,0 +1,65 @@
+import os
+import re
+
+import pytest
+
+import yatest.common as yc
+
+
+def clean_traceback(traceback):
+ traceback = re.sub(rb"\033\[(\d|;)+?m", b"", traceback) # strip ANSI codes
+ traceback = re.sub(rb" at 0x[0-9a-fA-F]+", b"", traceback) # remove object ids
+ return traceback
+
+
+@pytest.mark.parametrize(
+ "mode",
+ [
+ "default",
+ "ultratb_color",
+ "ultratb_verbose",
+ ],
+)
+@pytest.mark.parametrize(
+ "entry_point",
+ [
+ "main",
+ "custom",
+ ],
+)
+def test_traceback(mode, entry_point):
+ tb_tool = yc.build_path("library/python/runtime_py3/test/traceback/traceback")
+ stdout_path = yc.test_output_path("stdout_raw.txt")
+ stderr_path = yc.test_output_path("stderr_raw.txt")
+ filtered_stdout_path = yc.test_output_path("stdout.txt")
+ filtered_stderr_path = yc.test_output_path("stderr.txt")
+
+ env = os.environ.copy()
+ env.pop("PYTHONPATH", None) # Do not let program peek into its sources on filesystem
+ if entry_point == "custom":
+ env["Y_PYTHON_ENTRY_POINT"] = "library.python.runtime_py3.test.traceback.crash:main"
+
+ proc = yc.execute(
+ command=[tb_tool, mode],
+ env=env,
+ stdout=stdout_path,
+ stderr=stderr_path,
+ check_exit_code=False,
+ )
+
+ with open(filtered_stdout_path, "wb") as f:
+ f.write(clean_traceback(proc.std_out))
+
+ with open(filtered_stderr_path, "wb") as f:
+ f.write(clean_traceback(proc.std_err))
+
+ return {
+ "stdout": yc.canonical_file(
+ filtered_stdout_path,
+ local=True,
+ ),
+ "stderr": yc.canonical_file(
+ filtered_stderr_path,
+ local=True,
+ ),
+ }
diff --git a/library/python/runtime_py3/test/traceback/__main__.py b/library/python/runtime_py3/test/traceback/__main__.py
new file mode 100644
index 0000000000..364db169f0
--- /dev/null
+++ b/library/python/runtime_py3/test/traceback/__main__.py
@@ -0,0 +1,4 @@
+from library.python.runtime_py3.test.traceback.crash import main
+
+if __name__ == "__main__":
+ main()
diff --git a/library/python/runtime_py3/test/traceback/crash.py b/library/python/runtime_py3/test/traceback/crash.py
new file mode 100644
index 0000000000..dfd316d4c6
--- /dev/null
+++ b/library/python/runtime_py3/test/traceback/crash.py
@@ -0,0 +1,42 @@
+import argparse
+import sys
+import time
+
+from IPython.core import ultratb
+
+from .mod import modfunc
+
+
+def one():
+ modfunc(two) # aaa
+
+
+def two():
+ three(42)
+
+
+def three(x):
+ raise RuntimeError(f"Kaboom! I'm dead: {x}")
+
+
+def main():
+ hooks = {
+ "default": lambda: sys.excepthook,
+ "ultratb_color": lambda: ultratb.ColorTB(ostream=sys.stderr),
+ "ultratb_verbose": lambda: ultratb.VerboseTB(ostream=sys.stderr),
+ }
+
+ parser = argparse.ArgumentParser()
+ parser.add_argument("hook", choices=sorted(hooks), default="default")
+
+ args = parser.parse_args()
+
+ sys.excepthook = hooks[args.hook]()
+
+ print("__name__ =", __name__)
+ print("__file__ =", __file__)
+
+ time.time = lambda: 1531996624.0 # Freeze time
+ sys.executable = "<traceback test>"
+
+ one()
diff --git a/library/python/runtime_py3/test/traceback/mod/__init__.py b/library/python/runtime_py3/test/traceback/mod/__init__.py
new file mode 100644
index 0000000000..f00843d786
--- /dev/null
+++ b/library/python/runtime_py3/test/traceback/mod/__init__.py
@@ -0,0 +1,3 @@
+def modfunc(f):
+ # lalala
+ f() # call back to caller
diff --git a/library/python/runtime_py3/test/traceback/ya.make b/library/python/runtime_py3/test/traceback/ya.make
new file mode 100644
index 0000000000..0703e75401
--- /dev/null
+++ b/library/python/runtime_py3/test/traceback/ya.make
@@ -0,0 +1,16 @@
+PY3_PROGRAM()
+
+STYLE_PYTHON()
+
+PEERDIR(
+ contrib/python/ipython
+)
+
+PY_SRCS(
+ MAIN
+ __main__.py=main
+ crash.py
+ mod/__init__.py
+)
+
+END()
diff --git a/library/python/runtime_py3/test/ya.make b/library/python/runtime_py3/test/ya.make
new file mode 100644
index 0000000000..e0c4061ad2
--- /dev/null
+++ b/library/python/runtime_py3/test/ya.make
@@ -0,0 +1,37 @@
+PY3TEST()
+
+STYLE_PYTHON()
+
+DEPENDS(library/python/runtime_py3/test/traceback)
+
+PEERDIR(
+ contrib/python/parameterized
+ contrib/python/PyYAML
+)
+
+PY_SRCS(
+ TOP_LEVEL
+ resources/__init__.py
+ resources/submodule/__init__.py
+)
+
+TEST_SRCS(
+ test_metadata.py
+ test_resources.py
+ test_traceback.py
+ test_arcadia_source_finder.py
+)
+
+RESOURCE_FILES(
+ PREFIX library/python/runtime_py3/test/
+ .dist-info/METADATA
+ .dist-info/RECORD
+ .dist-info/entry_points.txt
+ .dist-info/top_level.txt
+ resources/foo.txt
+ resources/submodule/bar.txt
+)
+
+END()
+
+RECURSE_FOR_TESTS(traceback)
diff --git a/library/python/runtime_py3/ya.make b/library/python/runtime_py3/ya.make
new file mode 100644
index 0000000000..58e05a7295
--- /dev/null
+++ b/library/python/runtime_py3/ya.make
@@ -0,0 +1,43 @@
+PY3_LIBRARY()
+
+STYLE_PYTHON()
+
+PEERDIR(
+ contrib/tools/python3/src
+ contrib/tools/python3/lib/py
+ library/cpp/resource
+)
+
+CFLAGS(-DCYTHON_REGISTER_ABCS=0)
+
+NO_PYTHON_INCLUDES()
+
+ENABLE(PYBUILD_NO_PYC)
+
+PY_SRCS(
+ entry_points.py
+ TOP_LEVEL
+
+ CYTHON_DIRECTIVE
+ language_level=3
+
+ __res.pyx
+ sitecustomize.pyx
+)
+
+IF (CYTHON_COVERAGE)
+ # Let covarage support add all needed files to resources
+ELSE()
+ RESOURCE_FILES(
+ PREFIX ${MODDIR}/
+ __res.pyx
+ importer.pxi
+ sitecustomize.pyx
+ )
+ENDIF()
+
+END()
+
+RECURSE_FOR_TESTS(
+ test
+)