diff options
author | Tilman Roeder <tilman.roder@qt.io> | 2018-08-03 09:38:07 +0200 |
---|---|---|
committer | Tilman Roeder <tilman.roder@qt.io> | 2018-08-15 10:10:16 +0000 |
commit | 13e02b9aaea19ac21251d152a8fa69336ae76ebd (patch) | |
tree | a6320449c18251033d3a2557afaed6a8fcafbfc9 /plugins/pythonextensions/pyutil.cpp | |
parent | efea0c2e4a2966d88f65cdab90f841f7905dee14 (diff) |
Initial commit
This is a quite large commit containing:
* The main extension that runs and initializes Python
* Some (example) bindings
* An initial build script for the main extension
* Optional binding and examples of how to create them
* An initial build script for the optional bindings
* A simple extension manager written in Python
* A few example Python extensions
* Some documentation (both in the code and as markdown files)
* A collection of helpful python scripts
* A small collection of unit tests
* A TODO list
For any additional details the code / docs should be consulted.
Change-Id: I3937886cfefa2f64d5a78013889a8e097eec8261
Reviewed-by: Eike Ziller <eike.ziller@qt.io>
Diffstat (limited to 'plugins/pythonextensions/pyutil.cpp')
-rw-r--r-- | plugins/pythonextensions/pyutil.cpp | 271 |
1 files changed, 271 insertions, 0 deletions
diff --git a/plugins/pythonextensions/pyutil.cpp b/plugins/pythonextensions/pyutil.cpp new file mode 100644 index 0000000..56f7c03 --- /dev/null +++ b/plugins/pythonextensions/pyutil.cpp @@ -0,0 +1,271 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Python Extensions Plugin for QtCreator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "pyutil.h" + +#include <dlfcn.h> // dlopen + +#include <QtCore/QByteArray> +#include <QtCore/QCoreApplication> +#include <QtCore/QDebug> +#include <QtCore/QStringList> +#include <QtCore/QTemporaryFile> +#include <QtCore/QDir> + +// These are used in python and cause compile-time errors +// if still defined. +#undef signals +#undef slots + +#include <sbkpython.h> +#include <sbkconverter.h> +#include <sbkmodule.h> + +// Python has no concept of private variables and there +// is no way to declare a namespace or scope that will be +// inaccessible from the user script. +// To avoid naming collisions with the setup and tear down +// scripts that attempt to separate different extensions on +// the level of user code (or at least make them appear separated), +// this macro mangles the names of variables used. +// Use as: +// "... some Python code ..." privatName("my_var_name") "... more code ..." +#define privateName(name) "qt_creator_" name "_symbol_mchawrioklpilnjajqkfl" + +// Setup and utility functions for QtCreator bindings +// from typesystem.xml + +#if PY_MAJOR_VERSION >= 3 +extern "C" PyObject *PyInit_QtCreator(); +#else +extern "C" void initQtCreator(); +#endif + +// These variables store all Python types exported by QtCreators bindings, +// as well as the types exported for QtWidgets. +extern PyTypeObject **SbkPythonExtension_QtCreatorTypes; + +// This variable stores the Python module generated by Shiboken +extern PyObject *SbkPythonExtension_QtCreatorModuleObject; + +namespace PyUtil { + +static State state = PythonUninitialized; + +static void cleanup() +{ + if (state > PythonUninitialized) { + Py_Finalize(); + state = PythonUninitialized; + } +} + +State init() +{ + if (state > PythonUninitialized) + return state; + + // If there is an active python virtual environment, use that environment's packages location. + QByteArray virtualEnvPath = qgetenv("VIRTUAL_ENV"); + if (!virtualEnvPath.isEmpty()) + qputenv("PYTHONHOME", virtualEnvPath); + + // Python's shared libraries don't work properly if included from other + // shared libraries. See https://mail.python.org/pipermail/new-bugs-announce/2008-November/003322.html + #if PY_MAJOR_VERSION >= 3 + std::string version = "libpython"+std::to_string(PY_MAJOR_VERSION)+"."+std::to_string(PY_MINOR_VERSION)+"m.so"; + #else + std::string version = "libpython"+std::to_string(PY_MAJOR_VERSION)+"."+std::to_string(PY_MINOR_VERSION)+".so"; + #endif + dlopen(version.c_str(), RTLD_LAZY | RTLD_GLOBAL); + + Py_Initialize(); + qAddPostRoutine(cleanup); + state = PythonInitialized; + + #if PY_MAJOR_VERSION >= 3 + const bool pythonInitialized = PyInit_QtCreator() != nullptr; + #else + const bool pythonInitialized = true; + initQtCreator(); + #endif + const bool pyErrorOccurred = PyErr_Occurred() != nullptr; + if (pythonInitialized && !pyErrorOccurred) { + state = QtCreatorModuleLoaded; + } else { + if (pyErrorOccurred) + PyErr_Print(); + qWarning("Failed to initialize the QtCreator module."); + } + + // The Python interpreter eats SIGINT, which is not what we want. + // This stops it from happening. + // See https://mail.python.org/pipermail/cplusplus-sig/2012-December/016858.html + if (PyRun_SimpleString(std::string( + "import signal as " privateName("signal") "\n" + "" privateName("signal") ".signal(" privateName("signal") ".SIGINT, " privateName("signal") ".SIG_DFL)" + ).c_str()) == -1) { + if (PyErr_Occurred()) + PyErr_Print(); + qWarning("Failed to prevent SIGINT capture."); + } + + return state; +} + +bool createModule(const std::string &moduleName) +{ + if (init() != QtCreatorModuleLoaded) + return false; + + PyObject *module = PyImport_AddModule(moduleName.c_str()); + if (!module) { + if (PyErr_Occurred()) + PyErr_Print(); + qWarning() << __FUNCTION__ << "Failed to create module"; + return false; + } + + return true; +} + +bool bindObject(const QString &moduleName, const QString &name, int index, void *o) +{ + if (init() != QtCreatorModuleLoaded) + return false; + + // Generate the type + PyTypeObject *typeObject = SbkPythonExtension_QtCreatorTypes[index]; + + PyObject *po = Shiboken::Conversions::pointerToPython(reinterpret_cast<SbkObjectType *>(typeObject), o); + if (!po) { + qWarning() << __FUNCTION__ << "Failed to create wrapper for" << o; + return false; + } + Py_INCREF(po); + + return bindPyObject(moduleName, name, po); +} + +bool bindShibokenModuleObject(const QString &moduleName, const QString &name) +{ + return bindPyObject(moduleName, name, SbkPythonExtension_QtCreatorModuleObject); +} + +bool bindPyObject(const QString &moduleName, const QString &name, void *obj) +{ + if (init() != QtCreatorModuleLoaded) + return false; + + PyObject *module = PyImport_AddModule(moduleName.toLocal8Bit().constData()); + if (!module) { + if (PyErr_Occurred()) + PyErr_Print(); + qWarning() << __FUNCTION__ << "Failed to locate module" << moduleName; + return false; + } + + if (PyModule_AddObject(module, name.toLocal8Bit().constData(), (PyObject *)obj) < 0) { + if (PyErr_Occurred()) + PyErr_Print(); + qWarning() << __FUNCTION__ << "Failed to add object" << name << "to" << moduleName; + return false; + } + + return true; +} + +bool bindSubPyObject(const QString &moduleName, const QString &name, void *obj) +{ + PyObject *moduleDict = PyModule_GetDict((PyObject *)obj); + if (!moduleDict) { + if (PyErr_Occurred()) + PyErr_Print(); + qWarning("Could not obtain module dict"); + return false; + } + PyObject *moduleItem = PyDict_GetItemString(moduleDict, name.toLocal8Bit().constData()); + if (!moduleDict) { + if (PyErr_Occurred()) + PyErr_Print(); + qWarning() << "Could not obtain module item" << name; + return false; + } + return bindPyObject(moduleName, name, (void *)moduleItem); +} + +bool runScript(const std::string &script) +{ + if (init() == PythonUninitialized) + return false; + + if (PyRun_SimpleString(script.c_str()) == -1) { + if (PyErr_Occurred()) + PyErr_Print(); + return false; + } + + return true; +} + +bool runScriptWithPath(const std::string &script, const std::string &path) +{ + // I couldn't find a direct c api, but this should cause no variable name + // collisions. It also cleans the imported modules after the script finishes + const std::string s = +"import sys as " privateName("sys") "\n" +"" privateName("path") " = list(" privateName("sys") ".path)\n" +"" privateName("sys") ".path.insert(0, \"" + path + "\")\n" +"" privateName("loaded_modules") " = list(" privateName("sys") ".modules.keys())\n" +"" + script + "\n" +"" privateName("sys") ".path = " privateName("path") "\n" +"for m in list(" privateName("sys") ".modules):\n" +" if m not in " privateName("loaded_modules") ":\n" +" del(" privateName("sys") ".modules[m])\n"; + return runScript(s); +} + +bool addToSysPath(const std::string &path) +{ + // Add a path to Pythons sys.path + // Used for installing dependencies into custom + // directory + const std::string s = +"import sys as " privateName("sys") "\n" +"" privateName("sys") ".path.append(\"" + path + "\")"; + return runScript(s); +} + +bool pipInstallRequirements(const std::string &requirements, const std::string &target) +{ + // Run a requirements.txt file with pip + const std::string s = +"import subprocess, sys\n" +"subprocess.check_call(\"{} -m pip install -t " + target + " -r " + requirements + "\".format(sys.executable).split())\n" +"open(\"" + requirements + "\".replace(\"requirements.txt\",\"requirements.txt.installed\"),\"a\").close()\n"; + return runScriptWithPath(s, ""); +} + +} // namespace PyUtil |