From c24670ccd2d2d59c749c690335d3548f32c3ffd9 Mon Sep 17 00:00:00 2001 From: Thomas Perl Date: Thu, 9 Oct 2014 11:30:18 +0200 Subject: [PATCH 1/6] WIP QObject wrapper code --- examples/qobject_reference.py | 44 +++++++++ examples/qobject_reference.qml | 38 ++++++++ src/converter.h | 6 ++ src/pyobject_converter.h | 22 ++++- src/qobject_ref.cpp | 75 +++++++++++++++ src/qobject_ref.h | 60 ++++++++++++ src/qpython_priv.cpp | 163 +++++++++++++++++++++++++++++++++ src/qvariant_converter.h | 11 +++ src/src.pro | 4 + tests/tests.cpp | 13 +++ tests/tests.h | 1 + tests/tests.pro | 3 + 12 files changed, 439 insertions(+), 1 deletion(-) create mode 100644 examples/qobject_reference.py create mode 100644 examples/qobject_reference.qml create mode 100644 src/qobject_ref.cpp create mode 100644 src/qobject_ref.h diff --git a/examples/qobject_reference.py b/examples/qobject_reference.py new file mode 100644 index 0000000..e252164 --- /dev/null +++ b/examples/qobject_reference.py @@ -0,0 +1,44 @@ +import pyotherside +import threading +import time + +class Wrapper(object): + def __init__(self, capsule): + object.__setattr__(self, '_capsule', capsule) + + def __getattr__(self, attr): + return pyotherside.get_attribute(self._capsule, attr) + + def __setattr__(self, attr, value): + return pyotherside.set_attribute(self._capsule, attr, value) + +class MethodWrapper(object): + def __init__(self, capsule): + self._capsule = capsule + + def __call__(self, *args): + return pyotherside.call_method(self._capsule, args) + +def do_something(bar): + while True: + print('got: ', MethodWrapper(Wrapper(bar).dynamicFunction)(1, 2, 3)) + time.sleep(1) + +def foo(bar): + bar2 = Wrapper(bar) + print(bar2.x, bar2.color, bar2.scale) + bar2.x = bar2.x * 3 + + print(bar2.dynamicFunction) + method = MethodWrapper(bar2.dynamicFunction) + result = method(1, 2, 3) + print('result:', result) + + threading.Thread(target=do_something, args=[bar]).start() + + #print(bar) + #print(pyotherside.get_attribute(bar, 'x')) + #print('bar.x: ' + bar.x) + #return bar.x * 2 + return bar + diff --git a/examples/qobject_reference.qml b/examples/qobject_reference.qml new file mode 100644 index 0000000..00909cb --- /dev/null +++ b/examples/qobject_reference.qml @@ -0,0 +1,38 @@ + +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('dynamic: ' + a + ', ' + b + ', ' + c); + rotation += 4; + return 'hello'; + } + } + + Python { + id: py + + Component.onCompleted: { + addImportPath(Qt.resolvedUrl('.')); + importModule('qobject_reference', function () { + call('qobject_reference.foo', [foo], function (result) { + console.log('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..731632c 100644 --- a/src/pyobject_converter.h +++ b/src/pyobject_converter.h @@ -23,6 +23,7 @@ #include "Python.h" #include "datetime.h" +#include class PyObjectListBuilder : public ListBuilder { @@ -138,7 +139,10 @@ class PyObjectConverter : public Converter { } virtual enum Type type(PyObject *&o) { - if (PyBool_Check(o)) { + if (PyCapsule_CheckExact(o)) { + qDebug() << "Is a capsule"; + return QOBJECT; + } else if (PyBool_Check(o)) { return BOOLEAN; } else if (PyLong_Check(o)) { return INTEGER; @@ -203,6 +207,15 @@ class PyObjectConverter : public Converter { PyDateTime_DATE_GET_MICROSECOND(o) / 1000); } virtual PyObjectRef pyObject(PyObject *&o) { return PyObjectRef(o); } + virtual QObjectRef qObject(PyObject *&o) { + QObjectRef *ref = static_cast(PyCapsule_GetPointer(o, "QObjectRef")); + + if (ref) { + return QObjectRef(*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 +227,13 @@ 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) { + PyObject *result = PyCapsule_New(new QObjectRef(qobj), "QObjectRef", NULL); + //PyObject *x = PyLong_FromLong(1337); + //PyObject_SetAttrString(result, "x", x); + //Py_CLEAR(x); + return result; + } virtual ListBuilder *newList() { return new PyObjectListBuilder(); } virtual DictBuilder *newDict() { return new PyObjectDictBuilder(); } virtual PyObject * none() { Py_RETURN_NONE; } 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..5145dcb 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 @@ -135,6 +140,159 @@ pyotherside_qrc_list_dir(PyObject *self, PyObject *dirname) return convertQVariantToPyObject(dir.entryList()); } +PyObject * +pyotherside_get_attribute(PyObject *self, PyObject *args) +{ + // (capsule, attrname, [fallback]) + // + PyObject *capsule; + PyObject *attrname; + PyObject *fallback = NULL; + + if (!PyArg_UnpackTuple(args, "get_attribute", 2, 3, &capsule, &attrname, &fallback)) { + return NULL; + } + + if (!PyCapsule_CheckExact(capsule)) { + // TODO: Exception + return NULL; + } + + if (!PyUnicode_Check(attrname)) { + // TODO: Exception + return NULL; + } + + QObjectRef *ref = static_cast(PyCapsule_GetPointer(capsule, "QObjectRef")); + QObject *o = ref->value(); + + if (o) { + const QMetaObject *metaObject = o->metaObject(); + QString attrName = convertPyObjectToQVariant(attrname).toString(); + + for (int i=0; ipropertyCount(); i++) { + QMetaProperty property = metaObject->property(i); + if (attrName == property.name()) { + return convertQVariantToPyObject(property.read(o)); + } + } + + for (int i=0; imethodCount(); i++) { + QMetaMethod method = metaObject->method(i); + if (attrName == method.name()) { + return PyCapsule_New(new QObjectMethodRef(*ref, attrName), "QObjectMethodRef", NULL); + } + } + } + + Py_RETURN_NONE; +} + +PyObject * +pyotherside_set_attribute(PyObject *self, PyObject *args) +{ + // (capsule, attrname, value) + PyObject *capsule; + PyObject *attrname; + PyObject *value; + + if (!PyArg_UnpackTuple(args, "set_attribute", 3, 3, &capsule, &attrname, &value)) { + return NULL; + } + + if (!PyCapsule_CheckExact(capsule)) { + // TODO: Exception + return NULL; + } + + if (!PyUnicode_Check(attrname)) { + // TODO: Exception + return NULL; + } + + QObjectRef *ref = static_cast(PyCapsule_GetPointer(capsule, "QObjectRef")); + QObject *o = ref->value(); + + if (o) { + const QMetaObject *metaObject = o->metaObject(); + QString attrName = convertPyObjectToQVariant(attrname).toString(); + + for (int i=0; ipropertyCount(); i++) { + QMetaProperty property = metaObject->property(i); + if (attrName == property.name()) { + if (!property.write(o, convertPyObjectToQVariant(value))) { + // TODO: Exception + return NULL; + } + + Py_RETURN_NONE; + } + } + } + + Py_RETURN_NONE; +} + +PyObject * +pyotherside_call_method(PyObject *self, PyObject *args) +{ + // (capsule, args) + PyObject *capsule; + PyObject *methargs; + + if (!PyArg_UnpackTuple(args, "call_method", 2, 2, &capsule, &methargs)) { + return NULL; + } + + if (!PyCapsule_CheckExact(capsule)) { + qDebug() << "not a capsule"; + // TODO: Exception + return NULL; + } + + if (!PyTuple_Check(methargs)) { + qDebug() << "not a tuple"; + // TODO: Exception + return NULL; + } + + QList qargs = convertPyObjectToQVariant(methargs).toList(); + QObjectMethodRef *ref = static_cast(PyCapsule_GetPointer(capsule, "QObjectMethodRef")); + + QList genericArguments; + for (int j=0; jobject().value(); + 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); + } + + qDebug() << "No result"; + Py_RETURN_NONE; + } + } + + Py_RETURN_NONE; +} + + + static PyMethodDef PyOtherSideMethods[] = { /* Introduced in PyOtherSide 1.0 */ {"send", pyotherside_send, METH_VARARGS, "Send data to Qt."}, @@ -149,6 +307,11 @@ static PyMethodDef PyOtherSideMethods[] = { {"qrc_get_file_contents", pyotherside_qrc_get_file_contents, METH_O, "Get file contents from a Qt Resource."}, {"qrc_list_dir", pyotherside_qrc_list_dir, METH_O, "Get directory entries from a Qt Resource."}, + /* Introduced in PyOtherSide 1.4 */ + {"get_attribute", pyotherside_get_attribute, METH_VARARGS, "Get attribute of QObject"}, + {"set_attribute", pyotherside_set_attribute, METH_VARARGS, "Set attribute of QObject"}, + {"call_method", pyotherside_call_method, METH_VARARGS, "Call method on QObject"}, + /* sentinel */ {NULL, NULL, 0, NULL}, }; 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..bb4fd85 100644 --- a/src/src.pro +++ b/src/src.pro @@ -47,6 +47,10 @@ 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 + # 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 From 43f81e54e12b8560f533cb02ef734ff66a4b76c3 Mon Sep 17 00:00:00 2001 From: Thomas Perl Date: Fri, 10 Oct 2014 13:05:45 +0200 Subject: [PATCH 2/6] Implement proper Python wrapper objects --- examples/qobject_reference.py | 74 ++++---- examples/qobject_reference.qml | 7 +- src/pyobject_converter.h | 22 ++- src/pyqobject.h | 39 ++++ src/qpython_priv.cpp | 316 +++++++++++++++++++++++---------- src/qpython_priv.h | 2 + src/src.pro | 1 + 7 files changed, 318 insertions(+), 143 deletions(-) create mode 100644 src/pyqobject.h diff --git a/examples/qobject_reference.py b/examples/qobject_reference.py index e252164..f424d1f 100644 --- a/examples/qobject_reference.py +++ b/examples/qobject_reference.py @@ -1,44 +1,58 @@ -import pyotherside import threading import time +import pyotherside -class Wrapper(object): - def __init__(self, capsule): - object.__setattr__(self, '_capsule', capsule) - - def __getattr__(self, attr): - return pyotherside.get_attribute(self._capsule, attr) - - def __setattr__(self, attr, value): - return pyotherside.set_attribute(self._capsule, attr, value) - -class MethodWrapper(object): - def __init__(self, capsule): - self._capsule = capsule - - def __call__(self, *args): - return pyotherside.call_method(self._capsule, args) +# 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: ', MethodWrapper(Wrapper(bar).dynamicFunction)(1, 2, 3)) + print('got: ', bar.dynamicFunction(1, 2, 3)) time.sleep(1) -def foo(bar): - bar2 = Wrapper(bar) - print(bar2.x, bar2.color, bar2.scale) - bar2.x = bar2.x * 3 +def foo(bar, py): + # Printing the objects will give some info on the + # QObject class and memory address + print('got:', bar, py) - print(bar2.dynamicFunction) - method = MethodWrapper(bar2.dynamicFunction) - result = method(1, 2, 3) + # 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) + + # 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() - #print(bar) - #print(pyotherside.get_attribute(bar, 'x')) - #print('bar.x: ' + bar.x) - #return bar.x * 2 + # Returning QObject references from Python also works return bar - diff --git a/examples/qobject_reference.qml b/examples/qobject_reference.qml index 00909cb..8a83bb8 100644 --- a/examples/qobject_reference.qml +++ b/examples/qobject_reference.qml @@ -1,4 +1,3 @@ - import QtQuick 2.0 import io.thp.pyotherside 1.4 @@ -16,7 +15,7 @@ Rectangle { color: 'blue' function dynamicFunction(a, b, c) { - console.log('dynamic: ' + a + ', ' + b + ', ' + c); + console.log('In QML, dynamicFunction got: ' + a + ', ' + b + ', ' + c); rotation += 4; return 'hello'; } @@ -28,8 +27,8 @@ Rectangle { Component.onCompleted: { addImportPath(Qt.resolvedUrl('.')); importModule('qobject_reference', function () { - call('qobject_reference.foo', [foo], function (result) { - console.log('got result: ' + result); + call('qobject_reference.foo', [foo, py], function (result) { + console.log('In QML, got result: ' + result); result.color = 'green'; }); }); diff --git a/src/pyobject_converter.h b/src/pyobject_converter.h index 731632c..da7b54d 100644 --- a/src/pyobject_converter.h +++ b/src/pyobject_converter.h @@ -20,6 +20,7 @@ #define PYOTHERSIDE_PYOBJECT_CONVERTER_H #include "converter.h" +#include "pyqobject.h" #include "Python.h" #include "datetime.h" @@ -139,8 +140,8 @@ class PyObjectConverter : public Converter { } virtual enum Type type(PyObject *&o) { - if (PyCapsule_CheckExact(o)) { - qDebug() << "Is a capsule"; + if (PyObject_TypeCheck(o, &pyotherside_QObjectType)) { + qDebug() << "Is a wrapped qobject"; return QOBJECT; } else if (PyBool_Check(o)) { return BOOLEAN; @@ -208,12 +209,11 @@ class PyObjectConverter : public Converter { } virtual PyObjectRef pyObject(PyObject *&o) { return PyObjectRef(o); } virtual QObjectRef qObject(PyObject *&o) { - QObjectRef *ref = static_cast(PyCapsule_GetPointer(o, "QObjectRef")); - - if (ref) { - return QObjectRef(*ref); + if (PyObject_TypeCheck(o, &pyotherside_QObjectType)) { + pyotherside_QObject *result = reinterpret_cast(o); + return QObjectRef(*(result->m_qobject_ref)); } - + return QObjectRef(); } @@ -228,11 +228,9 @@ class PyObjectConverter : public Converter { } virtual PyObject * fromPyObject(const PyObjectRef &pyobj) { return pyobj.newRef(); } virtual PyObject * fromQObject(const QObjectRef &qobj) { - PyObject *result = PyCapsule_New(new QObjectRef(qobj), "QObjectRef", NULL); - //PyObject *x = PyLong_FromLong(1337); - //PyObject_SetAttrString(result, "x", x); - //Py_CLEAR(x); - return result; + 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(); } 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/qpython_priv.cpp b/src/qpython_priv.cpp index 5145dcb..2bf4431 100644 --- a/src/qpython_priv.cpp +++ b/src/qpython_priv.cpp @@ -48,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) { @@ -141,123 +192,173 @@ pyotherside_qrc_list_dir(PyObject *self, PyObject *dirname) } PyObject * -pyotherside_get_attribute(PyObject *self, PyObject *args) +pyotherside_QObject_repr(PyObject *o) { - // (capsule, attrname, [fallback]) - // - PyObject *capsule; - PyObject *attrname; - PyObject *fallback = NULL; - - if (!PyArg_UnpackTuple(args, "get_attribute", 2, 3, &capsule, &attrname, &fallback)) { - return NULL; + if (!PyObject_TypeCheck(o, &pyotherside_QObjectType)) { + return PyErr_Format(PyExc_TypeError, "Not a pyotherside.QObject"); } - if (!PyCapsule_CheckExact(capsule)) { - // TODO: Exception - return NULL; + 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); } - if (!PyUnicode_Check(attrname)) { - // TODO: Exception - return NULL; - } - - QObjectRef *ref = static_cast(PyCapsule_GetPointer(capsule, "QObjectRef")); - QObject *o = ref->value(); - - if (o) { - const QMetaObject *metaObject = o->metaObject(); - QString attrName = convertPyObjectToQVariant(attrname).toString(); - - for (int i=0; ipropertyCount(); i++) { - QMetaProperty property = metaObject->property(i); - if (attrName == property.name()) { - return convertQVariantToPyObject(property.read(o)); - } - } - - for (int i=0; imethodCount(); i++) { - QMetaMethod method = metaObject->method(i); - if (attrName == method.name()) { - return PyCapsule_New(new QObjectMethodRef(*ref, attrName), "QObjectMethodRef", NULL); - } - } - } - - Py_RETURN_NONE; + return PyUnicode_FromFormat(""); } PyObject * -pyotherside_set_attribute(PyObject *self, PyObject *args) +pyotherside_QObject_getattro(PyObject *o, PyObject *attr_name) { - // (capsule, attrname, value) - PyObject *capsule; - PyObject *attrname; - PyObject *value; - - if (!PyArg_UnpackTuple(args, "set_attribute", 3, 3, &capsule, &attrname, &value)) { - return NULL; + if (!PyObject_TypeCheck(o, &pyotherside_QObjectType)) { + return PyErr_Format(PyExc_TypeError, "Not a pyotherside.QObject"); } - if (!PyCapsule_CheckExact(capsule)) { - // TODO: Exception - return NULL; + if (!PyUnicode_Check(attr_name)) { + return PyErr_Format(PyExc_TypeError, "attr_name must be a string"); } - if (!PyUnicode_Check(attrname)) { - // TODO: Exception - return NULL; + 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"); } - QObjectRef *ref = static_cast(PyCapsule_GetPointer(capsule, "QObjectRef")); - QObject *o = ref->value(); + const QMetaObject *metaObject = qobject->metaObject(); + QString attrName = convertPyObjectToQVariant(attr_name).toString(); - if (o) { - const QMetaObject *metaObject = o->metaObject(); - QString attrName = convertPyObjectToQVariant(attrname).toString(); - - for (int i=0; ipropertyCount(); i++) { - QMetaProperty property = metaObject->property(i); - if (attrName == property.name()) { - if (!property.write(o, convertPyObjectToQVariant(value))) { - // TODO: Exception - return NULL; - } - - Py_RETURN_NONE; - } + for (int i=0; ipropertyCount(); i++) { + QMetaProperty property = metaObject->property(i); + if (attrName == property.name()) { + return convertQVariantToPyObject(property.read(qobject)); } } - Py_RETURN_NONE; + 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; } PyObject * -pyotherside_call_method(PyObject *self, PyObject *args) +pyotherside_QObjectMethod_repr(PyObject *o) { - // (capsule, args) - PyObject *capsule; - PyObject *methargs; - - if (!PyArg_UnpackTuple(args, "call_method", 2, 2, &capsule, &methargs)) { - return NULL; + if (!PyObject_TypeCheck(o, &pyotherside_QObjectMethodType)) { + return PyErr_Format(PyExc_TypeError, "Not a pyotherside.QObjectMethod"); } - if (!PyCapsule_CheckExact(capsule)) { - qDebug() << "not a capsule"; - // TODO: Exception - return NULL; + pyotherside_QObjectMethod *pyqobjectmethod = reinterpret_cast(o); + + QObjectMethodRef *ref = pyqobjectmethod->m_method_ref; + if (!ref) { + return PyUnicode_FromFormat(""); } - if (!PyTuple_Check(methargs)) { - qDebug() << "not a tuple"; - // TODO: Exception - return NULL; + QObjectRef oref = ref->object(); + QObject *qobject = oref.value(); + if (!qobject) { + return PyUnicode_FromFormat("", + ref->method().toUtf8().constData()); } - QList qargs = convertPyObjectToQVariant(methargs).toList(); - QObjectMethodRef *ref = static_cast(PyCapsule_GetPointer(capsule, "QObjectMethodRef")); + 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++) { @@ -283,16 +387,15 @@ pyotherside_call_method(PyObject *self, PyObject *args) return convertQVariantToPyObject(result); } - qDebug() << "No result"; - Py_RETURN_NONE; + return PyErr_Format(PyExc_RuntimeError, "QObject method call failed"); } } - Py_RETURN_NONE; + 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."}, @@ -307,11 +410,6 @@ static PyMethodDef PyOtherSideMethods[] = { {"qrc_get_file_contents", pyotherside_qrc_get_file_contents, METH_O, "Get file contents from a Qt Resource."}, {"qrc_list_dir", pyotherside_qrc_list_dir, METH_O, "Get directory entries from a Qt Resource."}, - /* Introduced in PyOtherSide 1.4 */ - {"get_attribute", pyotherside_get_attribute, METH_VARARGS, "Get attribute of QObject"}, - {"set_attribute", pyotherside_set_attribute, METH_VARARGS, "Set attribute of QObject"}, - {"call_method", pyotherside_call_method, METH_VARARGS, "Call method on QObject"}, - /* sentinel */ {NULL, NULL, 0, NULL}, }; @@ -347,6 +445,30 @@ 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; + 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; + 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; } diff --git a/src/qpython_priv.h b/src/qpython_priv.h index 2fb2bd0..03c4633 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 diff --git a/src/src.pro b/src/src.pro index bb4fd85..274e685 100644 --- a/src/src.pro +++ b/src/src.pro @@ -50,6 +50,7 @@ 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 From 24cb511bc9df1bbdd066f5220554f32af2554b04 Mon Sep 17 00:00:00 2001 From: Thomas Perl Date: Fri, 10 Oct 2014 13:14:09 +0200 Subject: [PATCH 3/6] Avoid conversion of QObjectMethod objects --- examples/qobject_reference.py | 7 +++++++ src/pyobject_converter.h | 5 ++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/examples/qobject_reference.py b/examples/qobject_reference.py index f424d1f..13eefb5 100644 --- a/examples/qobject_reference.py +++ b/examples/qobject_reference.py @@ -36,6 +36,13 @@ def foo(bar, py): 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 diff --git a/src/pyobject_converter.h b/src/pyobject_converter.h index da7b54d..19064fc 100644 --- a/src/pyobject_converter.h +++ b/src/pyobject_converter.h @@ -141,8 +141,11 @@ class PyObjectConverter : public Converter { virtual enum Type type(PyObject *&o) { if (PyObject_TypeCheck(o, &pyotherside_QObjectType)) { - qDebug() << "Is a wrapped qobject"; 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)) { From 65314f10a766d73cbf79392c15b452c88b6a93ae Mon Sep 17 00:00:00 2001 From: Thomas Perl Date: Fri, 10 Oct 2014 13:53:47 +0200 Subject: [PATCH 4/6] Free allocated resources --- src/qpython_priv.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/qpython_priv.cpp b/src/qpython_priv.cpp index 2bf4431..fe58e57 100644 --- a/src/qpython_priv.cpp +++ b/src/qpython_priv.cpp @@ -191,6 +191,13 @@ 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) { @@ -304,6 +311,13 @@ pyotherside_QObject_setattro(PyObject *o, PyObject *attr_name, PyObject *v) 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) { @@ -450,6 +464,7 @@ PyOtherSide_init() 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 @@ -461,6 +476,7 @@ PyOtherSide_init() 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 From 78180ce8404ebd09b945219de2e880584b312ff3 Mon Sep 17 00:00:00 2001 From: Thomas Perl Date: Sat, 11 Oct 2014 18:38:06 +0200 Subject: [PATCH 5/6] Self-import pyotherside to prevent crashes Otherwise pyotherside.QObject instances could not be used if the same module didn't do a "import pyotherside". --- src/qpython_priv.cpp | 7 +++++++ src/qpython_priv.h | 1 + 2 files changed, 8 insertions(+) diff --git a/src/qpython_priv.cpp b/src/qpython_priv.cpp index fe58e57..7230130 100644 --- a/src/qpython_priv.cpp +++ b/src/qpython_priv.cpp @@ -494,6 +494,7 @@ QPythonPriv::QPythonPriv() , atexit_callback() , image_provider() , traceback_mod() + , pyotherside_mod() , thread_state(NULL) { PyImport_AppendInittab("pyotherside", PyOtherSide_init); @@ -517,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 03c4633..4e3bfd9 100644 --- a/src/qpython_priv.h +++ b/src/qpython_priv.h @@ -52,6 +52,7 @@ class QPythonPriv : public QObject { PyObjectRef atexit_callback; PyObjectRef image_provider; PyObjectRef traceback_mod; + PyObjectRef pyotherside_mod; PyThreadState *thread_state; signals: From 295ef81db106e33247b6f49aac4e3a78b15f44d8 Mon Sep 17 00:00:00 2001 From: Thomas Perl Date: Sat, 11 Oct 2014 19:34:32 +0200 Subject: [PATCH 6/6] Document pyotherside.QObject wrapper usage --- docs/index.rst | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) 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) --------------------------