Skip to content

Commit b4ec9a3

Browse files
authored
[pyqgis] add QgsSettings.enumValue and flagValue to the bindings (#7024)
* [pyqgis] add QgsSettings.enumValue and flagValue to the bindings these are done in pure Python since no implementation is possible in SIP there is a dirty hack for flags since QgsMapLayerProxyModel.Filters.__qualname__ returns 'Filters' and not 'QgsMapLayerProxyModel.Filters' * fix typo
1 parent deccf20 commit b4ec9a3

8 files changed

Lines changed: 174 additions & 11 deletions

File tree

‎python/core/__init__.py‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from .additions.qgsfeature import mapping_feature
3636
from .additions.qgsfunction import register_function, qgsfunction
3737
from .additions.qgsgeometry import _geometryNonZero, mapping_geometry
38+
from .additions.qgssettings import _qgssettings_enum_value, _qgssettings_flag_value
3839
from .additions.qgstaskwrapper import QgsTaskWrapper
3940
from .additions.readwritecontextentercategory import ReadWriteContextEnterCategory
4041

@@ -48,27 +49,36 @@
4849
QgsProcessingOutputLayerDefinition.__repr__ = processing_output_layer_repr
4950
QgsProject.blockDirtying = ProjectDirtyBlocker
5051
QgsReadWriteContext.enterCategory = ReadWriteContextEnterCategory
52+
QgsSettings.enumValue = _qgssettings_enum_value
53+
QgsSettings.flagValue = _qgssettings_flag_value
5154
QgsTask.fromFunction = fromFunction
5255

5356
# -----------------
5457
# DO NOT EDIT BELOW
5558
# These are automatically added by calling sipify.pl script
56-
QgsTolerance.UnitType.baseClass = QgsTolerance
59+
5760
QgsAuthManager.MessageLevel.baseClass = QgsAuthManager
5861
QgsDataItem.Type.baseClass = QgsDataItem
5962
QgsDataItem.State.baseClass = QgsDataItem
6063
QgsLayerItem.LayerType.baseClass = QgsLayerItem
6164
QgsDataProvider.DataCapability.baseClass = QgsDataProvider
6265
QgsDataSourceUri.SslMode.baseClass = QgsDataSourceUri
66+
QgsFieldProxyModel.Filters.baseClass = QgsFieldProxyModel
67+
Filters = QgsFieldProxyModel # dirty hack since SIP seems to introduce the flags in module
68+
QgsMapLayerProxyModel.Filters.baseClass = QgsMapLayerProxyModel
69+
Filters = QgsMapLayerProxyModel # dirty hack since SIP seems to introduce the flags in module
6370
QgsNetworkContentFetcherRegistry.FetchingMode.baseClass = QgsNetworkContentFetcherRegistry
6471
QgsSnappingConfig.SnappingMode.baseClass = QgsSnappingConfig
6572
QgsSnappingConfig.SnappingType.baseClass = QgsSnappingConfig
73+
QgsTolerance.UnitType.baseClass = QgsTolerance
6674
QgsUnitTypes.DistanceUnit.baseClass = QgsUnitTypes
6775
QgsUnitTypes.AreaUnit.baseClass = QgsUnitTypes
6876
QgsUnitTypes.AngleUnit.baseClass = QgsUnitTypes
6977
QgsUnitTypes.RenderUnit.baseClass = QgsUnitTypes
7078
QgsUnitTypes.LayoutUnit.baseClass = QgsUnitTypes
7179
QgsVectorSimplifyMethod.SimplifyHint.baseClass = QgsVectorSimplifyMethod
80+
QgsVectorSimplifyMethod.SimplifyHints.baseClass = QgsVectorSimplifyMethod
81+
SimplifyHints = QgsVectorSimplifyMethod # dirty hack since SIP seems to introduce the flags in module
7282
QgsVectorSimplifyMethod.SimplifyAlgorithm.baseClass = QgsVectorSimplifyMethod
7383
QgsRasterProjector.Precision.baseClass = QgsRasterProjector
7484
QgsAbstractGeometry.SegmentationToleranceType.baseClass = QgsAbstractGeometry

‎python/core/additions/metaenum.py‎

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,27 @@
1818
"""
1919

2020

21-
def metaEnumFromValue(enumValue, raiseException=True, baseClass=None):
22-
return metaEnumFromType(enumValue.__class__, raiseException, baseClass)
21+
def metaEnumFromValue(enumValue, baseClass=None, raiseException=True):
22+
"""
23+
Returns the QMetaEnum for an enum value.
24+
The enum must have declared using the Q_ENUM macro
25+
:param enumValue: the enum value
26+
:param baseClass: the enum base class. If not given, it will try to get it by using `enumValue.__class__.baseClass`
27+
:param raiseException: if False, no exception will be raised and None will be return in case of failure
28+
:return: the QMetaEnum if it succeeds, None otherwise
29+
"""
30+
return metaEnumFromType(enumValue.__class__, baseClass, raiseException)
2331

2432

25-
def metaEnumFromType(enumClass, raiseException=True, baseClass=None):
33+
def metaEnumFromType(enumClass, baseClass=None, raiseException=True):
34+
"""
35+
Returns the QMetaEnum for an enum type.
36+
The enum must have declared using the Q_ENUM macro
37+
:param enumClass: the enum class
38+
:param baseClass: the enum base class. If not given, it will try to get it by using `enumValue.__class__.baseClass`
39+
:param raiseException: if False, no exception will be raised and None will be return in case of failure
40+
:return: the QMetaEnum if it succeeds, None otherwise
41+
"""
2642
if enumClass == int:
2743
if raiseException:
2844
raise TypeError("enumClass is an int, while it should be an enum")
@@ -32,7 +48,7 @@ def metaEnumFromType(enumClass, raiseException=True, baseClass=None):
3248
if baseClass is None:
3349
try:
3450
baseClass = enumClass.baseClass
35-
return metaEnumFromType(enumClass, raiseException, baseClass)
51+
return metaEnumFromType(enumClass, baseClass, raiseException)
3652
except AttributeError:
3753
if raiseException:
3854
raise ValueError("Enum type does not implement baseClass method. Provide the base class as argument.")
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# -*- coding: utf-8 -*-
2+
3+
"""
4+
***************************************************************************
5+
qgssettings.py
6+
---------------------
7+
Date : May 2018
8+
Copyright : (C) 2018 by Denis Rouzaud
9+
Email : denis@opengis.ch
10+
***************************************************************************
11+
* *
12+
* This program is free software; you can redistribute it and/or modify *
13+
* it under the terms of the GNU General Public License as published by *
14+
* the Free Software Foundation; either version 2 of the License, or *
15+
* (at your option) any later version. *
16+
* *
17+
***************************************************************************
18+
"""
19+
20+
from .metaenum import metaEnumFromValue
21+
import qgis
22+
23+
24+
def _qgssettings_enum_value(self, key, enumDefaultValue, section=None):
25+
"""
26+
Return the setting value for a setting based on an enum.
27+
This forces the output to be a valid and existing entry of the enum.
28+
Hence if the setting value is incorrect, the given default value is returned.
29+
This tries first with setting as a string (as the enum) and then as an integer value.
30+
31+
:param self: the QgsSettings object
32+
:param key: the setting key
33+
:param enumDefaultValue: the default value as an enum value
34+
:param section: optional section
35+
:return: the setting value
36+
37+
.. note:: The enum needs to be declared with Q_ENUM.
38+
39+
"""
40+
if section is None:
41+
section = self.NoSection
42+
43+
meta_enum = metaEnumFromValue(enumDefaultValue)
44+
if meta_enum is None or not meta_enum.isValid():
45+
# this should not happen
46+
raise ValueError("could not get the meta enum for given enum default value (type: {})".format(type(enumDefaultValue)))
47+
48+
str_val = self.value(key, meta_enum.valueToKey(enumDefaultValue))
49+
# need a new meta enum as QgsSettings.value is making a copy and leads to seg fault (proaby a PyQt issue)
50+
meta_enum_2 = metaEnumFromValue(enumDefaultValue)
51+
(enu_val, ok) = meta_enum_2.keyToValue(str_val)
52+
53+
if not ok:
54+
enu_val = enumDefaultValue
55+
56+
return enu_val
57+
58+
59+
def _qgssettings_flag_value(self, key, flagDefaultValue, section=None):
60+
"""
61+
Return the setting value for a setting based on a flag.
62+
This forces the output to be a valid and existing entry of the enum.
63+
Hence if the setting value is incorrect, the given default value is returned.
64+
This tries first with setting as a string (as the enum) and then as an integer value.
65+
66+
:param self: the QgsSettings object
67+
:param key: the setting key
68+
:param flagDefaultValue: the default value as a flag value
69+
:param section: optional section
70+
:return: the setting value
71+
72+
.. note:: The flag needs to be declared with Q_FLAG (not Q_FLAGS).
73+
74+
"""
75+
if section is None:
76+
section = self.NoSection
77+
78+
# There is an issue in SIP, flags.__class__ does not return the proper class
79+
# (e.g. Filters instead of QgsMapLayerProxyModel.Filters)
80+
# dirty hack to get the parent class
81+
__import__(flagDefaultValue.__module__)
82+
baseClass = None
83+
exec("baseClass={module}.{flag_class}".format(module=flagDefaultValue.__module__.replace('_', ''),
84+
flag_class=flagDefaultValue.__class__.__name__))
85+
86+
meta_enum = metaEnumFromValue(flagDefaultValue, baseClass)
87+
if meta_enum is None or not meta_enum.isValid():
88+
# this should not happen
89+
raise ValueError("could not get the meta enum for given enum default value (type: {})".format(type(flagDefaultValue)))
90+
91+
str_val = self.value(key, meta_enum.valueToKey(flagDefaultValue))
92+
# need a new meta enum as QgsSettings.value is making a copy and leads to seg fault (proaby a PyQt issue)
93+
meta_enum_2 = metaEnumFromValue(flagDefaultValue)
94+
(flag_val, ok) = meta_enum_2.keysToValue(str_val)
95+
96+
if not ok:
97+
flag_val = flagDefaultValue
98+
else:
99+
flag_val = flagDefaultValue.__class__(flag_val)
100+
101+
return flag_val

‎python/gui/__init__.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,20 @@
3030
# DO NOT EDIT BELOW
3131
# These are automatically added by calling sipify.pl script
3232
QgsAuthSettingsWidget.WarningType.baseClass = QgsAuthSettingsWidget
33+
QgsAdvancedDigitizingDockWidget.CadCapacities.baseClass = QgsAdvancedDigitizingDockWidget
34+
CadCapacities = QgsAdvancedDigitizingDockWidget # dirty hack since SIP seems to introduce the flags in module
3335
QgsColorButton.Behavior.baseClass = QgsColorButton
3436
QgsColorTextWidget.ColorTextFormat.baseClass = QgsColorTextWidget
3537
QgsFilterLineEdit.ClearMode.baseClass = QgsFilterLineEdit
3638
QgsFloatingWidget.AnchorPoint.baseClass = QgsFloatingWidget
3739
QgsFontButton.Mode.baseClass = QgsFontButton
40+
QgsMapLayerAction.Targets.baseClass = QgsMapLayerAction
41+
Targets = QgsMapLayerAction # dirty hack since SIP seems to introduce the flags in module
42+
QgsMapLayerAction.Flags.baseClass = QgsMapLayerAction
43+
Flags = QgsMapLayerAction # dirty hack since SIP seems to introduce the flags in module
3844
QgsMapToolIdentify.IdentifyMode.baseClass = QgsMapToolIdentify
45+
QgsMapToolIdentify.LayerType.baseClass = QgsMapToolIdentify
46+
LayerType = QgsMapToolIdentify # dirty hack since SIP seems to introduce the flags in module
3947
QgsAttributeTableFilterModel.FilterMode.baseClass = QgsAttributeTableFilterModel
4048
QgsAttributeTableFilterModel.ColumnType.baseClass = QgsAttributeTableFilterModel
4149
QgsDualView.ViewMode.baseClass = QgsDualView

‎scripts/sipify.pl‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -612,10 +612,11 @@ sub detect_non_method_member{
612612
}
613613
next;
614614
}
615-
if ($LINE =~ m/Q_ENUM\(\s*(\w+)\s*\)/ ){
615+
if ($LINE =~ m/Q_(ENUM|FLAG)\(\s*(\w+)\s*\)/ ){
616616
if ($LINE !~ m/SIP_SKIP/){
617-
my $enum_helper = "$ACTUAL_CLASS.$1.baseClass = $ACTUAL_CLASS";
618-
dbg_info("Q_ENUM $enum_helper");
617+
my $is_flag = $1 eq 'FLAG' ? 1 : 0;
618+
my $enum_helper = "$ACTUAL_CLASS.$2.baseClass = $ACTUAL_CLASS";
619+
dbg_info("Q_ENUM/Q_FLAG $enum_helper");
619620
if ($python_output ne ''){
620621
my $pl;
621622
open(FH, '+<', $python_output) or die $!;
@@ -626,6 +627,11 @@ sub detect_non_method_member{
626627
}
627628
}
628629
if ($enum_helper ne ''){
630+
if ($is_flag == 1){
631+
# SIP seems to introduce the flags in the module rather than in the class itself
632+
# as a dirty hack, inject directly in module, hopefully we don't have flags with the same name....
633+
$enum_helper .= "\n$2 = $ACTUAL_CLASS # dirty hack since SIP seems to introduce the flags in module";
634+
}
629635
print FH "$enum_helper\n";
630636
}
631637
close(FH);

‎src/core/qgssettings.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ class CORE_EXPORT QgsSettings : public QObject
227227
* Hence if the setting value is incorrect, the given default value is returned.
228228
* This tries first with setting as a string (as the enum) and then as an integer value.
229229
* \note The enum needs to be declared with Q_ENUM, and flags with Q_FLAG (not Q_FLAGS).
230+
* \note for Python bindings, a custom implementation is achieved in Python directly
230231
* \see setEnumValue
231232
* \see flagValue
232233
*/
@@ -304,6 +305,7 @@ class CORE_EXPORT QgsSettings : public QObject
304305
* Hence if the setting value is incorrect, the given default value is returned.
305306
* This tries first with setting as a string (using a byte array) and then as an integer value.
306307
* \note The flag needs to be declared with Q_FLAG (not Q_FLAGS).
308+
* \note for Python bindings, a custom implementation is achieved in Python directly.
307309
* \see setFlagValue
308310
* \see enumValue
309311
*/

‎tests/src/python/test_core_additions.py‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,19 @@
2424

2525
class TestCoreAdditions(unittest.TestCase):
2626

27-
def testEnum(self):
27+
def testMetaEnum(self):
2828
me = metaEnumFromValue(QgsTolerance.Pixels)
2929
self.assertIsNotNone(me)
3030
self.assertEqual(me.valueToKey(QgsTolerance.Pixels), 'Pixels')
3131

3232
# if using same variable twice (e.g. me = me2), this seg faults
33-
me2 = metaEnumFromValue(QgsTolerance.Pixels, True, QgsTolerance)
33+
me2 = metaEnumFromValue(QgsTolerance.Pixels, QgsTolerance)
3434
self.assertIsNotNone(me)
3535
self.assertEqual(me2.valueToKey(QgsTolerance.Pixels), 'Pixels')
3636

37+
# do not raise error
38+
self.assertIsNone(metaEnumFromValue(1, QgsTolerance, False))
39+
3740
# do not provide an int
3841
with self.assertRaises(TypeError):
3942
metaEnumFromValue(1)

‎tests/src/python/test_qgssettings.py‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
import os
1414
import tempfile
15-
from qgis.core import (QgsSettings,)
15+
from qgis.core import QgsSettings, QgsTolerance, QgsMapLayerProxyModel
1616
from qgis.testing import start_app, unittest
1717
from qgis.PyQt.QtCore import QSettings
1818

@@ -391,6 +391,23 @@ def test_remove(self):
391391
self.settings.remove('testQgisSettings/temp', section=QgsSettings.Core)
392392
self.assertEqual(self.settings.value('testqQgisSettings/temp', section=QgsSettings.Core), None)
393393

394+
def test_enumValue(self):
395+
self.settings.setValue('enum', 'LayerUnits')
396+
self.assertEqual(self.settings.enumValue('enum', QgsTolerance.Pixels), QgsTolerance.LayerUnits)
397+
self.settings.setValue('enum', 'dummy_setting')
398+
self.assertEqual(self.settings.enumValue('enum', QgsTolerance.Pixels), QgsTolerance.Pixels)
399+
self.assertEqual(type(self.settings.enumValue('enum', QgsTolerance.Pixels)), QgsTolerance.UnitType)
400+
401+
def test_flagValue(self):
402+
pointAndLine = QgsMapLayerProxyModel.Filters(QgsMapLayerProxyModel.PointLayer | QgsMapLayerProxyModel.LineLayer)
403+
pointAndPolygon = QgsMapLayerProxyModel.Filters(QgsMapLayerProxyModel.PointLayer | QgsMapLayerProxyModel.PolygonLayer)
404+
405+
self.settings.setValue('flag', 'PointLayer|PolygonLayer')
406+
self.assertEqual(self.settings.flagValue('flag', pointAndLine), pointAndPolygon)
407+
self.settings.setValue('flag', 'dummy_setting')
408+
self.assertEqual(self.settings.flagValue('flag', pointAndLine), pointAndLine)
409+
self.assertEqual(type(self.settings.flagValue('enum', pointAndLine)), QgsMapLayerProxyModel.Filters)
410+
394411

395412
if __name__ == '__main__':
396413
unittest.main()

0 commit comments

Comments
 (0)