File indexing completed on 2024-11-24 03:56:27
0001 /* 0002 * SPDX-FileCopyrightText: 2019-2023 Mattia Basaglia <dev@dragon.best> 0003 * 0004 * SPDX-License-Identifier: GPL-3.0-or-later 0005 */ 0006 0007 #include "python_engine.hpp" 0008 0009 #include "app/scripting/python/register_machinery.hpp" 0010 #include "app/log/log.hpp" 0011 #include "app/env.hpp" 0012 0013 0014 app::scripting::ScriptEngine::Autoregister<app::scripting::python::PythonEngine> app::scripting::python::PythonEngine::autoreg; 0015 0016 static int counter = 0; 0017 0018 0019 class PY_HIDDEN CaptureStream 0020 { 0021 public: 0022 using OwnerT = app::scripting::ScriptExecutionContext; 0023 using SignalT = void (OwnerT::*)(const QString&); 0024 0025 CaptureStream(OwnerT* owner = nullptr, SignalT signal = nullptr) 0026 : owner(owner), signal(signal) 0027 {} 0028 0029 ~CaptureStream() 0030 { 0031 if ( owner ) 0032 { 0033 py::delattr(stream, "write"); 0034 py::delattr(stream, "flush"); 0035 } 0036 } 0037 0038 void setup(OwnerT* owner, SignalT signal, py::object dest) 0039 { 0040 try { 0041 dest.attr("write") = py::cpp_function([this](const QString& s){ write(s); }); 0042 dest.attr("flush") = py::cpp_function([this](){ flush(); }); 0043 dest.attr("isatty") = py::cpp_function([](){ return false; }); 0044 stream = dest; 0045 this->owner = owner; 0046 this->signal = signal; 0047 } catch ( const py::error_already_set& pyexc ) { 0048 app::log::Log("Python").stream(app::log::Error) 0049 << "Could not initialize stream capture:" << pyexc.what(); 0050 } 0051 } 0052 0053 void write(const QString& data) 0054 { 0055 if ( data.isEmpty() ) 0056 return; 0057 0058 int from = 0; 0059 int to = data.indexOf('\n'); 0060 while ( to != -1 ) 0061 { 0062 QString txt; 0063 if ( !buf.isEmpty() ) 0064 { 0065 txt = buf; 0066 buf.clear(); 0067 } 0068 txt += data.mid(from, to-from); 0069 (owner->*signal)(txt); 0070 from = to+1; 0071 to = data.indexOf('\n', from); 0072 } 0073 buf += data.mid(from); 0074 } 0075 0076 void flush() 0077 { 0078 (owner->*signal)(buf); 0079 buf.clear(); 0080 } 0081 0082 private: 0083 OwnerT* owner; 0084 SignalT signal; 0085 QString buf; 0086 py::object stream; 0087 }; 0088 0089 0090 class PY_HIDDEN app::scripting::python::PythonContext::Private 0091 { 0092 public: 0093 void init_capture(PythonContext* ctx) 0094 { 0095 sys = py::module::import("sys"); 0096 py::object py_stdout = sys.attr("stdout"); 0097 py::object py_stderr = sys.attr("stderr"); 0098 py_stdin = py::module::import("io").attr("StringIO")(); 0099 sys.attr("stdin") = py_stdin; 0100 0101 if ( !py_stdout.is(py::none()) ) 0102 { 0103 stdout_cap.setup(ctx, &PythonContext::stdout_line, py_stdout); 0104 stderr_cap.setup(ctx, &PythonContext::stderr_line, py_stderr); 0105 } 0106 0107 } 0108 0109 std::vector<pybind11::module> my_modules; 0110 py::dict globals; 0111 py::function compile; 0112 const ScriptEngine* engine; 0113 py::module sys; 0114 0115 CaptureStream stderr_cap, stdout_cap; 0116 py::object py_stdin; 0117 0118 }; 0119 0120 app::scripting::python::PythonContext::PythonContext(const ScriptEngine* engine) 0121 { 0122 /// @note Not thread safe, pybind11 doesn't support multiple interpreters at once 0123 if ( counter == 0 ) 0124 py::initialize_interpreter(); 0125 counter++; 0126 0127 d = std::make_unique<Private>(); 0128 d->globals = py::globals(); 0129 d->compile = py::function(py::module(d->globals["__builtins__"]).attr("compile")); 0130 d->engine = engine; 0131 d->init_capture(this); 0132 } 0133 0134 app::scripting::python::PythonContext::~PythonContext() 0135 { 0136 d.reset(); 0137 0138 counter--; 0139 if ( counter == 0 ) 0140 py::finalize_interpreter(); 0141 } 0142 0143 void app::scripting::python::PythonContext::expose(const QString& name, const QVariant& obj) 0144 { 0145 try { 0146 d->globals[name.toStdString().c_str()] = obj; 0147 } catch ( const py::error_already_set& pyexc ) { 0148 throw ScriptError(pyexc.what()); 0149 } 0150 } 0151 0152 QString app::scripting::python::PythonContext::eval_to_string(const QString& code) 0153 { 0154 std::string std_code = code.toStdString(); 0155 bool eval = false; 0156 0157 try { 0158 d->compile(std_code, "", "eval"); 0159 eval = true; 0160 } catch ( const py::error_already_set& ) {} 0161 0162 try { 0163 if ( eval ) 0164 return QString::fromStdString(py::repr(py::eval(std_code)).cast<std::string>()); 0165 py::exec(std_code); 0166 return {}; 0167 } catch ( const py::error_already_set& pyexc ) { 0168 throw ScriptError(pyexc.what()); 0169 } catch ( const py::builtin_exception& pyexc ) { 0170 throw ScriptError(pyexc.what()); 0171 } 0172 } 0173 0174 QStringList app::scripting::python::PythonContext::eval_completions(const QString& code) 0175 { 0176 std::string std_code = code.toStdString(); 0177 std::string eval_code; 0178 0179 try 0180 { 0181 if ( std_code.empty() ) 0182 { 0183 eval_code = "[_glaxnimate_iter for _glaxnimate_iter in globals().keys() if not _glaxnimate_iter.startswith('_')]"; 0184 } 0185 else 0186 { 0187 py::exec("import inspect"); 0188 eval_code = "[_glaxnimate_iter[0] for _glaxnimate_iter in inspect.getmembers(" + std_code + ") if not _glaxnimate_iter[0].startswith('_')]"; 0189 } 0190 0191 return py::eval(eval_code).cast<QStringList>(); 0192 } 0193 catch ( const py::error_already_set& ) {} 0194 catch ( const py::builtin_exception& ) {} 0195 0196 return {}; 0197 } 0198 0199 void app::scripting::python::PythonContext::app_module ( const QString& name ) 0200 { 0201 try { 0202 auto sname = name.toStdString(); 0203 const char* cname = sname.c_str(); 0204 d->my_modules.push_back(py::module::import(cname)); 0205 d->globals[cname] = d->my_modules.back(); 0206 } catch ( const py::error_already_set& pyexc ) { 0207 log::Log("Python", name).log(pyexc.what(), log::Error); 0208 } 0209 } 0210 0211 0212 class PY_HIDDEN ModuleSetter 0213 { 0214 public: 0215 ModuleSetter(py::module& sys, const QString& append) : sys(sys) 0216 { 0217 python_path = py::list(sys.attr("path")); 0218 py::list python_path_new(sys.attr("path")); 0219 python_path_new.append(append); 0220 py::setattr(sys, "path", python_path_new); 0221 } 0222 0223 ~ModuleSetter() 0224 { 0225 py::setattr(sys, "path", python_path); 0226 } 0227 0228 py::module& sys; 0229 py::list python_path; 0230 }; 0231 0232 bool app::scripting::python::PythonContext::run_from_module ( 0233 const QDir& path, 0234 const QString& module, 0235 const QString& function, 0236 const QVariantList& args 0237 ) 0238 { 0239 ModuleSetter{d->sys, path.path()}; 0240 0241 try { 0242 py::module exec_module = py::module::import(module.toStdString().c_str()); 0243 std::string std_func = function.toStdString(); 0244 if ( !py::hasattr(exec_module, std_func.c_str()) ) 0245 return false; 0246 0247 py::tuple py_args(args.size()); 0248 int i = 0; 0249 for ( const auto& arg : args ) 0250 py_args[i++] = arg; 0251 exec_module.attr(std_func.c_str())(*py_args); 0252 } catch ( const py::error_already_set& pyexc ) { 0253 throw ScriptError(pyexc.what()); 0254 } 0255 0256 return true; 0257 } 0258 0259 const app::scripting::ScriptEngine * app::scripting::python::PythonContext::engine() const 0260 { 0261 return d->engine; 0262 } 0263 0264 app::scripting::ScriptContext app::scripting::python::PythonEngine::create_context() const 0265 { 0266 return std::make_unique<PythonContext>(this); 0267 } 0268 0269 void app::scripting::python::PythonEngine::add_module_search_paths(const QStringList& paths) 0270 { 0271 app::Environment::Variable("PYTHONPATH").push_back(paths); 0272 }