Merge branch 'dev/improvedPluginSettingsSaving' into devel

This commit is contained in:
Gina Häußge 2015-10-22 11:08:44 +02:00
commit 279f4014a6
7 changed files with 719 additions and 42 deletions

View file

@ -295,19 +295,16 @@ class PluginSettings(object):
self.set_preprocessors = dict(plugins=dict())
self.set_preprocessors["plugins"][plugin_key] = set_preprocessors
def prefix_path(path):
return ['plugins', self.plugin_key] + path
def prefix_path_in_args(args, index=0):
result = []
if index == 0:
result.append(prefix_path(args[0]))
result.append(self._prefix_path(args[0]))
result.extend(args[1:])
else:
args_before = args[:index - 1]
args_after = args[index + 1:]
result.extend(args_before)
result.append(prefix_path(args[index]))
result.append(self._prefix_path(args[index]))
result.extend(args_after)
return result
@ -326,6 +323,7 @@ class PluginSettings(object):
return kwargs
self.access_methods = dict(
has =("has", prefix_path_in_args, add_getter_kwargs),
get =("get", prefix_path_in_args, add_getter_kwargs),
get_int =("getInt", prefix_path_in_args, add_getter_kwargs),
get_float =("getFloat", prefix_path_in_args, add_getter_kwargs),
@ -333,7 +331,8 @@ class PluginSettings(object):
set =("set", prefix_path_in_args, add_setter_kwargs),
set_int =("setInt", prefix_path_in_args, add_setter_kwargs),
set_float =("setFloat", prefix_path_in_args, add_setter_kwargs),
set_boolean=("setBoolean", prefix_path_in_args, add_setter_kwargs)
set_boolean=("setBoolean", prefix_path_in_args, add_setter_kwargs),
remove =("remove", prefix_path_in_args)
)
self.deprecated_access_methods = dict(
getInt ="get_int",
@ -344,6 +343,17 @@ class PluginSettings(object):
setBoolean="set_boolean"
)
def _prefix_path(self, path=None):
if path is None:
path = list()
return ['plugins', self.plugin_key] + path
def global_has(self, path, **kwargs):
return self.settings.has(path, **kwargs)
def global_remove(self, path, **kwargs):
return self.settings.remove(path, **kwargs)
def global_get(self, path, **kwargs):
"""
Getter for retrieving settings not managed by the plugin itself from the core settings structure. Use this
@ -437,6 +447,24 @@ class PluginSettings(object):
os.makedirs(path)
return path
def get_all_data(self, **kwargs):
merged = kwargs.get("merged", True)
asdict = kwargs.get("asdict", True)
defaults = kwargs.get("defaults", self.defaults)
preprocessors = kwargs.get("preprocessors", self.get_preprocessors)
kwargs.update(dict(
merged=merged,
asdict=asdict,
defaults=defaults,
preprocessors=preprocessors
))
return self.settings.get(self._prefix_path(), **kwargs)
def clean_all_data(self):
self.settings.remove(self._prefix_path())
def __getattr__(self, item):
all_access_methods = self.access_methods.keys() + self.deprecated_access_methods.keys()
if item in all_access_methods:

View file

@ -1082,18 +1082,34 @@ class BlueprintPlugin(OctoPrintPlugin, RestartNeedingPlugin):
:return: the blueprint ready to be registered with Flask
"""
if hasattr(self, "_blueprint"):
# if we already constructed the blueprint and hence have it cached,
# return that instance - we don't want to instance it multiple times
return self._blueprint
import flask
kwargs = self.get_blueprint_kwargs()
blueprint = flask.Blueprint("plugin." + self._identifier, self._identifier, **kwargs)
# we now iterate over all members of ourselves and look if we find an attribute
# that has data originating from one of our decorators - we ignore anything
# starting with a _ to only handle public stuff
for member in [member for member in dir(self) if not member.startswith("_")]:
f = getattr(self, member)
if hasattr(f, "_blueprint_rules") and member in f._blueprint_rules:
# this attribute was annotated with our @route decorator
for blueprint_rule in f._blueprint_rules[member]:
rule, options = blueprint_rule
blueprint.add_url_rule(rule, options.pop("endpoint", f.__name__), view_func=f, **options)
if hasattr(f, "_blueprint_error_handler") and member in f._blueprint_error_handler:
# this attribute was annotated with our @error_handler decorator
for code_or_exception in f._blueprint_error_handler[member]:
blueprint.errorhandler(code_or_exception)(f)
# cache and return the blueprint object
self._blueprint = blueprint
return blueprint
def get_blueprint_kwargs(self):
@ -1189,6 +1205,9 @@ class SettingsPlugin(OctoPrintPlugin):
the plugin core system upon initialization of the implementation.
"""
config_version_key = "_config_version"
"""Key of the field in the settings that holds the configuration format version."""
def on_settings_load(self):
"""
Loads the settings for the plugin, called by the Settings API view in order to retrieve all settings from
@ -1206,9 +1225,9 @@ class SettingsPlugin(OctoPrintPlugin):
:return: the current settings of the plugin, as a dictionary
"""
data = self._settings.get([], asdict=True, merged=True)
if "_config_version" in data:
del data["_config_version"]
data = self._settings.get_all_data()
if self.config_version_key in data:
del data[self.config_version_key]
return data
def on_settings_save(self, data):
@ -1220,7 +1239,8 @@ class SettingsPlugin(OctoPrintPlugin):
.. note::
The default implementation will persist your plugin's settings as is, so just in the structure and in the
types that were received by the Settings API view.
types that were received by the Settings API view. Values identical to the default settings values
will *not* be persisted.
If you need more granular control here, e.g. over the used data types, you'll need to override this method
and iterate yourself over all your settings, retrieving them (if set) from the supplied received ``data``
@ -1228,15 +1248,38 @@ class SettingsPlugin(OctoPrintPlugin):
Arguments:
data (dict): The settings dictionary to be saved for the plugin
Returns:
dict: The settings that differed from the defaults and were actually saved.
"""
import octoprint.util
if "_config_version" in data:
del data["_config_version"]
# get the current data
current = self._settings.get_all_data()
if current is None:
current = dict()
current = self._settings.get([], asdict=True, merged=True)
merged = octoprint.util.dict_merge(current, data)
self._settings.set([], merged)
# merge our new data on top of it
new_current = octoprint.util.dict_merge(current, data)
if self.config_version_key in new_current:
del new_current[self.config_version_key]
# determine diff dict that contains minimal set of changes against the
# default settings - we only want to persist that, not everything
diff = octoprint.util.dict_diff(self.get_settings_defaults(), new_current)
version = self.get_settings_version()
to_persist = dict(diff)
if version:
to_persist[self.config_version_key] = version
if to_persist:
self._settings.set([], to_persist)
else:
self._settings.clean_all_data()
return diff
def get_settings_defaults(self):
"""
@ -1319,6 +1362,49 @@ class SettingsPlugin(OctoPrintPlugin):
"""
pass
def on_settings_cleanup(self):
"""
Called after migration and initialization but before call to :func:`on_settings_initialized`.
Plugins may overwrite this method to perform additional clean up tasks.
The default implementation just minimizes the data persisted on disk to only contain
the differences to the defaults (in case the current data was persisted with an older
version of OctoPrint that still duplicated default data).
"""
import octoprint.util
from octoprint.settings import NoSuchSettingsPath
try:
# let's fetch the current persisted config (so only the data on disk,
# without the defaults)
config = self._settings.get_all_data(merged=False, incl_defaults=False, error_on_path=True)
except NoSuchSettingsPath:
# no config persisted, nothing to do => get out of here
return
if config is None:
# config is set to None, that doesn't make sense, kill it and leave
self._settings.clean_all_data()
return
if self.config_version_key in config and config[self.config_version_key] is None:
# delete None entries for config version - it's the default, no need
del config[self.config_version_key]
# calculate a minimal diff between the settings and the current config -
# anything already in the settings will be removed from the persisted
# config, no need to duplicate it
defaults = self.get_settings_defaults()
diff = octoprint.util.dict_diff(defaults, config)
if not diff:
# no diff to defaults, no need to have anything persisted
self._settings.clean_all_data()
else:
# diff => persist only that
self._settings.set([], diff)
def on_settings_initialized(self):
"""
Called after the settings have been initialized and - if necessary - also been migrated through a call to

View file

@ -224,7 +224,7 @@ class Server(object):
set_preprocessors=set_preprocessors)
return dict(settings=plugin_settings)
def settings_plugin_config_migration(name, implementation):
def settings_plugin_config_migration_and_cleanup(name, implementation):
if not isinstance(implementation, octoprint.plugin.SettingsPlugin):
return
@ -232,11 +232,13 @@ class Server(object):
settings_migrator = implementation.on_settings_migrate
if settings_version is not None and settings_migrator is not None:
stored_version = implementation._settings.get_int(["_config_version"])
stored_version = implementation._settings.get_int([octoprint.plugin.SettingsPlugin.config_version_key])
if stored_version is None or stored_version < settings_version:
settings_migrator(settings_version, stored_version)
implementation._settings.set_int(["_config_version"], settings_version)
implementation._settings.save()
implementation._settings.set_int([octoprint.plugin.SettingsPlugin.config_version_key], settings_version)
implementation.on_settings_cleanup()
implementation._settings.save()
implementation.on_settings_initialized()
@ -246,11 +248,11 @@ class Server(object):
settingsPlugins = pluginManager.get_implementations(octoprint.plugin.SettingsPlugin)
for implementation in settingsPlugins:
try:
settings_plugin_config_migration(implementation._identifier, implementation)
settings_plugin_config_migration_and_cleanup(implementation._identifier, implementation)
except:
self._logger.exception("Error while trying to migrate settings for plugin {}, ignoring it".format(implementation._identifier))
pluginManager.implementation_post_inits=[settings_plugin_config_migration]
pluginManager.implementation_post_inits=[settings_plugin_config_migration_and_cleanup]
pluginManager.log_all_plugins()

View file

@ -312,6 +312,11 @@ default_settings = {
valid_boolean_trues = [True, "true", "yes", "y", "1"]
""" Values that are considered to be equivalent to the boolean ``True`` value, used for type conversion in various places."""
class NoSuchSettingsPath(BaseException):
pass
class Settings(object):
"""
The :class:`Settings` class allows managing all of OctoPrint's settings. It takes care of initializing the settings
@ -832,13 +837,13 @@ class Settings(object):
stat = os.stat(self._configfile)
return stat.st_mtime
#~~ getter
##~~ Internal getter
def get(self, path, asdict=False, config=None, defaults=None, preprocessors=None, merged=False, incl_defaults=True):
def _get_value(self, path, asdict=False, config=None, defaults=None, preprocessors=None, merged=False, incl_defaults=True):
import octoprint.util as util
if len(path) == 0:
return None
raise NoSuchSettingsPath()
if config is None:
config = self._config
@ -856,7 +861,7 @@ class Settings(object):
config = {}
defaults = defaults[key]
else:
return None
raise NoSuchSettingsPath()
if preprocessors and isinstance(preprocessors, dict) and key in preprocessors:
preprocessors = preprocessors[key]
@ -880,7 +885,7 @@ class Settings(object):
elif incl_defaults and key in defaults:
value = defaults[key]
else:
value = None
raise NoSuchSettingsPath()
if preprocessors and isinstance(preprocessors, dict) and key in preprocessors and callable(preprocessors[key]):
value = preprocessors[key](value)
@ -898,8 +903,34 @@ class Settings(object):
else:
return results
def getInt(self, path, config=None, defaults=None, preprocessors=None, incl_defaults=True):
value = self.get(path, config=config, defaults=defaults, preprocessors=preprocessors, incl_defaults=incl_defaults)
#~~ has
def has(self, path, **kwargs):
try:
self._get_value(path, **kwargs)
except NoSuchSettingsPath:
return False
else:
return True
#~~ getter
def get(self, path, **kwargs):
error_on_path = kwargs.get("error_on_path", False)
new_kwargs = dict(kwargs)
if "error_on_path" in new_kwargs:
del new_kwargs["error_on_path"]
try:
return self._get_value(path, **new_kwargs)
except NoSuchSettingsPath:
if error_on_path:
raise
else:
return None
def getInt(self, path, **kwargs):
value = self.get(path, **kwargs)
if value is None:
return None
@ -909,8 +940,8 @@ class Settings(object):
self._logger.warn("Could not convert %r to a valid integer when getting option %r" % (value, path))
return None
def getFloat(self, path, config=None, defaults=None, preprocessors=None, incl_defaults=True):
value = self.get(path, config=config, defaults=defaults, preprocessors=preprocessors, incl_defaults=incl_defaults)
def getFloat(self, path, **kwargs):
value = self.get(path, **kwargs)
if value is None:
return None
@ -920,8 +951,8 @@ class Settings(object):
self._logger.warn("Could not convert %r to a valid integer when getting option %r" % (value, path))
return None
def getBoolean(self, path, config=None, defaults=None, preprocessors=None, incl_defaults=True):
value = self.get(path, config=config, defaults=defaults, preprocessors=preprocessors, incl_defaults=incl_defaults)
def getBoolean(self, path, **kwargs):
value = self.get(path, **kwargs)
if value is None:
return None
if isinstance(value, bool):
@ -974,6 +1005,23 @@ class Settings(object):
return script
#~~ remove
def remove(self, path, config=None):
if config is None:
config = self._config
while len(path) > 1:
key = path.pop(0)
if not isinstance(config, dict) or key not in config:
return
config = config[key]
key = path.pop(0)
if isinstance(config, dict) and key in config:
del config[key]
self._dirty = True
#~~ setter
def set(self, path, value, force=False, defaults=None, config=None, preprocessors=None):
@ -1020,9 +1068,9 @@ class Settings(object):
config[key] = value
self._dirty = True
def setInt(self, path, value, force=False, defaults=None, config=None, preprocessors=None):
def setInt(self, path, value, **kwargs):
if value is None:
self.set(path, None, config=config, force=force, defaults=defaults, preprocessors=preprocessors)
self.set(path, None, **kwargs)
return
try:
@ -1031,11 +1079,11 @@ class Settings(object):
self._logger.warn("Could not convert %r to a valid integer when setting option %r" % (value, path))
return
self.set(path, intValue, config=config, force=force, defaults=defaults, preprocessors=preprocessors)
self.set(path, intValue, **kwargs)
def setFloat(self, path, value, force=False, defaults=None, config=None, preprocessors=None):
def setFloat(self, path, value, **kwargs):
if value is None:
self.set(path, None, config=config, force=force, defaults=defaults, preprocessors=preprocessors)
self.set(path, None, **kwargs)
return
try:
@ -1044,15 +1092,15 @@ class Settings(object):
self._logger.warn("Could not convert %r to a valid integer when setting option %r" % (value, path))
return
self.set(path, floatValue, config=config, force=force, defaults=defaults, preprocessors=preprocessors)
self.set(path, floatValue, **kwargs)
def setBoolean(self, path, value, force=False, defaults=None, config=None, preprocessors=None):
def setBoolean(self, path, value, **kwargs):
if value is None or isinstance(value, bool):
self.set(path, value, config=config, force=force, defaults=defaults, preprocessors=preprocessors)
self.set(path, value, **kwargs)
elif value.lower() in valid_boolean_trues:
self.set(path, True, config=config, force=force, defaults=defaults, preprocessors=preprocessors)
self.set(path, True, **kwargs)
else:
self.set(path, False, config=config, force=force, defaults=defaults, preprocessors=preprocessors)
self.set(path, False, **kwargs)
def setBaseFolder(self, type, path, force=False):
if type not in default_settings["folder"].keys():

View file

@ -432,6 +432,67 @@ def dict_clean(a, b):
return result
def dict_diff(a, b):
"""
Recursively calculates the minimal dict that would be needed to be deep merged with
a in order to produce the same result as deep merging a and b.
Example::
>>> a = dict(foo=dict(a=1, b=2), bar=dict(c=3, d=4))
>>> b = dict(bar=dict(c=3, d=5), fnord=None)
>>> c = dict_diff(a, b)
>>> c == dict(bar=dict(d=5), fnord=None)
True
>>> dict_merge(a, c) == dict_merge(a, b)
True
Arguments:
a (dict): Source dictionary
b (dict): Dictionary to compare to source dictionary and derive diff for
Returns:
dict: The minimal dictionary to deep merge on a to get the same result
as deep merging b on a.
"""
if not isinstance(a, dict) or not isinstance(b, dict):
raise ValueError("a and b must be dictionaries")
if a == b:
# shortcut: if both are equal, we return an empty dict as result
return dict()
from copy import deepcopy
all_keys = set(a.keys() + b.keys())
result = dict()
for k in all_keys:
if k not in b:
# key not contained in b => not contained in result
continue
if k in a:
# key is present in both dicts, we have to take a look at the value
value_a = a[k]
value_b = b[k]
if value_a != value_b:
# we only need to look further if the values are not equal
if isinstance(value_a, dict) and isinstance(value_b, dict):
# both are dicts => deeper down it goes into the rabbit hole
result[k] = dict_diff(value_a, value_b)
else:
# new b wins over old a
result[k] = deepcopy(value_b)
else:
# key is new, add it
result[k] = deepcopy(b[k])
return result
def dict_contains_keys(a, b):
"""
Recursively deep-checks if ``a`` contains all keys found in ``b``.

View file

@ -0,0 +1,139 @@
import unittest
import mock
import octoprint.plugin
class BlueprintPluginTest(unittest.TestCase):
def setUp(self):
self.basefolder = "/some/funny/basefolder"
self.plugin = octoprint.plugin.BlueprintPlugin()
self.plugin._basefolder = self.basefolder
class MyAssetPlugin(octoprint.plugin.BlueprintPlugin, octoprint.plugin.AssetPlugin):
def get_asset_folder(self):
return "/some/asset/folder"
class MyTemplatePlugin(octoprint.plugin.BlueprintPlugin, octoprint.plugin.TemplatePlugin):
def get_template_folder(self):
return "/some/template/folder"
self.assetplugin = MyAssetPlugin()
self.assetplugin._basefolder = self.basefolder
self.templateplugin = MyTemplatePlugin()
self.templateplugin._basefolder = self.basefolder
def test_route(self):
def test_method():
pass
octoprint.plugin.BlueprintPlugin.route("/test/method", methods=["GET"])(test_method)
octoprint.plugin.BlueprintPlugin.route("/test/method/{foo}", methods=["PUT"])(test_method)
self.assertTrue(hasattr(test_method, "_blueprint_rules"))
self.assertTrue("test_method" in test_method._blueprint_rules)
self.assertTrue(len(test_method._blueprint_rules["test_method"]) == 2)
self.assertListEqual(test_method._blueprint_rules["test_method"], [
("/test/method", dict(methods=["GET"])),
("/test/method/{foo}", dict(methods=["PUT"]))
])
def test_errorhandler(self):
def test_method():
pass
octoprint.plugin.BlueprintPlugin.errorhandler(404)(test_method)
self.assertTrue(hasattr(test_method, "_blueprint_error_handler"))
self.assertTrue("test_method" in test_method._blueprint_error_handler)
self.assertTrue(len(test_method._blueprint_error_handler["test_method"]) == 1)
self.assertListEqual(test_method._blueprint_error_handler["test_method"], [
404
])
def test_get_blueprint_kwargs(self):
import os
expected = dict(
static_folder=os.path.join(self.basefolder, "static"),
template_folder=os.path.join(self.basefolder, "templates")
)
result = self.plugin.get_blueprint_kwargs()
self.assertEquals(result, expected)
def test_get_blueprint_kwargs_assetplugin(self):
import os
expected = dict(
static_folder=self.assetplugin.get_asset_folder(),
template_folder=os.path.join(self.basefolder, "templates")
)
result = self.assetplugin.get_blueprint_kwargs()
self.assertEquals(result, expected)
def test_get_blueprint_kwargs_templateplugin(self):
import os
expected = dict(
static_folder=os.path.join(self.basefolder, "static"),
template_folder=self.templateplugin.get_template_folder()
)
result = self.templateplugin.get_blueprint_kwargs()
self.assertEquals(result, expected)
def test_get_blueprint(self):
import os
expected_kwargs = dict(
static_folder=os.path.join(self.basefolder, "static"),
template_folder=os.path.join(self.basefolder, "templates")
)
class MyPlugin(octoprint.plugin.BlueprintPlugin):
@octoprint.plugin.BlueprintPlugin.route("/some/path", methods=["GET"])
def route_method(self):
pass
@octoprint.plugin.BlueprintPlugin.errorhandler(404)
def errorhandler_method(self):
pass
@octoprint.plugin.BlueprintPlugin.route("/hidden/path", methods=["GET"])
def _hidden_method(self):
pass
plugin = MyPlugin()
plugin._basefolder = self.basefolder
plugin._identifier = "myplugin"
with mock.patch("flask.Blueprint") as MockBlueprint:
blueprint = mock.MagicMock()
MockBlueprint.return_value = blueprint
errorhandler = mock.MagicMock()
blueprint.errorhandler.return_value = errorhandler
result = plugin.get_blueprint()
self.assertEquals(result, blueprint)
MockBlueprint.assert_called_once_with("plugin.myplugin", "myplugin", **expected_kwargs)
blueprint.add_url_rule.assert_called_once_with("/some/path", "route_method", view_func=plugin.route_method, methods=["GET"])
blueprint.errorhandler.assert_called_once_with(404)
errorhandler.assert_called_once_with(plugin.errorhandler_method)
def test_get_blueprint_cached(self):
blueprint = mock.MagicMock()
self.plugin._blueprint = blueprint
result = self.plugin.get_blueprint()
self.assertEquals(blueprint, result)

View file

@ -0,0 +1,313 @@
import unittest
import mock
import octoprint.plugin
class TestSettingsPlugin(unittest.TestCase):
def setUp(self):
self.settings = mock.MagicMock()
self.plugin = octoprint.plugin.SettingsPlugin()
self.plugin._settings = self.settings
def test_on_settings_cleanup(self):
"""Tests that after cleanup only minimal config is left in storage."""
### setup
# settings defaults
defaults = dict(
foo=dict(
a=1,
b=2,
l1=["some", "list"],
l2=["another", "list"]
),
bar=True,
fnord=None
)
self.plugin.get_settings_defaults = mock.MagicMock()
self.plugin.get_settings_defaults.return_value = defaults
# stored config, containing one redundant entry (bar=True, same as default)
in_config = dict(
foo=dict(
l1=["some", "other", "list"],
l2=["another", "list"],
l3=["a", "third", "list"]
),
bar=True,
fnord=dict(
c=3,
d=4
)
)
self.settings.get_all_data.return_value = in_config
### execute
self.plugin.on_settings_cleanup()
### assert
# minimal config (current without redundant value) should have been set
expected = dict(
foo=dict(
l1=["some", "other", "list"],
l3=["a", "third", "list"]
),
fnord=dict(
c=3,
d=4
)
)
self.settings.set.assert_called_once_with([], expected)
def test_on_settings_cleanup_configversion(self):
"""Tests that set config version is always left stored."""
### setup
defaults = dict(
foo="fnord"
)
self.plugin.get_settings_defaults = mock.MagicMock()
self.plugin.get_settings_defaults.return_value = defaults
in_config = dict(
_config_version=1,
foo="fnord"
)
self.settings.get_all_data.return_value = in_config
### execute
self.plugin.on_settings_cleanup()
### assert
# minimal config incl. config version should have been set
self.settings.set.assert_called_once_with([], dict(_config_version=1))
def test_on_settings_cleanup_noconfigversion(self):
"""Tests that config versions of None are cleaned from stored data."""
### setup
defaults = dict(
foo="bar"
)
self.plugin.get_settings_defaults = mock.MagicMock()
self.plugin.get_settings_defaults.return_value = defaults
# stored config version is None
in_config = dict(
_config_version=None,
foo="fnord"
)
self.settings.get_all_data.return_value = in_config
### execute
self.plugin.on_settings_cleanup()
### assert
# minimal config without config version should have been set
self.settings.set.assert_called_once_with([], dict(foo="fnord"))
def test_on_settings_cleanup_emptydiff(self):
"""Tests that settings are cleaned up if the diff data <-> defaults is empty."""
### setup
defaults = dict(
foo="bar"
)
self.plugin.get_settings_defaults = mock.MagicMock()
self.plugin.get_settings_defaults.return_value = defaults
# current stored config, same as defaults
in_config = dict(
foo="bar"
)
self.settings.get_all_data.return_value = in_config
### execute
self.plugin.on_settings_cleanup()
### assert
# should have been cleared
self.settings.clean_all_data.assert_called_once_with()
def test_on_settings_cleanup_nosuchpath(self):
"""Tests that no processing is done if nothing is stored in settings."""
from octoprint.settings import NoSuchSettingsPath
### setup
# simulate no settings stored in config.yaml
self.settings.get_all_data.side_effect = NoSuchSettingsPath()
### execute
self.plugin.on_settings_cleanup()
### assert
# only get_all_data should have been called
self.settings.get_all_data.assert_called_once_with(merged=False, incl_defaults=False, error_on_path=True)
self.assertTrue(len(self.settings.method_calls) == 1)
def test_on_settings_cleanup_none(self):
"""Tests the None entries in config get cleaned up."""
### setup
# simulate None entry in config.yaml
self.settings.get_all_data.return_value = None
### execute
self.plugin.on_settings_cleanup()
### assert
# should have been cleaned
self.settings.clean_all_data.assert_called_once_with()
def test_on_settings_save(self):
"""Tests that only the diff is saved."""
### setup
current = dict(
foo="bar"
)
self.settings.get_all_data.return_value = current
defaults = dict(
foo="foo",
bar=dict(
a=1,
b=2
)
)
self.plugin.get_settings_defaults = mock.MagicMock()
self.plugin.get_settings_defaults.return_value = defaults
### execute
data = dict(
foo="fnord",
bar=dict(
a=1,
b=2
)
)
diff = self.plugin.on_settings_save(data)
### assert
# the minimal diff should have been saved
expected = dict(
foo="fnord"
)
self.settings.set.assert_called_once_with([], expected)
self.assertEquals(diff, expected)
def test_on_settings_save_nodiff(self):
"""Tests that data is cleaned if there's not difference between data and defaults."""
### setup
self.settings.get_all_data.return_value = None
defaults = dict(
foo="bar",
bar=dict(
a=1,
b=2,
l=["some", "list"]
)
)
self.plugin.get_settings_defaults = mock.MagicMock()
self.plugin.get_settings_defaults.return_value = defaults
### execute
data = dict(foo="bar")
diff = self.plugin.on_settings_save(data)
### assert
self.settings.clean_all_data.assert_called_once_with()
self.assertEquals(diff, dict())
def test_on_settings_save_configversion(self):
"""Tests that saved data gets stripped config version and set correct one."""
### setup
self.settings.get_all_data.return_value = None
defaults = dict(
foo="bar"
)
self.plugin.get_settings_defaults = mock.MagicMock()
self.plugin.get_settings_defaults.return_value = defaults
version = 1
self.plugin.get_settings_version = mock.MagicMock()
self.plugin.get_settings_version.return_value = version
### execute
data = dict(_config_version=None, foo="bar")
diff = self.plugin.on_settings_save(data)
### assert
expected_diff = dict()
expected_set = dict(_config_version=version)
# while there was no diff, we should still have saved the new config version
self.settings.set.assert_called_once_with([], expected_set)
self.assertEquals(diff, expected_diff)
def test_on_settings_load(self):
"""Tests that on_settings_load returns what's stored in the config, without config version."""
### setup
# current data incl. config version
current = dict(
_config_version=3,
foo="bar",
fnord=dict(
a=1,
b=2,
l=["some", "list"]
)
)
# expected is current without _config_version - we make the copy now
# since our current dict will be modified by the test
expected = dict(current)
del expected["_config_version"]
self.settings.get_all_data.return_value = expected
### execute
result = self.plugin.on_settings_load()
### assert
self.assertEquals(result, expected)