Merge branch 'dev/improvedPluginSettingsSaving' into devel
This commit is contained in:
commit
279f4014a6
7 changed files with 719 additions and 42 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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``.
|
||||
|
|
|
|||
139
tests/plugin/test_types_blueprint.py
Normal file
139
tests/plugin/test_types_blueprint.py
Normal 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)
|
||||
313
tests/plugin/test_types_settings.py
Normal file
313
tests/plugin/test_types_settings.py
Normal 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)
|
||||
Loading…
Reference in a new issue