QgsProcessingGuiRegistry returns wrong QgsAbstractProcessingParameterWidgetWrapper class
What is the bug or the crash?
QgsGui.processingGuiRegistry().createParameterWidgetWrapper(...) is used to construct widgets that collect user inputs for a QgsProcessingAlgorithms, e.g. within a ParametersPanel of a AlgorithmDialog.
Implementing and registering a CustomStringParameterWidgetFactory(QgsProcessingParameterWidgetFactoryInterface) can (theoretically) be used to provide a customized widget wrapper. This wrapper creates specialized input widgets, which can be very helpful if algorithms require data inputs beyond those types considered in the standard QGIS API.
In a QgsProcessingAlgorithm, the use of a customized widget wrapper (in QGIS 4 API) is done as in:
def initAlgorithm(self, configuration=None):
param = QgsProcessingParameterString('TEST', 'custom string', optional=False)
metadata = param.metadata()
metadata["widget_wrapper"] = {
"widget_type": (
CustomStringParameterWidgetFactory.NAME
)
}
param.setMetadata(metadata)
self.addParameter(param)The "widget_wrapper" name string is evaluated in QgsProcessingGuiRegistry::createParameterWidgetWrapper to run the factory->createWidgetWrapper( parameter, type ); of the requested CustomStringParameterWidgetFactory.
However, if the factory (QgsProcessingParameterWidgetFactoryInterface) is defined in Python and registered to the QgsProcessingGuiRegistry, QgsProcessingGuiRegistry.createParameterWidgetWrapper(...) only returns the base-class (QgsAbstractProcessingParameterWidgetWrapper) but not the inherited Python instance.
Steps to reproduce the issue
The following python code defines a CustomStringParameterWidgetWrapper. A CustomStringParameterWidgetFactory is registered to the QgsGui.processingGuiRegistry(). test_widget_wrapper_creation tries to create a CustomStringParameterWidgetWrapper instance from (a) the factory (works) and (b) the QgsGui.processingGuiRegistry(), which fails.
File: test_qgsparameterwidgetwrapper.py
import unittest
from qgis.PyQt.QtWidgets import QLineEdit
from qgis.core import Qgis, QgsProcessingParameterString
from qgis.gui import QgsProcessingGuiRegistry
from qgis.gui import QgsProcessingParameterWidgetFactoryInterface, QgsAbstractProcessingParameterWidgetWrapper, QgsGui
from qgis.testing import start_app
start_app()
class CustomStringParameterWidgetWrapper(QgsAbstractProcessingParameterWidgetWrapper):
def __init__(self, *args, **kwds):
super().__init__(*args, **kwds)
self._widget = None
self.setObjectName('CustomWrapper')
def createWidget(self):
self._widget = QLineEdit()
return self._widget
def setWidgetValue(self, value, context):
if self._widget is not None and value is not None:
self._widget.setText(str(value))
def widgetValue(self, context):
if self._widget is not None:
return self._widget.text()
return None
class CustomStringParameterWidgetFactory(QgsProcessingParameterWidgetFactoryInterface):
NAME = 'MyParameterWidget'
CREATED_WIDGET_WRAPPERS = 0
def createWidgetWrapper(self, *args, **kwds):
w = CustomStringParameterWidgetWrapper(*args, **kwds)
CustomStringParameterWidgetFactory.CREATED_WIDGET_WRAPPERS += 1
return w
def clone(self):
return CustomStringParameterWidgetFactory()
def parameterType(self):
return self.NAME
class TestCustomParameterWidget(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls._factory = CustomStringParameterWidgetFactory()
QgsGui.processingGuiRegistry().addParameterWidgetFactory(cls._factory)
@classmethod
def tearDownClass(cls):
QgsGui.processingGuiRegistry().removeParameterWidgetFactory(cls._factory)
def test_widget_wrapper_creation(self):
"""Test creating widget wrapper for custom parameter."""
# Create parameter
param = QgsProcessingParameterString('TEST', 'custom string', optional=False)
metadata = param.metadata()
metadata["widget_wrapper"] = {
"widget_type": (
CustomStringParameterWidgetFactory.NAME
)
}
param.setMetadata(metadata)
wrapper_type = Qgis.ProcessingMode.Standard
# use CustomStringParameterWidgetFactory directly
self.assertEqual(CustomStringParameterWidgetFactory.CREATED_WIDGET_WRAPPERS, 0)
wrapper1 = self._factory.createWidgetWrapper(param, wrapper_type)
self.assertEqual(CustomStringParameterWidgetFactory.CREATED_WIDGET_WRAPPERS, 1)
self.assertEqual(wrapper1.objectName(), "CustomWrapper")
self.assertIsInstance(wrapper1, QgsAbstractProcessingParameterWidgetWrapper)
self.assertIsInstance(wrapper1, CustomStringParameterWidgetWrapper)
# use the CustomStringParameterWidgetFactory, where we have registered the CustomStringParameterWidgetFactory
# to
reg: QgsProcessingGuiRegistry = QgsGui.processingGuiRegistry()
wrapper2 = reg.createParameterWidgetWrapper(param, wrapper_type)
self.assertEqual(CustomStringParameterWidgetFactory.CREATED_WIDGET_WRAPPERS, 2)
self.assertEqual(wrapper2.objectName(), "CustomWrapper")
self.assertIsInstance(wrapper2, QgsAbstractProcessingParameterWidgetWrapper)
# this fails, because QgsProcessingGuiRegistry returns the base class
# QgsAbstractProcessingParameterWidgetWrapper only
self.assertIsInstance(wrapper2, CustomStringParameterWidgetWrapper)
if __name__ == '__main__':
unittest.main()
Run this test, e.g. using the official QGIS docker images:
#!/bin/bash
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
docker run --rm \
--network host \
--user "$(id -u):$(id -g)" \
--workdir /workspace \
-e "PYTHONPATH=/usr/share/qgis/python:/usr/share/qgis/python/plugins" \
-e QT_QPA_PLATFORM=offscreen \
-v "${SCRIPT_DIR}:/workspace" \
-v "${HOME}:${HOME}" \
-v /tmp:/tmp \
qgis/qgis:stable \
python3 test_qgsparameterwidgetwrapper.py
Versions
QGIS: 4.3.0-Master d3b565c628d (Linux, qgis/qgis:latest)
Supported QGIS version
- I'm running a supported QGIS version according to the roadmap.
New profile
- I tried with a new QGIS profile
No Duplicate of the Issue
- I have verified that there are no existing issues raised related to my problem.
Additional context
This issue is likely related to #48816, which used an old (< QGIS 4.0) way to define custom widget wrapper.
Source: qgis/QGIS