diff options
author | Devtools Arcadia <arcadia-devtools@yandex-team.ru> | 2022-02-07 18:08:42 +0300 |
---|---|---|
committer | Devtools Arcadia <arcadia-devtools@mous.vla.yp-c.yandex.net> | 2022-02-07 18:08:42 +0300 |
commit | 1110808a9d39d4b808aef724c861a2e1a38d2a69 (patch) | |
tree | e26c9fed0de5d9873cce7e00bc214573dc2195b7 /library/python/runtime_py3 | |
download | ydb-1110808a9d39d4b808aef724c861a2e1a38d2a69.tar.gz |
intermediate changes
ref:cde9a383711a11544ce7e107a78147fb96cc4029
Diffstat (limited to 'library/python/runtime_py3')
38 files changed, 1862 insertions, 0 deletions
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..05098723cb --- /dev/null +++ b/library/python/runtime_py3/entry_points.py @@ -0,0 +1,52 @@ +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: + 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..904f94dea2 --- /dev/null +++ b/library/python/runtime_py3/importer.pxi @@ -0,0 +1,571 @@ +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_isdir, _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 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' + + 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'([^.]+)$') + for mod, path in self.module_path_cache.items(): + if path is not None: + m = rx.match(mod) + if m: + yield prefix + m.group(1), self.is_package(mod) + + # 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 _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 _path_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) + + +if YA_IDE_VENV: + sys.meta_path.append(importer) + sys.meta_path.append(BuiltinSubmoduleImporter) + if executable not in sys.path: + sys.path.append(executable) + 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..3159800615 --- /dev/null +++ b/library/python/runtime_py3/main/main.c @@ -0,0 +1,231 @@ +#include <Python.h> +#include <contrib/tools/python3/src/Include/internal/pycore_runtime.h> // _PyRuntime_Initialize() + +#include <stdlib.h> +#include <string.h> +#include <locale.h> + +void Py_InitArgcArgv(int argc, wchar_t **argv); +char* GetPyMain(); + +static const char* env_entry_point = "Y_PYTHON_ENTRY_POINT"; +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) { + PyStatus status = _PyRuntime_Initialize(); + if (PyStatus_Exception(status)) { + Py_ExitStatusException(status); + } + + int i, sts = 1; + char* oldloc = NULL; + wchar_t** argv_copy = NULL; + /* We need a second copies, as Python might modify the first one. */ + wchar_t** argv_copy2 = NULL; + char* entry_point_copy = NULL; + + if (argc > 0) { + argv_copy = PyMem_RawMalloc(sizeof(wchar_t*) * argc); + argv_copy2 = PyMem_RawMalloc(sizeof(wchar_t*) * argc); + if (!argv_copy || !argv_copy2) { + fprintf(stderr, "out of memory\n"); + goto error; + } + } + + PyConfig config; + PyConfig_InitPythonConfig(&config); + config.pathconfig_warnings = 0; /* Suppress errors from getpath.c */ + + const char* bytes_warning = getenv(env_bytes_warning); + if (bytes_warning) { + config.bytes_warning = atoi(bytes_warning); + } + + oldloc = _PyMem_RawStrdup(setlocale(LC_ALL, NULL)); + if (!oldloc) { + fprintf(stderr, "out of memory\n"); + goto error; + } + + setlocale(LC_ALL, ""); + for (i = 0; i < argc; i++) { + argv_copy[i] = Py_DecodeLocale(argv[i], NULL); + argv_copy2[i] = argv_copy[i]; + if (!argv_copy[i]) { + fprintf(stderr, "Unable to decode the command line argument #%i\n", + i + 1); + argc = i; + goto error; + } + } + setlocale(LC_ALL, oldloc); + PyMem_RawFree(oldloc); + oldloc = NULL; + + if (argc >= 1) + Py_SetProgramName(argv_copy[0]); + + status = Py_InitializeFromConfig(&config); + PyConfig_Clear(&config); + if (PyStatus_Exception(status)) { + Py_ExitStatusException(status); + } + + 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")) { + unsetenv(env_entry_point); + sts = Py_Main(argc, argv_copy); + free(entry_point_copy); + return sts; + } + + Py_InitArgcArgv(argc, argv_copy); + PySys_SetArgv(argc, argv_copy); + + { + 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); + PyMem_RawFree(argv_copy); + if (argv_copy2) { + for (i = 0; i < argc; i++) + PyMem_RawFree(argv_copy2[i]); + PyMem_RawFree(argv_copy2); + } + PyMem_RawFree(oldloc); + 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/ya.make b/library/python/runtime_py3/main/ya.make new file mode 100644 index 0000000000..f308a93b28 --- /dev/null +++ b/library/python/runtime_py3/main/ya.make @@ -0,0 +1,26 @@ +LIBRARY() + +OWNER( + pg + orivej +) + +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 +) + +END() diff --git a/library/python/runtime_py3/sitecustomize.pyx b/library/python/runtime_py3/sitecustomize.pyx new file mode 100644 index 0000000000..966bbe8ba6 --- /dev/null +++ b/library/python/runtime_py3/sitecustomize.pyx @@ -0,0 +1,69 @@ +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 + + 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..a7d045fc9c --- /dev/null +++ b/library/python/runtime_py3/test/canondata/result.json @@ -0,0 +1,62 @@ +{ + "test_traceback.test_traceback[custom-default]": { + "stderr": { + "checksum": "6c1a9b47baa51cc6903b85fd43c529b5", + "uri": "file://test_traceback.test_traceback_custom-default_/stderr.txt" + }, + "stdout": { + "checksum": "e120a1e0b7fdddc8e6b4d4b506403e89", + "uri": "file://test_traceback.test_traceback_custom-default_/stdout.txt" + } + }, + "test_traceback.test_traceback[custom-ultratb_color]": { + "stderr": { + "checksum": "048e27049fb8db64bd295b17f505b0ad", + "uri": "file://test_traceback.test_traceback_custom-ultratb_color_/stderr.txt" + }, + "stdout": { + "checksum": "e120a1e0b7fdddc8e6b4d4b506403e89", + "uri": "file://test_traceback.test_traceback_custom-ultratb_color_/stdout.txt" + } + }, + "test_traceback.test_traceback[custom-ultratb_verbose]": { + "stderr": { + "checksum": "e9af42aa3736141d9b67a1652eea412e", + "uri": "file://test_traceback.test_traceback_custom-ultratb_verbose_/stderr.txt" + }, + "stdout": { + "checksum": "e120a1e0b7fdddc8e6b4d4b506403e89", + "uri": "file://test_traceback.test_traceback_custom-ultratb_verbose_/stdout.txt" + } + }, + "test_traceback.test_traceback[main-default]": { + "stderr": { + "checksum": "6c1a9b47baa51cc6903b85fd43c529b5", + "uri": "file://test_traceback.test_traceback_main-default_/stderr.txt" + }, + "stdout": { + "checksum": "e120a1e0b7fdddc8e6b4d4b506403e89", + "uri": "file://test_traceback.test_traceback_main-default_/stdout.txt" + } + }, + "test_traceback.test_traceback[main-ultratb_color]": { + "stderr": { + "checksum": "048e27049fb8db64bd295b17f505b0ad", + "uri": "file://test_traceback.test_traceback_main-ultratb_color_/stderr.txt" + }, + "stdout": { + "checksum": "e120a1e0b7fdddc8e6b4d4b506403e89", + "uri": "file://test_traceback.test_traceback_main-ultratb_color_/stdout.txt" + } + }, + "test_traceback.test_traceback[main-ultratb_verbose]": { + "stderr": { + "checksum": "e9af42aa3736141d9b67a1652eea412e", + "uri": "file://test_traceback.test_traceback_main-ultratb_verbose_/stderr.txt" + }, + "stdout": { + "checksum": "e120a1e0b7fdddc8e6b4d4b506403e89", + "uri": "file://test_traceback.test_traceback_main-ultratb_verbose_/stdout.txt" + } + } +}
\ No newline at end of file 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..5eb7da170a --- /dev/null +++ b/library/python/runtime_py3/test/canondata/test_traceback.test_traceback_custom-default_/stderr.txt @@ -0,0 +1,12 @@ +Traceback (most recent call last): + File "library/python/runtime_py3/test/traceback/crash.py", line 44, in main + one() + File "library/python/runtime_py3/test/traceback/crash.py", line 12, 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 16, in two + three(42) + File "library/python/runtime_py3/test/traceback/crash.py", line 20, in three + raise RuntimeError('Kaboom! I\'m dead: {}'.format(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..9e5a474cbd --- /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", line 44, in main + one() + File "library/python/runtime_py3/test/traceback/crash.py", line 12, 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 16, in two + three(42) + File "library/python/runtime_py3/test/traceback/crash.py", line 20, in three + raise RuntimeError('Kaboom! I\'m dead: {}'.format(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..b0b299ebe6 --- /dev/null +++ b/library/python/runtime_py3/test/canondata/test_traceback.test_traceback_custom-ultratb_verbose_/stderr.txt @@ -0,0 +1,41 @@ +--------------------------------------------------------------------------- +RuntimeError Traceback (most recent call last) +library/python/runtime_py3/test/traceback/crash.py in main() + 42 sys.executable = '<traceback test>' + 43 +---> 44 one() + global one = <function one> + +library/python/runtime_py3/test/traceback/crash.py in one() + 10 + 11 def one(): +---> 12 modfunc(two) # aaa + global modfunc = <function modfunc> + global two = <function two> + 13 + 14 + +library/python/runtime_py3/test/traceback/mod/__init__.py in modfunc(f=<function two>) + 1 def modfunc(f): + 2 # lalala +----> 3 f() # call back to caller + f = <function two> + +library/python/runtime_py3/test/traceback/crash.py in two() + 14 + 15 def two(): +---> 16 three(42) + global three = <function three> + 17 + 18 + +library/python/runtime_py3/test/traceback/crash.py in three(x=42) + 18 + 19 def three(x): +---> 20 raise RuntimeError('Kaboom! I\'m dead: {}'.format(x)) + global RuntimeError.format = undefined + x = 42 + 21 + 22 + +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..5eb7da170a --- /dev/null +++ b/library/python/runtime_py3/test/canondata/test_traceback.test_traceback_main-default_/stderr.txt @@ -0,0 +1,12 @@ +Traceback (most recent call last): + File "library/python/runtime_py3/test/traceback/crash.py", line 44, in main + one() + File "library/python/runtime_py3/test/traceback/crash.py", line 12, 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 16, in two + three(42) + File "library/python/runtime_py3/test/traceback/crash.py", line 20, in three + raise RuntimeError('Kaboom! I\'m dead: {}'.format(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..9e5a474cbd --- /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", line 44, in main + one() + File "library/python/runtime_py3/test/traceback/crash.py", line 12, 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 16, in two + three(42) + File "library/python/runtime_py3/test/traceback/crash.py", line 20, in three + raise RuntimeError('Kaboom! I\'m dead: {}'.format(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..b0b299ebe6 --- /dev/null +++ b/library/python/runtime_py3/test/canondata/test_traceback.test_traceback_main-ultratb_verbose_/stderr.txt @@ -0,0 +1,41 @@ +--------------------------------------------------------------------------- +RuntimeError Traceback (most recent call last) +library/python/runtime_py3/test/traceback/crash.py in main() + 42 sys.executable = '<traceback test>' + 43 +---> 44 one() + global one = <function one> + +library/python/runtime_py3/test/traceback/crash.py in one() + 10 + 11 def one(): +---> 12 modfunc(two) # aaa + global modfunc = <function modfunc> + global two = <function two> + 13 + 14 + +library/python/runtime_py3/test/traceback/mod/__init__.py in modfunc(f=<function two>) + 1 def modfunc(f): + 2 # lalala +----> 3 f() # call back to caller + f = <function two> + +library/python/runtime_py3/test/traceback/crash.py in two() + 14 + 15 def two(): +---> 16 three(42) + global three = <function three> + 17 + 18 + +library/python/runtime_py3/test/traceback/crash.py in three(x=42) + 18 + 19 def three(x): +---> 20 raise RuntimeError('Kaboom! I\'m dead: {}'.format(x)) + global RuntimeError.format = undefined + x = 42 + 21 + 22 + +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..ff80d0a0a2 --- /dev/null +++ b/library/python/runtime_py3/test/test_arcadia_source_finder.py @@ -0,0 +1,317 @@ +import unittest +import yaml +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(object): + 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_isdir', wraps=self._path_isdir), + patch('__res._path_isfile', wraps=self._path_isfile), + patch('__res._os.listdir', wraps=self._os_listdir), + ] + 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 + l = len(prefix) + for k in self._mock_resources.keys(): + yield k, k[l:] + + 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 _path_isdir(self, filename): + f = self._lookup_mock_fs(filename) + return isinstance(f, dict) + + 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.')) 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..a269329f42 --- /dev/null +++ b/library/python/runtime_py3/test/test_resources.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- + +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..82087ce98a --- /dev/null +++ b/library/python/runtime_py3/test/test_traceback.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- + +from __future__ import print_function, absolute_import, division + +import os +import re + +import pytest + +import yatest.common as yc + + +def clean_traceback(traceback): + traceback = re.sub(br'\033\[(\d|;)+?m', b'', traceback) # strip ANSI codes + traceback = re.sub(br' 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..b5e36b3dd4 --- /dev/null +++ b/library/python/runtime_py3/test/traceback/crash.py @@ -0,0 +1,44 @@ +import argparse +import cgitb +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('Kaboom! I\'m dead: {}'.format(x)) + + +def main(): + hooks = { + 'default': lambda: sys.excepthook, + 'cgitb': lambda: cgitb.Hook(format='text'), + '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..b61fe9550b --- /dev/null +++ b/library/python/runtime_py3/test/traceback/ya.make @@ -0,0 +1,19 @@ +PY3_PROGRAM() + +OWNER( + abodrov + borman +) + +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..4ec3db74f5 --- /dev/null +++ b/library/python/runtime_py3/test/ya.make @@ -0,0 +1,40 @@ +PY3TEST() + +OWNER( + abodrov + borman +) + +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..fa5c11341a --- /dev/null +++ b/library/python/runtime_py3/ya.make @@ -0,0 +1,49 @@ +PY3_LIBRARY() + +OWNER( + borman + orivej + pg +) + +NO_WSHADOW() + +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 +) |