Skip to content

Commit

Permalink
[pyqgis] add QgsSettings.enumValue and flagValue to the bindings (#7024)
Browse files Browse the repository at this point in the history
* [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
  • Loading branch information
3nids authored May 18, 2018
1 parent deccf20 commit b4ec9a3
Show file tree
Hide file tree
Showing 8 changed files with 174 additions and 11 deletions.
12 changes: 11 additions & 1 deletion python/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from .additions.qgsfeature import mapping_feature
from .additions.qgsfunction import register_function, qgsfunction
from .additions.qgsgeometry import _geometryNonZero, mapping_geometry
from .additions.qgssettings import _qgssettings_enum_value, _qgssettings_flag_value
from .additions.qgstaskwrapper import QgsTaskWrapper
from .additions.readwritecontextentercategory import ReadWriteContextEnterCategory

Expand All @@ -48,27 +49,36 @@
QgsProcessingOutputLayerDefinition.__repr__ = processing_output_layer_repr
QgsProject.blockDirtying = ProjectDirtyBlocker
QgsReadWriteContext.enterCategory = ReadWriteContextEnterCategory
QgsSettings.enumValue = _qgssettings_enum_value
QgsSettings.flagValue = _qgssettings_flag_value
QgsTask.fromFunction = fromFunction

# -----------------
# DO NOT EDIT BELOW
# These are automatically added by calling sipify.pl script
QgsTolerance.UnitType.baseClass = QgsTolerance

QgsAuthManager.MessageLevel.baseClass = QgsAuthManager
QgsDataItem.Type.baseClass = QgsDataItem
QgsDataItem.State.baseClass = QgsDataItem
QgsLayerItem.LayerType.baseClass = QgsLayerItem
QgsDataProvider.DataCapability.baseClass = QgsDataProvider
QgsDataSourceUri.SslMode.baseClass = QgsDataSourceUri
QgsFieldProxyModel.Filters.baseClass = QgsFieldProxyModel
Filters = QgsFieldProxyModel # dirty hack since SIP seems to introduce the flags in module
QgsMapLayerProxyModel.Filters.baseClass = QgsMapLayerProxyModel
Filters = QgsMapLayerProxyModel # dirty hack since SIP seems to introduce the flags in module
QgsNetworkContentFetcherRegistry.FetchingMode.baseClass = QgsNetworkContentFetcherRegistry
QgsSnappingConfig.SnappingMode.baseClass = QgsSnappingConfig
QgsSnappingConfig.SnappingType.baseClass = QgsSnappingConfig
QgsTolerance.UnitType.baseClass = QgsTolerance
QgsUnitTypes.DistanceUnit.baseClass = QgsUnitTypes
QgsUnitTypes.AreaUnit.baseClass = QgsUnitTypes
QgsUnitTypes.AngleUnit.baseClass = QgsUnitTypes
QgsUnitTypes.RenderUnit.baseClass = QgsUnitTypes
QgsUnitTypes.LayoutUnit.baseClass = QgsUnitTypes
QgsVectorSimplifyMethod.SimplifyHint.baseClass = QgsVectorSimplifyMethod
QgsVectorSimplifyMethod.SimplifyHints.baseClass = QgsVectorSimplifyMethod
SimplifyHints = QgsVectorSimplifyMethod # dirty hack since SIP seems to introduce the flags in module
QgsVectorSimplifyMethod.SimplifyAlgorithm.baseClass = QgsVectorSimplifyMethod
QgsRasterProjector.Precision.baseClass = QgsRasterProjector
QgsAbstractGeometry.SegmentationToleranceType.baseClass = QgsAbstractGeometry
Expand Down
24 changes: 20 additions & 4 deletions python/core/additions/metaenum.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,27 @@
"""


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


def metaEnumFromType(enumClass, raiseException=True, baseClass=None):
def metaEnumFromType(enumClass, baseClass=None, raiseException=True):
"""
Returns the QMetaEnum for an enum type.
The enum must have declared using the Q_ENUM macro
:param enumClass: the enum class
:param baseClass: the enum base class. If not given, it will try to get it by using `enumValue.__class__.baseClass`
:param raiseException: if False, no exception will be raised and None will be return in case of failure
:return: the QMetaEnum if it succeeds, None otherwise
"""
if enumClass == int:
if raiseException:
raise TypeError("enumClass is an int, while it should be an enum")
Expand All @@ -32,7 +48,7 @@ def metaEnumFromType(enumClass, raiseException=True, baseClass=None):
if baseClass is None:
try:
baseClass = enumClass.baseClass
return metaEnumFromType(enumClass, raiseException, baseClass)
return metaEnumFromType(enumClass, baseClass, raiseException)
except AttributeError:
if raiseException:
raise ValueError("Enum type does not implement baseClass method. Provide the base class as argument.")
Expand Down
101 changes: 101 additions & 0 deletions python/core/additions/qgssettings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# -*- coding: utf-8 -*-

"""
***************************************************************************
qgssettings.py
---------------------
Date : May 2018
Copyright : (C) 2018 by Denis Rouzaud
Email : denis@opengis.ch
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************
"""

from .metaenum import metaEnumFromValue
import qgis


def _qgssettings_enum_value(self, key, enumDefaultValue, section=None):
"""
Return the setting value for a setting based on an enum.
This forces the output to be a valid and existing entry of the enum.
Hence if the setting value is incorrect, the given default value is returned.
This tries first with setting as a string (as the enum) and then as an integer value.
:param self: the QgsSettings object
:param key: the setting key
:param enumDefaultValue: the default value as an enum value
:param section: optional section
:return: the setting value
.. note:: The enum needs to be declared with Q_ENUM.
"""
if section is None:
section = self.NoSection

meta_enum = metaEnumFromValue(enumDefaultValue)
if meta_enum is None or not meta_enum.isValid():
# this should not happen
raise ValueError("could not get the meta enum for given enum default value (type: {})".format(type(enumDefaultValue)))

str_val = self.value(key, meta_enum.valueToKey(enumDefaultValue))
# need a new meta enum as QgsSettings.value is making a copy and leads to seg fault (proaby a PyQt issue)
meta_enum_2 = metaEnumFromValue(enumDefaultValue)
(enu_val, ok) = meta_enum_2.keyToValue(str_val)

if not ok:
enu_val = enumDefaultValue

return enu_val


def _qgssettings_flag_value(self, key, flagDefaultValue, section=None):
"""
Return the setting value for a setting based on a flag.
This forces the output to be a valid and existing entry of the enum.
Hence if the setting value is incorrect, the given default value is returned.
This tries first with setting as a string (as the enum) and then as an integer value.
:param self: the QgsSettings object
:param key: the setting key
:param flagDefaultValue: the default value as a flag value
:param section: optional section
:return: the setting value
.. note:: The flag needs to be declared with Q_FLAG (not Q_FLAGS).
"""
if section is None:
section = self.NoSection

# There is an issue in SIP, flags.__class__ does not return the proper class
# (e.g. Filters instead of QgsMapLayerProxyModel.Filters)
# dirty hack to get the parent class
__import__(flagDefaultValue.__module__)
baseClass = None
exec("baseClass={module}.{flag_class}".format(module=flagDefaultValue.__module__.replace('_', ''),
flag_class=flagDefaultValue.__class__.__name__))

meta_enum = metaEnumFromValue(flagDefaultValue, baseClass)
if meta_enum is None or not meta_enum.isValid():
# this should not happen
raise ValueError("could not get the meta enum for given enum default value (type: {})".format(type(flagDefaultValue)))

str_val = self.value(key, meta_enum.valueToKey(flagDefaultValue))
# need a new meta enum as QgsSettings.value is making a copy and leads to seg fault (proaby a PyQt issue)
meta_enum_2 = metaEnumFromValue(flagDefaultValue)
(flag_val, ok) = meta_enum_2.keysToValue(str_val)

if not ok:
flag_val = flagDefaultValue
else:
flag_val = flagDefaultValue.__class__(flag_val)

return flag_val
8 changes: 8 additions & 0 deletions python/gui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,20 @@
# DO NOT EDIT BELOW
# These are automatically added by calling sipify.pl script
QgsAuthSettingsWidget.WarningType.baseClass = QgsAuthSettingsWidget
QgsAdvancedDigitizingDockWidget.CadCapacities.baseClass = QgsAdvancedDigitizingDockWidget
CadCapacities = QgsAdvancedDigitizingDockWidget # dirty hack since SIP seems to introduce the flags in module
QgsColorButton.Behavior.baseClass = QgsColorButton
QgsColorTextWidget.ColorTextFormat.baseClass = QgsColorTextWidget
QgsFilterLineEdit.ClearMode.baseClass = QgsFilterLineEdit
QgsFloatingWidget.AnchorPoint.baseClass = QgsFloatingWidget
QgsFontButton.Mode.baseClass = QgsFontButton
QgsMapLayerAction.Targets.baseClass = QgsMapLayerAction
Targets = QgsMapLayerAction # dirty hack since SIP seems to introduce the flags in module
QgsMapLayerAction.Flags.baseClass = QgsMapLayerAction
Flags = QgsMapLayerAction # dirty hack since SIP seems to introduce the flags in module
QgsMapToolIdentify.IdentifyMode.baseClass = QgsMapToolIdentify
QgsMapToolIdentify.LayerType.baseClass = QgsMapToolIdentify
LayerType = QgsMapToolIdentify # dirty hack since SIP seems to introduce the flags in module
QgsAttributeTableFilterModel.FilterMode.baseClass = QgsAttributeTableFilterModel
QgsAttributeTableFilterModel.ColumnType.baseClass = QgsAttributeTableFilterModel
QgsDualView.ViewMode.baseClass = QgsDualView
12 changes: 9 additions & 3 deletions scripts/sipify.pl
Original file line number Diff line number Diff line change
Expand Up @@ -612,10 +612,11 @@ sub detect_non_method_member{
}
next;
}
if ($LINE =~ m/Q_ENUM\(\s*(\w+)\s*\)/ ){
if ($LINE =~ m/Q_(ENUM|FLAG)\(\s*(\w+)\s*\)/ ){
if ($LINE !~ m/SIP_SKIP/){
my $enum_helper = "$ACTUAL_CLASS.$1.baseClass = $ACTUAL_CLASS";
dbg_info("Q_ENUM $enum_helper");
my $is_flag = $1 eq 'FLAG' ? 1 : 0;
my $enum_helper = "$ACTUAL_CLASS.$2.baseClass = $ACTUAL_CLASS";
dbg_info("Q_ENUM/Q_FLAG $enum_helper");
if ($python_output ne ''){
my $pl;
open(FH, '+<', $python_output) or die $!;
Expand All @@ -626,6 +627,11 @@ sub detect_non_method_member{
}
}
if ($enum_helper ne ''){
if ($is_flag == 1){
# SIP seems to introduce the flags in the module rather than in the class itself
# as a dirty hack, inject directly in module, hopefully we don't have flags with the same name....
$enum_helper .= "\n$2 = $ACTUAL_CLASS # dirty hack since SIP seems to introduce the flags in module";
}
print FH "$enum_helper\n";
}
close(FH);
Expand Down
2 changes: 2 additions & 0 deletions src/core/qgssettings.h
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ class CORE_EXPORT QgsSettings : public QObject
* Hence if the setting value is incorrect, the given default value is returned.
* This tries first with setting as a string (as the enum) and then as an integer value.
* \note The enum needs to be declared with Q_ENUM, and flags with Q_FLAG (not Q_FLAGS).
* \note for Python bindings, a custom implementation is achieved in Python directly
* \see setEnumValue
* \see flagValue
*/
Expand Down Expand Up @@ -304,6 +305,7 @@ class CORE_EXPORT QgsSettings : public QObject
* Hence if the setting value is incorrect, the given default value is returned.
* This tries first with setting as a string (using a byte array) and then as an integer value.
* \note The flag needs to be declared with Q_FLAG (not Q_FLAGS).
* \note for Python bindings, a custom implementation is achieved in Python directly.
* \see setFlagValue
* \see enumValue
*/
Expand Down
7 changes: 5 additions & 2 deletions tests/src/python/test_core_additions.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,19 @@

class TestCoreAdditions(unittest.TestCase):

def testEnum(self):
def testMetaEnum(self):
me = metaEnumFromValue(QgsTolerance.Pixels)
self.assertIsNotNone(me)
self.assertEqual(me.valueToKey(QgsTolerance.Pixels), 'Pixels')

# if using same variable twice (e.g. me = me2), this seg faults
me2 = metaEnumFromValue(QgsTolerance.Pixels, True, QgsTolerance)
me2 = metaEnumFromValue(QgsTolerance.Pixels, QgsTolerance)
self.assertIsNotNone(me)
self.assertEqual(me2.valueToKey(QgsTolerance.Pixels), 'Pixels')

# do not raise error
self.assertIsNone(metaEnumFromValue(1, QgsTolerance, False))

# do not provide an int
with self.assertRaises(TypeError):
metaEnumFromValue(1)
Expand Down
19 changes: 18 additions & 1 deletion tests/src/python/test_qgssettings.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

import os
import tempfile
from qgis.core import (QgsSettings,)
from qgis.core import QgsSettings, QgsTolerance, QgsMapLayerProxyModel
from qgis.testing import start_app, unittest
from qgis.PyQt.QtCore import QSettings

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

def test_enumValue(self):
self.settings.setValue('enum', 'LayerUnits')
self.assertEqual(self.settings.enumValue('enum', QgsTolerance.Pixels), QgsTolerance.LayerUnits)
self.settings.setValue('enum', 'dummy_setting')
self.assertEqual(self.settings.enumValue('enum', QgsTolerance.Pixels), QgsTolerance.Pixels)
self.assertEqual(type(self.settings.enumValue('enum', QgsTolerance.Pixels)), QgsTolerance.UnitType)

def test_flagValue(self):
pointAndLine = QgsMapLayerProxyModel.Filters(QgsMapLayerProxyModel.PointLayer | QgsMapLayerProxyModel.LineLayer)
pointAndPolygon = QgsMapLayerProxyModel.Filters(QgsMapLayerProxyModel.PointLayer | QgsMapLayerProxyModel.PolygonLayer)

self.settings.setValue('flag', 'PointLayer|PolygonLayer')
self.assertEqual(self.settings.flagValue('flag', pointAndLine), pointAndPolygon)
self.settings.setValue('flag', 'dummy_setting')
self.assertEqual(self.settings.flagValue('flag', pointAndLine), pointAndLine)
self.assertEqual(type(self.settings.flagValue('enum', pointAndLine)), QgsMapLayerProxyModel.Filters)


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

0 comments on commit b4ec9a3

Please sign in to comment.