diff options
author | nkozlovskiy <nmk@ydb.tech> | 2023-09-29 12:24:06 +0300 |
---|---|---|
committer | nkozlovskiy <nmk@ydb.tech> | 2023-09-29 12:41:34 +0300 |
commit | e0e3e1717e3d33762ce61950504f9637a6e669ed (patch) | |
tree | bca3ff6939b10ed60c3d5c12439963a1146b9711 /library/python | |
parent | 38f2c5852db84c7b4d83adfcb009eb61541d1ccd (diff) | |
download | ydb-e0e3e1717e3d33762ce61950504f9637a6e669ed.tar.gz |
add ydb deps
Diffstat (limited to 'library/python')
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 +) |