Commit 34ffc272 authored by Jan Möbius's avatar Jan Möbius

Merge branch 'Python-Tests' into 'master'

Python support

See merge request !127
parents 56ca47c5 2b20da76
......@@ -59,6 +59,9 @@
/// The Menu will be added inside the Algorithms Menu
#define ALGORITHMMENU tr("Algorithms")
/// The Menu will be added inside the Python Menu
#define PYTHONMENU tr("Python")
/** \file MenuInterface.hh
*
* Interface for adding per plugin toolboxes to OpenFlippers UI.\ref menuInterfacePage
......
#include "PythonFunctions.hh"
#include "PythonFunctionsCore.hh"
#include <map>
#include <iostream>
#include <QStringList>
std::map<QString,QObject*> map_;
void setPluginPointer(QString _name , QObject* _pointer) {
map_[_name] = _pointer;
}
QObject* getPluginPointer(QString _name) {
auto it = map_.find(_name);
if ( it != map_.end() ) {
return it->second;
} else {
std::cerr << "No plugin found with name " << _name.toStdString() << " for PythonFunctions" << std::endl;
return nullptr;
}
}
const QStringList getPythonPlugins() {
QStringList plugins;
for ( auto it = map_.begin() ; it != map_.end() ; ++it ) {
plugins.push_back(it->first);
}
return plugins;
}
#include <OpenFlipper/common/GlobalDefines.hh>
#include <QObject>
#include <QString>
/** \brief Get the pointer to a plugin instance to register Python functions
*
* @param _name
* @return
*/
DLLEXPORT
QObject* getPluginPointer(QString _name);
#include <OpenFlipper/common/GlobalDefines.hh>
#include <QObject>
#include <QString>
DLLEXPORT
void setPluginPointer(QString _name , QObject* _pointer);
DLLEXPORT
const QStringList getPythonPlugins();
/*===========================================================================*\
* *
* OpenFlipper *
* Copyright (c) 2001-2015, RWTH-Aachen University *
* Department of Computer Graphics and Multimedia *
* All rights reserved. *
* www.openflipper.org *
* *
*---------------------------------------------------------------------------*
* This file is part of OpenFlipper. *
*---------------------------------------------------------------------------*
* *
* Redistribution and use in source and binary forms, with or without *
* modification, are permitted provided that the following conditions *
* are met: *
* *
* 1. Redistributions of source code must retain the above copyright notice, *
* this list of conditions and the following disclaimer. *
* *
* 2. Redistributions in binary form must reproduce the above copyright *
* notice, this list of conditions and the following disclaimer in the *
* documentation and/or other materials provided with the distribution. *
* *
* 3. Neither the name of the copyright holder nor the names of its *
* contributors may be used to endorse or promote products derived from *
* this software without specific prior written permission. *
* *
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS *
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED *
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A *
* PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER *
* OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, *
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, *
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR *
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF *
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING *
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS *
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *
* *
\*===========================================================================*/
#pragma once
/** \file PythonInterface.hh
*
* Interface for enabling Python Interfacing for a plugin. \ref pythonInterfacePage
*/
/** \brief Interface class for exporting functions to python
*
* \n
* \ref pythonInterfacePage "Detailed description"
* \n
*
*/
class PythonInterface {
public :
/// Destructor
virtual ~PythonInterface() {};
};
/** \page pythonInterfacePage Python Interface
\n
\n
The Python interface enables OpenFlipper to export plugin functions to the integrated python interpreter of OpenFlipper.
Only a few steps are required to export functions in a plugin.
1. Tell cmake, that your plugin supports the Python Interface. In your CMakeLists.txt add the Keyword PYTHONINTERFACE. E.g. :
\code
include (plugin)
openflipper_plugin (PYTHONINTERFACE )
\endcode
2. Add the corresponding includes and calls to your plugin header:
<ul>
<li> Add #include <OpenFlipper/BasePlugin/PythonInterface.hh> in your plugins header file
<li> derive your plugin from the class PythonInterface
<li> add Q_INTERFACES(PythonInterface) to your plugin class
</ul>
3. Add a subdirectory called PythonInterface to your plugin directory
4. In this directory create a file called Python.cc with contents similar to the following Code:
\code
#include <pybind11/include/pybind11/pybind11.h>
#include <pybind11/include/pybind11/embed.h>
#include <YourPlugin.hh>
#include <OpenFlipper/BasePlugin/PythonFunctions.hh>
namespace py = pybind11;
PYBIND11_EMBEDDED_MODULE(yourPlugin, m) { // yourPlugin will will be the module name in Python ( ... import yourPlugin )
QObject* pluginPointer = getPluginPointer("Your"); // This will retrieve your plugin instance pointer. The name is the plugin name set via the name() function.
// Export our plugin. Make sure that the c++ worlds core object is not deleted if
// the python side gets deleted!!
py::class_< YourPlugin,std::unique_ptr<YourPlugin, py::nodelete> > plugin(m, "Your"); // This will provide you with the existing instance of your plugin in Python.
// On the c++ side we will just return the existing core instance
// and prevent the system to recreate a new core as we need
// to work on the existing one.
plugin.def(py::init([&pluginPointer]() { return qobject_cast<YourPlugin*>(pluginPointer); })); // This will provide you with the existing instance of your plugin in Python.
plugin.def("viewAll", static_cast<void (YourPlugin::*)()>(&YourPlugin::viewAll),"Change View in all viewers to view whole scene"); // Example export of an overloaded member function
plugin.def("viewAll", static_cast<void (ViewControlPlugin::*)(int)>(&YourPlugin::viewAll),"Change View in given viewer to view whole scene",py::arg("Id of the viewer which should be switched") ); // Example export of an overloaded member function
plugin.def("orthographicProjection", static_cast<void (ViewControlPlugin::*)()>(&ViewControlPlugin::orthographicProjection) ); // simple export example
}
\endcode
*/
Q_DECLARE_INTERFACE(PythonInterface,"OpenFlipper.PythonInterface/1.0")
......@@ -1371,12 +1371,15 @@ private slots:
void slotSetSlotDescriptionGlobalFunction(QString _functionName, QString _slotDescription,
QStringList _parameters, QStringList _descriptions);
void slotScriptError(const QScriptValue &error);
public slots:
/// get available descriptions for a given public slot
void slotGetDescription(QString _function, QString& _fnDescription,
QStringList& _parameters, QStringList& _descriptions );
void slotScriptError(const QScriptValue &error);
private:
QList< SlotInfo > coreSlots_;
......
......@@ -67,6 +67,7 @@
#include "OpenFlipper/BasePlugin/ViewModeInterface.hh"
#include "OpenFlipper/BasePlugin/LoadSaveInterface.hh"
#include "OpenFlipper/BasePlugin/INIInterface.hh"
#include "OpenFlipper/BasePlugin/PythonInterface.hh"
#include "OpenFlipper/BasePlugin/RPCInterface.hh"
#include "OpenFlipper/BasePlugin/ScriptInterface.hh"
#include "OpenFlipper/BasePlugin/SecurityInterface.hh"
......@@ -81,6 +82,7 @@
#include <OpenFlipper/common/FileTypes.hh>
#include <OpenFlipper/common/PluginStorage.hh>
#include <OpenFlipper/BasePlugin/PythonFunctionsCore.hh>
/**
* The number of plugins to load simultaneously.
......@@ -1339,6 +1341,22 @@ void Core::loadPlugin(const QString& _filename,const bool _silent, QString& _lic
plugin , SLOT( loadIniFileOptionsLast( INIFile& ) ),Qt::DirectConnection);
}
#ifdef PYTHON_ENABLED
// Check if the plugin supports Python Interface
PythonInterface* pythonPlugin = qobject_cast< PythonInterface * >(plugin);
if ( pythonPlugin ) {
supported = supported + "PythonInterface ";
QObject* currentPluginPointer = qobject_cast< QObject * >(plugin);
setPluginPointer(basePlugin->name() , currentPluginPointer);
}
#endif
//Check if the plugin supports Selection-Interface
SelectionInterface* selectionPlugin = qobject_cast< SelectionInterface * >(plugin);
if ( selectionPlugin && OpenFlipper::Options::gui() ) {
......
include (ACGCommon)
include_directories (
..
${OPENMESH_INCLUDE_DIRS}
......@@ -68,9 +69,25 @@ set (directories
../widgets/snapshotDialog
../widgets/stereoSettingsWidget
../widgets/postProcessorWidget
../widgets/pythonWidget
../widgets/rendererWidget
${WIN_EXTRA_DIRS}
)
if ( PYTHON3_FOUND)
include_directories (
${Python3_INCLUDE_DIRS}
)
list(APPEND directories "../PythonInterpreter")
link_directories(${Python3_LIBRARY_DIRS})
add_definitions(-DPYTHON_ENABLED )
message("OpenFlipper builds with Python interpreter")
endif()
# collect all header,source and ui files
acg_append_files (headers "*.hh" ${directories})
......@@ -118,9 +135,13 @@ elseif (APPLE)
# generate bundle on mac
acg_add_executable (${OPENFLIPPER_PRODUCT_STRING} MACOSX_BUNDLE ${sources} ${headers} ${RC_SRC})
else ()
acg_add_executable (${OPENFLIPPER_PRODUCT_STRING} ${sources} ${headers} ${RC_SRC})
acg_add_executable (${OPENFLIPPER_PRODUCT_STRING} ${sources} ${headers} ${RC_SRC} )
endif ()
if ( PYTHON3_FOUND)
target_link_libraries(${OPENFLIPPER_PRODUCT_STRING} ${PYTHON_LIBRARY} pybind11::module pybind11::embed Python3::Python )
endif()
# Mark this build part as building OpenFlippers Core
add_definitions(-DOPENFLIPPERCORE )
......
......@@ -11,6 +11,7 @@
- Removed WhatsThisGenerator include from BaseInterface. You have to include it in the plugins now if you use it.
- Removed the classical ObjectTypes CMake search. Please use the nex style ( Type-<Typename>/ObjectTypes/<Typename> + Type-<Typename>/Plugin-Type<TypeName> ) scheme
- <b>Core</b>
- Introduced basic Python scripting support
- Use QOpenGLDebuglogger
- Queued cross plugin interconnection feature
- Use QOpenGLWidget
......
......@@ -66,6 +66,10 @@
#include <execinfo.h>
#endif
#ifdef PYTHON_ENABLED
#include <PythonInterpreter/PythonInterpreter.hh>
#endif
#ifdef USE_OPENMP
#endif
......@@ -764,6 +768,9 @@ int main(int argc, char **argv)
// create core ( this also reads the ini files )
Core * w = new Core( );
#ifdef PYTHON_ENABLED
setCorePointer(w);
#endif
QString tLang = OpenFlipperSettings().value("Core/Language/Translation","en_US").toString();
......@@ -820,6 +827,11 @@ int main(int argc, char **argv)
// create widget ( this also reads the ini files )
Core * w = new Core( );
#ifdef PYTHON_ENABLED
setCorePointer(w);
#endif
// After setting all Options from command line, build the real gui
w->init();
......
void jump(QString _method) {
}
namespace pybind11 { namespace detail {
template <> struct type_caster<QVariant> {
public:
/**
* This macro establishes the name 'bool' in
* function signatures and declares a local variable
* 'value' of type QVariant
*/
PYBIND11_TYPE_CASTER(QVariant, _("bool"));
/**
* Conversion part 1 (Python->C++): convert a PyObject into a inty
* instance or return false upon failure. The second argument
* indicates whether implicit conversions should be applied.
*/
bool load(handle src, bool ) {
/* Extract PyObject from handle */
PyObject *source = src.ptr();
/* Safety check, if we have a bool here */
if (!PyBool_Check(source))
return false;
/* Now try to convert into a C++ int */
value = QVariant::fromValue(source == Py_True);
/* Ensure return code was OK (to avoid out-of-range errors etc) */
return ( !PyErr_Occurred() );
}
/**
* Conversion part 2 (C++ -> Python): convert an QVariant instance into
* a Python object. The second and third arguments are used to
* indicate the return value policy and parent object (for
* ``return_value_policy::reference_internal``) and are generally
* ignored by implicit casters.
*/
static handle cast(QVariant src, return_value_policy /* policy */, handle /* parent */) {
return (PyBool_FromLong(src.toBool()) );
}
};
}} // namespace pybind11::detail
const QMetaObject* meta = core_->metaObject();
//std::cerr << "Number of methods:" << meta->methodCount() << std::end;
for ( int i = 0 ; i < meta->methodCount() ; ++i) {
QMetaMethod method = meta->method(i);
if ( method.access() == QMetaMethod::Public && method.methodType() == QMetaMethod::Slot) {
QString functionName = QString::fromLatin1(method.methodSignature());
functionName.truncate(functionName.indexOf("("));
// First try to
if (method.parameterCount() == 0) {
std::cerr << QString::fromLatin1(method.methodSignature()).toStdString() << std::endl;
QString function = QString::fromLatin1(method.methodSignature()).remove(QRegExp("[()]"));
QByteArray ba = function.toLocal8Bit();
std::cerr << "Registering " << ba.data() << " with return type : " << method.returnType() << " which is " << method.typeName() << std::endl;
// We are only registering functions with no return type for now!
if ( QString(method.typeName()) == "void") {
core.def(ba.data(), [ method ](const Core& a) {
std::cerr << "Calling " << QString::fromLatin1(method.methodSignature()).toStdString() << std::endl;
method.invoke( core_ , Qt::DirectConnection);
});
} else {
core.def(ba.data(), [ method ](const Core& a) { std::cerr << "No implementation for calling : " << QString::fromLatin1(method.methodSignature()).toStdString() << " due to return type" << std::endl; });
}
}
if (method.parameterCount() == 1) {
std::cerr << QString::fromLatin1(method.methodSignature()).toStdString() << std::endl;
QByteArray ba = functionName.toLocal8Bit();
std::cerr << "Registering " << ba.data() << " with return type : " << method.returnType() << " which is " << method.typeName() << std::endl;
std::cerr << "Parameters: " << QString::fromLatin1(method.parameterNames()[0]).toStdString() << QMetaType::typeName(method.parameterType(0)) <<std::endl;
QString description;
QStringList parameters;
QStringList descriptions;
core_->slotGetDescription("core."+QString::fromLatin1(method.methodSignature()), description, parameters, descriptions );
std::cerr << "Description: " << description.toStdString() << std::endl;
if ( QString(QMetaType::typeName(method.parameterType(0))) == "bool" || QString(QMetaType::typeName(method.parameterType(0))) == "int") {
core.def(ba.data(), [ method ](const Core& a, QVariant _arg) {
std::cerr << "Calling " << QString::fromLatin1(method.methodSignature()).toStdString() << std::endl;
method.invoke( core_ , Qt::DirectConnection, Q_ARG(bool, _arg.toBool()) );
}, py::arg(method.parameterNames()[0].data() ) );
} else {
std::cerr << "Unable to register on parameter function " << std::endl;
}
}
}
}
// Remove this!
// core.def("__del__",
// []() {
// std::cerr << "deleting Core" << std::endl;
// });
PYBIND11_EMBEDDED_MODULE(examples, m) {
m.doc() = "pybind11 example plugin"; // optional module docstring
m.def("add", &add, "A function which adds two numbers");
}
int add(int i, int j) {
return i + j;
}
//MIT License
//
//Copyright(c) 2017 Matthias M�ller
//https://github.com/TinyTinni/PyLogHook
//
//Permission is hereby granted, free of charge, to any person obtaining a copy
//of this software and associated documentation files(the "Software"), to deal
//in the Software without restriction, including without limitation the rights
//to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
//copies of the Software, and to permit persons to whom the Software is
//furnished to do so, subject to the following conditions :
//
//The above copyright notice and this permission notice shall be included in all
//copies or substantial portions of the Software.
//
//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
//SOFTWARE.
#pragma once
#include <utility>
#include <string>
#ifdef TYTI_PYLOGHOOK_USE_BOOST
#include <boost/python.hpp>
#else
#include <pybind11/pybind11.h>
#endif
namespace tyti {
namespace pylog {
namespace detail {
#ifdef TYTI_PYLOGHOOK_USE_BOOST
template <typename T>
inline boost::python::object LogHookMakeObject(T t)
{
return boost::python::object(boost::python::make_function(
std::forward<T>(t),
boost::python::default_call_policies(),
boost::mpl::vector<void, const char*>() // signature
));
}
#else
template<typename T>
inline pybind11::object LogHookMakeObject(T t)
{
return pybind11::cpp_function(std::forward<T>(t));
}
#endif
template<typename T>
void redirect_syspipe(T t, const char* pipename)
{
assert(Py_IsInitialized());
PyObject* out = PySys_GetObject(pipename);
// in case python couldnt create stdout/stderr
// this happens in some gui application
// just register a new nameless type for this
if (out == NULL || out == Py_None)
{
std::string register_read_write = std::string("import sys\n\
sys.") + pipename + std::string(" = type(\"\",(object,),{\"write\":lambda self, txt: None, \"flush\":lambda self: None})()\n");
PyRun_SimpleString(register_read_write.c_str());
out = PySys_GetObject(pipename);
}
// overwrite write function
PyObject_SetAttrString(out, "write",
detail::LogHookMakeObject(std::forward<T>(t)).ptr());
}
} // namespace detail
/** \brief redirects sys.stdout
Whenever sys.stdout.write is called, call the given function instead.
Given function has to be the signature "void (const char*)"
Preconditions:
Python has to be initialized.
Calling thread has to hold the GIL. (in case of multi threading)
Exceptions:
Maybe a PyErr occurs. This must be handled by the user.
@param t callbacl function of type "void (const char*)"
*/
template<typename T>
inline void redirect_stdout(T t)
{
detail::redirect_syspipe(std::forward<T>(t), "stdout");
}
template<typename T>
inline void redirect_stderr(T t)
{
detail::redirect_syspipe(std::forward<T>(t), "stderr");
}
}
}
/*===========================================================================*\
* *
* OpenFlipper *
* Copyright (c) 2001-2015, RWTH-Aachen University *
* Department of Computer Graphics and Multimedia *
* All rights reserved. *
* www.openflipper.org *
* *
*---------------------------------------------------------------------------*
* This file is part of OpenFlipper. *
*---------------------------------------------------------------------------*
* *
* Redistribution and use in source and binary forms, with or without *
* modification, are permitted provided that the following conditions *
* are met: *
* *
* 1. Redistributions of source code must retain the above copyright notice, *
* this list of conditions and the following disclaimer. *
* *
* 2. Redistributions in binary form must reproduce the above copyright *
* notice, this list of conditions and the following disclaimer in the *
* documentation and/or other materials provided with the distribution. *
* *
* 3. Neither the name of the copyright holder nor the names of its *
* contributors may be used to endorse or promote products derived from *
* this software without specific prior written permission. *
* *
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS *
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED *
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A *
* PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER *
* OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, *
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, *
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR *
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF *
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING *
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS *
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *
* *
\*===========================================================================*/
#include <pybind11/include/pybind11/pybind11.h>
#include <pybind11/include/pybind11/embed.h>
#include "PyLogHook.h"
#include "PythonInterpreter.hh"
#include <QMetaObject>
#include <QMetaMethod>
namespace py = pybind11;
static Core* core_;
static pybind11::module mainModule_;
static PythonInterpreter* interpreter_ = nullptr;
/** \brief Stores a clean dictionary for interpreter reset
*
*/
static PyObject* global_dict_clean_ = nullptr;
void setCorePointer( Core* _core ) {
core_ = _core;
}
PythonInterpreter* PythonInterpreter::getInstance() {
if (interpreter_ == nullptr)
{
interpreter_ = new PythonInterpreter();
}
return interpreter_;
}
PythonInterpreter::PythonInterpreter() :
externalLogging_(false)
{
}
PythonInterpreter::~PythonInterpreter() {
}
bool PythonInterpreter::modulesInitialized() {
///@TODO: Why does this work?
return static_cast<bool>(mainModule_);
}
void PythonInterpreter::initPython() {
if (Py_IsInitialized() )
return;
std::cerr << "A" << std::endl;
py::initialize_interpreter();
std::cerr << "B" << std::endl;
PyEval_InitThreads();
std::cerr << "C" << std::endl;
if (!modulesInitialized()) {
std::cerr << "D" << std::endl;
//dlopen("libpython3.5m.so.1.0", RTLD_LAZY | RTLD_GLOBAL);
py::module main{ py::module::import("__main__") };
// redirect python output
tyti::pylog::redirect_stderr([this](const char*w) {this->pyError(w); });
tyti::pylog::redirect_stdout([this](const char* w) {this->pyOutput(w); });
std::cerr << "E" << std::endl;
// add openflipper module
py::object main_namespace = main.attr("__dict__");
std::cerr << "F" << std::endl;
py::module of_module(py::module::import("openflipper"));
main_namespace["openflipper"] = of_module;
std::cerr << "G" << std::endl;
global_dict_clean_ = PyDict_Copy(PyModule_GetDict(main.ptr()));