diff --git a/docs/index.rst b/docs/index.rst index 0754936..d8c9442 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -359,6 +359,8 @@ between Python and QML (and vice versa): +--------------------+------------+-----------------------------+ | object | (opaque) | since PyOtherSide 1.4.0 | +--------------------+------------+-----------------------------+ +| pyotherside.QObject| QObject | since PyOtherSide 1.4.0 | ++--------------------+------------+-----------------------------+ Trying to pass in other types than the ones listed here is undefined behavior and will usually result in an error. @@ -461,6 +463,59 @@ using :func:`Qt.resolvedUrl` from within a QML file in Qt Resources. As an alternative, ``addImportPath('qrc:/')`` will add the root directory of the Qt Resources to Python's module search path. +.. _qobjects in python: + +Accessing QObjects from Python +============================== + +.. versionadded:: 1.4.0 + +Since version 1.4, PyOtherSide allows passing QObjects from QML to Python, and +accessing (setting / getting) properties and calling slots and dynamic methods. +References to QObjects passed to Python can be passed back to QML transparently: + +.. code-block:: python + + # Assume func will be called with a QObject as sole argument + def func(qobject): + # Getting properties + print(qobject.x) + + # Setting properties + qobject.x = 123 + + # Calling slots and dynamic functions + print(qobject.someFunction(123, 'b')) + + # Returning a QObject reference to the caller + return qobject + +It is possible to store a reference (bound method) to a method of a QObject. +Such references cannot be passed to QML, and can only be used in Python for the +lifetime of the QObject. If you need to pass such a bound method to QML, you +can wrap it into a Python object (or even just a lambda) and pass that instead: + +.. code-block:: python + + def func(qobject): + # Can store a reference to a bound method + bound_method = qobject.someFunction + + # Calling the bound method + bound_method(123, 'b') + + # If you need to return the bound method, you must wrap it + # in a lambda (or any other Python object), the bound method + # cannot be returned as-is for now + return lambda a, b: bound_method(a, b) + +It's not possible to instantiate new QObjects from within Python, and it's +not possible to subclass QObject from within Python. Also, be aware that a +reference to a QObject in Python will become invalid when the QObject is +deleted (there's no way for PyOtherSide to prevent referenced QObjects from +being deleted, but PyOtherSide tries hard to detect the deletion of objects +and give meaningful error messages in case the reference is accessed). + Cookbook ======== @@ -929,6 +984,7 @@ Version 1.4.0 (UNRELEASED) * Add :func:`getattr` to get an attribute from a Python object * :func:`call` and :func:`call_sync` now also accept a Python callable as first argument +* Support for `Accessing QObjects from Python`_ (properties and slots) Version 1.3.0 (2014-07-24) -------------------------- diff --git a/examples/qobject_reference.py b/examples/qobject_reference.py new file mode 100644 index 0000000..13eefb5 --- /dev/null +++ b/examples/qobject_reference.py @@ -0,0 +1,65 @@ +import threading +import time +import pyotherside + +# If you try to instantiate a QObject, it's unbound +unbound = pyotherside.QObject() +print(unbound) +try: + unbound.a = 1 +except Exception as e: + print('Got exception:', e) + +def do_something(bar): + while True: + print('got: ', bar.dynamicFunction(1, 2, 3)) + time.sleep(1) + +def foo(bar, py): + # Printing the objects will give some info on the + # QObject class and memory address + print('got:', bar, py) + + # Ok, this is pretty wicked - we can now call into + # the PyOtherSide QML element from within Python + # (not that it's a good idea to do this, mind you..) + print(py.evaluate) + print(py.evaluate('3*3')) + + try: + bar.i_am_pretty_sure_this_attr_does_not_exist = 147 + except Exception as e: + print('Got exception (as expected):', e) + + try: + bar.x = 'i dont think i can set this to a string' + except Exception as e: + print('Got exception (as expected):', e) + + # This doesn't work yet, because we can't convert a bound + # member function to a Qt/QML type yet (fallback to None) + try: + bar.dynamicFunction(bar.dynamicFunction, 2, 3) + except Exception as e: + print('Got exception (as expected):', e) + + # Property access works just like expected + print(bar.x, bar.color, bar.scale) + bar.x *= 3 + + # Printing a member function gives a bound method + print(bar.dynamicFunction) + + # Calling a member function is just as easy + result = bar.dynamicFunction(1, 2, 3) + print('result:', result) + + try: + bar.dynamicFunction(1, 2, 3, unexpected=123) + except Exception as e: + print('Got exception (as expected):', e) + + threading.Thread(target=do_something, args=[bar]).start() + + # Returning QObject references from Python also works + return bar diff --git a/examples/qobject_reference.qml b/examples/qobject_reference.qml new file mode 100644 index 0000000..8a83bb8 --- /dev/null +++ b/examples/qobject_reference.qml @@ -0,0 +1,37 @@ +import QtQuick 2.0 +import io.thp.pyotherside 1.4 + +Rectangle { + id: root + width: 400 + height: 400 + + Rectangle { + id: foo + x: 123 + y: 123 + width: 20 + height: 20 + color: 'blue' + + function dynamicFunction(a, b, c) { + console.log('In QML, dynamicFunction got: ' + a + ', ' + b + ', ' + c); + rotation += 4; + return 'hello'; + } + } + + Python { + id: py + + Component.onCompleted: { + addImportPath(Qt.resolvedUrl('.')); + importModule('qobject_reference', function () { + call('qobject_reference.foo', [foo, py], function (result) { + console.log('In QML, got result: ' + result); + result.color = 'green'; + }); + }); + } + } +} diff --git a/src/converter.h b/src/converter.h index c66d1d1..ff24c90 100644 --- a/src/converter.h +++ b/src/converter.h @@ -20,6 +20,7 @@ #define PYOTHERSIDE_CONVERTER_H #include "pyobject_ref.h" +#include "qobject_ref.h" struct ConverterDate { ConverterDate(int y, int m, int d) @@ -105,6 +106,7 @@ class Converter { TIME, DATETIME, PYOBJECT, + QOBJECT, }; virtual enum Type type(V&) = 0; @@ -118,6 +120,7 @@ class Converter { virtual ConverterTime time(V&) = 0; virtual ConverterDateTime dateTime(V&) = 0; virtual PyObjectRef pyObject(V&) = 0; + virtual QObjectRef qObject(V&) = 0; virtual V fromInteger(long long v) = 0; virtual V fromFloating(double v) = 0; @@ -127,6 +130,7 @@ class Converter { virtual V fromTime(ConverterTime time) = 0; virtual V fromDateTime(ConverterDateTime dateTime) = 0; virtual V fromPyObject(const PyObjectRef &pyobj) = 0; + virtual V fromQObject(const QObjectRef &qobj) = 0; virtual ListBuilder *newList() = 0; virtual DictBuilder *newDict() = 0; virtual V none() = 0; @@ -193,6 +197,8 @@ convert(F from) return tconv.fromDateTime(fconv.dateTime(from)); case FC::PYOBJECT: return tconv.fromPyObject(fconv.pyObject(from)); + case FC::QOBJECT: + return tconv.fromQObject(fconv.qObject(from)); } return tconv.none(); diff --git a/src/pyobject_converter.h b/src/pyobject_converter.h index d33350c..19064fc 100644 --- a/src/pyobject_converter.h +++ b/src/pyobject_converter.h @@ -20,9 +20,11 @@ #define PYOTHERSIDE_PYOBJECT_CONVERTER_H #include "converter.h" +#include "pyqobject.h" #include "Python.h" #include "datetime.h" +#include class PyObjectListBuilder : public ListBuilder { @@ -138,7 +140,13 @@ class PyObjectConverter : public Converter { } virtual enum Type type(PyObject *&o) { - if (PyBool_Check(o)) { + if (PyObject_TypeCheck(o, &pyotherside_QObjectType)) { + return QOBJECT; + } else if (PyObject_TypeCheck(o, &pyotherside_QObjectMethodType)) { + qWarning("Cannot convert QObjectMethod yet - falling back to None"); + // TODO: Implement passing QObjectMethod references around + return NONE; + } else if (PyBool_Check(o)) { return BOOLEAN; } else if (PyLong_Check(o)) { return INTEGER; @@ -203,6 +211,14 @@ class PyObjectConverter : public Converter { PyDateTime_DATE_GET_MICROSECOND(o) / 1000); } virtual PyObjectRef pyObject(PyObject *&o) { return PyObjectRef(o); } + virtual QObjectRef qObject(PyObject *&o) { + if (PyObject_TypeCheck(o, &pyotherside_QObjectType)) { + pyotherside_QObject *result = reinterpret_cast(o); + return QObjectRef(*(result->m_qobject_ref)); + } + + return QObjectRef(); + } virtual PyObject * fromInteger(long long v) { return PyLong_FromLong((long)v); } virtual PyObject * fromFloating(double v) { return PyFloat_FromDouble(v); } @@ -214,6 +230,11 @@ class PyObjectConverter : public Converter { return PyDateTime_FromDateAndTime(v.y, v.m, v.d, v.time.h, v.time.m, v.time.s, v.time.ms * 1000); } virtual PyObject * fromPyObject(const PyObjectRef &pyobj) { return pyobj.newRef(); } + virtual PyObject * fromQObject(const QObjectRef &qobj) { + pyotherside_QObject *result = PyObject_New(pyotherside_QObject, &pyotherside_QObjectType); + result->m_qobject_ref = new QObjectRef(qobj); + return reinterpret_cast(result); + } virtual ListBuilder *newList() { return new PyObjectListBuilder(); } virtual DictBuilder *newDict() { return new PyObjectDictBuilder(); } virtual PyObject * none() { Py_RETURN_NONE; } diff --git a/src/pyqobject.h b/src/pyqobject.h new file mode 100644 index 0000000..e42518b --- /dev/null +++ b/src/pyqobject.h @@ -0,0 +1,39 @@ + +/** + * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 + * Copyright (c) 2014, Thomas Perl + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + **/ + +#ifndef PYOTHERSIDE_PYQOBJECT_H +#define PYOTHERSIDE_PYQOBJECT_H + +#include "Python.h" + +#include "qobject_ref.h" + +typedef struct { + PyObject_HEAD + QObjectRef *m_qobject_ref; +} pyotherside_QObject; + +typedef struct { + PyObject_HEAD + QObjectMethodRef *m_method_ref; +} pyotherside_QObjectMethod; + +extern PyTypeObject pyotherside_QObjectType; +extern PyTypeObject pyotherside_QObjectMethodType; + +#endif /* PYOTHERSIDE_PYQOBJECT_H */ diff --git a/src/qobject_ref.cpp b/src/qobject_ref.cpp new file mode 100644 index 0000000..ff75911 --- /dev/null +++ b/src/qobject_ref.cpp @@ -0,0 +1,75 @@ + +/** + * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 + * Copyright (c) 2014, Thomas Perl + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + **/ + +#include "qobject_ref.h" + +QObjectRef::QObjectRef(QObject *obj) + : qobject(obj) +{ + if (qobject) { + QObject::connect(qobject, SIGNAL(destroyed(QObject *)), + this, SLOT(handleDestroyed(QObject *))); + } +} + +QObjectRef::QObjectRef(const QObjectRef &other) + : qobject(other.qobject) +{ + if (qobject) { + QObject::connect(qobject, SIGNAL(destroyed(QObject *)), + this, SLOT(handleDestroyed(QObject *))); + } +} + +QObjectRef::~QObjectRef() +{ + if (qobject) { + QObject::disconnect(qobject, SIGNAL(destroyed(QObject *)), + this, SLOT(handleDestroyed(QObject *))); + } +} + +QObjectRef & +QObjectRef::operator=(const QObjectRef &other) +{ + if (this != &other) { + if (qobject != other.qobject) { + if (qobject) { + QObject::disconnect(qobject, SIGNAL(destroyed(QObject *)), + this, SLOT(handleDestroyed(QObject *))); + } + + if (other.qobject) { + qobject = other.qobject; + QObject::connect(qobject, SIGNAL(destroyed(QObject *)), + this, SLOT(handleDestroyed(QObject *))); + } + } + } + + return *this; +} + +void +QObjectRef::handleDestroyed(QObject *obj) +{ + if (obj == qobject) { + // Have to remove internal reference, as qobject is destroyed + qobject = NULL; + } +} diff --git a/src/qobject_ref.h b/src/qobject_ref.h new file mode 100644 index 0000000..325a91d --- /dev/null +++ b/src/qobject_ref.h @@ -0,0 +1,60 @@ + +/** + * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 + * Copyright (c) 2014, Thomas Perl + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + **/ + +#ifndef PYOTHERSIDE_QOBJECT_REF_H +#define PYOTHERSIDE_QOBJECT_REF_H + +#include + +class QObjectRef : public QObject { + Q_OBJECT + +public: + explicit QObjectRef(QObject *obj=0); + virtual ~QObjectRef(); + + QObjectRef(const QObjectRef &other); + QObjectRef &operator=(const QObjectRef &other); + + QObject *value() const { return qobject; } + operator bool() const { return (qobject != 0); } + +private slots: + void handleDestroyed(QObject *obj); + +private: + QObject *qobject; +}; + +class QObjectMethodRef { +public: + QObjectMethodRef(const QObjectRef &object, const QString &method) + : m_object(object) + , m_method(method) + { + } + + const QObjectRef &object() { return m_object; } + const QString &method() { return m_method; } + +private: + QObjectRef m_object; + QString m_method; +}; + +#endif // PYOTHERSIDE_QOBJECT_REF_H diff --git a/src/qpython_priv.cpp b/src/qpython_priv.cpp index ba0d220..7230130 100644 --- a/src/qpython_priv.cpp +++ b/src/qpython_priv.cpp @@ -28,6 +28,11 @@ #include #include +#include +#include +#include +#include + static QPythonPriv *priv = NULL; static QString @@ -43,6 +48,57 @@ qstring_from_pyobject_arg(PyObject *object) return QString::fromUtf8(conv.string(object)); } + +PyTypeObject pyotherside_QObjectType = { + PyVarObject_HEAD_INIT(NULL, 0) + "pyotherside.QObject", /* tp_name */ + sizeof(pyotherside_QObject), /* tp_basicsize */ + 0, /* tp_itemsize */ + 0, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_reserved */ + 0, /* tp_repr */ + 0, /* tp_as_number */ + 0, /* tp_as_sequence */ + 0, /* tp_as_mapping */ + 0, /* tp_hash */ + 0, /* tp_call */ + 0, /* tp_str */ + 0, /* tp_getattro */ + 0, /* tp_setattro */ + 0, /* tp_as_buffer */ + Py_TPFLAGS_DEFAULT, /* tp_flags */ + "Wrapped QObject", /* tp_doc */ +}; + +PyTypeObject pyotherside_QObjectMethodType = { + PyVarObject_HEAD_INIT(NULL, 0) + "pyotherside.QObjectMethod", /* tp_name */ + sizeof(pyotherside_QObjectMethod), /* tp_basicsize */ + 0, /* tp_itemsize */ + 0, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_reserved */ + 0, /* tp_repr */ + 0, /* tp_as_number */ + 0, /* tp_as_sequence */ + 0, /* tp_as_mapping */ + 0, /* tp_hash */ + 0, /* tp_call */ + 0, /* tp_str */ + 0, /* tp_getattro */ + 0, /* tp_setattro */ + 0, /* tp_as_buffer */ + Py_TPFLAGS_DEFAULT, /* tp_flags */ + "Bound method of wrapped QObject", /* tp_doc */ +}; + + + PyObject * pyotherside_send(PyObject *self, PyObject *args) { @@ -135,6 +191,225 @@ pyotherside_qrc_list_dir(PyObject *self, PyObject *dirname) return convertQVariantToPyObject(dir.entryList()); } +void +pyotherside_QObject_dealloc(pyotherside_QObject *self) +{ + delete self->m_qobject_ref; + Py_TYPE(self)->tp_free((PyObject *)self); +} + +PyObject * +pyotherside_QObject_repr(PyObject *o) +{ + if (!PyObject_TypeCheck(o, &pyotherside_QObjectType)) { + return PyErr_Format(PyExc_TypeError, "Not a pyotherside.QObject"); + } + + pyotherside_QObject *pyqobject = reinterpret_cast(o); + QObjectRef *ref = pyqobject->m_qobject_ref; + if (ref) { + QObject *qobject = ref->value(); + const QMetaObject *metaObject = qobject->metaObject(); + + return PyUnicode_FromFormat("", + metaObject->className(), qobject); + } + + return PyUnicode_FromFormat(""); +} + +PyObject * +pyotherside_QObject_getattro(PyObject *o, PyObject *attr_name) +{ + if (!PyObject_TypeCheck(o, &pyotherside_QObjectType)) { + return PyErr_Format(PyExc_TypeError, "Not a pyotherside.QObject"); + } + + if (!PyUnicode_Check(attr_name)) { + return PyErr_Format(PyExc_TypeError, "attr_name must be a string"); + } + + pyotherside_QObject *pyqobject = reinterpret_cast(o); + QObjectRef *ref = pyqobject->m_qobject_ref; + if (!ref) { + return PyErr_Format(PyExc_ValueError, "Dangling QObject"); + } + QObject *qobject = ref->value(); + if (!qobject) { + return PyErr_Format(PyExc_ReferenceError, "Referenced QObject was deleted"); + } + + const QMetaObject *metaObject = qobject->metaObject(); + QString attrName = convertPyObjectToQVariant(attr_name).toString(); + + for (int i=0; ipropertyCount(); i++) { + QMetaProperty property = metaObject->property(i); + if (attrName == property.name()) { + return convertQVariantToPyObject(property.read(qobject)); + } + } + + for (int i=0; imethodCount(); i++) { + QMetaMethod method = metaObject->method(i); + if (attrName == method.name()) { + pyotherside_QObjectMethod *result = PyObject_New(pyotherside_QObjectMethod, + &pyotherside_QObjectMethodType); + result->m_method_ref = new QObjectMethodRef(*ref, attrName); + return reinterpret_cast(result); + } + } + + return PyErr_Format(PyExc_AttributeError, "Not a valid attribute"); +} + +int +pyotherside_QObject_setattro(PyObject *o, PyObject *attr_name, PyObject *v) +{ + if (!PyObject_TypeCheck(o, &pyotherside_QObjectType)) { + PyErr_Format(PyExc_TypeError, "Not a pyotherside.QObject"); + return -1; + } + + if (!PyUnicode_Check(attr_name)) { + PyErr_Format(PyExc_TypeError, "attr_name must be a string"); + return -1; + } + + pyotherside_QObject *pyqobject = reinterpret_cast(o); + QObjectRef *ref = pyqobject->m_qobject_ref; + if (!ref) { + PyErr_Format(PyExc_ValueError, "Dangling QObject"); + return -1; + } + QObject *qobject = ref->value(); + if (!qobject) { + PyErr_Format(PyExc_ReferenceError, "Referenced QObject was deleted"); + return -1; + } + + const QMetaObject *metaObject = qobject->metaObject(); + QString attrName = convertPyObjectToQVariant(attr_name).toString(); + + for (int i=0; ipropertyCount(); i++) { + QMetaProperty property = metaObject->property(i); + if (attrName == property.name()) { + QVariant variant(convertPyObjectToQVariant(v)); + if (!property.write(qobject, variant)) { + PyErr_Format(PyExc_AttributeError, "Could not set property %s to %s(%s)", + attrName.toUtf8().constData(), + variant.typeName(), + variant.toString().toUtf8().constData()); + return -1; + } + + return 0; + } + } + + PyErr_Format(PyExc_AttributeError, "Property does not exist: %s", + attrName.toUtf8().constData()); + return -1; +} + +void +pyotherside_QObjectMethod_dealloc(pyotherside_QObjectMethod *self) +{ + delete self->m_method_ref; + Py_TYPE(self)->tp_free((PyObject *)self); +} + +PyObject * +pyotherside_QObjectMethod_repr(PyObject *o) +{ + if (!PyObject_TypeCheck(o, &pyotherside_QObjectMethodType)) { + return PyErr_Format(PyExc_TypeError, "Not a pyotherside.QObjectMethod"); + } + + pyotherside_QObjectMethod *pyqobjectmethod = reinterpret_cast(o); + + QObjectMethodRef *ref = pyqobjectmethod->m_method_ref; + if (!ref) { + return PyUnicode_FromFormat(""); + } + + QObjectRef oref = ref->object(); + QObject *qobject = oref.value(); + if (!qobject) { + return PyUnicode_FromFormat("", + ref->method().toUtf8().constData()); + } + + const QMetaObject *metaObject = qobject->metaObject(); + return PyUnicode_FromFormat("", + ref->method().toUtf8().constData(), metaObject->className(), qobject); +} + + +PyObject * +pyotherside_QObjectMethod_call(PyObject *callable_object, PyObject *args, PyObject *kw) +{ + if (!PyObject_TypeCheck(callable_object, &pyotherside_QObjectMethodType)) { + return PyErr_Format(PyExc_TypeError, "Not a pyotherside.QObjectMethod"); + } + + if (!PyTuple_Check(args)) { + return PyErr_Format(PyExc_TypeError, "Argument list not a tuple"); + } + + if (kw) { + if (!PyMapping_Check(kw)) { + return PyErr_Format(PyExc_TypeError, "Keyword arguments not a mapping"); + } + + if (PyMapping_Size(kw) > 0) { + return PyErr_Format(PyExc_ValueError, "Keyword arguments not supported"); + } + } + + QList qargs = convertPyObjectToQVariant(args).toList(); + + pyotherside_QObjectMethod *pyqobjectmethod = reinterpret_cast(callable_object); + QObjectMethodRef *ref = pyqobjectmethod->m_method_ref; + if (!ref) { + return PyErr_Format(PyExc_ValueError, "Dangling QObject"); + } + + QList genericArguments; + for (int j=0; jobject().value(); + if (!o) { + return PyErr_Format(PyExc_ReferenceError, "Referenced QObject was deleted"); + } + const QMetaObject *metaObject = o->metaObject(); + + for (int i=0; imethodCount(); i++) { + QMetaMethod method = metaObject->method(i); + + if (method.name() == ref->method()) { + QVariant result; + if (method.invoke(o, Qt::DirectConnection, + Q_RETURN_ARG(QVariant, result), genericArguments.value(0), + genericArguments.value(1), genericArguments.value(2), + genericArguments.value(3), genericArguments.value(4), + genericArguments.value(5), genericArguments.value(6), + genericArguments.value(7), genericArguments.value(8), + genericArguments.value(9))) { + return convertQVariantToPyObject(result); + } + + return PyErr_Format(PyExc_RuntimeError, "QObject method call failed"); + } + } + + return PyErr_Format(PyExc_RuntimeError, "QObject method not found: %s", + ref->method().toUtf8().constData()); +} + + static PyMethodDef PyOtherSideMethods[] = { /* Introduced in PyOtherSide 1.0 */ {"send", pyotherside_send, METH_VARARGS, "Send data to Qt."}, @@ -184,6 +459,32 @@ PyOtherSide_init() // Version of PyOtherSide (new in 1.3) PyModule_AddStringConstant(pyotherside, "version", PYOTHERSIDE_VERSION); + // QObject wrappers (new in 1.4) + pyotherside_QObjectType.tp_new = PyType_GenericNew; + pyotherside_QObjectType.tp_repr = pyotherside_QObject_repr; + pyotherside_QObjectType.tp_getattro = pyotherside_QObject_getattro; + pyotherside_QObjectType.tp_setattro = pyotherside_QObject_setattro; + pyotherside_QObjectType.tp_dealloc = (destructor)pyotherside_QObject_dealloc; + if (PyType_Ready(&pyotherside_QObjectType) < 0) { + qFatal("Could not initialize QObjectType"); + // Not reached + return NULL; + } + Py_INCREF(&pyotherside_QObjectType); + PyModule_AddObject(pyotherside, "QObject", (PyObject *)(&pyotherside_QObjectType)); + + pyotherside_QObjectMethodType.tp_new = PyType_GenericNew; + pyotherside_QObjectMethodType.tp_repr = pyotherside_QObjectMethod_repr; + pyotherside_QObjectMethodType.tp_call = pyotherside_QObjectMethod_call; + pyotherside_QObjectMethodType.tp_dealloc = (destructor)pyotherside_QObjectMethod_dealloc; + if (PyType_Ready(&pyotherside_QObjectMethodType) < 0) { + qFatal("Could not initialize QObjectMethodType"); + // Not reached + return NULL; + } + Py_INCREF(&pyotherside_QObjectMethodType); + PyModule_AddObject(pyotherside, "QObjectMethod", (PyObject *)(&pyotherside_QObjectMethodType)); + return pyotherside; } @@ -193,6 +494,7 @@ QPythonPriv::QPythonPriv() , atexit_callback() , image_provider() , traceback_mod() + , pyotherside_mod() , thread_state(NULL) { PyImport_AppendInittab("pyotherside", PyOtherSide_init); @@ -216,6 +518,12 @@ QPythonPriv::QPythonPriv() PyEval_GetBuiltins()); } + // Need to "self-import" the pyotherside module here, so that Python code + // can use objects wrapped with pyotherside.QObject without crashing when + // the user's Python code doesn't "import pyotherside" + pyotherside_mod = PyObjectRef(PyImport_ImportModule("pyotherside"), true); + assert(pyotherside_mod); + // Release the GIL thread_state = PyEval_SaveThread(); } diff --git a/src/qpython_priv.h b/src/qpython_priv.h index 2fb2bd0..4e3bfd9 100644 --- a/src/qpython_priv.h +++ b/src/qpython_priv.h @@ -22,11 +22,13 @@ #include "Python.h" #include "pyobject_ref.h" +#include "pyqobject.h" #include #include #include + class QPythonPriv : public QObject { Q_OBJECT @@ -50,6 +52,7 @@ class QPythonPriv : public QObject { PyObjectRef atexit_callback; PyObjectRef image_provider; PyObjectRef traceback_mod; + PyObjectRef pyotherside_mod; PyThreadState *thread_state; signals: diff --git a/src/qvariant_converter.h b/src/qvariant_converter.h index 999a032..bf69524 100644 --- a/src/qvariant_converter.h +++ b/src/qvariant_converter.h @@ -112,6 +112,10 @@ class QVariantConverter : public Converter { virtual ~QVariantConverter() {} virtual enum Type type(QVariant &v) { + if (v.canConvert()) { + return QOBJECT; + } + QMetaType::Type t = (QMetaType::Type)v.type(); switch (t) { case QMetaType::Bool: @@ -196,6 +200,10 @@ class QVariantConverter : public Converter { return v.value(); } + virtual QObjectRef qObject(QVariant &v) { + return QObjectRef(v.value()); + } + virtual ListBuilder *newList() { return new QVariantListBuilder; } @@ -218,6 +226,9 @@ class QVariantConverter : public Converter { virtual QVariant fromPyObject(const PyObjectRef &pyobj) { return QVariant::fromValue(pyobj); } + virtual QVariant fromQObject(const QObjectRef &qobj) { + return QVariant::fromValue(qobj.value()); + } virtual QVariant none() { return QVariant(); }; private: diff --git a/src/src.pro b/src/src.pro index 5d28149..274e685 100644 --- a/src/src.pro +++ b/src/src.pro @@ -47,6 +47,11 @@ HEADERS += global_libpython_loader.h SOURCES += pyobject_ref.cpp HEADERS += pyobject_ref.h +# QObject wrapper class exposed to Python +SOURCES += qobject_ref.cpp +HEADERS += qobject_ref.h +HEADERS += pyqobject.h + # GIL helper HEADERS += ensure_gil_state.h diff --git a/tests/tests.cpp b/tests/tests.cpp index d3c222a..0dd8fb8 100644 --- a/tests/tests.cpp +++ b/tests/tests.cpp @@ -222,6 +222,19 @@ TestPyOtherSide::testPyObjectRefRoundTrip() QVERIFY(destructor_called); } +void +TestPyOtherSide::testQObjectRef() +{ + QObject *o = new QObject(); + QObjectRef ref(o); + + QVERIFY(ref.value() == o); + + delete o; + + QVERIFY(ref.value() == NULL); +} + void TestPyOtherSide::testQVariantConverter() { diff --git a/tests/tests.h b/tests/tests.h index 5479b8f..dcd7efa 100644 --- a/tests/tests.h +++ b/tests/tests.h @@ -36,6 +36,7 @@ class TestPyOtherSide : public QObject { void testPyObjectConverter(); void testPyObjectRefRoundTrip(); void testPyObjectRefAssignment(); + void testQObjectRef(); void testConvertToPythonAndBack(); void testSetToList(); }; diff --git a/tests/tests.pro b/tests/tests.pro index 5b930bd..8930c26 100644 --- a/tests/tests.pro +++ b/tests/tests.pro @@ -11,6 +11,7 @@ SOURCES += ../src/qpython.cpp SOURCES += ../src/qpython_worker.cpp SOURCES += ../src/qpython_priv.cpp SOURCES += ../src/pyobject_ref.cpp +SOURCES += ../src/qobject_ref.cpp HEADERS += ../src/qpython.h HEADERS += ../src/qpython_worker.h @@ -18,6 +19,8 @@ HEADERS += ../src/qpython_priv.h HEADERS += ../src/converter.h HEADERS += ../src/qvariant_converter.h HEADERS += ../src/pyobject_converter.h +HEADERS += ../src/pyobject_ref.h +HEADERS += ../src/qobject_ref.h DEPENDPATH += . ../src INCLUDEPATH += . ../src