Merge branch 'devel' of github.com:foosel/OctoPrint into dev/folderSupport
This commit is contained in:
commit
bb7ab5224f
19 changed files with 889 additions and 153 deletions
|
|
@ -6,4 +6,7 @@ python:
|
|||
install:
|
||||
- pip install -e .[develop]
|
||||
script:
|
||||
- nosetests tests/
|
||||
- nosetests --with-doctest
|
||||
sudo: false
|
||||
git:
|
||||
depth: 250
|
||||
|
|
|
|||
|
|
@ -10,13 +10,13 @@
|
|||
# master shall not use the lookup table, only tags
|
||||
master
|
||||
|
||||
# maintenance is currently the branch for preparation of maintenance release 1.2.7
|
||||
# maintenance is currently the branch for preparation of maintenance release 1.2.8
|
||||
# so are any fix/... branches
|
||||
maintenance 1.2.7 536bb31965db17b969e7c1c53e241ddac4ae1814
|
||||
fix/.* 1.2.7 536bb31965db17b969e7c1c53e241ddac4ae1814
|
||||
maintenance 1.2.8 6c622f7c4332b71c6ece59552ffc87c146155c84 pep440-dev
|
||||
fix/.* 1.2.8 6c622f7c4332b71c6ece59552ffc87c146155c84 pep440-dev
|
||||
|
||||
# Special case disconnected checkouts, e.g. 'git checkout <tag>'
|
||||
\(detached.*
|
||||
|
||||
# every other branch is a development branch and thus gets resolved to 1.3.0-dev for now
|
||||
.* 1.3.0 198d3450d94be1a2 pep440-dev
|
||||
.* 1.3.0 198d3450d94be1a2 pep440-dev
|
||||
|
|
|
|||
56
CHANGELOG.md
56
CHANGELOG.md
|
|
@ -69,6 +69,62 @@
|
|||
* [#1047](https://github.com/foosel/OctoPrint/issues/1047) - Fixed 90 degree
|
||||
webcam rotation for iOS Safari.
|
||||
|
||||
## 1.2.7 (2015-10-20)
|
||||
|
||||
### Improvements
|
||||
|
||||
* [#1062](https://github.com/foosel/OctoPrint/issues/1062) - Plugin Manager
|
||||
now has a configuration dialog that among other things allows defining the
|
||||
used `pip` command if auto detection proves to be insufficient here.
|
||||
* Allow defining additional `pip` parameters in Plugin Manager. That might
|
||||
make `sudo`-less installation of plugins possible in situations where it's
|
||||
tricky otherwise.
|
||||
* Improved timelapse processing (backported from `devel` branch):
|
||||
* Individually captured frames cannot "overtake" each other anymore through
|
||||
usage of a capture queue.
|
||||
* Notifications will now be shown when the capturing of the timelapse's
|
||||
post roll happens, including an approximation of how long that will take.
|
||||
* Usage of `requests` instead of `urllib` for fetching the snapshot,
|
||||
appears to also have [positive effects on webcam compatibility](https://github.com/foosel/OctoPrint/issues/1078).
|
||||
* Some more defensive escaping for various settings in the UI (e.g. webcam URL)
|
||||
* Switch to more error resilient saving of configuration files and other files
|
||||
modified during runtime (save to temporary file & move). Should reduce risk
|
||||
of file corruption.
|
||||
* Downloading GCODE and STL files should now set more fitting `Content-Type`
|
||||
headers (`text/plain` and `application/sla`) for better client side support
|
||||
for "Open with" like usage scenarios.
|
||||
* Selecting z-triggered timelapse mode will now inform about that not working
|
||||
when printing from SD card.
|
||||
* Software Update Plugin: Removed "The web interface will now be reloaded"
|
||||
notification after successful update since that became obsolete with
|
||||
introduction of the "Reload Now" overlay.
|
||||
* Updated required version of `psutil` and `netifaces` dependencies.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#1057](https://github.com/foosel/OctoPrint/issues/1057) - Better error
|
||||
resilience of the Software Update plugin against broken/incomplete update
|
||||
configurations.
|
||||
* [#1075](https://github.com/foosel/OctoPrint/issues/1075) - Fixed support
|
||||
of `sudo` for installing plugins, but added big visible warning about it
|
||||
as it's **not** recommended.
|
||||
* [#1077](https://github.com/foosel/OctoPrint/issues/1077) - Do not hiccup
|
||||
on [UTF-8 BOMs](https://en.wikipedia.org/wiki/Byte_order_mark) (or other
|
||||
BOMs for that matter) at the beginning of GCODE files.
|
||||
* Fixed an issue that caused user sessions to not be properly associated,
|
||||
leading to Sessions getting duplicated, wrongly saved etc.
|
||||
* Fixed internal server error (HTTP 500) response on REST API calls with
|
||||
unset `Content-Type` header.
|
||||
* Fixed an issue leading to drag-and-drop file uploads to trigger frontend
|
||||
processing in various other file upload widgets.
|
||||
* Fixed a documentation error.
|
||||
* Fixed caching behaviour on GCODE/STL downloads, was setting the `ETag`
|
||||
header improperly.
|
||||
* Fixed GCODE viewer not properly detecting change of currently visualized
|
||||
file on Windows systems.
|
||||
|
||||
([Commits](https://github.com/foosel/OctoPrint/compare/1.2.6...1.2.7))
|
||||
|
||||
## 1.2.6 (2015-09-02)
|
||||
|
||||
### Improvements
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
|
||||
# See the docstring in versioneer.py for instructions. Note that you must
|
||||
# re-run 'versioneer.py setup' after changing this section, and commit the
|
||||
# resulting files.
|
||||
[metadata]
|
||||
description-file = README.md
|
||||
|
||||
[versioneer]
|
||||
VCS = git
|
||||
|
|
|
|||
27
setup.py
27
setup.py
|
|
@ -49,7 +49,10 @@ EXTRA_REQUIRES = dict(
|
|||
# Documentation dependencies
|
||||
"sphinx>=1.3",
|
||||
"sphinxcontrib-httpdomain",
|
||||
"sphinx_rtd_theme"
|
||||
"sphinx_rtd_theme",
|
||||
|
||||
# PyPi upload related
|
||||
"pypandoc"
|
||||
],
|
||||
|
||||
# Dependencies for developing OctoPrint plugins
|
||||
|
|
@ -58,6 +61,9 @@ EXTRA_REQUIRES = dict(
|
|||
]
|
||||
)
|
||||
|
||||
# Additional requirements for setup
|
||||
SETUP_REQUIRES = []
|
||||
|
||||
# Dependency links for any of the aforementioned dependencies
|
||||
DEPENDENCY_LINKS = []
|
||||
|
||||
|
|
@ -118,8 +124,22 @@ def params():
|
|||
version = versioneer.get_version()
|
||||
cmdclass = get_cmdclass()
|
||||
|
||||
description = "A responsive web interface for 3D printers"
|
||||
description = "A snappy web interface for 3D printers"
|
||||
long_description = open("README.md").read()
|
||||
|
||||
install_requires = INSTALL_REQUIRES
|
||||
extras_require = EXTRA_REQUIRES
|
||||
dependency_links = DEPENDENCY_LINKS
|
||||
setup_requires = SETUP_REQUIRES
|
||||
|
||||
try:
|
||||
import pypandoc
|
||||
setup_requires += ["setuptools-markdown"]
|
||||
long_description_markdown_filename = "README.md"
|
||||
del pypandoc
|
||||
except:
|
||||
pass
|
||||
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Environment :: Web Environment",
|
||||
|
|
@ -156,9 +176,6 @@ def params():
|
|||
|
||||
include_package_data = True
|
||||
zip_safe = False
|
||||
install_requires = INSTALL_REQUIRES
|
||||
extras_require = EXTRA_REQUIRES
|
||||
dependency_links = DEPENDENCY_LINKS
|
||||
|
||||
if os.environ.get('READTHEDOCS', None) == 'True':
|
||||
# we can't tell read the docs to please perform a pip install -e .[develop], so we help
|
||||
|
|
|
|||
|
|
@ -744,24 +744,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)):
|
||||
|
|
@ -788,24 +770,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 "<stdin>", line 1, in <module>
|
||||
ValueError: name must not contain / or \
|
||||
>>> storage.sanitize_name("also\\no\\backslashes.gco")
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in <module>
|
||||
ValueError: name must not contain / or \
|
||||
"""
|
||||
if name is None:
|
||||
return None
|
||||
|
|
@ -824,22 +788,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 "<stdin>", line 1, in <module>
|
||||
ValueError: path not contained in base folder: /some/folder/out/of/the/basefolder
|
||||
"""
|
||||
if path[0] == "/" or path[0] == ".":
|
||||
path = path[1:]
|
||||
|
|
|
|||
|
|
@ -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_minimal_mergediff(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_minimal_mergediff(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
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ $(function() {
|
|||
return (item.key == data.key);
|
||||
});
|
||||
|
||||
OctoPrint.slicing.deleteProfileForSlicer("cura", data.key, {url: data.resource})
|
||||
OctoPrint.slicing.deleteProfileForSlicer("cura", data.key, {url: data.resource()})
|
||||
.done(function() {
|
||||
self.requestData();
|
||||
self.slicingViewModel.requestData();
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -408,7 +408,7 @@ class PrinterProfileManager(object):
|
|||
for path in (("volume", "width"), ("volume", "depth"), ("volume", "height"), ("extruder", "nozzleDiameter")):
|
||||
try:
|
||||
convert_value(profile, path, float)
|
||||
except:
|
||||
except Exception as e:
|
||||
self._logger.warn("Profile has invalid value for path {path!r}: {msg}".format(path=".".join(path), msg=str(e)))
|
||||
return False
|
||||
|
||||
|
|
@ -416,7 +416,7 @@ class PrinterProfileManager(object):
|
|||
for path in (("axes", "x", "inverted"), ("axes", "y", "inverted"), ("axes", "z", "inverted")):
|
||||
try:
|
||||
convert_value(profile, path, bool)
|
||||
except:
|
||||
except Exception as e:
|
||||
self._logger.warn("Profile has invalid value for path {path!r}: {msg}".format(path=".".join(path), msg=str(e)))
|
||||
return False
|
||||
|
||||
|
|
|
|||
|
|
@ -656,6 +656,8 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback):
|
|||
if filename is not None:
|
||||
if sd:
|
||||
path_in_storage = filename
|
||||
if path_in_storage.startswith("/"):
|
||||
path_in_storage = path_in_storage[1:]
|
||||
path_on_disk = None
|
||||
else:
|
||||
path_in_storage = self._fileManager.path_in_storage(FileDestinations.LOCAL, filename)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -62,6 +62,9 @@ def retrieveSystemCommandsForSource(source):
|
|||
def executeSystemCommand(source, command):
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if command == "divider":
|
||||
return make_response("Dividers cannot be executed", 400)
|
||||
|
||||
command_spec = _get_command_spec(source, command)
|
||||
if not command_spec:
|
||||
return make_response("Command {}:{} not found".format(source, command), 404)
|
||||
|
|
@ -158,12 +161,18 @@ def _get_core_command_spec(action):
|
|||
|
||||
def _get_custom_command_specs():
|
||||
specs = collections.OrderedDict()
|
||||
dividers = 0
|
||||
for spec in s().get(["system", "actions"]):
|
||||
if not "action" in spec:
|
||||
continue
|
||||
copied = dict(spec)
|
||||
copied["source"] = "custom"
|
||||
specs[spec["action"]] = copied
|
||||
|
||||
action = spec["action"]
|
||||
if action == "divider":
|
||||
dividers += 1
|
||||
action = "divider_{}".format(dividers)
|
||||
specs[action] = copied
|
||||
return specs
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -22,8 +22,21 @@
|
|||
}, opts);
|
||||
};
|
||||
|
||||
var sendGcodeScriptWithParameters = function(script, context, parameters, opts) {
|
||||
script = script || "";
|
||||
context = context || {};
|
||||
parameters = parameters || {};
|
||||
|
||||
return OctoPrint.postJson(commandUrl, {
|
||||
script: script,
|
||||
context: context,
|
||||
parameters: parameters
|
||||
}, opts);
|
||||
};
|
||||
|
||||
OctoPrint.control = {
|
||||
sendGcodeWithParameters: sendGcodeWithParameters,
|
||||
sendGcodeScriptWithParameters: sendGcodeScriptWithParameters,
|
||||
|
||||
getCustomControls: function (opts) {
|
||||
return OctoPrint.get(customUrl, opts);
|
||||
|
|
@ -34,13 +47,7 @@
|
|||
},
|
||||
|
||||
sendGcodeScript: function (script, context, opts) {
|
||||
script = script || "";
|
||||
context = context || {};
|
||||
|
||||
return OctoPrint.postJson(commandUrl, {
|
||||
script: script,
|
||||
context: context
|
||||
}, opts);
|
||||
return sendGcodeScriptWithParameters(script, context, undefined, opts);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -287,30 +287,26 @@ $(function() {
|
|||
};
|
||||
|
||||
self.sendCustomCommand = function (command) {
|
||||
if (!command)
|
||||
return;
|
||||
if (!command) return;
|
||||
|
||||
var parameters = {};
|
||||
if (command.hasOwnProperty("input")) {
|
||||
_.each(command.input, function (input) {
|
||||
if (!input.hasOwnProperty("parameter") || !input.hasOwnProperty("value")) {
|
||||
return;
|
||||
}
|
||||
|
||||
parameters[input.parameter] = input.value();
|
||||
});
|
||||
}
|
||||
|
||||
if (command.hasOwnProperty("command") || command.hasOwnProperty("commands")) {
|
||||
var commands = command.commands || [command.command];
|
||||
|
||||
if (command.hasOwnProperty("input")) {
|
||||
var parameters = {};
|
||||
_.each(command.input, function(input) {
|
||||
if (!input.hasOwnProperty("parameter") || !input.hasOwnProperty("value")) {
|
||||
return;
|
||||
}
|
||||
|
||||
parameters[input.parameter] = input.value();
|
||||
});
|
||||
OctoPrint.control.sendGcodeWithParameters(commands, parameters);
|
||||
} else {
|
||||
OctoPrint.control.sendGcode(commands);
|
||||
}
|
||||
OctoPrint.control.sendGcodeWithParameters(commands, parameters);
|
||||
} else if (command.hasOwnProperty("script")) {
|
||||
var script = command.script;
|
||||
var context = command.context || {};
|
||||
|
||||
OctoPrint.control.sendGcodeScript(script, context);
|
||||
OctoPrint.control.sendGcodeScriptWithParameters(script, context, parameters);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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,21 +444,87 @@ 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_contains_keys(a, b):
|
||||
def dict_minimal_mergediff(source, target):
|
||||
"""
|
||||
Recursively deep-checks if ``a`` contains all keys found in ``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::
|
||||
|
||||
>>> dict_contains_keys(dict(foo="bar", fnord=dict(a=1, b=2, c=3)), dict(foo="some_other_bar", fnord=dict(b=100)))
|
||||
>>> 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_minimal_mergediff(a, b)
|
||||
>>> c == dict(bar=dict(d=5), fnord=None)
|
||||
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_merge(a, c) == dict_merge(a, b)
|
||||
True
|
||||
|
||||
Arguments:
|
||||
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 ``source`` to get the same result
|
||||
as deep merging ``target`` on ``source``.
|
||||
"""
|
||||
|
||||
if not isinstance(source, dict) or not isinstance(target, dict):
|
||||
raise ValueError("source and target must be dictionaries")
|
||||
|
||||
if source == target:
|
||||
# shortcut: if both are equal, we return an empty dict as result
|
||||
return dict()
|
||||
|
||||
from copy import deepcopy
|
||||
|
||||
all_keys = set(source.keys() + target.keys())
|
||||
result = dict()
|
||||
for k in all_keys:
|
||||
if k not in target:
|
||||
# key not contained in b => not contained in result
|
||||
continue
|
||||
|
||||
if k in source:
|
||||
# key is present in both dicts, we have to take a look at the value
|
||||
value_source = source[k]
|
||||
value_target = target[k]
|
||||
|
||||
if value_source != value_target:
|
||||
# we only need to look further if the values are not equal
|
||||
|
||||
if isinstance(value_source, dict) and isinstance(value_target, dict):
|
||||
# both are dicts => deeper down it goes into the rabbit hole
|
||||
result[k] = dict_minimal_mergediff(value_source, value_target)
|
||||
else:
|
||||
# new b wins over old a
|
||||
result[k] = deepcopy(value_target)
|
||||
|
||||
else:
|
||||
# key is new, add it
|
||||
result[k] = deepcopy(target[k])
|
||||
return result
|
||||
|
||||
|
||||
def dict_contains_keys(keys, dictionary):
|
||||
"""
|
||||
Recursively deep-checks if ``dictionary`` contains all keys found in ``keys``.
|
||||
|
||||
Example::
|
||||
|
||||
>>> 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(negative, dictionary)
|
||||
False
|
||||
|
||||
Arguments:
|
||||
|
|
@ -451,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
|
||||
|
|
|
|||
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