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 }