aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--sources/pyside6/CMakeLists.txt4
-rw-r--r--sources/pyside6/PySide6/QtRemoteObjects/CMakeLists.txt9
-rw-r--r--sources/pyside6/PySide6/QtRemoteObjects/typesystem_remoteobjects.xml17
-rw-r--r--sources/pyside6/PySide6/glue/qtremoteobjects.cpp31
-rw-r--r--sources/pyside6/cmake/Macros/PySideModules.cmake1
-rw-r--r--sources/pyside6/cmake/PySideSetup.cmake3
-rw-r--r--sources/pyside6/doc/developer/index.rst1
-rw-r--r--sources/pyside6/doc/developer/remoteobjects.md162
-rw-r--r--sources/pyside6/libpysideremoteobjects/CMakeLists.txt88
-rw-r--r--sources/pyside6/libpysideremoteobjects/pysidecapsulemethod.cpp230
-rw-r--r--sources/pyside6/libpysideremoteobjects/pysidecapsulemethod_p.h87
-rw-r--r--sources/pyside6/libpysideremoteobjects/pysidedynamicclass.cpp506
-rw-r--r--sources/pyside6/libpysideremoteobjects/pysidedynamicclass_p.h15
-rw-r--r--sources/pyside6/libpysideremoteobjects/pysidedynamiccommon.cpp124
-rw-r--r--sources/pyside6/libpysideremoteobjects/pysidedynamiccommon_p.h89
-rw-r--r--sources/pyside6/libpysideremoteobjects/pysidedynamicenum.cpp158
-rw-r--r--sources/pyside6/libpysideremoteobjects/pysidedynamicenum_p.h15
-rw-r--r--sources/pyside6/libpysideremoteobjects/pysidedynamicpod.cpp260
-rw-r--r--sources/pyside6/libpysideremoteobjects/pysidedynamicpod_p.h15
-rw-r--r--sources/pyside6/libpysideremoteobjects/pysideremoteobjects.h16
-rw-r--r--sources/pyside6/libpysideremoteobjects/pysiderephandler.cpp459
-rw-r--r--sources/pyside6/libpysideremoteobjects/pysiderephandler_p.h35
-rw-r--r--sources/pyside6/tests/QtRemoteObjects/CMakeLists.txt12
-rw-r--r--sources/pyside6/tests/QtRemoteObjects/cpp_interop/CMakeLists.txt25
-rw-r--r--sources/pyside6/tests/QtRemoteObjects/cpp_interop/cpp_interop.cpp127
-rw-r--r--sources/pyside6/tests/QtRemoteObjects/cpp_interop/cpp_interop_test.py189
-rw-r--r--sources/pyside6/tests/QtRemoteObjects/dynamic_types_test.py97
-rw-r--r--sources/pyside6/tests/QtRemoteObjects/integration_test.py369
-rw-r--r--sources/pyside6/tests/QtRemoteObjects/repfile_test.py65
-rw-r--r--sources/pyside6/tests/QtRemoteObjects/simple.rep7
-rw-r--r--sources/pyside6/tests/QtRemoteObjects/test_shared.py126
31 files changed, 3337 insertions, 5 deletions
diff --git a/sources/pyside6/CMakeLists.txt b/sources/pyside6/CMakeLists.txt
index f45c07114..9c238e980 100644
--- a/sources/pyside6/CMakeLists.txt
+++ b/sources/pyside6/CMakeLists.txt
@@ -25,6 +25,10 @@ if(Qt${QT_MAJOR_VERSION}Qml_FOUND)
add_subdirectory(libpysideqml)
endif()
+if(Qt${QT_MAJOR_VERSION}RemoteObjects_FOUND)
+ add_subdirectory(libpysideremoteobjects)
+endif()
+
if(Qt${QT_MAJOR_VERSION}UiTools_FOUND)
add_subdirectory(plugins/uitools)
find_package(Qt6 COMPONENTS Designer)
diff --git a/sources/pyside6/PySide6/QtRemoteObjects/CMakeLists.txt b/sources/pyside6/PySide6/QtRemoteObjects/CMakeLists.txt
index 07835b2f6..2522ab54f 100644
--- a/sources/pyside6/PySide6/QtRemoteObjects/CMakeLists.txt
+++ b/sources/pyside6/PySide6/QtRemoteObjects/CMakeLists.txt
@@ -29,20 +29,23 @@ ${QtRemoteObjects_GEN_DIR}/qtroserveriodevice_wrapper.cpp
${QtRemoteObjects_GEN_DIR}/qtremoteobjects_module_wrapper.cpp
)
+find_package(Qt6 REQUIRED COMPONENTS Core)
+
set(QtRemoteObjects_include_dirs ${QtRemoteObjects_SOURCE_DIR}
${QtRemoteObjects_BINARY_DIR}
${Qt${QT_MAJOR_VERSION}RemoteObjects_INCLUDE_DIRS}
+ ${libpysideremoteobjects_SOURCE_DIR}
${SHIBOKEN_INCLUDE_DIR}
${libpyside_SOURCE_DIR}
${SHIBOKEN_PYTHON_INCLUDE_DIR}
${QtCore_GEN_DIR}
${QtNetwork_GEN_DIR})
-set(QtRemoteObjects_libraries pyside6
- ${Qt${QT_MAJOR_VERSION}RemoteObjects_LIBRARIES})
-
set(QtRemoteObjects_deps QtCore QtNetwork)
+set(QtRemoteObjects_libraries pyside6 pyside6remoteobjects
+ ${Qt${QT_MAJOR_VERSION}RemoteObjects_LIBRARIES})
+
create_pyside_module(NAME QtRemoteObjects
INCLUDE_DIRS QtRemoteObjects_include_dirs
LIBRARIES QtRemoteObjects_libraries
diff --git a/sources/pyside6/PySide6/QtRemoteObjects/typesystem_remoteobjects.xml b/sources/pyside6/PySide6/QtRemoteObjects/typesystem_remoteobjects.xml
index 86e4d9093..a6e54ee18 100644
--- a/sources/pyside6/PySide6/QtRemoteObjects/typesystem_remoteobjects.xml
+++ b/sources/pyside6/PySide6/QtRemoteObjects/typesystem_remoteobjects.xml
@@ -8,6 +8,9 @@
<load-typesystem name="templates/core_common.xml" generate="no"/>
<load-typesystem name="QtCore/typesystem_core.xml" generate="no"/>
<load-typesystem name="QtNetwork/typesystem_network.xml" generate="no"/>
+ <inject-code class="native" position="beginning">
+ #include "pysideremoteobjects.h"
+ </inject-code>
<rejection class="QRemoteObjectStringLiterals"/>
<rejection class="*" function-name="getTypeNameAndMetaobjectFromClassInfo"/>
@@ -26,6 +29,10 @@
</object-type>
<object-type name="QRemoteObjectNode">
<enum-type name="ErrorCode"/>
+ <add-function signature="acquire(PyTypeObject*, PyObject* @name@ = 0)"
+ return-type="PyTypeObject*">
+ <inject-code class="target" file="../glue/qtremoteobjects.cpp" snippet="node-acquire"/>
+ </add-function>
</object-type>
<object-type name="QRemoteObjectPendingCall">
<enum-type name="Error"/>
@@ -35,7 +42,12 @@
<object-type name="QRemoteObjectRegistryHost"/>
<object-type name="QRemoteObjectReplica">
<enum-type name="State"/>
- <!-- protected: <enum-type name="ConstructorType"/> -->
+ <enum-type name="ConstructorType" python-type="IntEnum"/> <!-- Needed even though protected -->
+ <modify-function signature="QRemoteObjectReplica(QRemoteObjectReplica::ConstructorType)">
+ <modify-argument index="1">
+ <replace-default-expression with="{}"/>
+ </modify-argument>
+ </modify-function>
</object-type>
<object-type name="QRemoteObjectSettingsStore"/>
<value-type name="QRemoteObjectSourceLocationInfo"/>
@@ -53,4 +65,7 @@
<!-- QtNetwork is pulled in via QtRemoteObjectsDepends. -->
<suppress-warning text="^Scoped enum 'Q(Ocsp)|(Dtls).*' does not have a type entry.*$"/>
+ <inject-code class="target" position="end"
+ file="../glue/qtremoteobjects.cpp" snippet="qtro-init"/>
+
</typesystem>
diff --git a/sources/pyside6/PySide6/glue/qtremoteobjects.cpp b/sources/pyside6/PySide6/glue/qtremoteobjects.cpp
new file mode 100644
index 000000000..88d585892
--- /dev/null
+++ b/sources/pyside6/PySide6/glue/qtremoteobjects.cpp
@@ -0,0 +1,31 @@
+// Copyright (C) 2024 Ford Motor Company
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+// @snippet qtro-init
+PySide::RemoteObjects::init(module);
+// @snippet qtro-init
+
+// @snippet node-acquire
+auto *typeObject = reinterpret_cast<PyTypeObject*>(%PYARG_1);
+if (!PySide::inherits(typeObject, SbkPySide6_QtRemoteObjectsTypeStructs[SBK_QRemoteObjectReplica_IDX].fullName)) {
+ PyErr_SetString(PyExc_TypeError, "First argument must be a type deriving from QRemoteObjectReplica.");
+ return nullptr;
+}
+
+static PyObject *pyConstructWithNode = Shiboken::Enum::newItem(
+ Shiboken::Module::get(SbkPySide6_QtRemoteObjectsTypeStructs[SBK_QRemoteObjectReplica_ConstructorType_IDX]),
+ 1 /* protected QRemoteObjectReplica::ConstructorType::ConstructWithNode */
+);
+
+Shiboken::AutoDecRef args;
+if (pyArgs[1])
+ args.reset(PyTuple_Pack(3, %PYSELF, pyConstructWithNode, pyArgs[1]));
+else
+ args.reset(PyTuple_Pack(2, %PYSELF, pyConstructWithNode));
+
+PyObject *instance = PyObject_CallObject(%PYARG_1, args.object());
+if (!instance)
+ return nullptr; // Propagate the exception
+
+%PYARG_0 = instance;
+// @snippet node-acquire
diff --git a/sources/pyside6/cmake/Macros/PySideModules.cmake b/sources/pyside6/cmake/Macros/PySideModules.cmake
index a83f2b745..5cd12b683 100644
--- a/sources/pyside6/cmake/Macros/PySideModules.cmake
+++ b/sources/pyside6/cmake/Macros/PySideModules.cmake
@@ -297,6 +297,7 @@ macro(create_pyside_module)
set(ld_prefix_list "")
list(APPEND ld_prefix_list "${pysidebindings_BINARY_DIR}/libpyside")
list(APPEND ld_prefix_list "${pysidebindings_BINARY_DIR}/libpysideqml")
+ list(APPEND ld_prefix_list "${pysidebindings_BINARY_DIR}/libpysideremoteobjects")
list(APPEND ld_prefix_list "${SHIBOKEN_SHARED_LIBRARY_DIR}")
if(WIN32)
list(APPEND ld_prefix_list "${QT6_INSTALL_PREFIX}/${QT6_INSTALL_BINS}")
diff --git a/sources/pyside6/cmake/PySideSetup.cmake b/sources/pyside6/cmake/PySideSetup.cmake
index c92b0c986..45a63a1a0 100644
--- a/sources/pyside6/cmake/PySideSetup.cmake
+++ b/sources/pyside6/cmake/PySideSetup.cmake
@@ -199,6 +199,9 @@ endforeach()
# Whether to add libpysideqml
find_package(Qt6 COMPONENTS Qml)
+# Whether to add libpysideremoteobjects
+find_package(Qt6 COMPONENTS RemoteObjects)
+
string(REGEX MATCHALL "[0-9]+" qt_version_helper "${Qt${QT_MAJOR_VERSION}Core_VERSION}")
list(GET qt_version_helper 0 QT_VERSION_MAJOR)
diff --git a/sources/pyside6/doc/developer/index.rst b/sources/pyside6/doc/developer/index.rst
index bddd39d91..0260ad272 100644
--- a/sources/pyside6/doc/developer/index.rst
+++ b/sources/pyside6/doc/developer/index.rst
@@ -36,3 +36,4 @@ many features and implementation details that the project has:
signature_doc.rst
mypy-correctness.rst
feature-motivation.rst
+ remoteobjects.md
diff --git a/sources/pyside6/doc/developer/remoteobjects.md b/sources/pyside6/doc/developer/remoteobjects.md
new file mode 100644
index 000000000..7d4c29aa0
--- /dev/null
+++ b/sources/pyside6/doc/developer/remoteobjects.md
@@ -0,0 +1,162 @@
+# Qt Remote Objects Overview
+
+[Qt Remote Objects](https://doc.qt.io/qt-6/qtremoteobjects-index.html) (or QtRO)
+is described as an IPC module. That puts the focus on the internal details.
+It should be looked at more as a Connected Framework.
+
+QtRO lets you easily take an existing Qt application and interact with it from
+other devices. QtRO allows you to create a
+[_Replica_](https://doc.qt.io/qt-6/qtremoteobjects-replica.html) QObject, making
+the Replica a surrogate for the real QOject in your program (called the
+[_Source_](https://doc.qt.io/qt-6/qtremoteobjects-source.html)). You interact with
+the Replica the same way you would the Source (with one important difference) and QtRO
+ensures those interactions are forwarded to the source for handling. Changes to the
+Source are cascaded to any Replicas.
+
+The mechanism Qt Remote Objects provides for enabling these objects to connect to each
+other are a network of
+[_Nodes_](https://doc.qt.io/qt-6/qtremoteobjects-node.html). Nodes handle the details of
+connecting processes or devices. A Replica is created by calling
+[acquire()](https://doc.qt.io/qt-6/qremoteobjectnode.html#acquire) on a Node, and Sources
+are shared on the network using
+[enableRemoting()](https://doc.qt.io/qt-6/qremoteobjecthostbase.html#enableRemoting).
+
+## Replicas are _latent copies_
+
+Qt Remote Object interactions are inherently asynchronous. This _can_ lead to
+confusing results initially
+
+```python
+# Assume a replica initially has an int property `i` with a value of 2
+print(f"Value of i on replica = {replica.i}") # prints 2
+replica.iChanged.connect(lambda i: print(f"Value of i on replica changed to {i}"))
+replica.i = 3
+print(f"Value of i on replica = {replica.i}") # prints 2, not 3
+
+# When the eventloop runs, the change will be forwarded to the source instance,
+# the change will be made, and the new i value will be sent back to the replica.
+# The iChanged signal will be fired
+# after some delay.
+```
+
+Note: To avoid this confusion, Qt Remote Objects can change setters to "push"
+slots on the Replica class, making the asynchronous nature of the behavior
+clear.
+
+```python
+replica.pushI(3) # Request a change to `i` on the source object.
+```
+
+## How does this affect PySide?
+
+PySide wraps the Qt C++ classes used by QtRO, so much of the needed
+functionality for QtRO is available in PySide. However, the interaction between
+a Source and Replica are in effect a contract that is defined on a _per object_
+basis. I.e., different objects have different APIs, and every participant must
+know about the contracts for the objects they intend to use.
+
+In C++, Qt Remote Objects leverages the
+[Replica Compiler (repc)](https://doc.qt.io/qt-6/qtremoteobjects-repc.html) to
+generate QObject header and C++ code that enforce the contracts for each type.
+REPC uses a simplified text syntax to describe the desired API in .rep files.
+REPC is integrated with qmake and cmake, simplifying the process of leveraging
+QtRO in a C++ project. The challenges in PySide are
+1) To parse the .rep file to extract the desired syntax
+2) Allow generation of types that expose the desired API and match the needed
+ contract
+3) Provide appropriate errors and handling in cases that can't be dynamically
+ handled in Python.
+For example, C++ can register templated types such as a QMap<double, MyType>
+and serialize such types once registered. While Python can create a similar
+type, there isn't a path to dynamically serialize such a type so C++ could
+interpret it correctly on the other side of a QtRO network.
+
+Under the covers, QtRO leverages Qt's QVariant infrastructure heavily. For
+instance, a Replica internally holds a QVariantList where each element
+represents one of the exposed QProperty values. The property's QVariant is
+typed appropriately for the property, allows an autogenerated getter to (for
+instance with a float property) return `return variant.value<float >();`. This
+works well with PySide converters.
+
+## RepFile PySide type
+
+The first challenge is handled by adding a Python type RepFile can takes a .rep
+file and parses it into an Abstract Syntax Tree (AST) describing the type.
+
+A simple .rep might look like:
+```cpp
+class Thermistat
+{
+ PROP(int temp)
+}
+```
+
+The desired interface would be
+```python
+from pathlib import Path
+from PySide6.QtRemoteObjects import RepFile
+
+input_file = Path(__file__).parent / "thermistat.rep"
+rep_file = RepFile(input_file)
+```
+
+The RepFile holds dictionaries `source`, `replica` and `pod`. These use the
+names of the types as the key, and the value is the PyTypeObject* of the
+generated type meeting the desired contract:
+
+```python
+Source = rep_file.source["Thermistat"] # A Type object for Source implementation of the type
+Replica = rep_file.replica["Thermistat"] # A Type object for Replica implementation of the type
+```
+
+## Replica type
+
+A Replica for a given interface will be a distinct type. It should be usable
+directly from Python once instantiated and initialized.
+
+```python
+Replica = rep_file.replica["Thermistat"] # A Type object matching the Replica contract
+replica = node.acquire(Replica) # We need to tell the node what type to instantiate
+# These two lines can be combined
+replica_instance = node.acquire(rep_file.replica["Thermistat"])
+
+# If there is a Thermistat source on the network, our replica will get connected to it.
+if replica.isInitialized():
+ print(f"The current tempeerature is {replica.temp}")
+else:
+ replica.initialized.connect(lambda: print(f"replica is now initialized. Temp = {replica.temp}"))
+```
+
+## Source type
+
+Unlike a Replica, whose interface is a passthrough of another object, the
+Source needs to actually define the desired behavior. In C++, QtRO supports two
+modes for Source objects. A MyTypeSource C++ class is autogenerated that
+defines pure virtual getters and setters. This enables full customization of
+the implementation. A MyTypeSimpleSource C++ class is also autogenerated that
+creates basic data members for properties and getters/setters that work on
+those data members.
+
+The intent is to follow the SimpleSource pattern in Python if possible.
+
+```python
+ Thermistat = rep_file.source["Thermistat"]
+ class MyThermistat(Thermistat):
+ def __init__(self, parent = None):
+ super().__init__(parent)
+ # Get the current temp from the system
+ self.temp = get_temp_from_system()
+```
+
+## Realizing Source/Replica types in python
+
+Assume there is a RepFile for thermistat.rep that defines a Thermistat class
+interface.
+
+`ThermistatReplica = repFile.replica["Thermistat"]` should be a Shiboken.ObjectType
+type, with a base of QRemoteObjectReplica's shiboken type.
+
+`ThermistatSource = repFile.source["Thermistat"]` should be a abstract class of
+Shiboken.ObjectType type, with a base of QObject's shiboken type.
+
+Both should support new classes based on their type to customize behavior.
diff --git a/sources/pyside6/libpysideremoteobjects/CMakeLists.txt b/sources/pyside6/libpysideremoteobjects/CMakeLists.txt
new file mode 100644
index 000000000..f73eba6ee
--- /dev/null
+++ b/sources/pyside6/libpysideremoteobjects/CMakeLists.txt
@@ -0,0 +1,88 @@
+# Copyright (C) 2025 Ford Motor Company
+# SPDX-License-Identifier: BSD-3-Clause
+
+if (NOT CMAKE_MINIMUM_REQUIRED_VERSION)
+ cmake_minimum_required(VERSION 3.18)
+ cmake_policy(VERSION 3.18)
+endif()
+
+project(libpysideremoteobjects LANGUAGES CXX)
+
+if (NOT libpyside_SOURCE_DIR) # Building standalone
+ message(STATUS "Building standalone. Setting C++ standard and build type.")
+ set(CMAKE_CXX_STANDARD 17)
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
+ if(NOT CMAKE_BUILD_TYPE)
+ set(CMAKE_BUILD_TYPE Release)
+ endif()
+ find_package(Python3 REQUIRED COMPONENTS Interpreter Development)
+ find_package(Shiboken6 REQUIRED)
+ find_package(libpyside REQUIRED)
+ get_target_property(pyside6_SOURCE_DIR PySide6::pyside6 INTERFACE_INCLUDE_DIRECTORIES)
+endif()
+
+find_package(Qt6 REQUIRED COMPONENTS Core RepParser RemoteObjects)
+
+set(libpysideremoteobjects_HEADERS
+ pysidecapsulemethod_p.h
+ pysidedynamicclass_p.h
+ pysidedynamiccommon_p.h
+ pysidedynamicenum_p.h
+ pysidedynamicpod_p.h
+ pysiderephandler_p.h
+)
+
+set(libpysideremoteobjects_SRC
+ pysiderephandler.cpp
+ pysidecapsulemethod.cpp
+ pysidedynamiccommon.cpp
+ pysidedynamicclass.cpp
+ pysidedynamicpod.cpp
+ pysidedynamicenum.cpp
+ ${libpysideremoteobjects_HEADERS}
+)
+
+list(GET Qt6RepParser_INCLUDE_DIRS 0 REPPARSER_DIR)
+
+include(QtTargetHelpers)
+include(QtTestHelpers)
+include(QtLalrHelpers)
+add_library(pyside6remoteobjects STATIC ${libpysideremoteobjects_SRC})
+
+target_include_directories(pyside6remoteobjects PRIVATE
+ ${REPPARSER_DIR}
+ ${Qt${QT_VERSION_MAJOR}Core_PRIVATE_INCLUDE_DIRS}
+ ${Qt${QT_MAJOR_VERSION}RemoteObjects_INCLUDE_DIRS}
+ ${Qt${QT_MAJOR_VERSION}RemoteObjects_PRIVATE_INCLUDE_DIRS}
+ ${pyside6_SOURCE_DIR} # Added internally by the create_pyside_module function
+ ${SHIBOKEN_INCLUDE_DIR}
+ ${libpyside_SOURCE_DIR}
+ ${SHIBOKEN_PYTHON_INCLUDE_DIR}
+ ${CMAKE_CURRENT_BINARY_DIR} # Include the component-specific build directory
+)
+
+target_link_libraries(pyside6remoteobjects PRIVATE
+ Shiboken6::libshiboken # Added internally by the create_pyside_module function
+ Qt6::Core
+ Qt6::RemoteObjectsPrivate
+)
+
+qt_process_qlalr(
+ pyside6remoteobjects
+ "${REPPARSER_DIR}/parser.g"
+ ""
+)
+
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D QT_NO_CAST_FROM_ASCII -D QT_NO_CAST_TO_ASCII")
+
+#
+# install stuff
+#
+
+install(FILES ${libpysideremoteobjects_HEADERS}
+ DESTINATION include/${BINDING_NAME}${pyside6remoteobjects_SUFFIX})
+
+install(TARGETS pyside6remoteobjects EXPORT PySide6RemoteObjectsTargets
+ LIBRARY DESTINATION "${LIB_INSTALL_DIR}"
+ ARCHIVE DESTINATION "${LIB_INSTALL_DIR}"
+ RUNTIME DESTINATION bin)
diff --git a/sources/pyside6/libpysideremoteobjects/pysidecapsulemethod.cpp b/sources/pyside6/libpysideremoteobjects/pysidecapsulemethod.cpp
new file mode 100644
index 000000000..d5a5454f0
--- /dev/null
+++ b/sources/pyside6/libpysideremoteobjects/pysidecapsulemethod.cpp
@@ -0,0 +1,230 @@
+// 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 "pysidecapsulemethod_p.h"
+
+extern "C"
+{
+
+// This struct is used for both CapsuleMethod and CapsuleProperty
+struct CapsuleDescriptor
+{
+ PyTypeObject base;
+ PyObject *capsule;
+ PyMethodDef methodDef;
+
+ void configure(PyObject *capsule, PyMethodDef *method)
+ {
+ this->capsule = capsule;
+ Py_INCREF(capsule);
+ // We make a copy of the input name and doc strings so they can be temporary on
+ // the input.
+ if (method->ml_name)
+ methodDef.ml_name = strdup(method->ml_name);
+ if (method->ml_doc)
+ methodDef.ml_doc = strdup(method->ml_doc);
+ methodDef.ml_meth = method->ml_meth;
+ methodDef.ml_flags = method->ml_flags;
+ }
+};
+
+static PyObject *CapsuleDescriptor_tp_new(PyTypeObject *type, PyObject * /* args */, PyObject * /* kwds */);
+static void CapsuleDescriptor_free(PyObject *self);
+static PyObject *CapsuleMethod_descr_get(PyObject *self, PyObject *instance, PyObject * /* owner */);
+static PyObject *CapsuleProperty_descr_get(PyObject *self, PyObject *instance, PyObject * /* owner */);
+static int CapsuleProperty_descr_set(PyObject *self, PyObject *instance, PyObject * /* owner */);
+
+/**
+ * We are creating two related types, CapsuleMethod and CapsuleProperty, that are
+ * used to enable lambda-like behavior. The difference is in usage, where
+ * CapsuleMethod's __get__ function returns a Callable (i.e., method-like usage:
+ * obj.capsuleMethodName(args)) and only supports the __get__ method.
+ * CapsuleProperty on the other hand is used for properties, and supports both
+ * __get__ and __set__ methods (i.e., obj.capsulePropertyName = value or val =
+ * obj.capsulePropertyName).
+ */
+static PyTypeObject *createCapsuleMethodType()
+{
+ PyType_Slot CapsuleMethodType_slots[] = {
+ {Py_tp_new, reinterpret_cast<void *>(CapsuleDescriptor_tp_new)},
+ {Py_tp_descr_get, reinterpret_cast<void *>(CapsuleMethod_descr_get)},
+ {Py_tp_free, reinterpret_cast<void *>(CapsuleDescriptor_free)},
+ {0, nullptr}
+ };
+
+ PyType_Spec CapsuleMethodType_spec = {
+ "2:PySide6.QtRemoteObjects.CapsuleMethod",
+ sizeof(CapsuleDescriptor),
+ 0,
+ Py_TPFLAGS_DEFAULT,
+ CapsuleMethodType_slots};
+
+ PyObject *type = PyType_FromSpec(&CapsuleMethodType_spec);
+ if (!type) {
+ PyErr_Print();
+ return nullptr;
+ }
+ return reinterpret_cast<PyTypeObject*>(type);
+}
+
+PyTypeObject *CapsuleMethod_TypeF(void)
+{
+ static auto *type = createCapsuleMethodType();
+ return type;
+}
+
+static PyTypeObject *createCapsulePropertyType(bool isWritable)
+{
+ PyType_Slot WritablePropertyType_slots[] = {
+ {Py_tp_new, reinterpret_cast<void *>(CapsuleDescriptor_tp_new)},
+ {Py_tp_descr_get, reinterpret_cast<void *>(CapsuleProperty_descr_get)},
+ {Py_tp_descr_set, reinterpret_cast<void *>(CapsuleProperty_descr_set)},
+ {Py_tp_free, reinterpret_cast<void *>(CapsuleDescriptor_free)},
+ {0, nullptr}
+ };
+
+ PyType_Slot ReadOnlyPropertyType_slots[] = {
+ {Py_tp_new, reinterpret_cast<void *>(CapsuleDescriptor_tp_new)},
+ {Py_tp_descr_get, reinterpret_cast<void *>(CapsuleProperty_descr_get)},
+ {Py_tp_free, reinterpret_cast<void *>(CapsuleDescriptor_free)},
+ {0, nullptr}
+ };
+
+ PyType_Spec CapsulePropertyType_spec = {
+ "2:PySide6.QtRemoteObjects.CapsuleProperty",
+ sizeof(CapsuleDescriptor),
+ 0,
+ Py_TPFLAGS_DEFAULT,
+ isWritable ? WritablePropertyType_slots : ReadOnlyPropertyType_slots};
+
+ PyObject *type = PyType_FromSpec(&CapsulePropertyType_spec);
+ if (!type) {
+ PyErr_Print();
+ return nullptr;
+ }
+ return reinterpret_cast<PyTypeObject*>(type);
+}
+
+PyTypeObject *CapsuleProperty_TypeF(bool isWritable=false)
+{
+ if (isWritable) {
+ static auto *type = createCapsulePropertyType(true);
+ return type;
+ }
+ static auto *type = createCapsulePropertyType(false);
+ return type;
+}
+
+static PyObject *CapsuleDescriptor_tp_new(PyTypeObject *type, PyObject * /* args */, PyObject * /* kwds */)
+{
+ auto *self = reinterpret_cast<CapsuleDescriptor *>(PyType_GenericAlloc(type, 0));
+ if (self != nullptr) {
+ self->capsule = nullptr;
+ self->methodDef = {nullptr, nullptr, METH_NOARGS, nullptr}; // Initialize methodDef
+ }
+ return reinterpret_cast<PyObject *>(self);
+}
+
+static void CapsuleDescriptor_free(PyObject *self)
+{
+ auto *d = reinterpret_cast<CapsuleDescriptor *>(self);
+ Py_XDECREF(d->capsule);
+ free(const_cast<char*>(d->methodDef.ml_name));
+ free(const_cast<char*>(d->methodDef.ml_doc));
+}
+
+static PyObject *CapsuleMethod_descr_get(PyObject *self, PyObject *instance, PyObject * /* owner */)
+{
+ if (instance == nullptr) {
+ // Return the descriptor object if accessed from the class
+ Py_INCREF(self);
+ return self;
+ }
+
+ auto *d = reinterpret_cast<CapsuleDescriptor *>(self);
+ CapsuleDescriptorData *data = new CapsuleDescriptorData{instance, d->capsule};
+ PyObject *payload = PyCapsule_New(data, "Payload", [](PyObject *capsule) {
+ delete reinterpret_cast<CapsuleDescriptorData *>(PyCapsule_GetPointer(capsule, "Payload"));
+ });
+ if (!payload)
+ return nullptr;
+
+ Py_INCREF(payload);
+ return PyCFunction_New(&d->methodDef, payload);
+}
+
+bool add_capsule_method_to_type(PyTypeObject *type, PyMethodDef *method, PyObject *capsule)
+{
+ if (PyType_Ready(type) < 0) {
+ PyErr_Print();
+ return false;
+ }
+ auto *descriptor = reinterpret_cast<CapsuleDescriptor *>(
+ PyObject_CallObject(reinterpret_cast<PyObject *>(CapsuleMethod_TypeF()), nullptr));
+ if (!descriptor) {
+ PyErr_Print();
+ return false;
+ }
+ descriptor->configure(capsule, method);
+
+ auto *descr = reinterpret_cast<PyObject *>(descriptor);
+ if (PyObject_SetAttrString(reinterpret_cast<PyObject *>(type), method->ml_name, descr) < 0) {
+ PyErr_Print();
+ return false;
+ }
+ return true;
+}
+
+static PyObject *CapsuleProperty_descr_get(PyObject *self, PyObject *instance, PyObject * /* owner */)
+{
+ if (instance == nullptr) {
+ // Return the descriptor object if accessed from the class
+ Py_INCREF(self);
+ return self;
+ }
+
+ auto *d = reinterpret_cast<CapsuleDescriptor *>(self);
+ CapsuleDescriptorData *data = new CapsuleDescriptorData{instance, d->capsule};
+ PyObject *payload = PyCapsule_New(data, "Payload", [](PyObject *capsule) {
+ delete reinterpret_cast<CapsuleDescriptorData *>(PyCapsule_GetPointer(capsule, "Payload"));
+ });
+ if (!payload)
+ return nullptr;
+
+ return PyObject_CallFunctionObjArgs(PyCFunction_New(&d->methodDef, payload), nullptr);
+}
+
+static int CapsuleProperty_descr_set(PyObject *self, PyObject *instance, PyObject *value)
+{
+ auto *d = reinterpret_cast<CapsuleDescriptor *>(self);
+ CapsuleDescriptorData *data = new CapsuleDescriptorData{instance, d->capsule};
+ PyObject *payload = PyCapsule_New(data, "Payload", [](PyObject *capsule) {
+ delete reinterpret_cast<CapsuleDescriptorData *>(PyCapsule_GetPointer(capsule, "Payload"));
+ });
+ if (!payload)
+ return -1;
+
+ Py_INCREF(payload);
+ PyObject *result = PyObject_CallFunctionObjArgs(PyCFunction_New(&d->methodDef, payload),
+ value, nullptr);
+ if (!result)
+ return -1;
+
+ Py_DECREF(result);
+ return 0;
+}
+
+// Returns a new CapsuleProperty descriptor object for use with PySideProperty
+PyObject *make_capsule_property(PyMethodDef *method, PyObject *capsule, bool isWritable)
+{
+ auto *type = CapsuleProperty_TypeF(isWritable);
+ auto *descriptor = PyObject_CallObject(reinterpret_cast<PyObject *>(type), nullptr);
+ if (!descriptor)
+ return nullptr;
+
+ reinterpret_cast<CapsuleDescriptor*>(descriptor)->configure(capsule, method);
+
+ return descriptor;
+}
+
+} // extern "C"
diff --git a/sources/pyside6/libpysideremoteobjects/pysidecapsulemethod_p.h b/sources/pyside6/libpysideremoteobjects/pysidecapsulemethod_p.h
new file mode 100644
index 000000000..7b6abc54b
--- /dev/null
+++ b/sources/pyside6/libpysideremoteobjects/pysidecapsulemethod_p.h
@@ -0,0 +1,87 @@
+// 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
+
+#ifndef PYSIDE_CAPSULEMETHOD_P_H
+#define PYSIDE_CAPSULEMETHOD_P_H
+
+#include <sbkpython.h>
+
+extern "C"
+{
+
+/**
+ * This code is needed to solve, in C++ and adhering to the stable API,
+ * creating what are in effect lambda functions as instance methods on custom
+ * types. The goal is to be able to add methods to a dynamic type. If the .rep
+ * file defines a slot `mySlot`, it need to be added to the dynamic type. For
+ * Source types, this should be an abstract method that raises a
+ * NotImplementedError unless defined in the Python subclass. For Replica
+ * types, this should include an implementation that forwards the request
+ * through the underlying QRemoteObjectReplica instance.
+ *
+ * The stable API doesn't currently provide a way define a method that can
+ * receive both the `self`, `args`, and runtime (but constant per method, i.e.,
+ * lambda like) data using Py_tp_methods. Possibly post 3.13 when METH_METHOD is
+ * part of the stable API. But for now, it is not.
+ *
+ * The solution is to create a custom descriptor
+ * (https://docs.python.org/3/howto/descriptor.html) that can hold the runtime
+ * data and then when called, will return a PyCFunction_New generated PyObject
+ * that is passed both class instance `self` and the runtime data (a PyCapsule)
+ * together as a tuple as a new `self` for the method. The static method
+ * definition needs to expect and handle this, but when combined in C++, we can
+ * define a single handler that receives both the original `self` of the instance
+ * and the runtime capsule with data for handling.
+ */
+
+/**
+ * The CapsuleDescriptorData struct is what will be passed as the pseudo `self`
+ * from a CapsuleMethod or CapsuleProperty to the associated handler method. The
+ * handler method (which should look like a standard PyMethodDef method) should
+ * parse it into the payload (the "lambda variables") and the actual instance
+ * (the "self").
+ */
+struct CapsuleDescriptorData
+{
+ PyObject *self;
+ PyObject *payload;
+};
+
+/**
+ * The new type defining a descriptor that stores a PyCapsule. This is used to
+ * store the runtime data, with the __get__ method returning a new Callable.
+ */
+PyTypeObject *CapsuleMethod_TypeF(void);
+
+/**
+ * The new type defining a descriptor that stores a PyCapsule. This is used to
+ * store the runtime data, with the __get__ (and __set__ if isWritable) providing
+ * property behavior.
+ */
+PyTypeObject *CapsuleProperty_TypeF(bool isWritable);
+
+/**
+ * Add a capsule method (a descriptor) to a type. This will create a new capsule
+ * method descriptor and add it as an attribute to the type, using the given name.
+ *
+ * A single handle can then respond to what appear to be distinct methods on the
+ * type, but using the runtime data (from the capsule) when handling each call.
+ *
+ * @param type The type to attach the created descriptor to.
+ * @param method The method definition to associate with the descriptor.
+ * The name of the method will be used as the attribute name.
+ * @param capsule The capsule to store in the descriptor.
+ * @return True if the descriptor was added successfully, false otherwise.
+ */
+bool add_capsule_method_to_type(PyTypeObject *type, PyMethodDef *method,
+ PyObject *capsule);
+
+/**
+ * Make a new CapsuleProperty type.
+ */
+PyObject *make_capsule_property(PyMethodDef *method, PyObject *capsule,
+ bool isWritable = false);
+
+} // extern "C"
+
+#endif // PYSIDE_CAPSULEMETHOD_P_H
diff --git a/sources/pyside6/libpysideremoteobjects/pysidedynamicclass.cpp b/sources/pyside6/libpysideremoteobjects/pysidedynamicclass.cpp
new file mode 100644
index 000000000..941e38c6e
--- /dev/null
+++ b/sources/pyside6/libpysideremoteobjects/pysidedynamicclass.cpp
@@ -0,0 +1,506 @@
+// 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
+
+// Workaround to access protected functions. PySide builds with this, but
+// since this is now a separate library, we need to add it too.
+
+#include "pysidedynamiccommon_p.h"
+#include "pysidedynamicclass_p.h"
+#include "pysidecapsulemethod_p.h"
+#include "pysiderephandler_p.h"
+
+#include <basewrapper.h>
+#include <sbkconverter.h>
+#include <sbkstring.h>
+
+#include <pyside_p.h>
+#include <pysideproperty.h>
+#include <pysideqobject.h>
+#include <pysidesignal.h>
+#include <pysideutils.h>
+
+#include <QtCore/qmetaobject.h>
+#include <QtCore/qvariantlist.h>
+
+#include <QtRemoteObjects/qremoteobjectpendingcall.h>
+#include <QtRemoteObjects/qremoteobjectreplica.h>
+
+using namespace Shiboken;
+
+class FriendlyReplica : public QRemoteObjectReplica
+{
+public:
+ using QRemoteObjectReplica::send;
+ using QRemoteObjectReplica::setProperties;
+ using QRemoteObjectReplica::propAsVariant;
+ using QRemoteObjectReplica::sendWithReply;
+};
+
+extern "C"
+{
+
+PyObject *propertiesAttr()
+{
+ static PyObject *const s = Shiboken::String::createStaticString("__PROPERTIES__");
+ return s;
+}
+
+struct SourceDefs
+{
+ static PyTypeObject *getSbkType()
+ {
+ static PyTypeObject *sbkType =
+ Shiboken::Conversions::getPythonTypeObject("QObject");
+ return sbkType;
+ }
+
+ static PyObject *getBases()
+ {
+ static PyObject *bases = PyTuple_Pack(1, getSbkType());
+ return bases;
+ }
+
+ static const char *getTypePrefix()
+ {
+ return "2:PySide6.QtRemoteObjects.DynamicSource.";
+ }
+
+ static int tp_init(PyObject *self, PyObject *args, PyObject *kwds)
+ {
+ static initproc initFunc = reinterpret_cast<initproc>(PepType_GetSlot(getSbkType(), Py_tp_init));
+ int res = initFunc(self, args, kwds);
+ if (res < 0) {
+ PyErr_Print();
+ return res;
+ }
+
+ // Get the properties from the type
+ PyTypeObject *type = Py_TYPE(self);
+ auto *pyProperties = PyObject_GetAttr(reinterpret_cast<PyObject *>(type), propertiesAttr());
+ if (!pyProperties) {
+ PyErr_SetString(PyExc_RuntimeError, "Failed to get properties from type");
+ return -1;
+ }
+ // Add a copy of the properties to the object
+ auto *propPtr = reinterpret_cast<QVariantList *>(PyCapsule_GetPointer(pyProperties, nullptr));
+ auto *propertiesCopy = new QVariantList(*propPtr);
+ PyObject *capsule = PyCapsule_New(propertiesCopy, nullptr, [](PyObject *capsule) {
+ delete reinterpret_cast<QVariantList *>(PyCapsule_GetPointer(capsule, nullptr));
+ });
+ PyObject_SetAttr(self, propertiesAttr(), capsule);
+ Py_DECREF(capsule);
+ return res;
+ }
+
+ static PyObject *capsule_method_handler(PyObject *payload, PyObject *args)
+ {
+ auto *methodData = reinterpret_cast<CapsuleDescriptorData *>(PyCapsule_GetPointer(payload,
+ "Payload"));
+ if (!methodData) {
+ PyErr_SetString(PyExc_RuntimeError, "Invalid call to dynamic method. Missing payload.");
+ return nullptr;
+ }
+ PyObject *self = methodData->self;
+ if (PyCapsule_IsValid(methodData->payload, "PropertyCapsule")) {
+ // Handle property getter/setter against our hidden properties attribute
+ auto *capsule = PyCapsule_GetPointer(methodData->payload, "PropertyCapsule");
+ if (capsule) {
+ auto *ob_dict = SbkObject_GetDict_NoRef(self);
+ auto *propPtr = PyCapsule_GetPointer(PyDict_GetItem(ob_dict, propertiesAttr()),
+ nullptr);
+ auto *currentProperties = reinterpret_cast<QVariantList *>(propPtr);
+ auto *callData = reinterpret_cast<PropertyCapsule *>(capsule);
+ if (callData->indexInObject < 0
+ || callData->indexInObject >= currentProperties->size()) {
+ PyErr_Format(PyExc_RuntimeError, "Unknown property method: %s",
+ callData->name.constData());
+ return nullptr;
+ }
+ const QVariant &currentVariant = currentProperties->at(callData->indexInObject);
+
+ // Handle getter
+ if (PyTuple_Size(args) == 0)
+ return toPython(currentVariant);
+
+ // Handle setter
+ if (PyTuple_Size(args) != 1) {
+ PyErr_SetString(PyExc_TypeError, "Property setter takes exactly one argument");
+ return nullptr;
+ }
+ Conversions::SpecificConverter converter(currentVariant.metaType().name());
+ QVariant variant{currentVariant.metaType()};
+ auto metaType = currentVariant.metaType();
+ if (metaType.flags().testFlag(QMetaType::IsEnumeration)) {
+ converter.toCpp(PyTuple_GetItem(args, 0), variant.data());
+ variant.convert(metaType);
+ } else {
+ converter.toCpp(PyTuple_GetItem(args, 0), variant.data());
+ }
+ if (PyErr_Occurred()) // POD conversion can produce an error
+ return nullptr;
+ if (variant == currentVariant)
+ Py_RETURN_NONE;
+
+ currentProperties->replace(callData->indexInObject, variant);
+ // Get the QMetaObject and emit the property changed signal if there is one
+ const auto *metaObject = PySide::retrieveMetaObject(self);
+ auto metaProperty = metaObject->property(callData->propertyIndex);
+ if (metaProperty.hasNotifySignal()) {
+ // We know our custom types don't have multiple cpp objects
+ void *cptr = reinterpret_cast<SbkObject *>(self)->d->cptr[0];
+ auto *qObject = reinterpret_cast<QObject *>(cptr);
+ void *_args[] = {nullptr, variant.data()};
+ QMetaObject::activate(qObject, metaProperty.notifySignalIndex(), _args);
+ }
+ Py_RETURN_NONE;
+ }
+ }
+ if (PyCapsule_IsValid(methodData->payload, "MethodCapsule")) {
+ auto *capsule = PyCapsule_GetPointer(methodData->payload, "MethodCapsule");
+ auto *callData = reinterpret_cast<MethodCapsule *>(capsule);
+ if (callData->name.startsWith("push") && callData->name.size() > 4) {
+ const auto *metaObject = PySide::retrieveMetaObject(self);
+ // The convention for QtRO is if a property is named "something" and uses
+ // push, the name of the push method will be "pushSomething". But it is
+ // possible the name would be "Something", so we need to check upper
+ // and lower case.
+ auto name = callData->name.sliced(4);
+ auto index = metaObject->indexOfProperty(name.constData());
+ if (index < 0) {
+ name[0] = tolower(name[0]); // Try lower case
+ index = metaObject->indexOfProperty(name.constData());
+ }
+ // It is possible a .rep names a Slot "push" or "pushSomething" that
+ // isn't generated for a property. Let that fall through to regular
+ // method handling.
+ if (index >= 0) {
+ // Call the custom descriptor's set method
+ auto result = PyObject_SetAttrString(self, name.constData(),
+ PyTuple_GetItem(args, 0));
+ if (result < 0) {
+ PyErr_Print();
+ return nullptr;
+ }
+ Py_RETURN_NONE;
+ }
+ }
+ // TODO: This doesn't do much, as it is "eaten" by a PyError_Print in
+ // SignalManager::handleMetaCallError()
+ // Is there a better way to address slots that need to be implemented?
+ PyErr_Format(PyExc_NotImplementedError, "** The method %s is not implemented",
+ callData->name.constData());
+ return nullptr;
+ }
+
+ PyErr_SetString(PyExc_RuntimeError, "Unknown capsule type");
+ return nullptr;
+ }
+};
+
+struct ReplicaDefs
+{
+ static PyTypeObject *getSbkType()
+ {
+ static PyTypeObject *sbkType =
+ Shiboken::Conversions::getPythonTypeObject("QRemoteObjectReplica");
+ return sbkType;
+ }
+
+ static PyObject *getBases()
+ {
+ static PyObject *bases = PyTuple_Pack(1, getSbkType());
+ return bases;
+ }
+
+ static const char *getTypePrefix()
+ {
+ return "2:PySide6.QtRemoteObjects.DynamicReplica.";
+ }
+
+ static int tp_init(PyObject *self, PyObject *args, PyObject *kwds)
+ {
+ static initproc initFunc = reinterpret_cast<initproc>(PepType_GetSlot(getSbkType(),
+ Py_tp_init));
+ QRemoteObjectReplica *replica = nullptr;
+ if (PyTuple_Size(args) == 0) {
+ if (initFunc(self, args, kwds) < 0)
+ return -1;
+ Shiboken::Conversions::pythonToCppPointer(getSbkType(), self, &replica);
+ } else { // Process replica with arguments passed from the added node.acquire method
+ PyObject *node = nullptr;
+ PyObject *constructorType = nullptr;
+ PyObject *name = nullptr;
+ static PyTypeObject *nodeType = Shiboken::Conversions::getPythonTypeObject("QRemoteObjectNode");
+ if (!PyArg_UnpackTuple(args, "Replica.__init__", 2, 3, &node, &constructorType, &name) ||
+ !PySide::inherits(Py_TYPE(node), nodeType->tp_name)) {
+ PyErr_SetString(PyExc_TypeError,
+ "Replicas can be initialized with no arguments or by node.acquire only");
+ return -1;
+ }
+ static auto *constructorArgs = PyTuple_Pack(1, constructorType);
+ if (initFunc(self, constructorArgs, kwds) < 0)
+ return -1;
+ if (name)
+ PyObject_CallMethod(self, "initializeNode", "OO", node, name);
+ else
+ PyObject_CallMethod(self, "initializeNode", "O", node);
+ Shiboken::Conversions::pythonToCppPointer(getSbkType(), self, &replica);
+ }
+ if (!replica) {
+ PyErr_SetString(PyExc_RuntimeError, "Failed to initialize replica");
+ return -1;
+ }
+ // Get the properties from the type
+ PyTypeObject *type = Py_TYPE(self);
+ auto *pyProperties = PyObject_GetAttr(reinterpret_cast<PyObject *>(type), propertiesAttr());
+ if (!pyProperties) {
+ PyErr_SetString(PyExc_RuntimeError, "Failed to get properties from type");
+ return -1;
+ }
+ // Make a copy of the properties and set them on the replica
+ auto *propPtr = reinterpret_cast<QVariantList *>(PyCapsule_GetPointer(pyProperties, nullptr));
+ auto propertiesCopy = QVariantList(*propPtr);
+ static_cast<FriendlyReplica *>(replica)->setProperties(std::move(propertiesCopy));
+ return 0;
+ }
+
+ static PyObject *capsule_method_handler(PyObject *payload, PyObject *args)
+ {
+ auto *methodData = reinterpret_cast<CapsuleDescriptorData *>(PyCapsule_GetPointer(payload,
+ "Payload"));
+ if (!methodData) {
+ PyErr_SetString(PyExc_RuntimeError, "Invalid call to dynamic method. Missing payload.");
+ return nullptr;
+ }
+ PyObject *self = methodData->self;
+ QRemoteObjectReplica *replica = nullptr;
+ Shiboken::Conversions::pythonToCppPointer(getSbkType(), self, &replica);
+ if (PyCapsule_IsValid(methodData->payload, "PropertyCapsule")) {
+ auto *capsule = PyCapsule_GetPointer(methodData->payload, "PropertyCapsule");
+ if (capsule) {
+ auto *callData = reinterpret_cast<PropertyCapsule *>(capsule);
+ QVariant currentVariant = static_cast<FriendlyReplica *>(replica)->propAsVariant(callData->indexInObject);
+
+ // Handle getter
+ if (PyTuple_Size(args) == 0) // Getter
+ return toPython(currentVariant);
+
+ // Handle setter - currentVariant is a copy, so we can modify it
+ if (PyTuple_Size(args) != 1) {
+ PyErr_SetString(PyExc_TypeError,
+ "Property setter takes exactly one argument");
+ return nullptr;
+ }
+ Conversions::SpecificConverter converter(currentVariant.metaType().name());
+ auto metaType = currentVariant.metaType();
+ if (metaType.flags().testFlag(QMetaType::IsEnumeration)) {
+ converter.toCpp(PyTuple_GetItem(args, 0), currentVariant.data());
+ currentVariant.convert(metaType);
+ } else {
+ converter.toCpp(PyTuple_GetItem(args, 0), currentVariant.data());
+ }
+ if (PyErr_Occurred()) // POD conversion can produce an error
+ return nullptr;
+ QVariantList _args{currentVariant};
+ static_cast<FriendlyReplica *>(replica)->send(QMetaObject::WriteProperty, callData->propertyIndex, _args);
+ Py_RETURN_NONE;
+ }
+ }
+ if (PyCapsule_IsValid(methodData->payload, "MethodCapsule")) {
+ auto *capsule = PyCapsule_GetPointer(methodData->payload, "MethodCapsule");
+ if (capsule) {
+ auto *callData = reinterpret_cast<MethodCapsule *>(capsule);
+ if (PyTuple_Size(args) != callData->argumentTypes.size()) {
+ PyErr_SetString(PyExc_TypeError,
+ "Method called with incorrect number of arguments");
+ return nullptr;
+ }
+ QVariantList _args;
+ static Conversions::SpecificConverter argsConverter("QVariantList");
+ argsConverter.toCpp(args, &_args);
+ if (PyErr_Occurred()) // POD conversion can produce an error
+ return nullptr;
+ if (!callData->returnType.isValid() ||
+ (callData->returnType.isValid() && callData->returnType.id() == QMetaType::Void)) {
+ static_cast<FriendlyReplica *>(replica)->send(QMetaObject::InvokeMetaMethod, callData->methodIndex, _args);
+ Py_RETURN_NONE;
+ }
+ QRemoteObjectPendingCall *cppResult = new QRemoteObjectPendingCall;
+ *cppResult = static_cast<FriendlyReplica *>(replica)->sendWithReply(QMetaObject::InvokeMetaMethod,
+ callData->methodIndex, _args);
+ static PyTypeObject *baseType =
+ Shiboken::Conversions::getPythonTypeObject("QRemoteObjectPendingCall");
+ Q_ASSERT(baseType);
+ auto *pyResult = Shiboken::Object::newObject(baseType, cppResult, true, true);
+ return pyResult;
+ }
+ }
+
+ PyErr_SetString(PyExc_RuntimeError, "Unknown capsule type");
+ return nullptr;
+ }
+};
+
+static int DynamicType_traverse(PyObject *self, visitproc visit, void *arg)
+{
+ auto traverseProc = reinterpret_cast<traverseproc>(PepType_GetSlot(SbkObject_TypeF(),
+ Py_tp_traverse));
+ return traverseProc(self, visit, arg);
+}
+
+static int DynamicType_clear(PyObject *self)
+{
+ auto clearProc = reinterpret_cast<inquiry>(PepType_GetSlot(SbkObject_TypeF(), Py_tp_clear));
+ return clearProc(self);
+}
+
+static PyMethodDef DynamicClass_methods[] = {
+ {"get_enum", reinterpret_cast<PyCFunction>(DynamicType_get_enum), METH_O | METH_CLASS,
+ "Get enum type by name"},
+ {nullptr, nullptr, 0, nullptr}
+};
+
+static PyType_Slot DynamicClass_slots[] = {
+ {Py_tp_base, nullptr}, // inserted by introduceWrapperType
+ {Py_tp_init, nullptr}, // inserted by createDynamicType
+ {Py_tp_traverse, reinterpret_cast<void *>(DynamicType_traverse)},
+ {Py_tp_clear, reinterpret_cast<void *>(DynamicType_clear)},
+ {Py_tp_methods, reinterpret_cast<void *>(DynamicClass_methods)},
+ {0, nullptr}
+};
+
+} // extern "C"
+
+template <typename T, typename BaseType>
+PyTypeObject *createDynamicClassImpl(QMetaObject *meta)
+{
+ DynamicClass_slots[1].pfunc = reinterpret_cast<void*>(T::tp_init);
+
+ auto fullTypeName = QByteArray{T::getTypePrefix()} + meta->className();
+ PyType_Spec spec = {
+ fullTypeName.constData(),
+ 0,
+ 0,
+ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
+ DynamicClass_slots
+ };
+
+ auto type = Shiboken::ObjectType::introduceWrapperType(
+ reinterpret_cast<PyObject *>(PySideRepFile_TypeF()),
+ meta->className(),
+ meta->className(),
+ &spec,
+ &Shiboken::callCppDestructor<BaseType>,
+ T::getBases(),
+ Shiboken::ObjectType::WrapperFlags::InternalWrapper);
+
+ auto *self = reinterpret_cast<PyObject *>(type);
+ if (create_managed_py_enums(self, meta) < 0)
+ return nullptr;
+
+ PySide::Signal::registerSignals(type, meta);
+ Shiboken::ObjectType::setSubTypeInitHook(type, &PySide::initQObjectSubType);
+ PySide::initDynamicMetaObject(type, meta, 0); // Size 0?
+
+ PyMethodDef method = {
+ nullptr,
+ reinterpret_cast<PyCFunction>(T::capsule_method_handler),
+ METH_VARARGS,
+ nullptr
+ };
+ for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) {
+ // Create a PropertyCapsule for each property to store the info needed for
+ // the handler. Assign the __get__ and (if needed) __set__ attributes to a
+ // PySideProperty which becomes the attribute set on the new type.
+ auto metaProperty = meta->property(i);
+ PyObject *kwds = PyDict_New();
+ auto metaType = metaProperty.metaType();
+ auto *pyPropertyType = PyUnicode_FromString(metaType.name());
+ PyDict_SetItemString(kwds, "type", pyPropertyType);
+ Py_DECREF(pyPropertyType);
+
+ method.ml_name = metaProperty.name();
+ auto *pc = new PropertyCapsule{metaProperty.name(), i, i - meta->propertyOffset()};
+ auto capsule = PyCapsule_New(pc, "PropertyCapsule", [](PyObject *capsule) {
+ delete static_cast<PropertyCapsule *>(PyCapsule_GetPointer(capsule, "PropertyCapsule"));
+ });
+ auto capsulePropObject = make_capsule_property(&method, capsule,
+ metaProperty.isWritable());
+ PyObject *fget = PyObject_GetAttrString(capsulePropObject, "__get__");
+ PyDict_SetItemString(kwds, "fget", fget);
+ if (metaProperty.isWritable()) {
+ PyObject *fset = PyObject_GetAttrString(capsulePropObject, "__set__");
+ PyDict_SetItemString(kwds, "fset", fset);
+ if (metaProperty.hasNotifySignal()) {
+ auto nameString = metaProperty.notifySignal().name();
+ auto *notify = PyObject_GetAttrString(reinterpret_cast<PyObject *>(type),
+ nameString.constData());
+ PyDict_SetItemString(kwds, "notify", notify);
+ }
+ }
+ PyObject *pyProperty = PyObject_Call(reinterpret_cast<PyObject *>(PySideProperty_TypeF()),
+ PyTuple_New(0), kwds);
+ if (PyObject_SetAttrString(reinterpret_cast<PyObject *>(type),
+ metaProperty.name(), pyProperty) < 0) {
+ return nullptr;
+ }
+ Py_DECREF(pyProperty);
+ }
+ for (int i = meta->methodOffset(); i < meta->methodCount(); ++i) {
+ // Create a CapsuleMethod for each Slot method to store the info needed
+ // for the handler.
+ auto metaMethod = meta->method(i);
+ // Note: We are creating our custom metatype ourselves, which makes our added
+ // (non-signal), methods return QMetaMethod::MethodType::Method, not
+ // MethodType::Slot. This is fine, we just need to create a CapsuleMethod
+ // for those methods.
+ if (metaMethod.methodType() == QMetaMethod::MethodType::Signal)
+ continue;
+ auto name = metaMethod.name();
+ method.ml_name = name.constData();
+ QList<QMetaType> argumentTypes;
+ for (int j = 0; j < metaMethod.parameterCount(); ++j)
+ argumentTypes << metaMethod.parameterMetaType(j);
+ MethodCapsule *capsuleData = new MethodCapsule{metaMethod.name(),
+ metaMethod.methodIndex(),
+ std::move(argumentTypes),
+ metaMethod.returnMetaType()};
+ add_capsule_method_to_type(type, &method,
+ PyCapsule_New(capsuleData, "MethodCapsule",
+ [](PyObject *capsule) {
+ delete reinterpret_cast<MethodCapsule *>(PyCapsule_GetPointer(capsule, "MethodCapsule"));
+ }));
+ }
+
+ return type;
+}
+
+PyTypeObject *createDynamicClass(QMetaObject *meta, PyObject *properties_capsule)
+{
+ bool isSource;
+ if (strncmp(meta->superClass()->className(), "QObject", 7) == 0) {
+ isSource = true;
+ } else if (strncmp(meta->superClass()->className(), "QRemoteObjectReplica", 20) == 0) {
+ isSource = false;
+ } else {
+ PyErr_SetString(PyExc_RuntimeError,
+ "Dynamic type must be a subclass of QObject or QRemoteObjectReplica");
+ return nullptr;
+ }
+
+ PyTypeObject *newType = nullptr;
+
+ if (isSource)
+ newType = createDynamicClassImpl<SourceDefs, QObject>(meta);
+ else
+ newType = createDynamicClassImpl<ReplicaDefs, QRemoteObjectReplica>(meta);
+
+ // Add the properties to the new type as an attribute
+ if (PyObject_SetAttr(reinterpret_cast<PyObject *>(newType), propertiesAttr(),
+ properties_capsule) < 0) {
+ Py_DECREF(newType);
+ return nullptr;
+ }
+
+ return newType;
+}
diff --git a/sources/pyside6/libpysideremoteobjects/pysidedynamicclass_p.h b/sources/pyside6/libpysideremoteobjects/pysidedynamicclass_p.h
new file mode 100644
index 000000000..4716f4f32
--- /dev/null
+++ b/sources/pyside6/libpysideremoteobjects/pysidedynamicclass_p.h
@@ -0,0 +1,15 @@
+// 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
+
+#ifndef PYSIDE_DYNAMIC_CLASS_P_H
+#define PYSIDE_DYNAMIC_CLASS_P_H
+
+#include <sbkpython.h>
+
+#include <QtCore/qtclasshelpermacros.h>
+
+QT_FORWARD_DECLARE_STRUCT(QMetaObject)
+
+PyTypeObject *createDynamicClass(QMetaObject *meta, PyObject *properties_capsule);
+
+#endif // PYSIDE_DYNAMIC_CLASS_P_H
diff --git a/sources/pyside6/libpysideremoteobjects/pysidedynamiccommon.cpp b/sources/pyside6/libpysideremoteobjects/pysidedynamiccommon.cpp
new file mode 100644
index 000000000..b1f01fed6
--- /dev/null
+++ b/sources/pyside6/libpysideremoteobjects/pysidedynamiccommon.cpp
@@ -0,0 +1,124 @@
+// 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 "pysidedynamiccommon_p.h"
+#include "pysidedynamicenum_p.h"
+
+#include <sbkstring.h>
+
+#include <QtCore/qmetaobject.h>
+
+using namespace Shiboken;
+
+PyObject *toPython(const QVariant &variant)
+{
+ auto metaType = variant.metaType();
+ Conversions::SpecificConverter converter(metaType.name());
+ auto *value = converter.toPython(variant.data());
+ if (metaType.flags().testFlag(QMetaType::IsGadget)) {
+ // A single converter is used for all POD types - it converts to a Python
+ // tuple. We need an additional step to convert to our Python type for the POD.
+ // Thankfully, the converter stores the specific type we created, so we can call
+ // the constructor with the tuple.
+ auto *podType = Conversions::getPythonTypeObject(converter);
+ if (!podType) {
+ Py_DECREF(value);
+ PyErr_SetString(PyExc_RuntimeError, "Failed to get Python type for POD");
+ return nullptr;
+ }
+ PyObject *podValue = PyObject_CallObject(reinterpret_cast<PyObject *>(podType), value);
+ Py_DECREF(value);
+ if (!podValue) {
+ PyErr_SetString(PyExc_RuntimeError, "Failed to create POD instance");
+ return nullptr;
+ }
+ return podValue;
+ }
+ if (metaType.flags().testFlag(QMetaType::IsEnumeration)) {
+ // Enums are converted to Python ints
+ auto *enumType = Conversions::getPythonTypeObject(converter);
+ if (!enumType) {
+ Py_DECREF(value);
+ PyErr_SetString(PyExc_RuntimeError, "Failed to get Python type for enum");
+ return nullptr;
+ }
+ PyObject *enumValue = PyObject_CallFunctionObjArgs(reinterpret_cast<PyObject *>(enumType),
+ value, nullptr);
+ Py_DECREF(value);
+ if (!enumValue) {
+ PyErr_Print();
+ PyErr_SetString(PyExc_RuntimeError, "Failed to create enum instance");
+ return nullptr;
+ }
+ return enumValue;
+ }
+ return value;
+}
+
+
+/**
+ * @brief Creates and manages memory for Python enum types for each QEnum in the
+ * provided QMetaObject.
+ *
+ * This function iterates over the enumerators in the provided QMetaObject,
+ * creates corresponding Python enum types, and stores them in a dictionary.
+ * The dictionary is then set as an attribute ()"_enum_data") on the provided
+ * Python object, to be accessed by the _get_enum that has been added to each
+ * of our dynamic types.
+ *
+ * These are "managed" in the sense that the enums clean up their converters
+ * using our PyCapsule method, and by adding the dictionary as a Python attribute,
+ * the dictionary will be cleaned up when the containing type is garbage
+ * collected.
+ *
+ * @param self A pointer to the Python object where the enum data will be stored.
+ * @param meta A pointer to the QMetaObject containing the enumerators.
+ * @return Returns 0 on success, or -1 on failure.
+ */
+int create_managed_py_enums(PyObject *self, QMetaObject *meta)
+{
+ PyObject *enum_data = PyDict_New();
+ for (int i = meta->enumeratorOffset(); i < meta->enumeratorCount(); ++i) {
+ auto metaEnum = meta->enumerator(i);
+ auto *enumType = createEnumType(&metaEnum);
+ if (!enumType) {
+ PyErr_Print();
+ PyErr_Format(PyExc_RuntimeError, "Failed to create enum type for POD '%s'",
+ meta->className());
+ return -1;
+ }
+ PyDict_SetItemString(enum_data, metaEnum.enumName(),
+ reinterpret_cast<PyObject *>(enumType));
+ Py_DECREF(enumType);
+ }
+ if (PyObject_SetAttrString(self, "_enum_data", enum_data) < 0) {
+ PyErr_Print();
+ qWarning() << "Failed to set _enum_data attribute on type"
+ << reinterpret_cast<PyTypeObject *>(self)->tp_name;
+ return -1;
+ }
+ Py_DECREF(enum_data);
+
+ return 0;
+}
+
+PyObject *DynamicType_get_enum(PyObject *self, PyObject *name)
+{
+ // Our enum types are always stored in a dictionary attribute named "_enum_data"
+ PyObject *enum_dict = PyObject_GetAttrString(self, "_enum_data");
+ if (!enum_dict) {
+ PyErr_SetString(PyExc_RuntimeError, "Failed to get _enum_data attribute");
+ return nullptr;
+ }
+
+ PyObject *enum_type = PyDict_GetItem(enum_dict, name);
+ Py_DECREF(enum_dict);
+
+ if (!enum_type) {
+ PyErr_Format(PyExc_KeyError, "Enum '%s' not found", String::toCString(name));
+ return nullptr;
+ }
+
+ Py_INCREF(enum_type);
+ return enum_type;
+}
diff --git a/sources/pyside6/libpysideremoteobjects/pysidedynamiccommon_p.h b/sources/pyside6/libpysideremoteobjects/pysidedynamiccommon_p.h
new file mode 100644
index 000000000..1e9f8d55a
--- /dev/null
+++ b/sources/pyside6/libpysideremoteobjects/pysidedynamiccommon_p.h
@@ -0,0 +1,89 @@
+// 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
+
+#ifndef PYSIDE_DYNAMIC_COMMON_P_H
+#define PYSIDE_DYNAMIC_COMMON_P_H
+
+#include <sbkconverter.h>
+
+#include <QtCore/qlist.h>
+#include <QtCore/qvariant.h>
+#include <QtCore/qmetatype.h>
+
+PyObject *toPython(const QVariant &variant);
+int create_managed_py_enums(PyObject *self, QMetaObject *meta);
+PyObject *DynamicType_get_enum(PyObject *self, PyObject *name);
+
+// Data for dynamically created property handlers
+struct PropertyCapsule
+{
+ QByteArray name;
+ int propertyIndex; // meta->indexOfProperty() - including offset
+ int indexInObject; // Index minus offset for indexing into QVariantList
+};
+
+// Data for dynamically created method handlers
+struct MethodCapsule
+{
+ QByteArray name;
+ int methodIndex;
+ QList<QMetaType> argumentTypes;
+ QMetaType returnType; // meta->indexOfMethod() - including offset
+};
+
+// These functions are used to create a PyCapsule holding a pointer to a C++
+// object, which is set as an attribute on a Python type. When the Python
+// type is garbage collected, the type's attributes are as well, resulting in
+// the capsule's cleanup running to delete the pointer. This won't be as
+// efficient as a custom tp_free on the type, but it's easier to manage.
+// And it only runs when as all references to the type (and all instances) are
+// released, so it won't be used frequently.
+
+static int capsule_count = 0;
+
+static PyObject *get_capsule_count()
+{
+ return PyLong_FromLong(capsule_count);
+}
+
+template <typename T>
+void Capsule_destructor(PyObject *capsule)
+{
+ capsule_count--;
+ T pointer = static_cast<T>(PyCapsule_GetPointer(capsule, nullptr));
+ delete pointer;
+ pointer = nullptr;
+}
+
+template <>
+inline void Capsule_destructor<SbkConverter *>(PyObject *capsule)
+{
+ capsule_count--;
+ SbkConverter *pointer = static_cast<SbkConverter *>(PyCapsule_GetPointer(capsule, nullptr));
+ Shiboken::Conversions::deleteConverter(pointer);
+ pointer = nullptr;
+}
+
+template <typename T>
+int set_cleanup_capsule_attr_for_pointer(PyTypeObject *type, const char *name, T pointer)
+{
+ static_assert(std::is_pointer<T>::value, "T must be a pointer type");
+
+ if (!pointer) {
+ PyErr_SetString(PyExc_RuntimeError, "Pointer is null");
+ return -1;
+ }
+ auto capsule = PyCapsule_New(pointer, nullptr, Capsule_destructor<T>);
+ if (!capsule)
+ return -1; // Propagate the error
+
+ if (PyObject_SetAttrString(reinterpret_cast<PyObject *>(type), name, capsule) < 0)
+ return -1; // Propagate the error
+
+ Py_DECREF(capsule);
+ capsule_count++;
+
+ return 0;
+}
+
+#endif // PYSIDE_DYNAMIC_COMMON_P_H
diff --git a/sources/pyside6/libpysideremoteobjects/pysidedynamicenum.cpp b/sources/pyside6/libpysideremoteobjects/pysidedynamicenum.cpp
new file mode 100644
index 000000000..1f92224f5
--- /dev/null
+++ b/sources/pyside6/libpysideremoteobjects/pysidedynamicenum.cpp
@@ -0,0 +1,158 @@
+// 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 "pysidedynamicenum_p.h"
+#include "pysidedynamiccommon_p.h"
+
+#include <autodecref.h>
+#include <sbkconverter.h>
+#include <sbkenum.h>
+
+#include <QtCore/qmetaobject.h>
+
+using namespace Shiboken;
+
+// Remote Objects transfer enums as integers of the underlying type.
+#define CREATE_ENUM_CONVERSION_FUNCTIONS(SUFFIX, INT_TYPE, PY_TYPE) \
+static void pythonToCpp_PyEnum_QEnum_##SUFFIX(PyObject *pyIn, void *cppOut) \
+{ \
+ Enum::EnumValueType value = Enum::getValue(pyIn); \
+ INT_TYPE val(value); \
+ *reinterpret_cast<INT_TYPE *>(cppOut) = val; \
+} \
+static PythonToCppFunc is_PyEnum_PythonToCpp_QEnum_##SUFFIX##_Convertible(PyObject *pyIn) \
+{ \
+ if (Enum::check(pyIn)) \
+ return pythonToCpp_PyEnum_QEnum_##SUFFIX; \
+ return {}; \
+} \
+static PyObject *cppToPython_QEnum_##SUFFIX##_PyEnum(const void *cppIn) \
+{ \
+ auto convertedCppIn = *reinterpret_cast<const INT_TYPE *>(cppIn); \
+ return PY_TYPE(convertedCppIn); \
+}
+
+CREATE_ENUM_CONVERSION_FUNCTIONS(I8, int8_t, PyLong_FromLong)
+CREATE_ENUM_CONVERSION_FUNCTIONS(I16, int16_t, PyLong_FromLong)
+CREATE_ENUM_CONVERSION_FUNCTIONS(I32, int32_t, PyLong_FromLong)
+CREATE_ENUM_CONVERSION_FUNCTIONS(U8, uint8_t, PyLong_FromUnsignedLong)
+CREATE_ENUM_CONVERSION_FUNCTIONS(U16, uint16_t, PyLong_FromUnsignedLong)
+CREATE_ENUM_CONVERSION_FUNCTIONS(U32, uint32_t, PyLong_FromUnsignedLong)
+CREATE_ENUM_CONVERSION_FUNCTIONS(I64, int64_t, PyLong_FromLongLong)
+CREATE_ENUM_CONVERSION_FUNCTIONS(U64, uint64_t, PyLong_FromUnsignedLongLong)
+
+PyTypeObject *createEnumType(QMetaEnum *metaEnum)
+{
+ static const auto namePrefix = QByteArrayLiteral("2:PySide6.QtRemoteObjects.DynamicEnum.");
+ auto fullName = namePrefix + metaEnum->scope() + "." + metaEnum->enumName();
+
+ AutoDecRef args(PyList_New(0));
+ auto *pyEnumItems = args.object();
+ auto metaType = metaEnum->metaType();
+ auto underlyingType = metaType.underlyingType();
+ bool isUnsigned = underlyingType.flags().testFlag(QMetaType::IsUnsignedEnumeration);
+ for (int idx = 0; idx < metaEnum->keyCount(); ++idx) {
+ auto *key = PyUnicode_FromString(metaEnum->key(idx));
+ auto *key_value = PyTuple_New(2);
+ PyTuple_SetItem(key_value, 0, key);
+ // Value should only return a nullopt if there is no metaObject or the index is not valid
+ auto valueOpt = metaEnum->value64(idx);
+ if (!valueOpt) {
+ PyErr_SetString(PyExc_RuntimeError, "Failed to get value64 from enum");
+ return nullptr;
+ }
+ if (isUnsigned) {
+ auto *value = PyLong_FromUnsignedLongLong(*valueOpt);
+ PyTuple_SetItem(key_value, 1, value);
+ } else {
+ auto *value = PyLong_FromLongLong(*valueOpt);
+ PyTuple_SetItem(key_value, 1, value);
+ }
+ PyList_Append(pyEnumItems, key_value);
+ }
+
+ PyTypeObject *newType{};
+ if (metaEnum->isFlag())
+ newType = Enum::createPythonEnum(fullName.constData(), pyEnumItems, "Flag");
+ else
+ newType = Enum::createPythonEnum(fullName.constData(), pyEnumItems);
+
+ SbkConverter *converter = nullptr;
+ switch (underlyingType.sizeOf()) {
+ case 1:
+ if (isUnsigned) {
+ converter = Conversions::createConverter(newType,
+ cppToPython_QEnum_U8_PyEnum);
+ Conversions::addPythonToCppValueConversion(converter,
+ pythonToCpp_PyEnum_QEnum_U8,
+ is_PyEnum_PythonToCpp_QEnum_U8_Convertible);
+ } else {
+ converter = Conversions::createConverter(newType,
+ cppToPython_QEnum_I8_PyEnum);
+ Conversions::addPythonToCppValueConversion(converter,
+ pythonToCpp_PyEnum_QEnum_I8,
+ is_PyEnum_PythonToCpp_QEnum_I8_Convertible);
+ }
+ break;
+ case 2:
+ if (isUnsigned) {
+ converter = Conversions::createConverter(newType,
+ cppToPython_QEnum_U16_PyEnum);
+ Conversions::addPythonToCppValueConversion(converter,
+ pythonToCpp_PyEnum_QEnum_U16,
+ is_PyEnum_PythonToCpp_QEnum_U16_Convertible);
+ } else {
+ converter = Conversions::createConverter(newType,
+ cppToPython_QEnum_I16_PyEnum);
+ Conversions::addPythonToCppValueConversion(converter,
+ pythonToCpp_PyEnum_QEnum_I16,
+ is_PyEnum_PythonToCpp_QEnum_I16_Convertible);
+ }
+ break;
+ case 4:
+ if (isUnsigned) {
+ converter = Conversions::createConverter(newType,
+ cppToPython_QEnum_U32_PyEnum);
+ Conversions::addPythonToCppValueConversion(converter,
+ pythonToCpp_PyEnum_QEnum_U32,
+ is_PyEnum_PythonToCpp_QEnum_U32_Convertible);
+ } else {
+ converter = Conversions::createConverter(newType,
+ cppToPython_QEnum_I32_PyEnum);
+ Conversions::addPythonToCppValueConversion(converter,
+ pythonToCpp_PyEnum_QEnum_I32,
+ is_PyEnum_PythonToCpp_QEnum_I32_Convertible);
+ }
+ break;
+ case 8:
+ if (isUnsigned) {
+ converter = Conversions::createConverter(newType,
+ cppToPython_QEnum_U64_PyEnum);
+ Conversions::addPythonToCppValueConversion(converter,
+ pythonToCpp_PyEnum_QEnum_U64,
+ is_PyEnum_PythonToCpp_QEnum_U64_Convertible);
+ } else {
+ converter = Conversions::createConverter(newType,
+ cppToPython_QEnum_I64_PyEnum);
+ Conversions::addPythonToCppValueConversion(converter,
+ pythonToCpp_PyEnum_QEnum_I64,
+ is_PyEnum_PythonToCpp_QEnum_I64_Convertible);
+ }
+ break;
+ default:
+ PyErr_SetString(PyExc_RuntimeError, "Unsupported enum underlying type");
+ return nullptr;
+ }
+ auto scopedName = QByteArray(metaEnum->scope()) + "::" + metaEnum->enumName();
+ Conversions::registerConverterName(converter, scopedName.constData());
+ Conversions::registerConverterName(converter, metaEnum->enumName());
+ // createConverter increases the ref count of type, but that will create a
+ // circular reference when we add the capsule with the converter's pointer
+ // to the type's attributes. So we need to decrease the ref count on the
+ // type after calling createConverter.
+ Py_DECREF(newType);
+ if (set_cleanup_capsule_attr_for_pointer(newType, "_converter_capsule", converter) < 0)
+ return nullptr;
+
+ return newType;
+}
diff --git a/sources/pyside6/libpysideremoteobjects/pysidedynamicenum_p.h b/sources/pyside6/libpysideremoteobjects/pysidedynamicenum_p.h
new file mode 100644
index 000000000..14181fac8
--- /dev/null
+++ b/sources/pyside6/libpysideremoteobjects/pysidedynamicenum_p.h
@@ -0,0 +1,15 @@
+// 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
+
+#ifndef PYSIDE_DYNAMIC_ENUM_P_H
+#define PYSIDE_DYNAMIC_ENUM_P_H
+
+#include <sbkpython.h>
+
+#include <QtCore/qtclasshelpermacros.h>
+
+QT_FORWARD_DECLARE_CLASS(QMetaEnum)
+
+PyTypeObject *createEnumType(QMetaEnum *metaEnum);
+
+#endif // PYSIDE_DYNAMIC_ENUM_P_H
diff --git a/sources/pyside6/libpysideremoteobjects/pysidedynamicpod.cpp b/sources/pyside6/libpysideremoteobjects/pysidedynamicpod.cpp
new file mode 100644
index 000000000..abfeaa037
--- /dev/null
+++ b/sources/pyside6/libpysideremoteobjects/pysidedynamicpod.cpp
@@ -0,0 +1,260 @@
+// 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 "pysidedynamicpod_p.h"
+#include "pysidecapsulemethod_p.h"
+#include "pysidedynamiccommon_p.h"
+
+#include <autodecref.h>
+#include <helper.h>
+#include <pep384ext.h>
+#include <sbkconverter.h>
+#include <sbkstaticstrings.h>
+#include <sbkstring.h>
+
+#include <pysidestaticstrings.h>
+
+#include <QtCore/qmetaobject.h>
+
+using namespace Shiboken;
+
+extern "C"
+{
+
+struct PodDefs
+{
+ static PyObject *tp_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
+ {
+ SBK_UNUSED(kwds);
+ AutoDecRef param_types(PyObject_GetAttrString(reinterpret_cast<PyObject *>(type),
+ "__param_types__"));
+ if (!param_types) {
+ PyErr_Format(PyExc_RuntimeError, "Failed to get POD attributes for type %s",
+ type->tp_name);
+ return nullptr;
+ }
+
+ // param_types is a tuple of PyTypeObject pointers
+ Py_ssize_t size = PyTuple_Size(param_types);
+ if (size != PyTuple_Size(args)) {
+ PyErr_Format(PyExc_TypeError,
+ "Incorrect number of arguments for type %s. Expected %zd.",
+ type->tp_name, size);
+ return nullptr;
+ }
+
+ PyObject *self = PepExt_Type_GetAllocSlot(type)(type, size);
+
+ if (!self)
+ return nullptr;
+
+ for (Py_ssize_t i = 0; i < size; ++i) {
+ PyObject *expected_type = PyTuple_GetItem(param_types, i);
+ PyObject *item = PyTuple_GetItem(args, i);
+ // Check if the item is an instance of the expected type
+ if (PyObject_IsInstance(item, expected_type)) {
+ Py_INCREF(item);
+ PyTuple_SetItem(self, i, item);
+ } else {
+ // Try to convert the item to the expected type
+ PyObject *converted_item = PyObject_CallFunctionObjArgs(expected_type, item, nullptr);
+ if (!converted_item) {
+ Py_DECREF(self);
+ PyErr_Format(PyExc_TypeError, "Argument %zd must be convertible to type %s", i,
+ reinterpret_cast<PyTypeObject *>(expected_type)->tp_name);
+ return nullptr;
+ }
+ PyTuple_SetItem(self, i, converted_item);
+ }
+ }
+
+ return self;
+ }
+
+ static PyObject *tp_repr(PyObject *self)
+ {
+ auto *type = Py_TYPE(self);
+ std::string repr(type->tp_name);
+ repr += "(";
+ for (Py_ssize_t i = 0; i < PyTuple_Size(self); ++i) {
+ if (i > 0)
+ repr += ", ";
+
+ PyObject *item_repr = PyObject_Repr(PyTuple_GetItem(self, i));
+ repr += String::toCString(item_repr);
+ }
+ repr += ")";
+ return PyUnicode_FromString(repr.c_str());
+ }
+
+ static PyObject *CapsuleMethod_handler(PyObject *payload, PyObject * /* args */)
+ {
+ auto *methodData = reinterpret_cast<CapsuleDescriptorData *>(
+ PyCapsule_GetPointer(payload, "Payload"));
+ if (!methodData) {
+ PyErr_SetString(PyExc_RuntimeError, "Invalid call to dynamic method. Missing payload.");
+ return nullptr;
+ }
+ PyObject *self = methodData->self;
+ if (PyCapsule_IsValid(methodData->payload, "PropertyCapsule")) {
+ // Handle property getter/setter against our hidden properties attribute
+ auto *capsule = PyCapsule_GetPointer(methodData->payload, "PropertyCapsule");
+ if (capsule) {
+ auto *callData = reinterpret_cast<PropertyCapsule *>(capsule);
+ if (callData->indexInObject < 0 || callData->indexInObject >= PyTuple_Size(self)) {
+ PyErr_Format(PyExc_RuntimeError, "Unknown property method: %s",
+ callData->name.constData());
+ return nullptr;
+ }
+ auto *val = PyTuple_GetItem(self, callData->indexInObject);
+ Py_INCREF(val);
+ return val;
+ }
+ }
+
+ PyErr_SetString(PyExc_RuntimeError, "Unknown capsule type");
+ return nullptr;
+ }
+};
+
+static PyMethodDef DynamicPod_tp_methods[] = {
+ {"get_enum", reinterpret_cast<PyCFunction>(DynamicType_get_enum), METH_O | METH_CLASS,
+ "Get enum type by name"},
+ {nullptr, nullptr, 0, nullptr}
+};
+
+static PyType_Slot DynamicPod_slots[] = {
+ {Py_tp_base, reinterpret_cast<void *>(&PyTuple_Type)},
+ {Py_tp_new, reinterpret_cast<void *>(PodDefs::tp_new)},
+ {Py_tp_repr, reinterpret_cast<void *>(PodDefs::tp_repr)},
+ {Py_tp_methods, reinterpret_cast<void *>(DynamicPod_tp_methods)},
+ {0, nullptr}
+};
+
+// C++ to Python conversion for POD types.
+static PyObject *cppToPython_POD_Tuple(const void *cppIn)
+{
+ const auto &cppInRef = *reinterpret_cast<const QVariantList *>(cppIn);
+ PyObject *pyOut = PyTuple_New(Py_ssize_t(cppInRef.size()));
+ Py_ssize_t idx = 0;
+ for (auto it = std::cbegin(cppInRef), end = std::cend(cppInRef); it != end; ++it, ++idx) {
+ static const Conversions::SpecificConverter argConverter("QVariant");
+ const auto &cppItem = *it;
+ PyTuple_SetItem(pyOut, idx, Shiboken::Conversions::copyToPython(argConverter, &cppItem));
+ }
+ return pyOut;
+}
+static void pythonToCpp_Tuple_POD(PyObject *pyIn, void *cppOut)
+{
+ auto &cppOutRef = *reinterpret_cast<QVariantList *>(cppOut);
+
+ Py_ssize_t tupleSize = PyTuple_Size(pyIn);
+ if (tupleSize != cppOutRef.size()) {
+ PyErr_Format(PyExc_ValueError,
+ "Size mismatch: tuple has %zd elements, but POD expects %d elements",
+ tupleSize, cppOutRef.size());
+ return;
+ }
+
+ for (Py_ssize_t i = 0; i < tupleSize; ++i) {
+ static const Conversions::SpecificConverter argConverter("QVariant");
+ PyObject *item = PyTuple_GetItem(pyIn, i);
+ QVariant &variant = cppOutRef[i];
+ Conversions::SpecificConverter converter(variant.metaType().name());
+ Shiboken::Conversions::pythonToCppCopy(converter, item, variant.data());
+ }
+}
+static PythonToCppFunc is_Tuple_PythonToCpp_POD_Convertible(PyObject *pyIn)
+{
+ if (PyTuple_Check(pyIn))
+ return pythonToCpp_Tuple_POD;
+
+ return {};
+}
+
+} // extern "C"
+
+PyTypeObject *createPodType(QMetaObject *meta)
+{
+ auto qualname = QByteArrayLiteral("DynamicPod.") + meta->className();
+ PyType_Spec spec = {
+ qualname.constData(),
+ 0,
+ 0,
+ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_TYPE_SUBCLASS,
+ DynamicPod_slots
+ };
+
+ PyObject *obType = PyType_FromSpec(&spec);
+ if (!obType)
+ return nullptr;
+
+ if (create_managed_py_enums(obType, meta) < 0)
+ return nullptr;
+
+ Py_ssize_t size = meta->propertyCount() - meta->propertyOffset();
+ AutoDecRef pyParamTypes(PyTuple_New(size));
+ for (int i = 0; i < size; ++i) {
+ auto metaProperty = meta->property(i + meta->propertyOffset());
+ auto metaType = metaProperty.metaType();
+ if (!metaType.isValid()) {
+ PyErr_Format(PyExc_RuntimeError, "Failed to get meta type for property %s",
+ metaProperty.name());
+ return nullptr;
+ }
+ auto *pyType = Conversions::getPythonTypeObject(metaType.name());
+ Py_INCREF(pyType);
+ PyTuple_SetItem(pyParamTypes, i, reinterpret_cast<PyObject *>(pyType));
+ }
+
+ auto *type = reinterpret_cast<PyTypeObject *>(obType);
+ PyMethodDef method = {
+ nullptr,
+ reinterpret_cast<PyCFunction>(PodDefs::CapsuleMethod_handler),
+ METH_VARARGS,
+ nullptr
+ };
+ for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) {
+ // Create a PropertyCapsule for each property to store the info needed
+ // for the handler.
+ auto metaProperty = meta->property(i);
+
+ method.ml_name = metaProperty.name();
+ auto *capsule = PyCapsule_New(new PropertyCapsule{metaProperty.name(),
+ i,
+ i - meta->propertyOffset()},
+ "PropertyCapsule",
+ [](PyObject *capsule) {
+ delete static_cast<PropertyCapsule *>(
+ PyCapsule_GetPointer(capsule, "PropertyCapsule"));
+ });
+ auto *capsulePropObject = make_capsule_property(&method, capsule);
+ if (PyObject_SetAttrString(reinterpret_cast<PyObject *>(type), metaProperty.name(),
+ capsulePropObject) < 0) {
+ return nullptr;
+ }
+
+ Py_DECREF(capsulePropObject);
+ }
+
+ // createConverter increases the ref count of type, but that will create
+ // a circular reference. When we add the capsule with the converter's pointer
+ // to the type's attributes. So we need to decrease the ref count on the type
+ // after calling createConverter.
+ auto *converter = Shiboken::Conversions::createConverter(type, cppToPython_POD_Tuple);
+ Py_DECREF(type);
+ if (set_cleanup_capsule_attr_for_pointer(type, "_converter_capsule", converter) < 0)
+ return nullptr;
+ Shiboken::Conversions::registerConverterName(converter, meta->className());
+ Shiboken::Conversions::registerConverterName(converter, type->tp_name);
+ Shiboken::Conversions::addPythonToCppValueConversion(converter, pythonToCpp_Tuple_POD,
+ is_Tuple_PythonToCpp_POD_Convertible);
+
+ static PyObject *const module = String::createStaticString("PySide6.QtRemoteObjects");
+ AutoDecRef pyQualname(String::fromCString(qualname.constData()));
+ PyObject_SetAttr(obType, PyMagicName::qualname(), pyQualname);
+ PyObject_SetAttr(obType, PyMagicName::module(), module);
+ PyObject_SetAttrString(obType, "__param_types__", pyParamTypes);
+
+ return type;
+}
diff --git a/sources/pyside6/libpysideremoteobjects/pysidedynamicpod_p.h b/sources/pyside6/libpysideremoteobjects/pysidedynamicpod_p.h
new file mode 100644
index 000000000..6dc9db9dd
--- /dev/null
+++ b/sources/pyside6/libpysideremoteobjects/pysidedynamicpod_p.h
@@ -0,0 +1,15 @@
+// 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
+
+#ifndef PYSIDE_DYNAMIC_POD_P_H
+#define PYSIDE_DYNAMIC_POD_P_H
+
+#include <sbkpython.h>
+
+#include <QtCore/qtclasshelpermacros.h>
+
+QT_FORWARD_DECLARE_STRUCT(QMetaObject)
+
+PyTypeObject *createPodType(QMetaObject *meta);
+
+#endif // PYSIDE_DYNAMIC_POD_P_H
diff --git a/sources/pyside6/libpysideremoteobjects/pysideremoteobjects.h b/sources/pyside6/libpysideremoteobjects/pysideremoteobjects.h
new file mode 100644
index 000000000..e0828c960
--- /dev/null
+++ b/sources/pyside6/libpysideremoteobjects/pysideremoteobjects.h
@@ -0,0 +1,16 @@
+// 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
+
+#ifndef PYSIDEREMOTEOBJECTS_H
+#define PYSIDEREMOTEOBJECTS_H
+
+#include <sbkpython.h>
+
+namespace PySide::RemoteObjects
+{
+
+void init(PyObject *module);
+
+} // namespace PySide::RemoteObjects
+
+#endif // PYSIDEREMOTEOBJECTS_H
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
diff --git a/sources/pyside6/libpysideremoteobjects/pysiderephandler_p.h b/sources/pyside6/libpysideremoteobjects/pysiderephandler_p.h
new file mode 100644
index 000000000..5956f0b49
--- /dev/null
+++ b/sources/pyside6/libpysideremoteobjects/pysiderephandler_p.h
@@ -0,0 +1,35 @@
+// 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
+
+#ifndef PYSIDE_REPHANDLER_P_H
+#define PYSIDE_REPHANDLER_P_H
+
+#include <sbkpython.h>
+
+#include <QtRemoteObjects/repparser.h>
+
+#include <QtCore/qstringlist.h>
+
+struct PySideRepFilePrivate
+{
+ AST ast;
+ PyObject *podDict{};
+ PyObject *replicaDict{};
+ PyObject *sourceDict{};
+ QStringList classes;
+ QStringList pods;
+};
+
+extern "C"
+{
+ extern PyTypeObject *PySideRepFile_TypeF(void);
+
+ // Internal object
+ struct PySideRepFile
+ {
+ PyObject_HEAD
+ PySideRepFilePrivate *d;
+ };
+}; // extern "C"
+
+#endif // PYSIDE_REPHANDLER_P_H
diff --git a/sources/pyside6/tests/QtRemoteObjects/CMakeLists.txt b/sources/pyside6/tests/QtRemoteObjects/CMakeLists.txt
index 2f7cb08b9..ace1a00fa 100644
--- a/sources/pyside6/tests/QtRemoteObjects/CMakeLists.txt
+++ b/sources/pyside6/tests/QtRemoteObjects/CMakeLists.txt
@@ -1 +1,11 @@
-# Please add some tests, here
+# Copyright (C) 2025 Ford Motor Company
+# SPDX-License-Identifier: BSD-3-Clause
+
+# FIXME: TypeError: Failed to generate default value. Error: name 'int' is not defined. Problematic code: int(2)
+if(NOT APPLE)
+PYSIDE_TEST(repfile_test.py)
+PYSIDE_TEST(dynamic_types_test.py)
+PYSIDE_TEST(integration_test.py)
+
+add_subdirectory(cpp_interop)
+endif()
diff --git a/sources/pyside6/tests/QtRemoteObjects/cpp_interop/CMakeLists.txt b/sources/pyside6/tests/QtRemoteObjects/cpp_interop/CMakeLists.txt
new file mode 100644
index 000000000..407a8f874
--- /dev/null
+++ b/sources/pyside6/tests/QtRemoteObjects/cpp_interop/CMakeLists.txt
@@ -0,0 +1,25 @@
+# Copyright (C) 2024 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+set(CMAKE_INCLUDE_CURRENT_DIR ON)
+
+find_package(Qt6 REQUIRED COMPONENTS Core RemoteObjects)
+
+add_executable(cpp_interop ${MOC_SOURCES} cpp_interop.cpp)
+set_target_properties(cpp_interop PROPERTIES AUTOMOC ON)
+
+target_link_libraries(cpp_interop PUBLIC
+ Qt6::Core
+ Qt6::RemoteObjects
+)
+
+# Add a custom target to build the C++ program
+add_custom_target(build_cpp_interop
+ COMMAND ${CMAKE_COMMAND} --build ${CMAKE_BINARY_DIR} --target cpp_interop
+ WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+)
+
+# Exclude windows (see cpp_interop.cpp)
+if(NOT WIN32)
+ PYSIDE_TEST(cpp_interop_test.py)
+endif() \ No newline at end of file
diff --git a/sources/pyside6/tests/QtRemoteObjects/cpp_interop/cpp_interop.cpp b/sources/pyside6/tests/QtRemoteObjects/cpp_interop/cpp_interop.cpp
new file mode 100644
index 000000000..6aeef91dd
--- /dev/null
+++ b/sources/pyside6/tests/QtRemoteObjects/cpp_interop/cpp_interop.cpp
@@ -0,0 +1,127 @@
+// Copyright (C) 2025 Ford Motor Company
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#include <QtCore/qcoreapplication.h>
+#include <QtCore/qsocketnotifier.h>
+#include <QtCore/qtimer.h>
+
+#include <QtRemoteObjects/qremoteobjectreplica.h>
+#include <QtRemoteObjects/qremoteobjectnode.h>
+
+#ifdef Q_OS_WIN
+# include <QtCore/qt_windows.h>
+# include <QtCore/qwineventnotifier.h>
+#endif // Q_OS_WIN
+
+#include <iostream>
+
+using namespace Qt::StringLiterals;
+
+class CommandReader : public QObject
+{
+ Q_OBJECT
+public:
+ explicit CommandReader(QObject *parent = nullptr) : QObject(parent)
+ {
+#ifndef Q_OS_WIN
+ auto *notifier = new QSocketNotifier(fileno(stdin), QSocketNotifier::Read, this);
+ connect(notifier, &QSocketNotifier::activated, this, &CommandReader::handleInput);
+#else
+ // FIXME: Does not work, signals triggers too often, the app is stuck in getline()
+ auto notifier = new QWinEventNotifier(GetStdHandle(STD_INPUT_HANDLE), this);
+ connect(notifier, &QWinEventNotifier::activated, this, &CommandReader::handleInput);
+#endif
+ }
+
+signals:
+ void started();
+
+private slots:
+ void handleInput()
+ {
+ std::string line;
+ if (!std::getline(std::cin, line))
+ return;
+
+ if (line == "quit") {
+ std::cerr << "harness: Received quit. Stopping harness event loop.\n";
+ QCoreApplication::quit();
+ } else if (line == "start") {
+ std::cerr << "harness: Received start. Initializing harness nodes.\n";
+ emit started();
+ } else {
+ std::cerr << "harness: Unknown command \"" << line << "\"\n";
+ }
+ }
+};
+
+class Runner : public QObject
+{
+ Q_OBJECT
+public:
+ Runner(const QUrl &url, const QString &repName, QObject *parent = nullptr)
+ : QObject(parent)
+ , m_url(url)
+ , m_repName(repName)
+ {
+ m_host.setObjectName("cpp_host");
+ if (!m_host.setHostUrl(QUrl("tcp://127.0.0.1:0"_L1))) {
+ qWarning() << "harness: setHostUrl failed: " << m_host.lastError() << m_host.hostUrl();
+ std::cerr << "harness: Fatal harness error.\n";
+ QCoreApplication::exit(-2);
+ }
+
+ m_node.setObjectName("cpp_node");
+ std::cerr << "harness: Host url:" << m_host.hostUrl().toEncoded().constData() << '\n';
+ }
+
+public slots:
+ void onStart()
+ {
+ m_node.connectToNode(m_url);
+ m_replica.reset(m_node.acquireDynamic(m_repName));
+ if (!m_replica->waitForSource(1000)) {
+ std::cerr << "harness: Failed to acquire replica.\n";
+ QCoreApplication::exit(-1);
+ }
+
+ m_host.enableRemoting(m_replica.get());
+ }
+
+private:
+ QUrl m_url;
+ QString m_repName;
+ QRemoteObjectHost m_host;
+ QRemoteObjectNode m_node;
+ std::unique_ptr<QRemoteObjectDynamicReplica> m_replica;
+};
+
+int main(int argc, char *argv[])
+{
+ QCoreApplication a(argc, argv);
+ if (argc < 3) {
+ std::cerr << "Usage: " << argv[0] << " <url> <name of type>\n";
+ return -1;
+ }
+ QUrl url = QUrl::fromUserInput(QString::fromUtf8(argv[1]));
+ QString repName = QString::fromUtf8(argv[2]);
+
+ if (!url.isValid()) {
+ std::cerr << "Invalid URL: " << argv[1] << '\n';
+ return -1;
+ }
+
+ CommandReader reader;
+ Runner runner(url, repName);
+
+
+ QRemoteObjectNode node;
+ node.setObjectName("cpp_node");
+ std::unique_ptr<QRemoteObjectDynamicReplica> replica;
+
+ QObject::connect(&reader, &CommandReader::started, &runner, &Runner::onStart);
+
+ return QCoreApplication::exec();
+}
+
+#include "cpp_interop.moc"
diff --git a/sources/pyside6/tests/QtRemoteObjects/cpp_interop/cpp_interop_test.py b/sources/pyside6/tests/QtRemoteObjects/cpp_interop/cpp_interop_test.py
new file mode 100644
index 000000000..d9ab60c23
--- /dev/null
+++ b/sources/pyside6/tests/QtRemoteObjects/cpp_interop/cpp_interop_test.py
@@ -0,0 +1,189 @@
+#!/usr/bin/python
+# Copyright (C) 2025 Ford Motor Company
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+from __future__ import annotations
+
+'''Verify Python <--> C++ interop'''
+
+import os
+import sys
+import textwrap
+
+import unittest
+
+from pathlib import Path
+sys.path.append(os.fspath(Path(__file__).resolve().parents[2])) # For init_paths
+from init_paths import init_test_paths
+init_test_paths(False)
+
+from PySide6.QtCore import QUrl, QProcess, QObject, Signal
+from PySide6.QtRemoteObjects import (QRemoteObjectHost, QRemoteObjectNode, QRemoteObjectReplica,
+ RepFile)
+from PySide6.QtTest import QSignalSpy, QTest
+
+sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) # For wrap_tests_for_cleanup
+from test_shared import wrap_tests_for_cleanup
+from helper.usesqapplication import UsesQApplication
+
+
+"""
+The previous tests all verify Remote Objects integration, but only
+using Python for both Source and Replica. We need to make sure there
+aren't any surprises in the interplay between Python and C++.
+
+This implements an initial test harness with a C++ app that is
+started by the Python unittest. We leverage the fact that Remote
+Objects can
+1) Allow remoting any QObject as a Source with enableRemoting
+2) Acquire Dynamic Replicas, where the definition needed for the
+ Replica is sent from the source.
+
+With these, we can create a working C++ app that doesn't need to be
+compiled with any information about the types being used. We have
+a host node in Python that shares a class derived from a RepFile
+Source type. The address of this node is passed to the C++ app via
+QProcess, and there a C++ node connects to that address to acquire
+(dynamically) a replica of the desired object.
+
+The C++ code also creates a host node and sends the address/port
+back to Python via the QProcess interface. Once the Python code
+receives the C++ side address and port, it connects a node to that
+URL and acquires the RepFile based type from Python.
+
+Python C++
+Host -----> Node (Dynamic acquire)
+ |
+ | Once initialized, the dynamic replica is
+ | shared (enable_remoting) from the C++ Host
+ |
+Node <----- Host
+"""
+
+
+def msg_cannot_start(process, executable):
+ return ('Cannot start "' + executable + '" in "'
+ + os.fspath(Path.cwd()) + '": ' + process.errorString())
+
+
+def stop_process(process):
+ result = process.waitForFinished(2000)
+ if not result:
+ process.kill()
+ result = process.waitForFinished(2000)
+ return result
+
+
+class Controller(QObject):
+ ready = Signal()
+
+ def __init__(self, utest: unittest.TestCase):
+ super().__init__()
+ # Store utest so we can make assertions
+ self.utest = utest
+
+ # Set up nodes
+ self.host = QRemoteObjectHost()
+ self.host.setObjectName("py_host")
+ self.host.setHostUrl(QUrl("tcp://127.0.0.1:0"))
+ self.cpp_url = None
+ self.node = QRemoteObjectNode()
+ self.node.setObjectName("py_node")
+ self._executable = "cpp_interop.exe" if os.name == "nt" else "./cpp_interop"
+
+ def start(self):
+ # Start the C++ application
+ self.process = QProcess()
+ self.process.readyReadStandardOutput.connect(self.process_harness_output)
+ self.process.readyReadStandardError.connect(self.process_harness_output)
+ urls = self.host.hostUrl().toDisplayString()
+ print(f'Starting C++ application "{self._executable}" "{urls}"', file=sys.stderr)
+ self.process.start(self._executable, [self.host.hostUrl().toDisplayString(), "Simple"])
+ self.utest.assertTrue(self.process.waitForStarted(2000),
+ msg_cannot_start(self.process, self._executable))
+
+ # Wait for the C++ application to output the host url
+ spy = QSignalSpy(self.ready)
+ self.utest.assertTrue(spy.wait(1000))
+ self.utest.assertTrue(self.cpp_url.isValid())
+
+ self.utest.assertTrue(self.node.connectToNode(self.cpp_url))
+ return True
+
+ def stop(self):
+ if self.process.state() == QProcess.ProcessState.Running:
+ print(f'Stopping C++ application "{self._executable}" {self.process.processId()}',
+ file=sys.stderr)
+ self.process.write("quit\n".encode())
+ self.process.closeWriteChannel()
+ self.utest.assertTrue(stop_process(self.process))
+ self.utest.assertEqual(self.process.exitStatus(), QProcess.ExitStatus.NormalExit)
+
+ def add_source(self, Source, Replica):
+ """
+ Source and Replica are types.
+
+ Replica is from the rep file
+ Source is a class derived from the rep file's Source type
+ """
+ self.process.write("start\n".encode())
+ source = Source()
+ self.host.enableRemoting(source)
+ replica = self.node.acquire(Replica)
+ self.utest.assertTrue(replica.waitForSource(5000))
+ self.utest.assertEqual(replica.state(), QRemoteObjectReplica.State.Valid)
+ return source, replica
+
+ def process_harness_output(self):
+ '''Process stderr from the C++ application'''
+ output = self.process.readAllStandardError().trimmed()
+ lines = output.data().decode().split("\n")
+ HOST_LINE = "harness: Host url:"
+ for line in lines:
+ print(line, file=sys.stderr)
+ if line.startswith(HOST_LINE):
+ urls = line[len(HOST_LINE):].strip()
+ print(f'url="{urls}"', file=sys.stderr)
+ self.cpp_url = QUrl(urls)
+ self.ready.emit()
+
+
+class HarnessTest(UsesQApplication):
+ def setUp(self):
+ super().setUp()
+ self.rep = RepFile(self.__class__.contents)
+ self.controller = Controller(self)
+ self.assertTrue(self.controller.start())
+
+ def tearDown(self):
+ self.controller.stop()
+ self.app.processEvents()
+ super().tearDown()
+ QTest.qWait(100) # Wait for 100 msec
+
+
+@wrap_tests_for_cleanup(extra=['rep'])
+class TestBasics(HarnessTest):
+ contents = textwrap.dedent("""\
+ class Simple
+ {
+ PROP(int i = 2);
+ PROP(float f = -1. READWRITE);
+ }
+ """)
+
+ def compare_properties(self, instance, values):
+ '''Compare properties of instance with values'''
+ self.assertEqual(instance.i, values[0])
+ self.assertAlmostEqual(instance.f, values[1], places=5)
+
+ def testInitialization(self):
+ '''Test constructing RepFile from a path string'''
+ class Source(self.rep.source["Simple"]):
+ pass
+ source, replica = self.controller.add_source(Source, self.rep.replica["Simple"])
+ self.compare_properties(source, [2, -1])
+ self.compare_properties(replica, [2, -1])
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/sources/pyside6/tests/QtRemoteObjects/dynamic_types_test.py b/sources/pyside6/tests/QtRemoteObjects/dynamic_types_test.py
new file mode 100644
index 000000000..5fb828cd2
--- /dev/null
+++ b/sources/pyside6/tests/QtRemoteObjects/dynamic_types_test.py
@@ -0,0 +1,97 @@
+#!/usr/bin/python
+# Copyright (C) 2025 Ford Motor Company
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+from __future__ import annotations
+
+'''Test cases for dynamic source/replica types'''
+
+import os
+import sys
+import unittest
+from pathlib import Path
+sys.path.append(os.fspath(Path(__file__).resolve().parents[1]))
+from init_paths import init_test_paths
+init_test_paths(False)
+
+from PySide6.QtRemoteObjects import RepFile
+
+from test_shared import wrap_tests_for_cleanup
+
+
+contents = """
+class Simple
+{
+ PROP(int i = 2);
+ PROP(float f = -1. READWRITE);
+ SIGNAL(random(int i));
+ SLOT(void reset());
+};
+"""
+
+
+@wrap_tests_for_cleanup(extra=['rep_file'])
+class QDynamicReplicas(unittest.TestCase):
+ '''Test case for dynamic Replicas'''
+
+ def setUp(self):
+ '''Set up test environment'''
+ self.rep_file = RepFile(contents)
+
+ def testDynamicReplica(self):
+ '''Verify that a valid Replica is created'''
+ Replica = self.rep_file.replica["Simple"]
+ self.assertIsNotNone(Replica)
+ replica = Replica()
+ self.assertIsNotNone(replica)
+ self.assertIsNotNone(replica.metaObject())
+ meta = replica.metaObject()
+ self.assertEqual(meta.className(), "Simple")
+ self.assertEqual(meta.superClass().className(), "QRemoteObjectReplica")
+ i = meta.indexOfProperty("i")
+ self.assertNotEqual(i, -1)
+ self.assertEqual(replica.propAsVariant(0), int(2))
+ self.assertEqual(replica.propAsVariant(1), float(-1.0))
+ self.assertEqual(replica.i, int(2))
+ self.assertEqual(replica.f, float(-1.0))
+
+
+@wrap_tests_for_cleanup(extra=['rep_file'])
+class QDynamicSources(unittest.TestCase):
+ '''Test case for dynamic Sources'''
+
+ def setUp(self):
+ '''Set up test environment'''
+ self.rep_file = RepFile(contents)
+ self.test_val = 0
+
+ def on_changed(self, val):
+ self.test_val = val
+
+ def testDynamicSource(self):
+ '''Verify that a valid Source is created'''
+ Source = self.rep_file.source["Simple"]
+ self.assertIsNotNone(Source)
+ source = Source()
+ self.assertIsNotNone(source)
+ self.assertIsNotNone(source.metaObject())
+ meta = source.metaObject()
+ self.assertEqual(meta.className(), "SimpleSource")
+ self.assertEqual(meta.superClass().className(), "QObject")
+ i = meta.indexOfProperty("i")
+ self.assertNotEqual(i, -1)
+ self.assertIsNotNone(source.__dict__.get('__PROPERTIES__'))
+ self.assertEqual(source.i, int(2))
+ self.assertEqual(source.f, float(-1.0))
+ source.iChanged.connect(self.on_changed)
+ source.fChanged.connect(self.on_changed)
+ source.i = 7
+ self.assertEqual(source.i, int(7))
+ self.assertEqual(self.test_val, int(7))
+ source.i = 3
+ self.assertEqual(self.test_val, int(3))
+ source.f = 3.14
+ self.assertAlmostEqual(self.test_val, float(3.14), places=5)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/sources/pyside6/tests/QtRemoteObjects/integration_test.py b/sources/pyside6/tests/QtRemoteObjects/integration_test.py
new file mode 100644
index 000000000..69b4930da
--- /dev/null
+++ b/sources/pyside6/tests/QtRemoteObjects/integration_test.py
@@ -0,0 +1,369 @@
+#!/usr/bin/python
+# Copyright (C) 2025 Ford Motor Company
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+from __future__ import annotations
+
+'''Test cases for basic Source/Replica communication'''
+
+import os
+import sys
+import textwrap
+import enum
+import gc
+
+import unittest
+
+from pathlib import Path
+sys.path.append(os.fspath(Path(__file__).resolve().parents[1]))
+from init_paths import init_test_paths
+init_test_paths(False)
+
+from PySide6.QtCore import QUrl, qWarning
+from PySide6.QtRemoteObjects import (QRemoteObjectHost, QRemoteObjectNode, QRemoteObjectReplica,
+ QRemoteObjectPendingCall, RepFile, getCapsuleCount)
+from PySide6.QtTest import QSignalSpy, QTest
+
+from test_shared import wrap_tests_for_cleanup
+from helper.usesqapplication import UsesQApplication
+
+contents = """
+class Simple
+{
+ PROP(int i = 2);
+ PROP(float f = -1. READWRITE);
+ SIGNAL(random(int i));
+ SLOT(void reset());
+ SLOT(int add(int i));
+};
+"""
+
+
+class QBasicTest(UsesQApplication):
+ '''Test case for basic source/replica communication'''
+ def setUp(self):
+ # Separate output to make debugging easier
+ qWarning(f"\nSet up {self.__class__.__qualname__}")
+ super().setUp()
+ '''Set up test environment'''
+ if hasattr(self.__class__, "contents"):
+ qWarning(f"Using class contents >{self.__class__.contents}<")
+ self.rep = RepFile(self.__class__.contents)
+ else:
+ self.rep = RepFile(contents)
+ self.host = QRemoteObjectHost(QUrl("tcp://127.0.0.1:0"))
+ self.host.setObjectName("host")
+ self.node = QRemoteObjectNode()
+ self.node.setObjectName("node")
+ self.node.connectToNode(self.host.hostUrl()) # pick up the url with the assigned port
+
+ def compare_properties(self, instance, values):
+ '''Compare properties of instance with values'''
+ self.assertEqual(instance.i, values[0])
+ self.assertAlmostEqual(instance.f, values[1], places=5)
+
+ def default_setup(self):
+ '''Set up default test environment'''
+ replica = self.node.acquire(self.rep.replica["Simple"])
+ # Make sure the replica is initialized with default values
+ self.compare_properties(replica, [2, -1])
+ self.assertEqual(replica.isInitialized(), False)
+ source = self.rep.source["Simple"]()
+ # Make sure the source is initialized with default values
+ self.compare_properties(source, [2, -1])
+ return replica, source
+
+ def tearDown(self):
+ self.assertEqual(getCapsuleCount(), 0)
+ self.app.processEvents()
+ super().tearDown()
+ # Separate output to make debugging easier
+ qWarning(f"Tore down {self.__class__.__qualname__}\n")
+
+
+@wrap_tests_for_cleanup(extra=['rep', 'host', 'node'])
+class ReplicaInitialization(QBasicTest):
+ def test_ReplicaInitialization(self):
+ replica, source = self.default_setup()
+ source.i = -1
+ source.f = 3.14
+ self.compare_properties(source, [-1, 3.14])
+ init_spy = QSignalSpy(replica.initialized)
+ self.host.enableRemoting(source)
+ self.assertEqual(replica.waitForSource(1000), True)
+ self.assertEqual(replica.state(), QRemoteObjectReplica.State.Valid)
+ # Make sure the replica values are updated to the source values
+ self.compare_properties(replica, [-1, 3.14])
+ self.assertEqual(init_spy.count(), 1)
+ self.assertEqual(replica.isInitialized(), True)
+
+
+@wrap_tests_for_cleanup(extra=['rep', 'host', 'node'])
+class SourcePropertyChange(QBasicTest):
+ def test_SourcePropertyChange(self):
+ replica, source = self.default_setup()
+ self.host.enableRemoting(source)
+ self.assertEqual(replica.waitForSource(1000), True)
+ # Make sure the replica values are unchanged since the source had the same values
+ self.compare_properties(replica, [2, -1])
+ source_spy = QSignalSpy(source.iChanged)
+ replica_spy = QSignalSpy(replica.iChanged)
+ source.i = 42
+ self.assertEqual(source_spy.count(), 1)
+ # Make sure the source value is updated
+ self.compare_properties(source, [42, source.f])
+ self.assertTrue(replica_spy.wait(1000))
+ self.assertEqual(replica_spy.count(), 1)
+ # Make sure the replica value is updated
+ self.compare_properties(replica, [42, replica.f])
+
+
+@wrap_tests_for_cleanup(extra=['rep', 'host', 'node'])
+class ReplicaPropertyChange(QBasicTest):
+ def test_ReplicaPropertyChange(self):
+ replica, source = self.default_setup()
+ self.host.enableRemoting(source)
+ self.assertEqual(replica.waitForSource(1000), True)
+ # Make sure push methods are working
+ source_spy = QSignalSpy(source.iChanged)
+ replica_spy = QSignalSpy(replica.iChanged)
+ replica.pushI(11)
+ # # Let eventloop run to update the source and verify the values
+ self.assertTrue(source_spy.wait(1000))
+ self.assertEqual(source_spy.count(), 1)
+ self.compare_properties(source, [11, source.f])
+ # Let eventloop run to update the replica and verify the values
+ self.assertTrue(replica_spy.wait(1000))
+ self.assertEqual(replica_spy.count(), 1)
+ self.compare_properties(replica, [11, replica.f])
+
+ # Test setter on replica
+ source_spy = QSignalSpy(source.fChanged)
+ replica_spy = QSignalSpy(replica.fChanged)
+ replica.f = 4.2
+ # Make sure the replica values are ** NOT CHANGED ** since the eventloop hasn't run
+ self.compare_properties(replica, [11, -1])
+ # Let eventloop run to update the source and verify the values
+ self.assertTrue(source_spy.wait(1000))
+ self.assertEqual(source_spy.count(), 1)
+ self.compare_properties(source, [source.i, 4.2])
+ # Let eventloop run to update the replica and verify the values
+ self.assertTrue(replica_spy.wait(1000))
+ self.assertEqual(replica_spy.count(), 1)
+ self.compare_properties(replica, [replica.i, 4.2])
+
+
+@wrap_tests_for_cleanup(extra=['rep', 'host', 'node'])
+class DerivedReplicaPropertyChange(QBasicTest):
+ def test_DerivedReplicaPropertyChange(self):
+ # Don't use default_setup(), instead create a derived replica
+ Replica = self.rep.replica["Simple"]
+
+ class DerivedReplica(Replica):
+ pass
+
+ replica = self.node.acquire(DerivedReplica)
+ # Make sure the replica is initialized with default values
+ self.compare_properties(replica, [2, -1])
+ self.assertEqual(replica.isInitialized(), False)
+ source = self.rep.source["Simple"]()
+ self.host.enableRemoting(source)
+ self.assertEqual(replica.waitForSource(1000), True)
+
+
+@wrap_tests_for_cleanup(extra=['rep', 'host', 'node'])
+class ReplicaSlotNotImplementedChange(QBasicTest):
+ def test_ReplicaSlotNotImplementedChange(self):
+ replica, source = self.default_setup()
+ self.host.enableRemoting(source)
+ self.assertEqual(replica.waitForSource(1000), True)
+ # Ideally this would fail as the slot is not implemented on the source
+ res = replica.reset()
+ self.assertEqual(type(res), type(None))
+ QTest.qWait(100) # Wait for 100 ms for async i/o. There isn't a signal to wait on
+ res = replica.add(5)
+ self.assertEqual(type(res), QRemoteObjectPendingCall)
+
+
+@wrap_tests_for_cleanup(extra=['rep', 'host', 'node'])
+class ReplicaSlotImplementedChange(QBasicTest):
+ def test_ReplicaSlotImplementedChange(self):
+ replica = self.node.acquire(self.rep.replica["Simple"])
+ replica.setObjectName("replica")
+
+ class Source(self.rep.source["Simple"]):
+ def __init__(self):
+ super().__init__()
+ self.i = 6
+ self.f = 3.14
+
+ def reset(self):
+ self.i = 0
+ self.f = 0
+
+ def add(self, i):
+ return self.i + i
+
+ source = Source()
+ source.setObjectName("source")
+ self.host.enableRemoting(source)
+ self.assertEqual(replica.waitForSource(1000), True)
+ self.compare_properties(source, [6, 3.14])
+ self.compare_properties(replica, [6, 3.14])
+ replica_spy = QSignalSpy(replica.iChanged)
+ res = replica.reset()
+ self.assertEqual(type(res), type(None))
+ self.assertEqual(replica_spy.wait(1000), True)
+ self.compare_properties(source, [0, 0])
+ self.compare_properties(replica, [0, 0])
+ res = replica.add(5)
+ self.assertEqual(type(res), QRemoteObjectPendingCall)
+ self.assertEqual(res.waitForFinished(1000), True)
+ self.assertEqual(res.returnValue(), 5)
+
+
+@wrap_tests_for_cleanup(extra=['rep', 'host', 'node'])
+class RefCountTest(QBasicTest):
+ contents = textwrap.dedent("""\
+ POD MyPOD{
+ ENUM class Position : unsigned short {position1=1, position2=2, position3=4}
+ Position pos,
+ QString name
+ }
+ class Simple
+ {
+ ENUM Position {Left, Right, Top, Bottom}
+ PROP(MyPOD myPod);
+ PROP(Position pos);
+ }
+ """)
+
+ def test_RefCount(self):
+ # Once the rep file is loaded, we should be tracking 4 converter capsules
+ # - 1 for the POD itself
+ # - 1 for the enum in the POD
+ # - 1 for the enum in the Source
+ # - 1 for the enum in the Replica
+ # We should be tracking 3 qobject capsules (POD, Replica, Source)
+ # Note: Source and Replica are distinct types, so Source::EPosition and
+ # Replica::EPosition are distinct as well.
+ # Note 2: The name of the enum ("Position") can be reused for different
+ # types in different classes as shown above.
+ self.assertEqual(getCapsuleCount(), 7)
+ MyPod = self.rep.pod["MyPOD"]
+ self.assertTrue(isinstance(MyPod, type))
+ self.assertTrue(issubclass(MyPod, tuple))
+ MyEnum = MyPod.get_enum("Position")
+ self.assertTrue(isinstance(MyEnum, type))
+ self.assertTrue(issubclass(MyEnum, enum.Enum))
+ e = MyEnum(4) # noqa: F841
+ Source = self.rep.source["Simple"]
+ source = Source() # noqa: F841
+ source = None # noqa: F841
+ Source = None
+ Replica = self.rep.replica["Simple"]
+ replica = self.node.acquire(Replica) # noqa: F841
+ replica = None # noqa: F841
+ Replica = None
+ MyEnum = None
+ MyPod = None
+ self.rep = None
+ e = None # noqa: F841
+ gc.collect()
+ # The enum and POD capsules will only be deleted (garbage collected) if
+ # the types storing them (RepFile, Replica and Source) are garbage
+ # collected first.
+ self.assertEqual(getCapsuleCount(), 0)
+
+
+@wrap_tests_for_cleanup(extra=['rep', 'host', 'node'])
+class EnumTest(QBasicTest):
+ contents = textwrap.dedent("""\
+ POD MyPOD{
+ ENUM class Position : unsigned short {position1=1, position2=2, position3=4}
+ Position pos,
+ QString name
+ }
+ class Simple
+ {
+ ENUM Position {Left, Right, Top, Bottom}
+ PROP(MyPOD myPod);
+ PROP(Position pos);
+ }
+ """)
+
+ def test_Enum(self):
+ MyPod = self.rep.pod["MyPOD"]
+ self.assertTrue(isinstance(MyPod, type))
+ self.assertTrue(issubclass(MyPod, tuple))
+ PodEnum = MyPod.get_enum("Position")
+ self.assertTrue(isinstance(PodEnum, type))
+ self.assertTrue(issubclass(PodEnum, enum.Enum))
+ t = (PodEnum(4), "test")
+ myPod = MyPod(*t)
+ with self.assertRaises(ValueError):
+ myPod = MyPod(PodEnum(0), "thing") # 0 isn't a valid enum value
+ myPod = MyPod(PodEnum(2), "thing")
+ self.assertEqual(myPod.pos, PodEnum.position2)
+ replica = self.node.acquire(self.rep.replica["Simple"])
+ replica.setObjectName("replica")
+ source = self.rep.source["Simple"]()
+ source.setObjectName("source")
+ source.myPod = (PodEnum.position2, "Hello")
+ SourceEnum = source.get_enum("Position")
+ self.assertTrue(isinstance(SourceEnum, type))
+ self.assertTrue(issubclass(SourceEnum, enum.Enum))
+ source.pos = SourceEnum.Top
+ self.assertEqual(source.myPod, (PodEnum.position2, "Hello"))
+ self.assertNotEqual(source.pos, 2)
+ self.host.enableRemoting(source)
+ self.assertEqual(replica.waitForSource(1000), True)
+ self.assertEqual(replica.myPod, (PodEnum.position2, "Hello"))
+ ReplicaEnum = replica.get_enum("Position")
+ # Test invalid comparisons
+ self.assertNotEqual(replica.pos, 2)
+ self.assertNotEqual(replica.pos, SourceEnum.Top)
+ self.assertNotEqual(replica.myPod, (SourceEnum(2), "Hello"))
+ self.assertNotEqual(replica.myPod, (ReplicaEnum(2), "Hello"))
+ self.assertNotEqual(replica.myPod, (2, "Hello"))
+ # Test valid comparisons to Replica enum
+ self.assertEqual(replica.pos, ReplicaEnum.Top)
+ self.assertEqual(replica.myPod, (PodEnum(2), "Hello"))
+
+
+@wrap_tests_for_cleanup(extra=['rep', 'host', 'node'])
+class PodTest(QBasicTest):
+ contents = textwrap.dedent("""\
+ POD MyPod(int i, QString s)
+
+ class Simple
+ {
+ PROP(MyPod pod);
+ }
+ """)
+
+ def test_Pod(self):
+ MyPod = self.rep.pod["MyPod"]
+ self.assertTrue(isinstance(MyPod, type))
+ self.assertTrue(issubclass(MyPod, tuple))
+ source = self.rep.source["Simple"]()
+ t = (42, "Hello")
+ pod = MyPod(*t)
+ source.pod = t
+ self.assertEqual(source.pod, t)
+ self.assertEqual(source.pod, pod)
+ source.pod = pod
+ self.assertEqual(source.pod, t)
+ self.assertEqual(source.pod, pod)
+ with self.assertRaises(ValueError):
+ source.pod = (11, "World", "!")
+ with self.assertRaises(TypeError):
+ source.pod = MyPod("Hello", "World")
+ self.assertEqual(source.pod, pod)
+ self.assertTrue(isinstance(pod, MyPod))
+ self.assertEqual(pod.i, 42)
+ self.assertEqual(pod.s, "Hello")
+ self.assertTrue(isinstance(source.pod, MyPod))
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/sources/pyside6/tests/QtRemoteObjects/repfile_test.py b/sources/pyside6/tests/QtRemoteObjects/repfile_test.py
new file mode 100644
index 000000000..b73c84f3a
--- /dev/null
+++ b/sources/pyside6/tests/QtRemoteObjects/repfile_test.py
@@ -0,0 +1,65 @@
+#!/usr/bin/python
+# Copyright (C) 2025 Ford Motor Company
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+from __future__ import annotations
+
+'''Test cases for RepFile'''
+
+import os
+import sys
+import unittest
+from pathlib import Path
+sys.path.append(os.fspath(Path(__file__).resolve().parents[1]))
+from init_paths import init_test_paths
+init_test_paths(False)
+from PySide6.QtRemoteObjects import RepFile
+
+from test_shared import wrap_tests_for_cleanup
+
+contents = """
+class Simple
+{
+ PROP(int i = 2);
+ PROP(float f = -1. READWRITE);
+ SIGNAL(random(int i));
+ SLOT(void reset());
+};
+"""
+
+
+@wrap_tests_for_cleanup()
+class QRepFileConstructor(unittest.TestCase):
+ '''Test case for RepFile constructors'''
+ expected = "RepFile(Classes: [Simple], PODs: [])"
+
+ def setUp(self):
+ '''Set up test environment'''
+ self.cwd = Path(__file__).parent
+ self.path = self.cwd / "simple.rep"
+
+ def testRepFileFromPath(self):
+ '''Test constructing RepFile from a path'''
+ with open(self.path, 'r') as f:
+ rep_file = RepFile(f.read())
+ self.assertEqual(str(rep_file), self.expected)
+
+ def testRepFileFromString(self):
+ '''Test constructing RepFile from a string'''
+ rep_file = RepFile(contents)
+ self.assertEqual(str(rep_file), self.expected)
+
+ def testRepFileInvalidString(self):
+ '''Test constructing RepFile from a string'''
+ with self.assertRaises(RuntimeError) as result:
+ RepFile("\n\n}\n\n")
+ self.assertEqual(str(result.exception),
+ "Error parsing input, line 3: error: Unknown token encountered")
+
+ def testRepFileNoArguments(self):
+ '''Test constructing RepFile with no arguments'''
+ with self.assertRaises(TypeError):
+ RepFile()
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/sources/pyside6/tests/QtRemoteObjects/simple.rep b/sources/pyside6/tests/QtRemoteObjects/simple.rep
new file mode 100644
index 000000000..7e801a8c6
--- /dev/null
+++ b/sources/pyside6/tests/QtRemoteObjects/simple.rep
@@ -0,0 +1,7 @@
+class Simple
+{
+ PROP(int i = 2);
+ PROP(float f = -1. READWRITE);
+ SIGNAL(random(int i));
+ SLOT(void reset());
+};
diff --git a/sources/pyside6/tests/QtRemoteObjects/test_shared.py b/sources/pyside6/tests/QtRemoteObjects/test_shared.py
new file mode 100644
index 000000000..5b176ce9d
--- /dev/null
+++ b/sources/pyside6/tests/QtRemoteObjects/test_shared.py
@@ -0,0 +1,126 @@
+# Copyright (C) 2025 Ford Motor Company
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+from __future__ import annotations
+
+import gc
+import sys
+from functools import wraps
+
+
+def _cleanup_local_variables(self, extra, debug):
+ """
+ Function to clean up local variables after a unit test.
+
+ This method will set any local variables defined in the test run to None. It also
+ sets variables of self to None, if they are provided in the extra list.
+
+ The self argument is passed by the decorator, so we can access the instance variables.
+ """
+ local_vars = self._locals
+ if debug:
+ print(f" Cleaning up locals: {local_vars.keys()} and member of self: {extra}",
+ file=sys.stderr)
+ exclude_vars = {'__builtins__', 'self', 'args', 'kwargs'}
+ for var in list(local_vars.keys()):
+ if var not in exclude_vars:
+ local_vars[var] = None
+ if debug:
+ print(f" Set {var} to None", file=sys.stderr)
+ # Remove variables added to 'self' during our test
+ for var in list(vars(self).keys()):
+ if var in extra:
+ setattr(self, var, None)
+ if debug:
+ print(f" Set self.{var} to None", file=sys.stderr)
+ gc.collect()
+
+
+# This leverages the tip from # https://stackoverflow.com/a/9187022/169296
+# for capturing local variables using sys.setprofile and a tracer function
+def wrap_tests_for_cleanup(extra: str | list[str] = None, debug: bool = False):
+ """
+ Method that returns a decorator for setting variables used in a test to
+ None, thus allowing the garbage collection to clean up properly and ensure
+ destruction behavior is correct. Using a method to return the decorator
+ allows us to pass extra arguments to the decorator, in this case for extra
+ data members on `self` to set to None or whether to output additional debug
+ logging.
+
+ It simply returns the class decorator to be used.
+ """
+ def decorator(cls):
+ """
+ This is a class decorator that finds and wraps all test methods in a
+ class.
+
+ The provided extra is used to define a set() of variables that are set
+ to None on `self` after the test method has run. This is useful for
+ making sure the local and self variables can be garbage collected.
+ """
+ _extra = set()
+ if extra:
+ if isinstance(extra, str):
+ _extra.add(extra)
+ else:
+ _extra.update(extra)
+ for name, attr in cls.__dict__.items():
+ if name.startswith("test") and callable(attr):
+ """
+ Only wrap methods that start with 'test' and are callable.
+ """
+ def make_wrapper(method):
+ """
+ This is the actual wrapper that will be used to wrap the
+ test methods. It will set a tracer function to capture the
+ local variables and then calls our cleanup function to set
+ the variables to None.
+ """
+ @wraps(method)
+ def wrapper(self, *args, **kwargs):
+ if debug:
+ print(f"wrap_tests_for_cleanup - calling {method.__name__}",
+ file=sys.stderr)
+
+ def tracer(frame, event, arg):
+ if event == 'return':
+ self._locals = frame.f_locals.copy()
+
+ # tracer is activated on next call, return or exception
+ sys.setprofile(tracer)
+ try:
+ # trace the function call
+ return method(self, *args, **kwargs)
+ finally:
+ # disable tracer and replace with old one
+ sys.setprofile(None)
+ # call our cleanup function
+ _cleanup_local_variables(self, _extra, debug)
+ if debug:
+ print(f"wrap_tests_for_cleanup - done calling {method.__name__}",
+ file=sys.stderr)
+ return wrapper
+ setattr(cls, name, make_wrapper(attr))
+ return cls
+ return decorator
+
+
+if __name__ == "__main__":
+ # Set up example test class
+ @wrap_tests_for_cleanup(extra="name", debug=True)
+ class test:
+ def __init__(self):
+ self.name = "test"
+
+ def testStuff(self):
+ value = 42
+ raise ValueError("Test")
+ temp = 11 # noqa: F841
+ return value
+
+ t = test()
+ try:
+ t.testStuff()
+ except ValueError:
+ pass
+ # Should print that `value` and `self.name` are set to None, even with the
+ # exception being raised.