aboutsummaryrefslogtreecommitdiffstats
path: root/sources/pyside6/libpysideremoteobjects/pysiderephandler.cpp
diff options
context:
space:
mode:
authorBrett Stottlemyer <bstottle@ford.com>2024-12-18 10:33:56 -0500
committerFriedemann Kleint <Friedemann.Kleint@qt.io>2025-03-13 16:28:42 +0100
commit19abd816e73bebdd489408d0a3b7676822bff39c (patch)
tree8459ae9401f5e190995b3e24b6ae6968cf457baf /sources/pyside6/libpysideremoteobjects/pysiderephandler.cpp
parent3c66c456aeab597b7cb046f81c7f015433bb57a4 (diff)
Make Remote Objects usable beyond Models
While present, the Qt Remote Objects bindings to Python have not been very useful. The only usable components were those based on QAbstractItemModel, due to the lack of a way to interpret .rep files from Python. This addresses that limitation. Fixes: PYSIDE-862 Change-Id: Ice57c0c64f11c3c7e74d50ce3c48617bd9b422a3 Reviewed-by: Friedemann Kleint <Friedemann.Kleint@qt.io> Reviewed-by: Brett Stottlemyer <brett.stottlemyer@gmail.com>
Diffstat (limited to 'sources/pyside6/libpysideremoteobjects/pysiderephandler.cpp')
-rw-r--r--sources/pyside6/libpysideremoteobjects/pysiderephandler.cpp459
1 files changed, 459 insertions, 0 deletions
diff --git a/sources/pyside6/libpysideremoteobjects/pysiderephandler.cpp b/sources/pyside6/libpysideremoteobjects/pysiderephandler.cpp
new file mode 100644
index 000000000..25bdbef9b
--- /dev/null
+++ b/sources/pyside6/libpysideremoteobjects/pysiderephandler.cpp
@@ -0,0 +1,459 @@
+// Copyright (C) 2025 Ford Motor Company
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include "pysiderephandler_p.h"
+#include "pysidedynamicclass_p.h"
+#include "pysidedynamicpod_p.h"
+#include "pysidedynamiccommon_p.h"
+
+#include <pep384ext.h>
+#include <sbkstring.h>
+#include <sbktypefactory.h>
+#include <signature.h>
+
+#include <pysideutils.h>
+
+#include <QtCore/qbuffer.h>
+#include <QtCore/qiodevice.h>
+#include <QtCore/qmetaobject.h>
+
+#include <QtRemoteObjects/qremoteobjectreplica.h>
+#include <QtRemoteObjects/qremoteobjectpendingcall.h>
+
+#include <private/qremoteobjectrepparser_p.h>
+
+using namespace Qt::StringLiterals;
+using namespace Shiboken;
+
+/**
+ * @file pysiderephandler.cpp
+ * @brief This file contains the implementation of the PySideRepFile type and its
+ * associated methods for handling Qt Remote Objects in PySide6.
+ *
+ * The PySideRepFile type provides functionality to parse and handle Qt Remote Objects
+ * (QtRO) files, and dynamically generate Python types for QtRO sources, replicas, and
+ * PODs (Plain Old Data structures).
+ *
+ * The RepFile_tp_methods array defines the methods available on the PySideRepFile object:
+ * - source: Generates a dynamic Python type for a QtRO source class.
+ * - replica: Generates a dynamic Python type for a QtRO replica class.
+ * - pod: Generates a dynamic Python type for a QtRO POD class.
+ *
+ * When generating a source or replica type, the generateDynamicType function is
+ * used, creating a new Python type based on the generated QMetaObject, and adds
+ * method descriptors for the required methods. A QVariantList for the types
+ * properties is also created, populated with default values if set in the input
+ * .rep file.
+*/
+
+static QVariantList generateProperties(QMetaObject *meta, const ASTClass &astClass);
+
+extern "C"
+{
+
+// Code for the PySideRepFile type
+static PyObject *RepFile_tp_string(PyObject *self);
+static PyObject *RepFile_tp_new(PyTypeObject *subtype, PyObject *args, PyObject *kwds);
+static int RepFile_tp_init(PyObject *self, PyObject *args, PyObject *kwds);
+static void RepFile_tp_free(void *self);
+static void RepFile_tp_dealloc(PySideRepFile *self);
+
+static PyObject *RepFile_get_pods(PySideRepFile *self, void * /*unused*/);
+static PyObject *RepFile_get_replicas(PySideRepFile *self, void * /*unused*/);
+static PyObject *RepFile_get_sources(PySideRepFile *self, void * /*unused*/);
+
+bool instantiateFromDefaultValue(QVariant &variant, const QString &defaultValue);
+
+static PyObject *cppToPython_POD_Tuple(const void *cppIn);
+static void pythonToCpp_Tuple_POD(PyObject *pyIn, void *cppOut);
+static PythonToCppFunc is_Tuple_PythonToCpp_POD_Convertible(PyObject *pyIn);
+
+static PyGetSetDef RepFile_tp_getters[] = {
+ {"pod", reinterpret_cast<getter>(RepFile_get_pods), nullptr, "POD dictionary", nullptr},
+ {"replica", reinterpret_cast<getter>(RepFile_get_replicas), nullptr, "Replica dictionary", nullptr},
+ {"source", reinterpret_cast<getter>(RepFile_get_sources), nullptr, "Source dictionary", nullptr},
+ {nullptr, nullptr, nullptr, nullptr, nullptr} // Sentinel
+};
+
+static PyTypeObject *createRepFileType()
+{
+ PyType_Slot PySideRepFileType_slots[] = {
+ {Py_tp_str, reinterpret_cast<void *>(RepFile_tp_string)},
+ {Py_tp_init, reinterpret_cast<void *>(RepFile_tp_init)},
+ {Py_tp_new, reinterpret_cast<void *>(RepFile_tp_new)},
+ {Py_tp_free, reinterpret_cast<void *>(RepFile_tp_free)},
+ {Py_tp_dealloc, reinterpret_cast<void *>(RepFile_tp_dealloc)},
+ {Py_tp_getset, reinterpret_cast<void *>(RepFile_tp_getters)},
+ {0, nullptr}
+ };
+
+ PyType_Spec PySideRepFileType_spec = {
+ "2:PySide6.QtRemoteObjects.RepFile",
+ sizeof(PySideRepFile),
+ 0,
+ Py_TPFLAGS_DEFAULT,
+ PySideRepFileType_slots};
+ return SbkType_FromSpec(&PySideRepFileType_spec);
+}
+
+PyTypeObject *PySideRepFile_TypeF(void)
+{
+ static auto *type = createRepFileType();
+ return type;
+}
+
+static PyObject *RepFile_tp_string(PyObject *self)
+{
+ auto *cppSelf = reinterpret_cast<PySideRepFile *>(self);
+ QString result = QStringLiteral("RepFile(Classes: [%1], PODs: [%2])")
+ .arg(cppSelf->d->classes.join(", "_L1), cppSelf->d->pods.join(", "_L1));
+ return PyUnicode_FromString(result.toUtf8().constData());
+}
+
+static PyObject *RepFile_tp_new(PyTypeObject *subtype, PyObject * /* args */, PyObject * /* kwds */)
+{
+ auto *me = PepExt_TypeCallAlloc<PySideRepFile>(subtype, 0);
+ auto *priv = new PySideRepFilePrivate;
+ priv->podDict = PyDict_New();
+ if (!priv->podDict) {
+ delete priv;
+ return nullptr;
+ }
+ priv->replicaDict = PyDict_New();
+ if (!priv->replicaDict) {
+ Py_DECREF(priv->podDict);
+ delete priv;
+ return nullptr;
+ }
+ priv->sourceDict = PyDict_New();
+ if (!priv->sourceDict) {
+ Py_DECREF(priv->podDict);
+ Py_DECREF(priv->replicaDict);
+ delete priv;
+ return nullptr;
+ }
+ me->d = priv;
+ return reinterpret_cast<PyObject *>(me);
+}
+
+static PyObject *RepFile_get_pods(PySideRepFile *self, void * /* closure */)
+{
+ Py_INCREF(self->d->podDict);
+ return self->d->podDict;
+}
+
+static PyObject *RepFile_get_replicas(PySideRepFile *self, void * /* closure */)
+{
+ Py_INCREF(self->d->replicaDict);
+ return self->d->replicaDict;
+}
+
+static PyObject *RepFile_get_sources(PySideRepFile *self, void * /* closure */)
+{
+ Py_INCREF(self->d->sourceDict);
+ return self->d->sourceDict;
+}
+
+static void RepFile_tp_dealloc(PySideRepFile *self)
+{
+ Py_XDECREF(self->d->podDict);
+ Py_XDECREF(self->d->replicaDict);
+ Py_XDECREF(self->d->sourceDict);
+ PepExt_TypeCallFree(reinterpret_cast<PyObject *>(self));
+}
+
+static int parseArgsToAST(PyObject *args, PySideRepFile *repFile)
+{
+ // Verify args is a single string argument
+ if (PyTuple_Size(args) != 1 || !PyUnicode_Check(PyTuple_GetItem(args, 0))) {
+ PyErr_SetString(PyExc_TypeError, "RepFile constructor requires a single string argument");
+ return -1;
+ }
+
+ // Wrap contents into a QBuffer
+ const auto contents = PySide::pyStringToQString(PyTuple_GetItem(args, 0));
+ auto byteArray = contents.toUtf8();
+ QBuffer buffer(&byteArray);
+ buffer.open(QIODevice::ReadOnly);
+ RepParser repparser(buffer);
+ if (!repparser.parse()) {
+ PyErr_Format(PyExc_RuntimeError, "Error parsing input, line %d: error: %s",
+ repparser.lineNumber(), qPrintable(repparser.errorString()));
+ auto lines = contents.split("\n"_L1);
+ auto lMin = std::max(1, repparser.lineNumber() - 2);
+ auto lMax = std::min(repparser.lineNumber() + 2, int(lines.size()));
+ // Print a few lines around the error
+ qWarning() << "Contents:";
+ for (int i = lMin; i <= lMax; ++i) {
+ if (i == repparser.lineNumber())
+ qWarning().nospace() << " line " << i << ": > " << lines.at(i - 1);
+ else
+ qWarning().nospace() << " line " << i << ": " << lines.at(i - 1);
+ }
+ return -1;
+ }
+
+ repFile->d->ast = repparser.ast();
+
+ return 0;
+}
+
+static const char *repName(QMetaObject *meta)
+{
+ const int ind = meta->indexOfClassInfo(QCLASSINFO_REMOTEOBJECT_TYPE);
+ return ind >= 0 ? meta->classInfo(ind).value() : "<Invalid RemoteObject>";
+}
+
+static int RepFile_tp_init(PyObject *self, PyObject *args, PyObject * /* kwds */)
+{
+ auto *cppSelf = reinterpret_cast<PySideRepFile *>(self);
+ if (parseArgsToAST(args, cppSelf) < 0)
+ return -1;
+
+ for (const auto &pod : std::as_const(cppSelf->d->ast.pods)) {
+ cppSelf->d->pods << pod.name;
+ auto *qobject = new QObject;
+ auto *meta = createAndRegisterMetaTypeFromPOD(pod, qobject);
+ if (!meta) {
+ delete qobject;
+ PyErr_Format(PyExc_RuntimeError, "Failed to create meta object for POD '%s'",
+ pod.name.toUtf8().constData());
+ return -1;
+ }
+
+ PyTypeObject *newType = createPodType(meta);
+ if (!newType) {
+ delete qobject;
+ PyErr_Print();
+ PyErr_Format(PyExc_RuntimeError, "Failed to create POD type for POD '%s'",
+ pod.name.toUtf8().constData());
+ return -1;
+ }
+ if (set_cleanup_capsule_attr_for_pointer(newType, "_qobject_capsule", qobject) < 0) {
+ delete qobject;
+ return -1;
+ }
+
+ PyDict_SetItemString(cppSelf->d->podDict, meta->className(),
+ reinterpret_cast<PyObject *>(newType));
+ Py_DECREF(newType);
+ }
+
+ if (PyErr_Occurred())
+ PyErr_Print();
+
+ for (const auto &cls : std::as_const(cppSelf->d->ast.classes)) {
+ cppSelf->d->classes << cls.name;
+
+ // Create Source type
+ {
+ auto *qobject = new QObject;
+ auto *meta = createAndRegisterSourceFromASTClass(cls, qobject);
+ if (!meta) {
+ delete qobject;
+ PyErr_Format(PyExc_RuntimeError, "Failed to create Source meta object for class '%s'",
+ cls.name.toUtf8().constData());
+ return -1;
+ }
+
+ auto properties = generateProperties(meta, cls);
+ // Check if an error occurred during generateProperties
+ if (PyErr_Occurred()) {
+ delete qobject;
+ return -1;
+ }
+ auto *propertiesPtr = new QVariantList(properties);
+ auto *pyCapsule = PyCapsule_New(propertiesPtr, nullptr, [](PyObject *capsule) {
+ delete reinterpret_cast<QVariantList *>(PyCapsule_GetPointer(capsule, nullptr));
+ });
+
+ PyTypeObject *newType = createDynamicClass(meta, pyCapsule);
+ if (!newType) {
+ delete qobject;
+ PyErr_Format(PyExc_RuntimeError,
+ "Failed to create Source Python type for class '%s'",
+ meta->className());
+ return -1;
+ }
+ if (set_cleanup_capsule_attr_for_pointer(newType, "_qobject_capsule", qobject) < 0) {
+ delete qobject;
+ return -1;
+ }
+
+ PyDict_SetItemString(cppSelf->d->sourceDict, repName(meta),
+ reinterpret_cast<PyObject *>(newType));
+ Py_DECREF(newType);
+ }
+
+ // Create Replica type
+ {
+ auto *qobject = new QObject;
+ auto *meta = createAndRegisterReplicaFromASTClass(cls, qobject);
+ if (!meta) {
+ delete qobject;
+ PyErr_Format(PyExc_RuntimeError,
+ "Failed to create Replica meta object for class '%s'",
+ qPrintable(cls.name));
+ return -1;
+ }
+
+ auto properties = generateProperties(meta, cls);
+ // Check if an error occurred during generateProperties
+ if (PyErr_Occurred()) {
+ delete qobject;
+ return -1;
+ }
+ auto *propertiesPtr = new QVariantList(properties);
+ auto *pyCapsule = PyCapsule_New(propertiesPtr, nullptr, [](PyObject *capsule) {
+ delete reinterpret_cast<QVariantList *>(PyCapsule_GetPointer(capsule, nullptr));
+ });
+
+ PyTypeObject *newType = createDynamicClass(meta, pyCapsule);
+ if (!newType) {
+ delete qobject;
+ PyErr_Format(PyExc_RuntimeError,
+ "Failed to create Replica Python type for class '%s'",
+ meta->className());
+ return -1;
+ }
+ if (set_cleanup_capsule_attr_for_pointer(newType, "_qobject_capsule", qobject) < 0) {
+ delete qobject;
+ return -1;
+ }
+
+ PyDict_SetItemString(cppSelf->d->replicaDict, repName(meta),
+ reinterpret_cast<PyObject *>(newType));
+ Py_DECREF(newType);
+ }
+ }
+
+ return 0;
+}
+
+static void RepFile_tp_free(void *self)
+{
+ PySideRepFile *obj = reinterpret_cast<PySideRepFile*>(self);
+ delete obj->d;
+}
+
+/**
+ * @brief Sets the QVariant value based on the provided default value text.
+ *
+ * This function attempts to set the provided QVariant's value based on the
+ * provided text. It evaluates the text as a Python expression, the the python
+ * type associated with the provided QMetaType. It first retrieves the Python
+ * type object corresponding to the given QMetaType, then constructs a Python
+ * expression to instantiate the type with the default value. The expression is
+ * evaluated using PyRun_String, and the result is then set on the QVariant.
+ * Note: The variant is passed by reference and modified in place.
+ *
+ * @return True if the instantiation is successful, false otherwise.
+ */
+bool instantiateFromDefaultValue(QVariant &variant, const QString &defaultValue)
+{
+ auto metaType = variant.metaType();
+ auto *pyType = Shiboken::Conversions::getPythonTypeObject(metaType.name());
+ if (!pyType) {
+ PyErr_Format(PyExc_TypeError, "Failed to find Python type for meta type: %s",
+ metaType.name());
+ return false;
+ }
+
+ // Evaluate the code
+ static PyObject *pyLocals = PyDict_New();
+
+ // Create the Python expression to evaluate
+ std::string code = std::string(pyType->tp_name) + '('
+ + defaultValue.toUtf8().constData() + ')';
+ PyObject *pyResult = PyRun_String(code.c_str(), Py_eval_input, pyLocals, pyLocals);
+
+ if (!pyResult) {
+ PyObject *ptype = nullptr;
+ PyObject *pvalue = nullptr;
+ PyObject *ptraceback = nullptr;
+ PyErr_Fetch(&ptype, &pvalue, &ptraceback);
+ PyErr_NormalizeException(&ptype, &pvalue, &ptraceback);
+ PyErr_Format(PyExc_TypeError,
+ "Failed to generate default value. Error: %s. Problematic code: %s",
+ Shiboken::String::toCString(PyObject_Str(pvalue)), code.c_str());
+ Py_XDECREF(ptype);
+ Py_XDECREF(pvalue);
+ Py_XDECREF(ptraceback);
+ Py_DECREF(pyLocals);
+ return false;
+ }
+
+ Conversions::SpecificConverter converter(metaType.name());
+ if (!converter) {
+ PyErr_Format(PyExc_TypeError, "Failed to find converter from Python type: %s to Qt type: %s",
+ pyResult->ob_type->tp_name, metaType.name());
+ Py_DECREF(pyResult);
+ return false;
+ }
+ converter.toCpp(pyResult, variant.data());
+ Py_DECREF(pyResult);
+
+ return true;
+}
+
+} // extern "C"
+
+static QVariantList generateProperties(QMetaObject *meta, const ASTClass &astClass)
+{
+ QVariantList properties;
+ auto propertyCount = astClass.properties.size();
+ properties.reserve(propertyCount);
+ for (auto i = 0; i < propertyCount; ++i) {
+ auto j = i + meta->propertyOffset(); // Corresponding property index in the meta object
+ auto metaProperty = meta->property(j);
+ auto metaType = metaProperty.metaType();
+ if (!metaType.isValid()) {
+ PyErr_Format(PyExc_RuntimeError, "Invalid meta type for property %d: %s", i,
+ astClass.properties[i].type.toUtf8().constData());
+ return {};
+ }
+ auto variant = QVariant(metaType);
+ if (auto defaultValue = astClass.properties[i].defaultValue; !defaultValue.isEmpty()) {
+ auto success = instantiateFromDefaultValue(variant, defaultValue);
+ if (!success) {
+ // Print a warning giving the property name, then propagate the error
+ qWarning() << "Failed to instantiate default value for property: "
+ << metaProperty.name();
+ return {};
+ }
+ }
+ properties << variant;
+ }
+ return properties;
+}
+
+namespace PySide::RemoteObjects
+{
+
+static const char *RepFile_SignatureStrings[] = {
+ "PySide6.RemoteObjects.RepFile(self,content:str)",
+ nullptr}; // Sentinel
+
+void init(PyObject *module)
+{
+ if (InitSignatureStrings(PySideRepFile_TypeF(), RepFile_SignatureStrings) < 0)
+ return;
+
+ qRegisterMetaType<QRemoteObjectPendingCall>();
+ qRegisterMetaType<QRemoteObjectPendingCallWatcher>();
+
+ Py_INCREF(PySideRepFile_TypeF());
+ PyModule_AddObject(module, "RepFile", reinterpret_cast<PyObject *>(PySideRepFile_TypeF()));
+
+ // Add a test helper to verify type reference counting
+ static PyMethodDef get_capsule_count_def = {
+ "getCapsuleCount", // name of the function in Python
+ reinterpret_cast<PyCFunction>(get_capsule_count), // C function pointer
+ METH_NOARGS, // flags indicating parameters
+ "Returns the current count of PyCapsule objects" // docstring
+ };
+
+ PyModule_AddObject(module, "getCapsuleCount", PyCFunction_New(&get_capsule_count_def, nullptr));
+}
+
+} // namespace PySide::RemoteObjects