Date 2017/01/17
Author Alessandro Pasotti (@elpaso)
Contact apasotti at boundlessgeo dot com
maintainer @elpaso
Version QGIS 3.x
This QEP proposes to move all default settings outside C++ code into a (configurable) ini file.
The motivation is doublefold:
- hard-coding configuration in C++ code makes maintainance harder
- sysadmins in large organizations need an easy way to customize
or pre-configure the application settings before deployment, this includes
for example:
- plugin repositories
- WMS/WFS endpoints
- pre-installed and/or pre-enabled plugins
- default and preferred folders
There is also another useful side-effect of having our own implementation of the settings: we could easily manipulate the settings values if we need to, for instance by applying some sort of variable substitution.
The actual QGIS setting implementation is based on QSettings
.
In order to provide defaults and/or initial configurations, inline values
are provided inside the C++ code, such as in
void QgisApp::reportaBug()
{
QSettings settings;
QString reportaBugUrl = settings.value( "Qgis/reportaBugUrl", tr( "https://qgis.org/en/site/getinvolved/development/bugreporting.html" ) ).toString();
openURL( reportaBugUrl, false );
}
In other parts of the codebase, some logic is implemented in order to populate the settings with default values, for example this happens in plugin manager when the official plugin repo is added, or (following a user action) in WMS for example servers.
Introduce a new QgsSettings
class that subclasses the actual QSettings
but
can optionally provide default values taken from another ini file.
Values contained in the "default" ini file will provide the initial configuration and override possible inline defaults.
The override will work in a similar fashion like in CSSs: a setting value in the default ini file will be overridden by a user setting.
The default ini file path will be searched in a standard location that will be overridable through an environment variable.
For example:
; defaults
[group]
key=value1
; user settings
[group]
key=value2
; will return key=value2
Groups will be inheritable, for example:
; defaults
[group]
key1=value1
key2=value2
; user settings
[group]
key3=value2
; a call to allKeys() will return [key1, key2, key3]
A secondary effect of the proposed implementation is the possibility to
add some "magic" (like variable replacements) inside the QgsSettings
class.
The subclass solution has the advantage of being a drop-in replacement of
QSettings
while maintaining the same API, but it has some disadvantages:
for example it would not be easy (without a singleton) to pass the path of
the default file from the QGIS application (i.e. implement a --defaultsinipath
command line option).
An alternate implementation would be adding a QgsSettings
instance to
QgsApplication
and use a new method (settings()
) to access the
QgsSettings
instance from QGIS and plugins.
"""
QSettings subclass that reads default values from an ini file
"""
import os
import unittest
from PyQt5.QtCore import QSettings
class QgsSettings(QSettings):
"""
A QSettings subclass that reads default values from an ini file
FIXME: this is a partial implementation: many methods are missing
"""
defaults = None
def __init__(self, orgname, appname, defaults_file=None):
super(QgsSettings, self).__init__(orgname, appname)
if self.defaults is None:
# Note: it will be SystemScope in the real implementation,
# here it is UserScope for testing
if defaults_file is not None:
if os.path.isfile(defaults_file):
self.defaults = QSettings(defaults_file, QSettings.IniFormat)
else:
self.defaults = None
else:
self.defaults = QSettings(QSettings.IniFormat, QSettings.UserScope, orgname, appname + '_defaults')
def beginGroup(self, group):
self.group = group
self.defaults.beginGroup(group)
super(QgsSettings, self).beginGroup(group)
def endGroup(self):
self.group = None
self.defaults.endGroup()
super(QgsSettings, self).endGroup()
def allKeys(self):
keys = set(super(QgsSettings, self).allKeys())
keys = keys.union(self.defaults.allKeys())
return keys
def childKeys(self):
keys = set(super(QgsSettings, self).childKeys())
keys = keys.union(self.defaults.childKeys())
return keys
def childGroups(self):
groups = set(super(QgsSettings, self).childGroups())
groups = groups.union(self.defaults.childGroups())
return groups
def value(self, key, default=None):
value = super(QgsSettings, self).value(key)
if value is None and self.defaults is not None:
value = self.defaults.value(key, default)
return value
class TestQgsSettings(unittest.TestCase):
cnt = 0
def setUp(self):
self.cnt += 1
self.settings = QgsSettings('itopen', 'itopen%s' % self.cnt)
def tearDown(self):
settings_file = self.settings.fileName()
settings_default_file = self.settings.defaults.fileName()
del(self.settings)
try:
os.unlink(settings_file)
except:
pass
try:
os.unlink(settings_default_file)
except:
pass
def addToDefaults(self, key, value):
defaults = QSettings(self.settings.defaults.fileName(), QSettings.IniFormat)
defaults.setValue(key, value)
defaults.sync()
def test_basic_functionality(self):
self.settings.setValue('itopen/name', 'elpaso')
self.settings.sync()
self.assertEqual(self.settings.value('itopen/name'), 'elpaso')
def test_defaults(self):
self.assertIsNone(self.settings.value('itopen/name'))
self.addToDefaults('itopen/name', 'elpaso')
self.assertEqual(self.settings.value('itopen/name'), 'elpaso')
def test_allkeys(self):
self.assertEqual(self.settings.allKeys(), set())
self.addToDefaults('itopen/name', 'elpaso')
self.settings.setValue('nepoti/eman', 'osaple')
self.assertEqual(2, len(self.settings.allKeys()))
self.assertIn('itopen/name', self.settings.allKeys())
self.assertIn('nepoti/eman', self.settings.allKeys())
self.assertEqual('elpaso', self.settings.value('itopen/name'))
self.assertEqual('osaple', self.settings.value('nepoti/eman'))
def test_precedence(self):
self.assertEqual(self.settings.allKeys(), set())
self.addToDefaults('itopen/names/name1', 'elpaso1')
self.settings.setValue('itopen/names/name1', 'elpaso-1')
self.assertEqual(self.settings.value('itopen/names/name1'), 'elpaso-1')
def test_groups(self):
self.assertEqual(self.settings.allKeys(), set())
self.addToDefaults('itopen/names/name1', 'elpaso1')
self.addToDefaults('itopen/names/name2', 'elpaso2')
self.addToDefaults('itopen/names/name3', 'elpaso3')
self.addToDefaults('itopen/name', 'elpaso')
self.settings.beginGroup('itopen')
self.assertEquals({'names'}, self.settings.childGroups())
self.settings.setValue('surnames/name1', 'elpaso-1')
self.assertEquals({'names', 'surnames'}, self.settings.childGroups())
self.settings.setValue('names/name1', 'elpaso-1')
self.assertEqual('elpaso-1', self.settings.value('names/name1'))
self.settings.endGroup()
self.settings.beginGroup('itopen/names')
self.settings.setValue('name4', 'elpaso-4')
keys = list(self.settings.childKeys())
keys.sort()
self.assertEqual(keys, ['name1', 'name2', 'name3', 'name4'])
self.settings.endGroup()
self.assertEqual('elpaso-1', self.settings.value('itopen/names/name1'))
self.assertEqual('elpaso-4', self.settings.value('itopen/names/name4'))
if __name__ == '__main__':
unittest.main()
The QGIS project will provide such a file with all the necessary entries to maintain the current behavior of the application but it will be possible for packagers to ship different default setting files and for sysadmins to provide a default configuration file.
/*
; defaults
[group]
key1=value1
key2=value2
[group2]
key4=value4
; user settings
[group]
key1=value-1
key3=value-3
*/
QgsSettings settings;
settings.value('group/key1', 'my value 1') == "value-1"; // true
settings.value('group/key3', 'my value 3') == "value-3"; // true
settings.value('group2/key4', 'my value 4') == "value4"; // true
settings.value('notexists/key8', 'my value 8') == "my value 8"; // true
All files where QSettings
is used (and there are many)
Probably negligible, provided that there are no setting read in loops (that would probably be a bad design anyway).
No problems are expectedin this regard.
(required)
Thanks @elpaso
I see that a
QgsSettings
class already exist insrc/core/qgssettings.cpp
.Can you write a quick update on this QEP? I would like to add some default values (a few URL of slippy maps to let them be added quickly from the browser panel). Should I add those URL in C++ (as usual) or can I use some ini file already?