diff --git a/.travis.yml b/.travis.yml index df4eb121..66a9e7dc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,4 +6,4 @@ python: install: - pip install -e .[develop] script: -- nosetests tests/ \ No newline at end of file +- nosetests --with-doctest diff --git a/src/octoprint/filemanager/storage.py b/src/octoprint/filemanager/storage.py index 9f238cf9..1fe59e35 100644 --- a/src/octoprint/filemanager/storage.py +++ b/src/octoprint/filemanager/storage.py @@ -584,24 +584,6 @@ class LocalFileStorage(StorageInterface): Note that for a ``path`` without a trailing slash the last part will be considered a file name and hence be returned at second position. If you only need to convert a folder path, be sure to include a trailing slash for a string ``path`` or an empty last element for a list ``path``. - - Examples:: - - >>> storage = LocalFileStorage("/some/base/folder") - >>> storage.sanitize("some/folder/and/some file.gco") - ("/some/base/folder/some/folder/and", "some_file.gco") - >>> storage.sanitize(("some", "folder", "and", "some file.gco")) - ("/some/base/folder/some/folder/and", "some_file.gco") - >>> storage.sanitize("some file.gco") - ("/some/base/folder", "some_file.gco") - >>> storage.sanitize(("some file.gco",)) - ("/some/base/folder", "some_file.gco") - >>> storage.sanitize("") - ("/some/base/folder", "") - >>> storage.sanitize("some/folder/with/trailing/slash/") - ("/some/base/folder/some/folder/with/trailing/slash", "") - >>> storage.sanitize("some", "folder", "") - ("/some/base/folder/some/folder", "") """ name = None if isinstance(path, (str, unicode, basestring)): @@ -628,24 +610,6 @@ class LocalFileStorage(StorageInterface): Raises a :class:`ValueError` for a ``name`` containing ``/`` or ``\``. Otherwise strips any characters from the given ``name`` that are not any of the ASCII characters, digits, ``-``, ``_``, ``.``, ``(``, ``)`` or space and replaces and spaces with ``_``. - - Examples:: - - >>> storage = LocalFileStorage("/some/base/folder") - >>> storage.sanitize_name("some_file.gco") - "some_file.gco" - >>> storage.sanitize_name("some_file with (parentheses) and ümläuts and digits 123.gco") - "some_file_with_(parentheses)_and_mluts_and_digits_123.gco" - >>> storage.sanitize_name("pengüino pequeño.stl") - "pengino_pequeo.stl" - >>> storage.sanitize_name("some/folder/still/left.gco") - Traceback (most recent call last): - File "", line 1, in - ValueError: name must not contain / or \ - >>> storage.sanitize_name("also\\no\\backslashes.gco") - Traceback (most recent call last): - File "", line 1, in - ValueError: name must not contain / or \ """ if name is None: return None @@ -664,22 +628,6 @@ class LocalFileStorage(StorageInterface): Ensures that the on disk representation of ``path`` is located under the configured basefolder. Resolves all relative path elements (e.g. ``..``) and sanitizes folder names using :func:`sanitize_name`. Final path is the absolute path including leading ``basefolder`` path. - - Examples:: - - >>> storage = LocalFileStorage("/some/base/folder") - >>> storage.sanitize_path("folder/with/subfolder") - "/some/base/folder/folder/with/subfolder" - >>> storage.sanitize_path("folder/with/subfolder/../other/folder") - "/some/base/folder/folder/with/other/folder" - >>> storage.sanitize_path("/folder/with/leading/slash") - "/some/base/folder/folder/with/leading/slash" - >>> storage.sanitize_path(".folder/with/leading/dot") - "/some/base/folder/folder/with/leading/dot - >>> storage.sanitize_path("../../folder/out/of/the/basefolder") - Traceback (most recent call last): - File "", line 1, in - ValueError: path not contained in base folder: /some/folder/out/of/the/basefolder """ if path[0] == "/" or path[0] == ".": path = path[1:] diff --git a/src/octoprint/plugin/types.py b/src/octoprint/plugin/types.py index 7319032e..50f5be59 100644 --- a/src/octoprint/plugin/types.py +++ b/src/octoprint/plugin/types.py @@ -1266,7 +1266,7 @@ class SettingsPlugin(OctoPrintPlugin): # 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) + diff = octoprint.util.dict_minimal_mergediff(self.get_settings_defaults(), new_current) version = self.get_settings_version() @@ -1396,7 +1396,7 @@ class SettingsPlugin(OctoPrintPlugin): # 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) + diff = octoprint.util.dict_minimal_mergediff(defaults, config) if not diff: # no diff to defaults, no need to have anything persisted diff --git a/src/octoprint/printer/profile.py b/src/octoprint/printer/profile.py index 88bdfe3e..7ef630a0 100644 --- a/src/octoprint/printer/profile.py +++ b/src/octoprint/printer/profile.py @@ -12,7 +12,7 @@ import re import logging from octoprint.settings import settings -from octoprint.util import dict_merge, dict_clean, dict_contains_keys +from octoprint.util import dict_merge, dict_sanitize, dict_contains_keys class SaveError(Exception): pass @@ -214,7 +214,7 @@ class PrinterProfileManager(object): identifier = self._sanitize(identifier) profile["id"] = identifier - profile = dict_clean(profile, self.__class__.default) + profile = dict_sanitize(profile, self.__class__.default) if identifier == "_default": default_profile = dict_merge(self._load_default(), profile) diff --git a/src/octoprint/util/__init__.py b/src/octoprint/util/__init__.py index 3cf932ce..d5066e48 100644 --- a/src/octoprint/util/__init__.py +++ b/src/octoprint/util/__init__.py @@ -383,6 +383,14 @@ def dict_merge(a, b): Taken from https://www.xormedia.com/recursively-merge-dictionaries-in-python/ + Example:: + + >>> a = dict(foo="foo", bar="bar", fnord=dict(a=1)) + >>> b = dict(foo="other foo", fnord=dict(b=2, l=["some", "list"])) + >>> expected = dict(foo="other foo", bar="bar", fnord=dict(a=1, b=2, l=["some", "list"])) + >>> dict_merge(a, b) == expected + True + Arguments: a (dict): The dictionary to merge ``b`` into b (dict): The dictionary to merge into ``a`` @@ -404,14 +412,24 @@ def dict_merge(a, b): return result -def dict_clean(a, b): +def dict_sanitize(a, b): """ - Recursively deep-cleans ``b`` from ``a``, removing all keys and corresponding values from ``a`` that appear in - ``b``. + Recursively deep-sanitizes ``a`` based on ``b``, removing all keys (and + associated values) from ``a`` that do not appear in ``b``. + + Example:: + + >>> a = dict(foo="foo", bar="bar", fnord=dict(a=1, b=2, l=["some", "list"])) + >>> b = dict(foo=None, fnord=dict(a=None, b=None)) + >>> expected = dict(foo="foo", fnord=dict(a=1, b=2)) + >>> dict_sanitize(a, b) == expected + True + >>> dict_clean(a, b) == expected + True Arguments: - a (dict): The dictionary to clean from ``b``. - b (dict): The dictionary to clean ``b`` from. + a (dict): The dictionary to clean against ``b``. + b (dict): The dictionary containing the key structure to clean from ``a``. Results: dict: A new dict based on ``a`` with all keys (and corresponding values) found in ``b`` removed. @@ -426,13 +444,15 @@ def dict_clean(a, b): if not k in b: del result[k] elif isinstance(v, dict): - result[k] = dict_clean(v, b[k]) + result[k] = dict_sanitize(v, b[k]) else: result[k] = deepcopy(v) return result +dict_clean = deprecated("dict_clean has been renamed to dict_sanitize", + includedoc="Replaced by :func:`dict_sanitize`")(dict_sanitize) -def dict_diff(a, b): +def dict_minimal_mergediff(source, target): """ 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. @@ -441,67 +461,70 @@ def dict_diff(a, b): >>> 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_minimal_mergediff(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 + source (dict): Source dictionary + target (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. + dict: The minimal dictionary to deep merge on ``source`` to get the same result + as deep merging ``target`` on ``source``. """ - if not isinstance(a, dict) or not isinstance(b, dict): - raise ValueError("a and b must be dictionaries") + if not isinstance(source, dict) or not isinstance(target, dict): + raise ValueError("source and target must be dictionaries") - if a == b: + if source == target: # 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()) + all_keys = set(source.keys() + target.keys()) result = dict() for k in all_keys: - if k not in b: + if k not in target: # key not contained in b => not contained in result continue - if k in a: + if k in source: # key is present in both dicts, we have to take a look at the value - value_a = a[k] - value_b = b[k] + value_source = source[k] + value_target = target[k] - if value_a != value_b: + if value_source != value_target: # we only need to look further if the values are not equal - if isinstance(value_a, dict) and isinstance(value_b, dict): + if isinstance(value_source, dict) and isinstance(value_target, dict): # both are dicts => deeper down it goes into the rabbit hole - result[k] = dict_diff(value_a, value_b) + result[k] = dict_minimal_mergediff(value_source, value_target) else: # new b wins over old a - result[k] = deepcopy(value_b) + result[k] = deepcopy(value_target) else: # key is new, add it - result[k] = deepcopy(b[k]) + result[k] = deepcopy(target[k]) return result -def dict_contains_keys(a, b): +def dict_contains_keys(keys, dictionary): """ - Recursively deep-checks if ``a`` contains all keys found in ``b``. + Recursively deep-checks if ``dictionary`` contains all keys found in ``keys``. Example:: - >>> dict_contains_keys(dict(foo="bar", fnord=dict(a=1, b=2, c=3)), dict(foo="some_other_bar", fnord=dict(b=100))) + >>> positive = dict(foo="some_other_bar", fnord=dict(b=100)) + >>> negative = dict(foo="some_other_bar", fnord=dict(b=100, d=20)) + >>> dictionary = dict(foo="bar", fnord=dict(a=1, b=2, c=3)) + >>> dict_contains_keys(positive, dictionary) True - >>> dict_contains_keys(dict(foo="bar", fnord=dict(a=1, b=2, c=3)), dict(foo="some_other_bar", fnord=dict(b=100, d=20))) + >>> dict_contains_keys(negative, dictionary) False Arguments: @@ -512,14 +535,14 @@ def dict_contains_keys(a, b): boolean: True if all keys found in ``b`` are also present in ``a``, False otherwise. """ - if not isinstance(a, dict) or not isinstance(b, dict): + if not isinstance(keys, dict) or not isinstance(dictionary, dict): return False - for k, v in a.iteritems(): - if not k in b: + for k, v in keys.iteritems(): + if not k in dictionary: return False elif isinstance(v, dict): - if not dict_contains_keys(v, b[k]): + if not dict_contains_keys(v, dictionary[k]): return False return True