See More

// Copyright (c) 2017-2018 Manuel Schneider #include #include #include "pythonmodulev1.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "cast_specialization.h" #include "albert/query.h" #include "albert/util/standarditem.h" #include "xdg/iconlookup.h" using namespace std; using namespace Core; namespace py = pybind11; Q_LOGGING_CATEGORY(qlc_python_modulev1, "python.modulev1") #define DEBUG qCDebug(qlc_python_modulev1).noquote() #define INFO qCInfo(qlc_python_modulev1).noquote() #define WARNING qCWarning(qlc_python_modulev1).noquote() #define CRITICAL qCCritical(qlc_python_modulev1).noquote() namespace { uint majorInterfaceVersion = 0; uint minorInterfaceVersion = 2; enum Target { IID, NAME, VERSION, TRIGGER, AUTHOR, DEPS}; const QStringList targetNames = { "__iid__", "__prettyname__", "__version__", "__trigger__", "__author__", "__dependencies__" }; } class Python::PythonModuleV1Private { public: QString path; QString sourceFilePath; QString id; // id, __name__, Effectively the module name PythonModuleV1::State state; QString errorString; py::module module; struct Spec { QString iid; QString prettyName; QString author; QString version; QString trigger; QString description; QStringList dependencies; } spec; }; /** ***************************************************************************/ Python::PythonModuleV1::PythonModuleV1(const QString &path) : d(new PythonModuleV1Private) { d->path = path; QFileInfo fileInfo{d->path}; if (!fileInfo.exists()) throw std::runtime_error("Path does not exist"); else if (fileInfo.isDir()){ QDir dir{path}; if (dir.exists("__init__.py")) d->sourceFilePath = dir.filePath("__init__.py"); else throw std::runtime_error("Dir does not contain an init file"); } else if (fileInfo.isFile()) d->sourceFilePath = fileInfo.absoluteFilePath(); else qFatal("This should never happen"); d->spec.prettyName = d->id = fileInfo.completeBaseName(); d->state = State::InvalidMetadata; readMetadata(); } void Python::PythonModuleV1::readMetadata() { DEBUG << "Reading metadata of python module:" << QFileInfo(d->path).fileName(); py::gil_scoped_acquire acquire; try { // Get the extension spec source code QFile file(d->sourceFilePath); if(!file.open(QIODevice::ReadOnly)) throw QString("Cant open init file: %1").arg(d->sourceFilePath); QString source = QTextStream(&file).readAll(); file.close(); // Parse it with ast py::module ast = py::module::import("ast"); py::object ast_root = ast.attr("parse")(source.toStdString()); // Get all FunctionDef and Assign ast nodes std::map metadata_values; for (auto node : ast_root.attr("body")){ if (py::isinstance(node, ast.attr("FunctionDef"))) metadata_values.emplace(node.attr("name").cast(), node.attr("args").attr("args")); if (py::isinstance(node, ast.attr("Assign"))){ py::list targets = node.attr("targets"); if (py::len(targets) == 1 && py::isinstance(targets[0], ast.attr("Name"))){ QString targetName = targets[0].attr("id").cast(); if (targetNames.contains(targetName)) metadata_values.emplace(targetName, node.attr("value")); } } } // Check interface id QString targetName = targetNames[Target::IID]; if (!metadata_values.count(targetName)) throw QString("Module has no %1 specified").arg(targetName); py::object astStringType = py::module::import("ast").attr("Str"); py::object obj = metadata_values[targetName]; if (!py::isinstance(obj, astStringType)) throw QString("%1 is not of type ast.Str").arg(targetName); d->spec.iid = obj.attr("s").cast<:str>().cast(); QRegularExpression re("^PythonInterface\\/v(\\d)\\.(\\d)$"); QRegularExpressionMatch match = re.match(d->spec.iid); if (!match.hasMatch()) throw QString("Invalid interface id: %1").arg(d->spec.iid); uint maj = match.captured(1).toUInt(); if (maj != majorInterfaceVersion) throw QString("Incompatible major interface version. Expected %1, got %2").arg(majorInterfaceVersion).arg(maj); uint min = match.captured(2).toUInt(); if (min > minorInterfaceVersion) throw QString("Incompatible minor interface version. Up to %1 supported, got %2").arg(minorInterfaceVersion).arg(min); // Check mandatory handleQuery if (!metadata_values.count("handleQuery")) throw QString("Modules does not contain a function definition for 'handleQuery'"); if (py::len(metadata_values.at("handleQuery")) != 1) throw QString("handleQuery function definition does not take exactly one argument"); // Extract mandatory metadata obj = ast.attr("get_docstring")(ast_root); if (py::isinstance<:str>(obj)) d->spec.description = obj.cast<:str>().cast(); else throw QString("Module does not contain a docstring"); map zip{{Target::NAME, d->spec.prettyName}, {Target::VERSION, d->spec.version}, {Target::AUTHOR, d->spec.author}}; for (const auto &pair : zip) { targetName = targetNames[pair.first]; if (metadata_values.count(targetName)){ obj = metadata_values[targetName]; if (py::isinstance(obj, astStringType)) pair.second = obj.attr("s").cast<:str>().cast(); else throw QString("%1 is not of type ast.Str").arg(targetName); } else throw QString("Module has no %1 specified").arg(targetName); } // Extract optional metadata targetName = targetNames[Target::TRIGGER]; if (metadata_values.count(targetName)){ obj = metadata_values[targetName]; if (py::isinstance(obj, astStringType)) d->spec.trigger = obj.attr("s").cast<:str>().cast(); else throw QString("%1 is not of type ast.Str").arg(targetName); } targetName = targetNames[Target::DEPS]; if (metadata_values.count(targetName)) { py::list deps = metadata_values[targetName].attr("elts").cast<:list>(); for (const py::handle dep : deps) { if (py::isinstance(dep, astStringType)) d->spec.dependencies.append(dep.attr("s").cast<:str>().cast()); else throw QString("Dependencies contain non string values"); } } d->state = State::Unloaded; } catch(const QString &error) { d->errorString = error; WARNING << QString("[%1] %2").arg(d->id).arg(d->errorString); d->state = State::InvalidMetadata; } catch(const std::exception &e) { d->errorString = e.what(); WARNING << QString("[%1] %2").arg(d->id).arg(d->errorString); d->state = State::InvalidMetadata; } } /** ***************************************************************************/ Python::PythonModuleV1::~PythonModuleV1() { unload(); } /** ***************************************************************************/ void Python::PythonModuleV1::load(){ if (d->state == State::Loaded || d->state == State::InvalidMetadata) return; py::gil_scoped_acquire acquire; try { DEBUG << "Loading" << d->path; py::module importlib = py::module::import("importlib"); py::module importli_util = py::module::import("importlib.util"); py::object spec = importli_util.attr("spec_from_file_location")(QString("albert.%1").arg(d->id), d->sourceFilePath); // Prefix to avoid conflicts d->module = importli_util.attr("module_from_spec")(spec); spec.attr("loader").attr("exec_module")(d->module); // Call init function, if exists if (py::hasattr(d->module, "initialize")) if (py::isinstance<:function>(d->module.attr("initialize"))) d->module.attr("initialize")(); } catch(const std::exception &e) { d->errorString = e.what(); WARNING << QString("[%1] %2.").arg(QFileInfo(d->path).fileName()).arg(d->errorString); d->module = py::object(); d->state = State::Error; return; } d->state = State::Loaded; } /** ***************************************************************************/ void Python::PythonModuleV1::unload(){ if (d->state == State::Unloaded) return; if (d->state == State::Loaded) { DEBUG << "Unloading" << d->path; py::gil_scoped_acquire acquire; try { // Call fini function, if exists if (py::hasattr(d->module, "finalize")) if (py::isinstance<:function>(d->module.attr("finalize"))) d->module.attr("finalize")(); // Dereference module, unloads hopefully d->module = py::object(); } catch(std::exception const &e) { WARNING << QString("[%1] %2.").arg(QFileInfo(d->path).fileName()).arg(e.what()); } } d->errorString.clear(); d->state = State::Unloaded; } /** ***************************************************************************/ void Python::PythonModuleV1::handleQuery(Query *query) const { py::gil_scoped_acquire acquire; try { vector,uint>> results; py::function f = py::function(d->module.attr("handleQuery")); py::object pythonResult = f(query); if ( !query->isValid() ) return; if (py::isinstance<:list>(pythonResult)) { py::list list(pythonResult); for(py::size_t i = 0; i < py::len(pythonResult); ++i) { py::object elem = list[i]; results.emplace_back(elem.cast>(), 0); } query->addMatches(std::make_move_iterator(results.begin()), std::make_move_iterator(results.end())); } if (py::isinstance(pythonResult)) { query->addMatch(pythonResult.cast>()); } } catch(const exception &e) { WARNING << QString("[%1] %2.").arg(d->id).arg(e.what()); } } /** ***************************************************************************/ Python::PythonModuleV1::State Python::PythonModuleV1::state() const { return d->state; } const QString &Python::PythonModuleV1::errorString() const { return d->errorString; } const QString &Python::PythonModuleV1::path() const { return d->path; } const QString &Python::PythonModuleV1::sourcePath() const { return d->sourceFilePath; } const QString &Python::PythonModuleV1::id() const { return d->id; } const QString &Python::PythonModuleV1::name() const { return d->spec.prettyName; } const QString &Python::PythonModuleV1::author() const { return d->spec.author; } const QString &Python::PythonModuleV1::version() const { return d->spec.version; } const QString &Python::PythonModuleV1::description() const { return d->spec.description; } const QString &Python::PythonModuleV1::trigger() const { return d->spec.trigger; } const QStringList &Python::PythonModuleV1::dependencies() const { return d->spec.dependencies; }