diff options
author | snermolaev <[email protected]> | 2025-04-29 06:44:21 +0300 |
---|---|---|
committer | snermolaev <[email protected]> | 2025-04-29 06:57:18 +0300 |
commit | 713adc6a88be8af8a342a9a72a055b7ef6514563 (patch) | |
tree | 19dada616a0fd41ead73ad79d463a11c6cd8279d /library/python | |
parent | 407f7c0bc156862b8263bccf3eaaf0687ba75f8d (diff) |
Subinterpretor compatible __res module (2nd attempt)
Cython is not yet subinterpreter compatible. There are no ETA when cython is going to support subinterpreters.
This PR removes cython from hermetic python imoprt hooks in order to make them subinterpretr-compatible.
commit_hash:427b6f9db6afa6695659ee147621e1ccb391d3cb
Diffstat (limited to 'library/python')
15 files changed, 617 insertions, 83 deletions
diff --git a/library/python/runtime_py3/__res.cpp b/library/python/runtime_py3/__res.cpp new file mode 100644 index 00000000000..e4b01ce227f --- /dev/null +++ b/library/python/runtime_py3/__res.cpp @@ -0,0 +1,224 @@ +#include <library/cpp/resource/resource.h> + +#include <util/generic/scope.h> +#include <util/generic/strbuf.h> + +#include <Python.h> +#include <marshal.h> + +#include <type_traits> +#include <concepts> + +namespace { + +namespace NWrap { + +template<typename F> + requires std::convertible_to<std::invoke_result_t<F>, PyObject*> +PyObject* CallWithErrorTranslation(F&& f) noexcept { + try { + return std::forward<F>(f)(); + } catch (const std::bad_alloc& err) { + PyErr_SetString(PyExc_MemoryError, err.what()); + } catch(const std::out_of_range& err) { + PyErr_SetString(PyExc_IndexError, err.what()); + } catch (const std::exception& err) { + PyErr_SetString(PyExc_RuntimeError, err.what()); + } catch(...) { + PyErr_SetString(PyExc_RuntimeError, "Unhandled C++ exception of unknown type"); + } + return nullptr; +} + +PyObject* Count(PyObject* self[[maybe_unused]], PyObject *args[[maybe_unused]]) noexcept { + static_assert( + noexcept(NResource::Count()), + "Python3 Arcadia runtime binding assumes that NResource::Count do not throw exception. If this function start " + "to throw someone must add code translating C++ exceptions into Python exceptions here." + ); + return PyLong_FromLong(NResource::Count()); +} + +PyObject* KeyByIndex(PyObject* self[[maybe_unused]], PyObject *const *args, Py_ssize_t nargs) noexcept { + if (nargs != 1) { + PyErr_Format(PyExc_TypeError, "__res.key_by_index takes 1 positional arguments but %z were given", nargs); + return nullptr; + } + if (PyFloat_Check(args[0])) { + PyErr_SetString(PyExc_TypeError, "integer argument expected, got float"); + return nullptr; + } + PyObject* asNum = PyNumber_Index(args[0]); + if (!asNum) { + return nullptr; + } + const auto idx = PyLong_AsSize_t(asNum); + Py_DECREF(asNum); + if (idx == static_cast<size_t>(-1)) { + return nullptr; + } + return CallWithErrorTranslation([&]{ + const auto res = NResource::KeyByIndex(idx); + return PyBytes_FromStringAndSize(res.data(), res.size()); + }); +} + +PyObject* Find(PyObject* self[[maybe_unused]], PyObject *const* args, Py_ssize_t nargs) noexcept { + if (nargs != 1) { + PyErr_Format(PyExc_TypeError, "__res.find takes 1 positional arguments but %z were given", nargs); + return nullptr; + } + + TStringBuf key; + if (PyUnicode_Check(args[0])) { + Py_ssize_t sz; + const char* data = PyUnicode_AsUTF8AndSize(args[0], &sz); + if (sz < 0) { + return nullptr; + } + key = {data, static_cast<size_t>(sz)}; + } else { + char* data = nullptr; + Py_ssize_t sz; + if (PyBytes_AsStringAndSize(args[0], &data, &sz) != 0) { + return nullptr; + } + key = {data, static_cast<size_t>(sz)}; + } + + return CallWithErrorTranslation([&]{ + TString res; + if (!NResource::FindExact(key, &res)) { + Py_RETURN_NONE; + } + return PyBytes_FromStringAndSize(res.data(), res.size()); + }); +} + +PyObject* Has(PyObject* self[[maybe_unused]], PyObject *const* args, Py_ssize_t nargs) noexcept { + if (nargs != 1) { + PyErr_Format(PyExc_TypeError, "__res.has takes 1 positional arguments but %z were given", nargs); + return nullptr; + } + + TStringBuf key; + if (PyUnicode_Check(args[0])) { + Py_ssize_t sz; + const char* data = PyUnicode_AsUTF8AndSize(args[0], &sz); + if (sz < 0) { + return nullptr; + } + key = {data, static_cast<size_t>(sz)}; + } else { + char* data = nullptr; + Py_ssize_t sz; + if (PyBytes_AsStringAndSize(args[0], &data, &sz) != 0) { + return nullptr; + } + key = {data, static_cast<size_t>(sz)}; + } + + return CallWithErrorTranslation([&]{ + int res = NResource::Has(key); + return PyBool_FromLong(res); + }); +} + +} + +const unsigned char res_importer_pyc[] = { + #include "__res.pyc.inc" +}; + +int mod__res_exec(PyObject *mod) noexcept { + PyObject* modules = PySys_GetObject("modules"); + Y_ASSERT(modules); + Y_ASSERT(PyMapping_Check(modules)); + if (PyMapping_SetItemString(modules, "run_import_hook", mod) == -1) { + return -1; + } + + PyObject *bytecode = PyMarshal_ReadObjectFromString( + reinterpret_cast<const char*>(res_importer_pyc), + std::size(res_importer_pyc) + ); + if (bytecode == NULL) { + return -1; + } + + // The code below which sets "__builtins__" is a workarownd for issue + // reported here https://github.com/python/cpython/issues/130272 . + // The problem can be seen for Y_PYTHON_SOURCE_ROOT mode when trying + // compiling the code wich contains non-ascii identifiers. In this case + // call to `compile` in get_code function raises the exception + // KeyError: '__builtins__' inside `PyImport_Import` function. + PyObject* builtinsKey = NULL; + Y_DEFER { + Py_DECREF(bytecode); + Py_DECREF(builtinsKey); + }; + PyObject* modns = PyModule_GetDict(mod); + if (!modns) { + return -1; + } + builtinsKey = PyUnicode_FromString("__builtins__"); + if (builtinsKey == NULL) { + return -1; + } + int r = PyDict_Contains(modns, builtinsKey); + if (r < 0) { + return -1; + } if (r == 0) { + PyObject* builtins = PyEval_GetBuiltins(); + if (builtins == NULL) { + return -1; + } + if (PyDict_SetItem(modns, builtinsKey, builtins) < 0) { + return -1; + } + } + + if (PyObject* evalRes = PyEval_EvalCode(bytecode, modns, modns)) { + Py_DECREF(evalRes); + } + if (PyErr_Occurred()) { + return -1; + } + return 0; +} + +PyDoc_STRVAR(mod__res_doc, +"resfs python bindings module with importer hook supporting hermetic python programs."); + +PyMethodDef mod__res_methods[] = { + {"count", _PyCFunction_CAST(NWrap::Count), METH_NOARGS, PyDoc_STR("Returns number of embedded resources.")}, + {"key_by_index", _PyCFunction_CAST(NWrap::KeyByIndex), METH_FASTCALL, PyDoc_STR("Returns resource key by resource index.")}, + {"find", _PyCFunction_CAST(NWrap::Find), METH_FASTCALL, PyDoc_STR("Finds resource content by key.")}, + {"has", _PyCFunction_CAST(NWrap::Has), METH_FASTCALL, PyDoc_STR("Checks if the resource with the given key exists.")}, + {nullptr, nullptr, 0, nullptr} +}; + +PyModuleDef_Slot mod__res_slots[] = { + {Py_mod_exec, reinterpret_cast<void*>(&mod__res_exec)}, + {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED}, + {0, nullptr}, +}; + +PyModuleDef mod__res = { + .m_base = PyModuleDef_HEAD_INIT, + .m_name = "__res", + .m_doc = mod__res_doc, + .m_size = 0, + .m_methods = mod__res_methods, + .m_slots = mod__res_slots, + .m_traverse = nullptr, + .m_clear = nullptr, + .m_free = nullptr +}; + +} + +PyMODINIT_FUNC +PyInit___res() noexcept { + return PyModuleDef_Init(&mod__res); +} diff --git a/library/python/runtime_py3/importer.pxi b/library/python/runtime_py3/__res.py index 51bc3020a3f..9fb2d09481a 100644 --- a/library/python/runtime_py3/importer.pxi +++ b/library/python/runtime_py3/__res.py @@ -1,3 +1,15 @@ +# def count() -> int: +# # implemented in C++ part of this module +# +# def key_by_index(idx: key) -> bytes: +# # implemented in C++ part of this module +# +# def find(key: str | bytes) -> bytes: +# # implemented in C++ part of this module +# +# def has(key: str | bytes) -> bool: +# # implemented in C++ part of this module + import marshal import sys from _codecs import utf_8_decode, utf_8_encode @@ -16,8 +28,6 @@ from _frozen_importlib_external import ( 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_source_root = b'Y_PYTHON_SOURCE_ROOT' @@ -25,7 +35,6 @@ 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 def _probe(environ_dict, key, default_value=None): """ Probe bytes and str variants for environ. @@ -47,9 +56,10 @@ def _probe(environ_dict, key, default_value=None): py_prefix = b'py/' py_prefix_len = len(py_prefix) -EXTERNAL_PY_FILES_MODE = __resource.find(b'py/conf/ENABLE_EXTERNAL_PY_FILES') in (b'1', b'yes') +EXTERNAL_PY_FILES_MODE = find(b'py/conf/ENABLE_EXTERNAL_PY_FILES') in (b'1', b'yes') + +YA_IDE_VENV = find(res_ya_ide_venv) -YA_IDE_VENV = __resource.find(res_ya_ide_venv) Y_PYTHON_EXTENDED_SOURCE_SEARCH = _probe(_os.environ, env_extended_source_search) or YA_IDE_VENV @@ -182,8 +192,8 @@ def _print(*xs): def iter_keys(prefix): l = len(prefix) - for idx in range(__resource.count()): - key = __resource.key_by_index(idx) + for idx in range(count()): + key = key_by_index(idx) if key.startswith(prefix): yield key, key[l:] @@ -229,14 +239,14 @@ def resfs_src(key, resfs_file=False): """ if resfs_file: key = b'resfs/file/' + _b(key) - return __resource.find(b'resfs/src/' + _b(key)) + return find(b'resfs/src/' + _b(key)) def resfs_has(path): """ Return true if the requested file is embedded in the program """ - return __resource.has(b'resfs/file/' + _b(path)) + return has(b'resfs/file/' + _b(path)) def resfs_read(path, builtin=None): @@ -253,7 +263,7 @@ def resfs_read(path, builtin=None): return file_bytes(fspath) if builtin is not False: - return __resource.find(b'resfs/file/' + _b(path)) + return find(b'resfs/file/' + _b(path)) def resfs_files(prefix=b''): @@ -605,7 +615,7 @@ class ArcadiaSourceFinder: 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':') + namespaces = 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)) diff --git a/library/python/runtime_py3/__res.pyx b/library/python/runtime_py3/__res.pyx deleted file mode 100644 index 2c1d0c3ab4d..00000000000 --- a/library/python/runtime_py3/__res.pyx +++ /dev/null @@ -1,44 +0,0 @@ -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 bool Has(const TStringBuf key) except + - 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 - - -def has(s): - if isinstance(s, str): - s = utf_8_encode(s)[0] - - return Has(s) - - -include "importer.pxi" diff --git a/library/python/runtime_py3/runtime_reg_py3.cpp b/library/python/runtime_py3/runtime_reg_py3.cpp new file mode 100644 index 00000000000..283fa70254d --- /dev/null +++ b/library/python/runtime_py3/runtime_reg_py3.cpp @@ -0,0 +1,17 @@ +#include <Python.h> + +extern "C" PyObject* PyInit___res(); +extern "C" PyObject* PyInit_sitecustomize(); + +namespace { + struct TRegistrar { + inline TRegistrar() { + _inittab mods[] = { + {"__res", PyInit___res}, + {"sitecustomize", PyInit_sitecustomize}, + {nullptr, nullptr} + }; + PyImport_ExtendInittab(mods); + } + } REG; +} diff --git a/library/python/runtime_py3/sitecustomize.cpp b/library/python/runtime_py3/sitecustomize.cpp new file mode 100644 index 00000000000..be68ad39780 --- /dev/null +++ b/library/python/runtime_py3/sitecustomize.cpp @@ -0,0 +1,63 @@ +#include <Python.h> +#include <marshal.h> + +#include <iterator> + +namespace { + +const unsigned char sitecustomize_pyc[] = { + #include "sitecustomize.pyc.inc" +}; + +int modsitecustomize_exec(PyObject *mod) noexcept { + PyObject *bytecode = PyMarshal_ReadObjectFromString( + reinterpret_cast<const char*>(sitecustomize_pyc), + std::size(sitecustomize_pyc) + ); + if (!bytecode) { + return -1; + } + PyObject* modns = PyModule_GetDict(mod); + if (!modns) { + return -1; + } + if (PyObject* evalRes = PyEval_EvalCode(bytecode, modns, modns)) { + Py_DECREF(evalRes); + } + if (PyErr_Occurred()) { + return -1; + } + return 0; +} + +PyDoc_STRVAR(modsitecustomize_doc, +"bridge between Arcadia resource system and python importlib resources interface."); + +PyMethodDef modsitecustomize_methods[] = { + {nullptr, nullptr, 0, nullptr} +}; + +PyModuleDef_Slot modsitecustomize_slots[] = { + {Py_mod_exec, reinterpret_cast<void*>(&modsitecustomize_exec)}, + {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED}, + {0, nullptr}, +}; + +PyModuleDef modsitecustomize = { + .m_base = PyModuleDef_HEAD_INIT, + .m_name = "sitecustomize", + .m_doc = modsitecustomize_doc, + .m_size = 0, + .m_methods = modsitecustomize_methods, + .m_slots = modsitecustomize_slots, + .m_traverse = nullptr, + .m_clear = nullptr, + .m_free = nullptr +}; + +} + +PyMODINIT_FUNC +PyInit_sitecustomize() noexcept { + return PyModuleDef_Init(&modsitecustomize); +} diff --git a/library/python/runtime_py3/sitecustomize.pyx b/library/python/runtime_py3/sitecustomize.py index 8d30073d7d1..3b30b8807e8 100644 --- a/library/python/runtime_py3/sitecustomize.pyx +++ b/library/python/runtime_py3/sitecustomize.py @@ -11,7 +11,7 @@ from importlib.metadata import ( ) from importlib.resources.abc import Traversable -import __res +from __res import _ResfsResourceReader, find, iter_keys, resfs_read, resfs_files METADATA_NAME = re.compile("^Name: (.*)$", re.MULTILINE) @@ -55,7 +55,7 @@ class ArcadiaResource(ArcadiaTraversable): return False def open(self, mode="r", *args, **kwargs): - data = __res.find(self._resfs.encode("utf-8")) + data = find(self._resfs.encode("utf-8")) if data is None: raise FileNotFoundError(self._resfs) @@ -85,7 +85,7 @@ class ArcadiaResourceContainer(ArcadiaTraversable): def iterdir(self): seen = set() - for key, path_without_prefix in __res.iter_keys(self._resfs.encode("utf-8")): + for key, path_without_prefix in iter_keys(self._resfs.encode("utf-8")): if b"/" in path_without_prefix: subdir = path_without_prefix.split(b"/", maxsplit=1)[0].decode("utf-8") if subdir not in seen: @@ -127,7 +127,7 @@ class ArcadiaDistribution(Distribution): self._path = pathlib.Path(prefix) def read_text(self, filename): - data = __res.resfs_read(f"{self._prefix}{filename}") + data = resfs_read(f"{self._prefix}{filename}") if data is not None: return data.decode("utf-8") @@ -149,11 +149,11 @@ class MetadataArcadiaFinder(DistributionFinder): def _init_prefixes(cls): cls.prefixes.clear() - for resource in __res.resfs_files(): + for resource in resfs_files(): resource = resource.decode("utf-8") if not resource.endswith("METADATA"): continue - data = __res.resfs_read(resource).decode("utf-8") + data = resfs_read(resource).decode("utf-8") metadata_name = METADATA_NAME.search(data) if metadata_name: cls.prefixes[Prepared(metadata_name.group(1)).normalized] = resource.removesuffix("METADATA") diff --git a/library/python/runtime_py3/stage0pycc/main.cpp b/library/python/runtime_py3/stage0pycc/main.cpp new file mode 100644 index 00000000000..f66ea33b6e1 --- /dev/null +++ b/library/python/runtime_py3/stage0pycc/main.cpp @@ -0,0 +1,70 @@ +#include <util/folder/path.h> +#include <util/generic/scope.h> +#include <util/generic/string.h> +#include <util/stream/file.h> +#include <util/stream/output.h> + +#include <Python.h> +#include <marshal.h> + +#include <cstdio> +#include <system_error> + +struct TPyObjDeleter { + static void Destroy(PyObject* o) noexcept { + Py_XDECREF(o); + } +}; +using TPyObject = THolder<PyObject, TPyObjDeleter>; + +constexpr TStringBuf modPrefix = "mod="; + +int main(int argc, char** argv) { + if ((argc - 1) % 3 != 0) { + Cerr << "Usage:\n\t" << argv[0] << " (mod=SRC_PATH_X SRC OUT)+" << Endl; + return 1; + } + + PyConfig cfg{}; + PyConfig_InitIsolatedConfig(&cfg); + cfg._install_importlib = 0; + Y_SCOPE_EXIT(&cfg) {PyConfig_Clear(&cfg);}; + + for (int i = 0; i < (argc - 1)/3; ++i) { + const TString srcpath{TStringBuf{argv[3*i + 1]}.substr(modPrefix.size())}; + const TFsPath inPath{argv[3*i + 2]}; + const char* outPath = argv[3*i + 3]; + + const auto status = Py_InitializeFromConfig(&cfg); + if (PyStatus_Exception(status)) { + Py_ExitStatusException(status); + } + Y_SCOPE_EXIT() {Py_Finalize();}; + + TPyObject bytecode{Py_CompileString( + TFileInput{inPath}.ReadAll().c_str(), + srcpath.c_str(), + Py_file_input + )}; + if (!bytecode) { + Cerr << "Failed to compile " << outPath << Endl; + PyErr_Print(); + return 1; + } + + if (FILE* out = fopen(outPath, "wb")) { + PyMarshal_WriteObjectToFile(bytecode.Get(), out, Py_MARSHAL_VERSION); + fclose(out); + if (PyErr_Occurred()) { + Cerr << "Failed to marshal " << outPath << Endl; + PyErr_Print(); + return 1; + } + } else { + Cerr << "Failed to write " << outPath << ": " << std::error_code{errno, std::system_category()}.message() << Endl; + return 1; + } + } + + return 0; +} diff --git a/library/python/runtime_py3/stage0pycc/ya.make b/library/python/runtime_py3/stage0pycc/ya.make new file mode 100644 index 00000000000..b182365da87 --- /dev/null +++ b/library/python/runtime_py3/stage0pycc/ya.make @@ -0,0 +1,10 @@ +PROGRAM() + +PYTHON3_ADDINCL() +PEERDIR( + contrib/tools/python3 +) + +SRCS(main.cpp) + +END() diff --git a/library/python/runtime_py3/test/subinterpreter/py3_subinterpreters.cpp b/library/python/runtime_py3/test/subinterpreter/py3_subinterpreters.cpp new file mode 100644 index 00000000000..0a934d4db50 --- /dev/null +++ b/library/python/runtime_py3/test/subinterpreter/py3_subinterpreters.cpp @@ -0,0 +1,82 @@ +#include "stdout_interceptor.h" + +#include <util/stream/str.h> + +#include <library/cpp/testing/gtest/gtest.h> + +#include <Python.h> + +#include <thread> +#include <algorithm> + +struct TSubinterpreters: ::testing::Test { + static void SetUpTestSuite() { + Py_InitializeEx(0); + EXPECT_TRUE(TPyStdoutInterceptor::SetupInterceptionSupport()); + } + static void TearDownTestSuite() { + Py_Finalize(); + } + + static void ThreadPyRun(PyInterpreterState* interp, IOutputStream& pyout, const char* pycode) { + PyThreadState* state = PyThreadState_New(interp); + PyEval_RestoreThread(state); + + { + TPyStdoutInterceptor interceptor{pyout}; + PyRun_SimpleString(pycode); + } + + PyThreadState_Clear(state); + PyThreadState_DeleteCurrent(); + } +}; + +TEST_F(TSubinterpreters, NonSubinterpreterFlowStillWorks) { + TStringStream pyout; + TPyStdoutInterceptor interceptor{pyout}; + + PyRun_SimpleString("print('Hello World')"); + EXPECT_EQ(pyout.Str(), "Hello World\n"); +} + +TEST_F(TSubinterpreters, ThreadedSubinterpretersFlowWorks) { + TStringStream pyout[2]; + + PyInterpreterConfig cfg = { + .use_main_obmalloc = 0, + .allow_fork = 0, + .allow_exec = 0, + .allow_threads = 1, + .allow_daemon_threads = 0, + .check_multi_interp_extensions = 1, + .gil = PyInterpreterConfig_OWN_GIL, + }; + + PyThreadState* mainState = PyThreadState_Get(); + PyThreadState *sub[2] = {nullptr, nullptr}; + Py_NewInterpreterFromConfig(&sub[0], &cfg); + ASSERT_NE(sub[0], nullptr); + Py_NewInterpreterFromConfig(&sub[1], &cfg); + ASSERT_NE(sub[1], nullptr); + PyThreadState_Swap(mainState); + + PyThreadState* savedState = PyEval_SaveThread(); + std::array<std::thread, 2> threads{ + std::thread{ThreadPyRun, sub[0]->interp, std::ref(pyout[0]), "print('Hello Thread 0')"}, + std::thread{ThreadPyRun, sub[1]->interp, std::ref(pyout[1]), "print('Hello Thread 1')"} + }; + std::ranges::for_each(threads, &std::thread::join); + PyEval_RestoreThread(savedState); + + PyThreadState_Swap(sub[0]); + Py_EndInterpreter(sub[0]); + + PyThreadState_Swap(sub[1]); + Py_EndInterpreter(sub[1]); + + PyThreadState_Swap(mainState); + + EXPECT_EQ(pyout[0].Str(), "Hello Thread 0\n"); + EXPECT_EQ(pyout[1].Str(), "Hello Thread 1\n"); +} diff --git a/library/python/runtime_py3/test/subinterpreter/stdout_interceptor.cpp b/library/python/runtime_py3/test/subinterpreter/stdout_interceptor.cpp new file mode 100644 index 00000000000..3cd4b69d012 --- /dev/null +++ b/library/python/runtime_py3/test/subinterpreter/stdout_interceptor.cpp @@ -0,0 +1,77 @@ +#include "stdout_interceptor.h" + +#include <util/stream/output.h> + +namespace { + +struct TOStreamWrapper { + PyObject_HEAD + IOutputStream* Stm = nullptr; +}; + +PyObject* Write(TOStreamWrapper *self, PyObject *const *args, Py_ssize_t nargs) noexcept { + try { + Py_buffer view; + for (Py_ssize_t i = 0; i < nargs; ++i) { + PyObject* buf = args[i]; + if (PyUnicode_Check(args[i])) { + buf = PyUnicode_AsUTF8String(buf); + if (!buf) { + return nullptr; + } + } + + if (PyObject_GetBuffer(buf, &view, PyBUF_SIMPLE | PyBUF_C_CONTIGUOUS) == -1) { + return nullptr; + } + self->Stm->Write(reinterpret_cast<const char*>(view.buf), view.len); + PyBuffer_Release(&view); + } + + return Py_None; + } catch(const std::exception& err) { + PyErr_SetString(PyExc_IOError, err.what()); + } catch (...) { + PyErr_SetString(PyExc_RuntimeError, "Unhandled C++ exception of unknown type"); + } + return nullptr; +} + +PyMethodDef TOStreamWrapperMethods[] = { + {"write", reinterpret_cast<PyCFunction>(Write), METH_FASTCALL, PyDoc_STR("write buffer to wrapped C++ stream")}, + {} +}; + +PyTypeObject TOStreamWrapperType { + .ob_base = PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "testwrap.OStream", + .tp_basicsize = sizeof(TOStreamWrapper), + .tp_itemsize = 0, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_doc = PyDoc_STR("C++ IOStream wrapper"), + .tp_methods = TOStreamWrapperMethods, + .tp_new = PyType_GenericNew, +}; + +} + +TPyStdoutInterceptor::TPyStdoutInterceptor(IOutputStream& redirectionStream) noexcept + : RealStdout_{PySys_GetObject("stdout")} +{ + Py_INCREF(RealStdout_); + + PyObject* redirect = TOStreamWrapperType.tp_alloc(&TOStreamWrapperType, 0); + reinterpret_cast<TOStreamWrapper*>(redirect)->Stm = &redirectionStream; + + PySys_SetObject("stdout", redirect); + Py_DECREF(redirect); +} + +TPyStdoutInterceptor::~TPyStdoutInterceptor() noexcept { + PySys_SetObject("stdout", RealStdout_); + Py_DECREF(RealStdout_); +} + +bool TPyStdoutInterceptor::SetupInterceptionSupport() noexcept { + return PyType_Ready(&TOStreamWrapperType) == 0; +} diff --git a/library/python/runtime_py3/test/subinterpreter/stdout_interceptor.h b/library/python/runtime_py3/test/subinterpreter/stdout_interceptor.h new file mode 100644 index 00000000000..a1e219953f0 --- /dev/null +++ b/library/python/runtime_py3/test/subinterpreter/stdout_interceptor.h @@ -0,0 +1,16 @@ +#pragma once + +#include <Python.h> + +class IOutputStream; + +class TPyStdoutInterceptor { +public: + TPyStdoutInterceptor(IOutputStream& redirectionStream) noexcept; + ~TPyStdoutInterceptor() noexcept; + + static bool SetupInterceptionSupport() noexcept; + +private: + PyObject* RealStdout_; +}; diff --git a/library/python/runtime_py3/test/subinterpreter/ya.make b/library/python/runtime_py3/test/subinterpreter/ya.make new file mode 100644 index 00000000000..78cc82304c8 --- /dev/null +++ b/library/python/runtime_py3/test/subinterpreter/ya.make @@ -0,0 +1,10 @@ +GTEST() + +USE_PYTHON3() + +SRCS( + py3_subinterpreters.cpp + stdout_interceptor.cpp +) + +END() diff --git a/library/python/runtime_py3/test/test_arcadia_source_finder.py b/library/python/runtime_py3/test/test_arcadia_source_finder.py index 9f794f03591..835e60c6710 100644 --- a/library/python/runtime_py3/test/test_arcadia_source_finder.py +++ b/library/python/runtime_py3/test/test_arcadia_source_finder.py @@ -18,7 +18,7 @@ class ImporterMocks: 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.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), diff --git a/library/python/runtime_py3/test/ya.make b/library/python/runtime_py3/test/ya.make index e0c4061ad2c..fde64236dca 100644 --- a/library/python/runtime_py3/test/ya.make +++ b/library/python/runtime_py3/test/ya.make @@ -34,4 +34,7 @@ RESOURCE_FILES( END() -RECURSE_FOR_TESTS(traceback) +RECURSE_FOR_TESTS( + subinterpreter + traceback +) diff --git a/library/python/runtime_py3/ya.make b/library/python/runtime_py3/ya.make index dc97c8e2e08..b2d0dbf51ee 100644 --- a/library/python/runtime_py3/ya.make +++ b/library/python/runtime_py3/ya.make @@ -8,21 +8,18 @@ PEERDIR( library/cpp/resource ) -CFLAGS(-DCYTHON_REGISTER_ABCS=0) - NO_PYTHON_INCLUDES() ENABLE(PYBUILD_NO_PYC) +SRCS( + __res.cpp + sitecustomize.cpp + GLOBAL runtime_reg_py3.cpp +) + PY_SRCS( entry_points.py - TOP_LEVEL - - CYTHON_DIRECTIVE - language_level=3 - - __res.pyx - sitecustomize.pyx ) IF (EXTERNAL_PY_FILES) @@ -31,17 +28,16 @@ IF (EXTERNAL_PY_FILES) ) ENDIF() -IF (CYTHON_COVERAGE) - # Let covarage support add all needed files to resources -ELSE() - RESOURCE_FILES( - DONT_COMPRESS - PREFIX ${MODDIR}/ - __res.pyx - importer.pxi - sitecustomize.pyx - ) -ENDIF() +RUN_PROGRAM( + library/python/runtime_py3/stage0pycc + mod=${MODDIR}/__res.py __res.py __res.pyc + mod=${MODDIR}/sitecustomize.py sitecustomize.py sitecustomize.pyc + IN __res.py sitecustomize.py + OUT_NOAUTO __res.pyc sitecustomize.pyc + ENV PYTHONHASHSEED=0 +) +ARCHIVE(NAME __res.pyc.inc DONTCOMPRESS __res.pyc) +ARCHIVE(NAME sitecustomize.pyc.inc DONTCOMPRESS sitecustomize.pyc) END() |