Merge branch 'devel' into dev/gcodeScripts

Conflicts:
	CHANGELOG.md
	src/octoprint/static/css/octoprint.css
	src/octoprint/static/js/app/viewmodels/timelapse.js
	src/octoprint/util/comm.py
This commit is contained in:
Gina Häußge 2015-03-01 14:11:43 +01:00
commit c3834c16fa
44 changed files with 1434 additions and 334 deletions

View file

@ -25,6 +25,8 @@
- `action:pause`: Pauses the current job in OctoPrint
- `action:resume`: Resumes the current job in OctoPrint
- `action:disconnect`: Disconnects OctoPrint from the printer
Plugins can add supported commands by [hooking](http://docs.octoprint.org/en/devel/plugins/hooks.html) into the
``octoprint.comm.protocol.action`` hook
* Mousing over the webcam image in the control tab enables key control mode, allowing you to quickly move the axis of your
printer with your computer's keyboard ([#610](https://github.com/foosel/OctoPrint/pull/610)):
- arrow keys: X and Y axes
@ -83,6 +85,14 @@
message for now.
* Daemonized OctoPrint now cleans up its pidfile when receiving a TERM signal ([#711](https://github.com/foosel/OctoPrint/issues/711))
* Added serial types for OpenBSD ([#551](https://github.com/foosel/OctoPrint/pull/551))
* Improved behaviour of terminal:
* Disabling autoscrolling now also stops cutting of the log while it's enabled, effectively preventing log lines from
being modified at all ([#735](https://github.com/foosel/OctoPrint/issues/735))
* Applying filters displays ``[...]`` where lines where removed
* Added a link to scroll to the end of the terminal log (useful for when autoscroll is disabled)
* Added a link to select all current contents of the terminal log for easy copy-pasting
* Added a display of how many lines are displayed, how many are filtered and how many are available in total
* Frame rate for timelapses can now be configured per timelapse ([#782](https://github.com/foosel/OctoPrint/pull/782))
### Bug Fixes
@ -126,6 +136,7 @@
* Color code successful or failed print results directly in file list, not just after a reload
* Changing Timelapse post roll activates save button
* Timelapse post roll is loaded properly from config
* Handling of files on the printer's SD card contained in folders now works correctly
([Commits](https://github.com/foosel/OctoPrint/compare/master...devel))

View file

@ -4,3 +4,5 @@ recursive-include src/octoprint/plugins *
recursive-include src/octoprint/translations *
include versioneer.py
include src/octoprint/_version.py
include README.md
include requirements.txt

View file

@ -27,11 +27,11 @@ year_current = date.today().year
# -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
#needs_sphinx = '1.3'
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.todo', 'sphinx.ext.autodoc', 'sphinxcontrib.httpdomain']
extensions = ['sphinx.ext.todo', 'sphinx.ext.autodoc', 'sphinxcontrib.httpdomain', 'sphinx.ext.autosummary', 'sphinxcontrib.napoleon']
todo_include_todos = True
# Add any paths that contain templates here, relative to this directory.
@ -103,9 +103,9 @@ pygments_style = 'sphinx'
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
if not on_rtd: # only import and set the theme if we're building docs locally
import sphinx_rtd_theme
html_theme = 'sphinx_rtd_theme'
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
import sphinx_rtd_theme
html_theme = 'sphinx_rtd_theme'
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
@ -139,11 +139,9 @@ if not on_rtd: # only import and set the theme if we're building docs locally
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
html_context = {
'css_files': [
'_static/theme_overrides.css', # overrides for wide tables in RTD theme
],
}
def setup(app):
app.add_stylesheet("theme_overrides.css")
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.

View file

@ -1,7 +1,7 @@
.. _sec-development-interface:
Interfaces
==========
Internal API
============
.. toctree::
:maxdepth: 3
@ -9,3 +9,4 @@ Interfaces
filemanager.rst
plugin.rst
printer.rst
util.rst

View file

@ -1,8 +1,19 @@
.. _sec-development-interface-plugin:
``octoprint.plugin``
--------------------
octoprint.plugin
----------------
.. automodule:: octoprint.plugin
:members: plugin_manager, plugin_settings, call_plugin, PluginSettings
:undoc-members:
.. _sec-development-interface-plugin-core:
octoprint.plugin.core
---------------------
.. automodule:: octoprint.plugin.core
octoprint.plugin.types
----------------------
.. automodule:: octoprint.plugin.types

View file

@ -0,0 +1,9 @@
.. _sec-development-interface-util:
octoprint.util
--------------
.. automodule:: octoprint.util
:members:

View file

@ -3,7 +3,99 @@
Available plugin mixins
=======================
.. automodule:: octoprint.plugin
:members: AppPlugin, AssetPlugin, BlueprintPlugin, EventHandlerPlugin, ProgressPlugin, SettingsPlugin, ShutdownPlugin, SimpleApiPlugin, SlicerPlugin, StartupPlugin, TemplatePlugin
:undoc-members:
The following plugin mixins are currently available:
.. contents::
:local:
Please note that all plugin mixins inherit from :class:`~octoprint.plugin.core.Plugin` and
:class:`~octoprint.plugin.OctoPrintPlugin`, which also provide attributes of interest to plugin developers.
.. _sec-plugins-mixins-startupplugin:
StartupPlugin
-------------
.. autoclass:: octoprint.plugin.StartupPlugin
:members:
.. _sec-plugins-mixins-shutdownplugin:
ShutdownPlugin
--------------
.. autoclass:: octoprint.plugin.ShutdownPlugin
:members:
.. _sec-plugins-mixins-settingsplugin:
SettingsPlugin
--------------
.. autoclass:: octoprint.plugin.SettingsPlugin
:members:
.. _sec-plugins-mixins-assetplugin:
AssetPlugin
-----------
.. autoclass:: octoprint.plugin.AssetPlugin
:members:
.. _sec-plugins-mixins-templateplugin:
TemplatePlugin
--------------
.. autoclass:: octoprint.plugin.TemplatePlugin
:members:
.. _sec-plugins-mixins-simpleapiplugin:
SimpleApiPlugin
---------------
.. autoclass:: octoprint.plugin.SimpleApiPlugin
:members:
.. _sec-plugins-mixins-blueprintplugin:
BlueprintPlugin
---------------
.. autoclass:: octoprint.plugin.BlueprintPlugin
:members:
.. _sec-plugins-mixins-eventhandlerplugin:
EventHandlerPlugin
------------------
.. autoclass:: octoprint.plugin.EventHandlerPlugin
:members:
.. _sec-plugins-mixins-progressplugin:
ProgressPlugin
--------------
.. autoclass:: octoprint.plugin.ProgressPlugin
:members:
.. _sec-plugins-mixins-slicerplugin:
SlicerPlugin
------------
.. autoclass:: octoprint.plugin.SlicerPlugin
:members:
.. _sec-plugins-mixins-appplugin:
AppPlugin
---------
.. autoclass:: octoprint.plugin.AppPlugin
:members:

View file

@ -1,2 +1,3 @@
sphinxcontrib-httpdomain
sphinxcontrib-napoleon
sphinx_rtd_theme

View file

@ -13,7 +13,7 @@ import tempfile
import octoprint.filemanager
from octoprint.util import safeRename
from octoprint.util import safe_rename
class StorageInterface(object):
"""
@ -1003,7 +1003,7 @@ class LocalFileStorage(StorageInterface):
with open(metadata_temporary_path, "w") as f:
import yaml
yaml.safe_dump(metadata, stream=f, default_flow_style=False, indent=" ", allow_unicode=True)
safeRename(metadata_temporary_path, metadata_path, throw_error=True)
safe_rename(metadata_temporary_path, metadata_path, throw_error=True)
except:
self._logger.exception("Error while writing .metadata.yaml to {path}".format(**locals()))
else:

View file

@ -1,4 +1,19 @@
# coding=utf-8
"""
This module represents OctoPrint's plugin subsystem. This includes management and helper methods as well as the
registered plugin types.
.. autofunction:: plugin_manager
.. autofunction:: plugin_settings
.. autofunction:: call_plugin
.. autoclass:: PluginSettings
:members:
"""
from __future__ import absolute_import
__author__ = "Gina Häußge <osd@foosel.net>"
@ -12,13 +27,47 @@ from octoprint.settings import settings
from octoprint.plugin.core import (PluginInfo, PluginManager, Plugin)
from octoprint.plugin.types import *
from octoprint.util import deprecated
# singleton
_instance = None
def plugin_manager(init=False, plugin_folders=None, plugin_types=None, plugin_entry_points=None, plugin_disabled_list=None):
"""
Factory method for initially constructing and consecutively retrieving the :class:`~octoprint.plugin.core.PluginManager`
singleton.
Arguments:
init (boolean): A flag indicating whether this is the initial call to construct the singleton (True) or not
(False, default). If this is set to True and the plugin manager has already been initialized, a :class:`ValueError`
will be raised. The same will happen if the plugin manager has not yet been initialized and this is set to
False.
plugin_folders (list): A list of folders (as strings containing the absolute path to them) in which to look for
potential plugin modules. If not provided this defaults to the configured ``plugins`` base folder and
``src/plugins`` within OctoPrint's code base.
plugin_types (list): A list of recognized plugin types for which to look for provided implementations. If not
provided this defaults to the plugin types found in :mod:`octoprint.plugin.types` without
:class:`~octoprint.plugin.OctoPrintPlugin`.
plugin_entry_points (list): A list of entry points pointing to modules which to load as plugins. If not provided
this defaults to the entry point ``octoprint.plugin``.
plugin_disabled_list (list): A list of plugin identifiers that are currently disabled. If not provided this
defaults to all plugins for which ``enabled`` is set to ``False`` in the settings.
Returns:
PluginManager: A fully initialized :class:`~octoprint.plugin.core.PluginManager` instance to be used for plugin
management tasks.
Raises:
ValueError: ``init`` was True although the plugin manager was already initialized, or it was False although
the plugin manager was not yet initialized.
"""
global _instance
if _instance is None:
if init:
if _instance is not None:
raise ValueError("Plugin Manager already initialized")
if plugin_folders is None:
plugin_folders = (settings().getBaseFolder("plugins"), os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), "..", "plugins")))
if plugin_types is None:
@ -49,10 +98,59 @@ def plugin_manager(init=False, plugin_folders=None, plugin_types=None, plugin_en
def plugin_settings(plugin_key, defaults=None):
"""
Factory method for creating a :class:`PluginSettings` instance.
Arguments:
plugin_key (string): The plugin identifier for which to create the settings instance.
defaults (dict): The default settings for the plugin.
Returns:
PluginSettings: A fully initialized :class:`PluginSettings` instance to be used to access the plugin's
settings
"""
return PluginSettings(settings(), plugin_key, defaults=defaults)
def call_plugin(types, method, args=None, kwargs=None, callback=None, error_callback=None):
"""
Helper method to invoke the indicated ``method`` on all registered plugin implementations implementing the
indicated ``types``. Allows providing method arguments and registering callbacks to call in case of success
and/or failure of each call which can be used to return individual results to the calling code.
Example:
.. sourcecode:: python
def my_success_callback(name, plugin, result):
print("{name} was called successfully and returned {result!r}".format(**locals()))
def my_error_callback(name, plugin, exc):
print("{name} raised an exception: {exc!s}".format(**locals()))
octoprint.plugin.call_plugin(
[octoprint.plugin.StartupPlugin],
"on_startup",
args=(my_host, my_port),
callback=my_success_callback,
error_callback=my_error_callback
)
Arguments:
types (list): A list of plugin implementation types to match against.
method (string): Name of the method to call on all matching implementations.
args (tuple): A tuple containing the arguments to supply to the called ``method``. Optional.
kwargs (dict): A dictionary containing the keyword arguments to supply to the called ``method``. Optional.
callback (function): A callback to invoke after an implementation has been called successfully. Will be called
with the three arguments ``name``, ``plugin`` and ``result``. ``name`` will be the plugin identifier,
``plugin`` the plugin implementation instance itself and ``result`` the result returned from the
``method`` invocation.
error_callback (function): A callback to invoke after the call of an implementation resulted in an exception.
Will be called with the three arguments ``name``, ``plugin`` and ``exc``. ``name`` will be the plugin
identifier, ``plugin`` the plugin implementation instance itself and ``exc`` the caught exception.
"""
if not isinstance(types, (list, tuple)):
types = [types]
if args is None:
@ -67,14 +165,53 @@ def call_plugin(types, method, args=None, kwargs=None, callback=None, error_call
result = getattr(plugin, method)(*args, **kwargs)
if callback:
callback(name, plugin, result)
except Exception as e:
except Exception as exc:
logging.getLogger(__name__).exception("Error while calling plugin %s" % name)
if error_callback:
error_callback(name, plugin, e)
error_callback(name, plugin, exc)
class PluginSettings(object):
"""
The :class:`PluginSettings` class is the interface for plugins to their own or globally defined settings.
It provides a couple of convenience methods for directly accessing plugin settings via the regular
:class:`octoprint.settings.Settings` interfaces as well as means to access plugin specific folder locations.
.. method:: get(path, merged=False, asdict=False)
Retrieves a raw key from the settings for ``path``, optionally merging the raw value with the default settings
if ``merged`` is set to True.
:param list path: a list of path elements to navigate to the settings value
:param boolean merged: whether to merge the returned result with the default settings (True) or not (False, default)
:return: the retrieved settings value
.. method:: get_int(path)
.. method:: get_float(path)
.. method:: get_boolean(path)
.. method:: set(path, value, force=False)
.. method:: set_int(path, value, force=False)
.. method:: set_float(path, value, force=False)
.. method:: set_boolean(path, value, force=False)
"""
def __init__(self, settings, plugin_key, defaults=None):
"""
Initializes the object with the provided :class:`octoprint.settings.Settings` manager as ``settings``, using
the ``plugin_key`` and optional ``defaults``.
:param settings:
:param plugin_key:
:param defaults:
:return:
"""
self.settings = settings
self.plugin_key = plugin_key
@ -103,45 +240,46 @@ class PluginSettings(object):
kwargs.update(dict(defaults=self.defaults))
return kwargs
self.access_methods = {
'get': (lambda args: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)),
'getInt': (lambda args,: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)),
'getFloat': (lambda args,: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)),
'getBoolean': (lambda args,: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)),
'set': (lambda args: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)),
'setInt': (lambda args: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)),
'setFloat': (lambda args: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)),
'setBoolean': (lambda args: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs))
}
self.access_methods = dict(
get=("get", lambda args: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)),
get_int=("getInt", lambda args,: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)),
get_float=("getFloat", lambda args,: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)),
get_boolean=("getBoolean", lambda args,: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)),
set=("set", lambda args: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)),
set_int=("setInt", lambda args: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)),
set_float=("setFloat", lambda args: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)),
set_boolean=("setBoolean", lambda args: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs))
)
self.deprecated_access_methods = dict(getInt="get_int", getFloat="get_float", getBoolean="get_boolean", setInt="set_int", setFloat="set_float", setBoolean="set_boolean")
def globalGet(self, path):
return self.settings.get(path)
def global_get(self, path, **kwargs):
return self.settings.get(path, **kwargs)
def globalGetInt(self, path):
return self.settings.getInt(path)
def global_get_int(self, path, **kwargs):
return self.settings.getInt(path, **kwargs)
def globalGetFloat(self, path):
return self.settings.getFloat(path)
def global_get_float(self, path, **kwargs):
return self.settings.getFloat(path, **kwargs)
def globalGetBoolean(self, path):
return self.settings.getBoolean(path)
def global_get_boolean(self, path, **kwargs):
return self.settings.getBoolean(path, **kwargs)
def globalSet(self, path, value):
self.settings.set(path, value)
def global_set(self, path, value, **kwargs):
self.settings.set(path, value, **kwargs)
def globalSetInt(self, path, value):
self.settings.setInt(path, value)
def global_set_int(self, path, value, **kwargs):
self.settings.setInt(path, value, **kwargs)
def globalSetFloat(self, path, value):
self.settings.setFloat(path, value)
def global_set_float(self, path, value, **kwargs):
self.settings.setFloat(path, value, **kwargs)
def globalSetBoolean(self, path, value):
self.settings.setBoolean(path, value)
def global_set_boolean(self, path, value, **kwargs):
self.settings.setBoolean(path, value, **kwargs)
def globalGetBaseFolder(self, folder_type):
return self.settings.getBaseFolder(folder_type)
def global_get_basefolder(self, folder_type, **kwargs):
return self.settings.getBaseFolder(folder_type, **kwargs)
def getPluginLogfilePath(self, postfix=None):
def get_plugin_logfile_path(self, postfix=None):
filename = "plugin_" + self.plugin_key
if postfix is not None:
filename += "_" + postfix
@ -149,10 +287,60 @@ class PluginSettings(object):
return os.path.join(self.settings.getBaseFolder("logs"), filename)
def __getattr__(self, item):
if item in self.access_methods and hasattr(self.settings, item) and callable(getattr(self.settings, item)):
orig_item = getattr(self.settings, item)
args_mapper, kwargs_mapper = self.access_methods[item]
all_access_methods = self.access_methods.keys() + self.deprecated_access_methods.keys()
if item in all_access_methods:
decorator = None
if item in self.deprecated_access_methods:
new = self.deprecated_access_methods[item]
decorator = deprecated("{old} has been renamed to {new}".format(old=item, new=new), stacklevel=2)
item = new
return lambda *args, **kwargs: orig_item(*args_mapper(args), **kwargs_mapper(kwargs))
else:
return getattr(self.settings, item)
settings_name, args_mapper, kwargs_mapper = self.access_methods[item]
if hasattr(self.settings, settings_name) and callable(getattr(self.settings, settings_name)):
orig_func = getattr(self.settings, settings_name)
if decorator is not None:
orig_func = decorator(orig_func)
def _func(*args, **kwargs):
return orig_func(*args_mapper(args), **kwargs_mapper(kwargs))
_func.__name__ = item
_func.__doc__ = orig_func.__doc__ if "__doc__" in dir(orig_func) else None
return _func
return getattr(self.settings, item)
##~~ deprecated methods follow
# TODO: Remove with release of 1.2.0
globalGet = deprecated("globalGet has been renamed to global_get",
includedoc="Replaced by :func:`global_get`",
since="1.2.0-dev-546")(global_get)
globalGetInt = deprecated("globalGetInt has been renamed to global_get_int",
includedoc="Replaced by :func:`global_get_int`",
since="1.2.0-dev-546")(global_get_int)
globalGetFloat = deprecated("globalGetFloat has been renamed to global_get_float",
includedoc="Replaced by :func:`global_get_float`",
since="1.2.0-dev-546")(global_get_float)
globalGetBoolean = deprecated("globalGetBoolean has been renamed to global_get_boolean",
includedoc="Replaced by :func:`global_get_boolean`",
since="1.2.0-dev-546")(global_get_boolean)
globalSet = deprecated("globalSet has been renamed to global_set",
includedoc="Replaced by :func:`global_set`",
since="1.2.0-dev-546")(global_set)
globalSetInt = deprecated("globalSetInt has been renamed to global_set_int",
includedoc="Replaced by :func:`global_set_int`",
since="1.2.0-dev-546")(global_set_int)
globalSetFloat = deprecated("globalSetFloat has been renamed to global_set_float",
includedoc="Replaced by :func:`global_set_float`",
since="1.2.0-dev-546")(global_set_float)
globalSetBoolean = deprecated("globalSetBoolean has been renamed to global_set_boolean",
includedoc="Replaced by :func:`global_set_boolean`",
since="1.2.0-dev-546")(global_set_boolean)
globalGetBaseFolder = deprecated("globalGetBaseFolder has been renamed to global_get_basefolder",
includedoc="Replaced by :func:`global_get_basefolder`",
since="1.2.0-dev-546")(global_get_basefolder)
getPluginLogfilePath = deprecated("getPluginLogfilePath has been renamed to get_plugin_logfile_path",
includedoc="Replaced by :func:`get_plugin_logfile_path`",
since="1.2.0-dev-546")(get_plugin_logfile_path)

View file

@ -1,4 +1,16 @@
# coding=utf-8
"""
In this module resides the core data structures and logic of the plugin system. It is implemented in an OctoPrint-agnostic
way and could be extracted into a separate Python module in the future.
.. autoclass:: PluginManager
.. autoclass:: PluginInfo
.. autoclass:: Plugin
"""
from __future__ import absolute_import
__author__ = "Gina Häußge <osd@foosel.net>"
@ -13,6 +25,15 @@ import logging
class PluginInfo(object):
"""
The :class:`PluginInfo` class wraps all available information about a registered plugin.
This includes its meta data (like name, description, version, etc) as well as the actual plugin extensions like
implementations, hooks and helpers.
It works on Python module objects and extracts the relevant data from those via accessing the
:ref:`control properties <sec-plugins-infrastructure-controlproperties>`.
"""
attr_name = '__plugin_name__'
@ -125,6 +146,12 @@ class PluginInfo(object):
class PluginManager(object):
"""
The :class:`PluginManager` is the central component for finding, loading and accessing plugins provided to the
system.
It is able to discover plugins both through possible file system locations as well as customizable entry points.
"""
def __init__(self, plugin_folders, plugin_types, plugin_entry_points, logging_prefix=None, plugin_disabled_list=None):
self.logger = logging.getLogger(__name__)
@ -405,5 +432,34 @@ class PluginManager(object):
class Plugin(object):
"""
The parent class of all plugin implementations.
.. attribute:: _identifier
The identifier of the plugin. Injected by the plugin core system upon initialization of the implementation.
.. attribute:: _plugin_name
The name of the plugin. Injected by the plugin core system upon initialization of the implementation.
.. attribute:: _plugin_version
The version of the plugin. Injected by the plugin core system upon initialization of the implementation.
.. attribute:: _basefolder
The base folder of the plugin. Injected by the plugin core system upon initialization of the implementation.
.. attribute:: _logger
The logger instance to use, with the logging name set to the :attr:`PluginManager.logging_prefix` of the
:class:`PluginManager` concatenated with :attr:`_identifier`. Injected by the plugin core system upon
initialization of the implementation.
"""
def initialize(self):
"""
Called by the plugin core after performing all injections.
"""
pass

View file

@ -1,4 +1,15 @@
# coding=utf-8
"""
This module bundles all of OctoPrint's supported plugin implementation types as well as their common parent
class, :class:`OctoPrintPlugin`.
Please note that the plugin implementation types are documented in the section
:ref:`Available plugin mixins <sec-plugins-mixins>`.
.. autoclass: OctoPrintPlugin
"""
from __future__ import absolute_import
__author__ = "Gina Häußge <osd@foosel.net>"
@ -9,7 +20,29 @@ __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms
from .core import Plugin
class StartupPlugin(Plugin):
class OctoPrintPlugin(Plugin):
"""
.. attribute:: _plugin_manager
.. attribute:: _printer_profile_manager
.. attribute:: _event_bus
.. attribute:: _analysis_queue
.. attribute:: _slicing_manager
.. attribute:: _file_manager
.. attribute:: _printer
.. attribute:: _app_session_manager
"""
pass
class StartupPlugin(OctoPrintPlugin):
"""
The ``StartupPlugin`` allows hooking into the startup of OctoPrint. It can be used to start up additional services
on or just after the startup of the server.
@ -37,7 +70,7 @@ class StartupPlugin(Plugin):
pass
class ShutdownPlugin(Plugin):
class ShutdownPlugin(OctoPrintPlugin):
"""
The ``ShutdownPlugin`` allows hooking into the shutdown of OctoPrint. It's usually used in conjunction with the
:class:`StartupPlugin` mixin, to cleanly shut down additional services again that where started by the :class:`StartupPlugin`
@ -51,14 +84,14 @@ class ShutdownPlugin(Plugin):
pass
class AssetPlugin(Plugin):
class AssetPlugin(OctoPrintPlugin):
"""
The ``AssetPlugin`` mixin allows plugins to define additional static assets such as Javascript or CSS files to
be automatically embedded into the pages delivered by the server to be used within the client sided part of
the plugin.
A typical usage of the ``AssetPlugin`` functionality is to embed a custom view model to be used by templates injected
through :class:`TemplatePlugin`s.
through a :class:`TemplatePlugin`.
"""
def get_asset_folder(self):
@ -104,7 +137,7 @@ class AssetPlugin(Plugin):
return dict()
class TemplatePlugin(Plugin):
class TemplatePlugin(OctoPrintPlugin):
"""
Using the ``TemplatePlugin`` mixin plugins may inject their own components into the OctoPrint web interface.
@ -408,7 +441,7 @@ class TemplatePlugin(Plugin):
return os.path.join(self._basefolder, "templates")
class SimpleApiPlugin(Plugin):
class SimpleApiPlugin(OctoPrintPlugin):
"""
Utilizing the ``SimpleApiPlugin`` mixin plugins may implement a simple API based around one GET resource and one
resource accepting JSON commands POSTed to it. This is the easy alternative for plugin's which don't need the
@ -543,7 +576,7 @@ class SimpleApiPlugin(Plugin):
return None
class BlueprintPlugin(Plugin):
class BlueprintPlugin(OctoPrintPlugin):
"""
The ``BlueprintPlugin`` mixin allows plugins to define their own full fledged endpoints for whatever purpose,
be it a more sophisticated API than what is possible via the :class:`SimpleApiPlugin` or a custom web frontend.
@ -646,7 +679,7 @@ class BlueprintPlugin(Plugin):
return True
class SettingsPlugin(Plugin):
class SettingsPlugin(OctoPrintPlugin):
"""
Including the ``SettingsPlugin`` mixin allows plugins to store and retrieve their own settings within OctoPrint's
configuration.
@ -697,6 +730,11 @@ class SettingsPlugin(Plugin):
Of course, you are always free to completely override both :func:`on_settings_load` and :func:`on_settings_save` if the
default implementations do not fit your requirements.
.. attribute:: _settings
The :class:`~octoprint.plugin.PluginSettings` instance to use for accessing the plugin's settings. Injected by
the plugin core system upon initialization of the implementation.
"""
def on_settings_load(self):
@ -751,7 +789,7 @@ class SettingsPlugin(Plugin):
return dict()
class EventHandlerPlugin(Plugin):
class EventHandlerPlugin(OctoPrintPlugin):
"""
The ``EventHandlerPlugin`` mixin allows OctoPrint plugins to react to any of :ref:`OctoPrint's events <sec-events>`.
OctoPrint will call the :func:`on_event` method for any event fired on its internal event bus, supplying the
@ -774,7 +812,7 @@ class EventHandlerPlugin(Plugin):
pass
class SlicerPlugin(Plugin):
class SlicerPlugin(OctoPrintPlugin):
"""
Via the ``SlicerPlugin`` mixin plugins can add support for slicing engines to be used by OctoPrint.
@ -835,10 +873,10 @@ class SlicerPlugin(Plugin):
``False`` (defaults to ``True``), an ``IOError`` should be raised.
:param string path: the path to which to save the profile
:param :class:`SlicingProfile` profile: the profile to save
:param SlicingProfile profile: the profile to save
:param bool allow_overwrite: whether to allow to overwrite an existing profile at the indicated path (``True``, default)
or not (``False``) - if a profile already exists on the path and this is ``False``
and :class:`IOError` should be raised
or not (``False``) - if a profile already exists on the path and this is ``False``
and :class:`IOError` should be raised
:param dict overrides: profile overrides to apply to the ``profile`` before saving it
"""
pass
@ -881,7 +919,7 @@ class SlicerPlugin(Plugin):
pass
class ProgressPlugin(Plugin):
class ProgressPlugin(OctoPrintPlugin):
"""
Via the ``ProgressPlugin`` mixing plugins can let themselves be called upon progress in print jobs or slicing jobs,
limited to minimally 1% steps.
@ -911,7 +949,7 @@ class ProgressPlugin(Plugin):
pass
class AppPlugin(Plugin):
class AppPlugin(OctoPrintPlugin):
def get_additional_apps(self):
return []

View file

@ -39,26 +39,26 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin,
def on_startup(self, host, port):
# setup our custom logger
cura_logging_handler = logging.handlers.RotatingFileHandler(self._settings.getPluginLogfilePath(postfix="engine"), maxBytes=2*1024*1024)
cura_logging_handler = logging.handlers.RotatingFileHandler(self._settings.get_plugin_logfile_path(postfix="engine"), maxBytes=2*1024*1024)
cura_logging_handler.setFormatter(logging.Formatter("%(asctime)s %(message)s"))
cura_logging_handler.setLevel(logging.DEBUG)
self._cura_logger.addHandler(cura_logging_handler)
self._cura_logger.setLevel(logging.DEBUG if self._settings.getBoolean(["debug_logging"]) else logging.CRITICAL)
self._cura_logger.setLevel(logging.DEBUG if self._settings.get_boolean(["debug_logging"]) else logging.CRITICAL)
self._cura_logger.propagate = False
##~~ BlueprintPlugin API
@octoprint.plugin.BlueprintPlugin.route("/import", methods=["POST"])
def importCuraProfile(self):
def import_cura_profile(self):
import datetime
import tempfile
from octoprint.server import slicingManager
input_name = "file"
input_upload_name = input_name + "." + self._settings.globalGet(["server", "uploads", "nameSuffix"])
input_upload_path = input_name + "." + self._settings.globalGet(["server", "uploads", "pathSuffix"])
input_upload_name = input_name + "." + self._settings.global_get(["server", "uploads", "nameSuffix"])
input_upload_path = input_name + "." + self._settings.global_get(["server", "uploads", "pathSuffix"])
if input_upload_name in flask.request.values and input_upload_path in flask.request.values:
filename = flask.request.values[input_upload_name]
@ -92,7 +92,7 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin,
# default values for name, display name and description
profile_name = _sanitize_name(name)
profile_display_name = name
profile_description = "Imported from {filename} on {date}".format(filename=filename, date=octoprint.util.getFormattedDateTime(datetime.datetime.now()))
profile_description = "Imported from {filename} on {date}".format(filename=filename, date=octoprint.util.get_formatted_datetime(datetime.datetime.now()))
profile_allow_overwrite = False
# overrides
@ -134,11 +134,11 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin,
##~~ SettingsPlugin API
def on_settings_save(self, data):
old_debug_logging = self._settings.getBoolean(["debug_logging"])
old_debug_logging = self._settings.get_boolean(["debug_logging"])
super(CuraPlugin, self).on_settings_save(data)
new_debug_logging = self._settings.getBoolean(["debug_logging"])
new_debug_logging = self._settings.get_boolean(["debug_logging"])
if old_debug_logging != new_debug_logging:
if new_debug_logging:
self._cura_logger.setLevel(logging.DEBUG)

View file

@ -66,7 +66,7 @@
<div class="input-prepend">
<span class="btn fileinput-button">
<span>{{ _('Browse...') }}</span>
<input id="settings-cura-import" type="file" name="file" data-url="{{ url_for("plugin.cura.importCuraProfile") }}">
<input id="settings-cura-import" type="file" name="file" data-url="{{ url_for("plugin.cura.import_cura_profile") }}">
</span>
<span class="add-on" data-bind="text: fileName"></span>
</div>

View file

@ -467,7 +467,7 @@ class DiscoveryPlugin(octoprint.plugin.StartupPlugin,
if self._settings.get(["pathPrefix"]):
path = self._settings.get(["pathPrefix"])
else:
prefix = self._settings.globalGet(["server", "reverseProxy", "prefixFallback"])
prefix = self._settings.global_get(["server", "reverseProxy", "prefixFallback"])
if prefix:
path = prefix
@ -676,7 +676,7 @@ class DiscoveryPlugin(octoprint.plugin.StartupPlugin,
return upnpUuid
def get_instance_name(self):
name = self._settings.globalGet(["appearance", "name"])
name = self._settings.global_get(["appearance", "name"])
if name:
return u"OctoPrint instance \"{}\"".format(name)
else:

View file

@ -30,7 +30,7 @@ def getConnectionOptions():
"autoconnect": settings().getBoolean(["serial", "autoconnect"])
}
class Printer():
class Printer(comm.MachineComPrintCallback):
def __init__(self, fileManager, analysisQueue, printerProfileManager):
from collections import deque
@ -329,7 +329,7 @@ class Printer():
return
self._printAfterSelect = printAfterSelect
self._comm.selectFile(filename, sd)
self._comm.selectFile("/" + filename if sd else filename, sd)
self._setProgressData(0, None, None, None)
self._setCurrentZ(None)
@ -491,8 +491,14 @@ class Printer():
def _setJobData(self, filename, filesize, sd):
if filename is not None:
if sd:
path_in_storage = filename[1:]
path_on_disk = None
else:
path_in_storage = self._fileManager.path_in_storage(FileDestinations.LOCAL, filename)
path_on_disk = self._fileManager.path_on_disk(FileDestinations.LOCAL, filename)
self._selectedFile = {
"filename": filename,
"filename": path_in_storage,
"filesize": filesize,
"sd": sd,
"estimatedPrintTime": None
@ -518,14 +524,14 @@ class Printer():
averagePrintTime = None
date = None
filament = None
if filename:
if path_on_disk:
# Use a string for mtime because it could be float and the
# javascript needs to exact match
if not sd:
date = int(os.stat(filename).st_ctime)
date = int(os.stat(path_on_disk).st_ctime)
try:
fileData = self._fileManager.get_metadata(FileDestinations.SDCARD if sd else FileDestinations.LOCAL, filename)
fileData = self._fileManager.get_metadata(FileDestinations.SDCARD if sd else FileDestinations.LOCAL, path_on_disk)
except:
fileData = None
if fileData is not None:
@ -549,7 +555,7 @@ class Printer():
self._stateMonitor.setJobData({
"file": {
"name": os.path.basename(filename) if filename is not None else None,
"name": path_in_storage,
"origin": FileDestinations.SDCARD if sd else FileDestinations.LOCAL,
"size": filesize,
"date": date
@ -691,7 +697,7 @@ class Printer():
def getSdFiles(self):
if self._comm is None or not self._comm.isSdReady():
return []
return self._comm.getSdFiles()
return map(lambda x: (x[0][1:], x[1]), self._comm.getSdFiles())
def addSdFile(self, filename, absolutePath, streamingFinishedCallback):
if not self._comm or self._comm.isBusy() or not self._comm.isSdReady():
@ -703,16 +709,16 @@ class Printer():
self.refreshSdFiles(blocking=True)
existingSdFiles = map(lambda x: x[0], self._comm.getSdFiles())
remoteName = util.getDosFilename(filename, existingSdFiles)
remoteName = util.get_dos_filename(filename, existing_filenames=existingSdFiles, extension="gco")
self._timeEstimationData = TimeEstimationHelper()
self._comm.startFileTransfer(absolutePath, filename, remoteName)
self._comm.startFileTransfer(absolutePath, filename, "/" + remoteName)
return remoteName
def deleteSdFile(self, filename):
if not self._comm or not self._comm.isSdReady():
return
self._comm.deleteSdFile(filename)
self._comm.deleteSdFile("/" + filename)
def initSdCard(self):
if not self._comm or self._comm.isSdReady():
@ -728,7 +734,7 @@ class Printer():
"""
Refreshs the list of file stored on the SD card attached to printer (if available and printer communication
available). Optional blocking parameter allows making the method block (max 10s) until the file list has been
received (and can be accessed via self._comm.getSdFiles()). Defaults to a asynchronous operation.
received (and can be accessed via self._comm.getSdFiles()). Defaults to an asynchronous operation.
"""
if not self._comm or not self._comm.isSdReady():
return

View file

@ -534,23 +534,33 @@ class Server():
printer = Printer(fileManager, analysisQueue, printerProfileManager)
appSessionManager = util.flask.AppSessionManager()
def plugin_settings_factory(name, implementation):
def octoprint_plugin_inject_factory(name, implementation):
if not isinstance(implementation, octoprint.plugin.OctoPrintPlugin):
return None
return dict(
plugin_manager=pluginManager,
printer_profile_manager=printerProfileManager,
event_bus=eventManager,
analysis_queue=analysisQueue,
slicing_manager=slicingManager,
file_manager=fileManager,
printer=printer,
app_session_manager=appSessionManager
)
def settings_plugin_inject_factory(name, implementation):
if not isinstance(implementation, octoprint.plugin.SettingsPlugin):
return None
default_settings = implementation.get_settings_defaults()
plugin_settings = octoprint.plugin.plugin_settings(name, defaults=default_settings)
return dict(settings=plugin_settings)
pluginManager.initialize_implementations(additional_injects=dict(
plugin_manager=pluginManager,
printer_profile_manager=printerProfileManager,
event_bus=eventManager,
analysis_queue=analysisQueue,
slicing_manager=slicingManager,
file_manager=fileManager,
printer=printer,
app_session_manager=appSessionManager
), additional_inject_factories=[plugin_settings_factory])
pluginManager.initialize_implementations(
additional_inject_factories=[
octoprint_plugin_inject_factory,
settings_plugin_inject_factory
]
)
slicingManager.initialize()
# configure additional template folders for jinja2
@ -581,7 +591,7 @@ class Server():
if settings().getBoolean(["accessControl", "enabled"]):
userManagerName = settings().get(["accessControl", "userManager"])
try:
clazz = octoprint.util.getClass(userManagerName)
clazz = octoprint.util.get_class(userManagerName)
userManager = clazz()
except AttributeError, e:
logger.exception("Could not instantiate user manager %s, will run with accessControl disabled!" % userManagerName)
@ -784,6 +794,10 @@ class Server():
config = octoprint.util.dict_merge(defaultConfig, configFromFile)
logging.config.dictConfig(config)
logging.captureWarnings(True)
import warnings
warnings.simplefilter("always")
if settings().getBoolean(["serial", "log"]):
# enable debug logging to serial.log

View file

@ -20,7 +20,7 @@ import octoprint.plugin
from octoprint.server import admin_permission, NO_CONTENT
from octoprint.settings import settings as s, valid_boolean_trues
from octoprint.server.util import apiKeyRequestHandler, corsResponseHandler
from octoprint.server.util.flask import restricted_access
from octoprint.server.util.flask import restricted_access, get_remote_address, get_json_command_from_request
#~~ init api blueprint, including sub modules
@ -73,7 +73,7 @@ def pluginCommand(name):
if valid_commands is None:
return make_response("Method not allowed", 405)
command, data, response = util.getJsonCommandFromRequest(request, valid_commands)
command, data, response = get_json_command_from_request(request, valid_commands)
if response is not None:
return response
@ -204,7 +204,7 @@ def login():
localNetworks.add(ip)
try:
remoteAddr = util.getRemoteAddress(request)
remoteAddr = get_remote_address(request)
if netaddr.IPAddress(remoteAddr) in localNetworks:
user = octoprint.server.userManager.findUser(autologinAs)
if user is not None:

View file

@ -11,7 +11,7 @@ from octoprint.settings import settings
from octoprint.printer import getConnectionOptions
from octoprint.server import printer, printerProfileManager, NO_CONTENT
from octoprint.server.api import api
from octoprint.server.util.flask import restricted_access
from octoprint.server.util.flask import restricted_access, get_json_command_from_request
import octoprint.util as util
@ -36,7 +36,7 @@ def connectionCommand():
"disconnect": []
}
command, data, response = util.getJsonCommandFromRequest(request, valid_commands)
command, data, response = get_json_command_from_request(request, valid_commands)
if response is not None:
return response

View file

@ -11,7 +11,7 @@ import octoprint.util as util
from octoprint.filemanager.destinations import FileDestinations
from octoprint.settings import settings, valid_boolean_trues
from octoprint.server import printer, fileManager, slicingManager, eventManager, NO_CONTENT
from octoprint.server.util.flask import restricted_access
from octoprint.server.util.flask import restricted_access, get_json_command_from_request
from octoprint.server.api import api
from octoprint.events import Events
import octoprint.filemanager
@ -27,7 +27,7 @@ def readGcodeFiles():
filter = request.values["filter"]
files = _getFileList(FileDestinations.LOCAL, filter=filter)
files.extend(_getFileList(FileDestinations.SDCARD))
return jsonify(files=files, free=util.getFreeBytes(settings().getBaseFolder("uploads")))
return jsonify(files=files, free=util.get_free_bytes(settings().getBaseFolder("uploads")))
@api.route("/files/<string:origin>", methods=["GET"])
@ -38,7 +38,7 @@ def readGcodeFilesForOrigin(origin):
files = _getFileList(origin)
if origin == FileDestinations.LOCAL:
return jsonify(files=files, free=util.getFreeBytes(settings().getBaseFolder("uploads")))
return jsonify(files=files, free=util.get_free_bytes(settings().getBaseFolder("uploads")))
else:
return jsonify(files=files)
@ -276,7 +276,7 @@ def gcodeFileCommand(filename, target):
"slice": []
}
command, data, response = util.getJsonCommandFromRequest(request, valid_commands)
command, data, response = get_json_command_from_request(request, valid_commands)
if response is not None:
return response

View file

@ -8,7 +8,7 @@ __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms
from flask import request, make_response, jsonify
from octoprint.server import printer, NO_CONTENT
from octoprint.server.util.flask import restricted_access
from octoprint.server.util.flask import restricted_access, get_json_command_from_request
from octoprint.server.api import api
import octoprint.util as util
@ -26,7 +26,7 @@ def controlJob():
"cancel": []
}
command, data, response = util.getJsonCommandFromRequest(request, valid_commands)
command, data, response = get_json_command_from_request(request, valid_commands)
if response is not None:
return response

View file

@ -15,7 +15,7 @@ from octoprint.settings import settings
from octoprint.server import NO_CONTENT, admin_permission
from octoprint.server.util.flask import redirect_to_tornado, restricted_access
from octoprint.server.api import api
from octoprint.util import getFreeBytes
from octoprint.util import get_free_bytes
@api.route("/logs", methods=["GET"])
@ -23,7 +23,7 @@ from octoprint.util import getFreeBytes
@admin_permission.require(403)
def getLogFiles():
files = _getLogFiles()
return jsonify(files=files, free=getFreeBytes(settings().getBaseFolder("logs")))
return jsonify(files=files, free=get_free_bytes(settings().getBaseFolder("logs")))
@api.route("/logs/<path:filename>", methods=["GET"])

View file

@ -12,7 +12,7 @@ import re
from octoprint.settings import settings, valid_boolean_trues
from octoprint.server import printer, NO_CONTENT
from octoprint.server.api import api
from octoprint.server.util.flask import restricted_access
from octoprint.server.util.flask import restricted_access, get_json_command_from_request
import octoprint.util as util
#~~ Printer
@ -64,7 +64,7 @@ def printerToolCommand():
"extrude": ["amount"],
"flowrate": ["factor"]
}
command, data, response = util.getJsonCommandFromRequest(request, valid_commands)
command, data, response = get_json_command_from_request(request, valid_commands)
if response is not None:
return response
@ -166,7 +166,7 @@ def printerBedCommand():
"target": ["target"],
"offset": ["offset"]
}
command, data, response = util.getJsonCommandFromRequest(request, valid_commands)
command, data, response = get_json_command_from_request(request, valid_commands)
if response is not None:
return response
@ -232,7 +232,7 @@ def printerPrintheadCommand():
"home": ["axes"],
"feedrate": ["factor"]
}
command, data, response = util.getJsonCommandFromRequest(request, valid_commands)
command, data, response = get_json_command_from_request(request, valid_commands)
if response is not None:
return response
@ -293,7 +293,7 @@ def printerSdCommand():
"refresh": [],
"release": []
}
command, data, response = util.getJsonCommandFromRequest(request, valid_commands)
command, data, response = get_json_command_from_request(request, valid_commands)
if response is not None:
return response

View file

@ -48,6 +48,7 @@ def getSettings():
"snapshotUrl": s.get(["webcam", "snapshot"]),
"ffmpegPath": s.get(["webcam", "ffmpeg"]),
"bitrate": s.get(["webcam", "bitrate"]),
"ffmpegThreads": s.get(["webcam", "ffmpegThreads"]),
"watermark": s.getBoolean(["webcam", "watermark"]),
"flipH": s.getBoolean(["webcam", "flipH"]),
"flipV": s.getBoolean(["webcam", "flipV"])
@ -139,6 +140,7 @@ def setSettings():
if "snapshotUrl" in data["webcam"].keys(): s.set(["webcam", "snapshot"], data["webcam"]["snapshotUrl"])
if "ffmpegPath" in data["webcam"].keys(): s.set(["webcam", "ffmpeg"], data["webcam"]["ffmpegPath"])
if "bitrate" in data["webcam"].keys(): s.set(["webcam", "bitrate"], data["webcam"]["bitrate"])
if "ffmpegThreads" in data["webcam"].keys(): s.setInt(["webcam", "ffmpegThreads"], data["webcam"]["ffmpegThreads"])
if "watermark" in data["webcam"].keys(): s.setBoolean(["webcam", "watermark"], data["webcam"]["watermark"])
if "flipH" in data["webcam"].keys(): s.setBoolean(["webcam", "flipH"], data["webcam"]["flipH"])
if "flipV" in data["webcam"].keys(): s.setBoolean(["webcam", "flipV"], data["webcam"]["flipV"])

View file

@ -7,7 +7,7 @@ __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms
import os
from flask import request, jsonify, url_for
from flask import request, jsonify, url_for, make_response
from werkzeug.utils import secure_filename
import octoprint.timelapse
@ -30,9 +30,11 @@ def getTimelapseData():
if timelapse is not None and isinstance(timelapse, octoprint.timelapse.ZTimelapse):
config["type"] = "zchange"
config["postRoll"] = timelapse.postRoll()
config["fps"] = timelapse.fps()
elif timelapse is not None and isinstance(timelapse, octoprint.timelapse.TimedTimelapse):
config["type"] = "timed"
config["postRoll"] = timelapse.postRoll()
config["fps"] = timelapse.fps()
config.update({
"interval": timelapse.interval()
})
@ -55,7 +57,7 @@ def downloadTimelapse(filename):
@api.route("/timelapse/<filename>", methods=["DELETE"])
@restricted_access
def deleteTimelapse(filename):
if util.isAllowedFile(filename, {"mpg"}):
if util.is_allowed_file(filename, {"mpg"}):
timelapse_folder = settings().getBaseFolder("timelapse")
full_path = os.path.realpath(os.path.join(timelapse_folder, filename))
if full_path.startswith(timelapse_folder) and os.path.exists(full_path):
@ -70,26 +72,46 @@ def setTimelapseConfig():
config = {
"type": request.values["type"],
"postRoll": 0,
"fps": 25,
"options": {}
}
if "postRoll" in request.values:
try:
config["postRoll"] = int(request.values["postRoll"])
postRoll = int(request.values["postRoll"])
except ValueError:
pass
return make_response("Invalid value for postRoll: %r" % request.values["postRoll"], 400)
else:
if postRoll >= 0:
config["postRoll"] = postRoll
else:
return make_response("Invalid value for postRoll: %d" % postRoll, 400)
if "fps" in request.values:
try:
fps = int(request.values["fps"])
except ValueError:
return make_response("Invalid value for fps: %r" % request.values["fps"], 400)
else:
if fps > 0:
config["fps"] = fps
else:
return make_response("Invalid value for fps: %d" % fps, 400)
if "interval" in request.values:
interval = 10
config["options"] = {
"interval": 10
}
try:
interval = int(request.values["interval"])
except ValueError:
pass
config["options"] = {
"interval": interval
}
return make_response("Invalid value for interval: %r" % request.values["interval"])
else:
if interval > 0:
config["options"]["interval"] = interval
else:
return make_response("Invalid value for interval: %d" % interval)
if admin_permission.can() and "save" in request.values and request.values["save"] in valid_boolean_trues:
octoprint.timelapse.configureTimelapse(config, True)

View file

@ -1,5 +1,6 @@
# coding=utf-8
from __future__ import absolute_import
from flask import make_response
__author__ = "Gina Häußge <osd@foosel.net>"
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
@ -245,3 +246,25 @@ class AppSessionManager(object):
self._logger.debug("App sessions after cleanup: %r" % self._sessions)
def get_remote_address(request):
forwardedFor = request.headers.get("X-Forwarded-For", None)
if forwardedFor is not None:
return forwardedFor.split(",")[0]
return request.remote_addr
def get_json_command_from_request(request, valid_commands):
if not "application/json" in request.headers["Content-Type"]:
return None, None, make_response("Expected content-type JSON", 400)
data = request.json
if not "command" in data.keys() or not data["command"] in valid_commands.keys():
return None, None, make_response("Expected valid command", 400)
command = data["command"]
for parameter in valid_commands[command]:
if not parameter in data:
return None, None, make_response("Mandatory parameter %s missing for command %s" % (parameter, command), 400)
return command, data, None

View file

@ -373,7 +373,7 @@ class UploadStorageFallbackHandler(tornado.web.RequestHandler):
finally:
# make sure the temporary files are removed again
for f in self._files:
octoprint.util.silentRemove(f)
octoprint.util.silent_remove(f)
# make all http methods trigger _handle_method
get = _handle_method

View file

@ -36,7 +36,7 @@ class GcodeWatchdogHandler(watchdog.events.PatternMatchingEventHandler):
self.filename = os.path.basename(self._path)
def save(self, target):
octoprint.util.safeRename(self._path, target)
octoprint.util.safe_rename(self._path, target)
file_wrapper = WatchdogFileWrapper(path)

View file

@ -61,6 +61,7 @@ default_settings = {
"stream": None,
"snapshot": None,
"ffmpeg": None,
"ffmpegThreads": 1,
"bitrate": "5000k",
"watermark": True,
"flipH": False,
@ -68,7 +69,8 @@ default_settings = {
"timelapse": {
"type": "off",
"options": {},
"postRoll": 0
"postRoll": 0,
"fps": 25
}
},
"gcodeViewer": {

File diff suppressed because one or more lines are too long

View file

@ -236,6 +236,26 @@ $(function() {
return $(window).height() - 165;
};
// jquery plugin to select all text in an element
// originally from: http://stackoverflow.com/a/987376
$.fn.selectText = function() {
var doc = document;
var element = this[0];
var range, selection;
if (doc.body.createTextRange) {
range = document.body.createTextRange();
range.moveToElementText(element);
range.select();
} else if (window.getSelection) {
selection = window.getSelection();
range = document.createRange();
range.selectNodeContents(element);
selection.removeAllRanges();
selection.addRange(range);
}
};
// Use bootstrap tabdrop for tabs and pills
$('.nav-pills, .nav-tabs').tabdrop();

View file

@ -55,6 +55,7 @@ $(function() {
self.webcam_snapshotUrl = ko.observable(undefined);
self.webcam_ffmpegPath = ko.observable(undefined);
self.webcam_bitrate = ko.observable(undefined);
self.webcam_ffmpegThreads = ko.observable(undefined);
self.webcam_watermark = ko.observable(undefined);
self.webcam_flipH = ko.observable(undefined);
self.webcam_flipV = ko.observable(undefined);
@ -188,6 +189,7 @@ $(function() {
self.webcam_snapshotUrl(response.webcam.snapshotUrl);
self.webcam_ffmpegPath(response.webcam.ffmpegPath);
self.webcam_bitrate(response.webcam.bitrate);
self.webcam_ffmpegThreads(response.webcam.ffmpegThreads);
self.webcam_watermark(response.webcam.watermark);
self.webcam_flipH(response.webcam.flipH);
self.webcam_flipV(response.webcam.flipV);
@ -255,6 +257,7 @@ $(function() {
"snapshotUrl": self.webcam_snapshotUrl(),
"ffmpegPath": self.webcam_ffmpegPath(),
"bitrate": self.webcam_bitrate(),
"ffmpegThreads": self.webcam_ffmpegThreads(),
"watermark": self.webcam_watermark(),
"flipH": self.webcam_flipH(),
"flipV": self.webcam_flipV()
@ -326,4 +329,4 @@ $(function() {
["loginStateViewModel", "usersViewModel", "printerProfilesViewModel"],
["#settings_dialog", "#navbar_settings"]
]);
});
});

View file

@ -5,7 +5,8 @@ $(function() {
self.loginState = parameters[0];
self.settings = parameters[1];
self.log = [];
self.log = ko.observableArray([]);
self.buffer = ko.observable(300);
self.command = ko.observable(undefined);
@ -20,15 +21,56 @@ $(function() {
self.autoscrollEnabled = ko.observable(true);
self.filters = self.settings.terminalFilters;
self.filterRegex = undefined;
self.filterRegex = ko.observable();
self.cmdHistory = [];
self.cmdHistoryIdx = -1;
self.displayedLines = ko.computed(function() {
var regex = self.filterRegex();
var lineVisible = function(entry) {
return regex == undefined || !entry.line.match(regex);
};
var filtered = false;
var result = [];
_.each(self.log(), function(entry) {
if (lineVisible(entry)) {
result.push(entry);
filtered = false;
} else if (!filtered) {
result.push(self._toInternalFormat("[...]", "filtered"));
filtered = true;
}
});
return result;
});
self.displayedLines.subscribe(function() {
self.updateOutput();
});
self.lineCount = ko.computed(function() {
var total = self.log().length;
var displayed = _.filter(self.displayedLines(), function(entry) { return entry.type == "line" }).length;
var filtered = total - displayed;
if (total == displayed) {
return _.sprintf(gettext("showing %(displayed)d lines"), {displayed: displayed});
} else {
return _.sprintf(gettext("showing %(displayed)d lines (%(filtered)d of %(total)d total lines filtered)"), {displayed: displayed, total: total, filtered: filtered});
}
});
self.autoscrollEnabled.subscribe(function(newValue) {
if (newValue) {
self.log(self.log.slice(-self.buffer()));
}
});
self.activeFilters = ko.observableArray([]);
self.activeFilters.subscribe(function(e) {
self.updateFilterRegex();
self.updateOutput();
});
self.fromCurrentData = function(data) {
@ -42,16 +84,21 @@ $(function() {
};
self._processCurrentLogData = function(data) {
if (!self.log)
self.log = [];
self.log = self.log.concat(data);
self.log = self.log.slice(-300);
self.updateOutput();
self.log(self.log().concat(_.map(data, function(line) { return self._toInternalFormat(line) })));
if (self.autoscrollEnabled()) {
self.log(self.log.slice(-300));
}
};
self._processHistoryLogData = function(data) {
self.log = data;
self.updateOutput();
self.log(_.map(data, function(line) { return self._toInternalFormat(line) }));
};
self._toInternalFormat = function(line, type) {
if (type == undefined) {
type = "line";
}
return {line: line, type: type}
};
self._processStateData = function(data) {
@ -67,29 +114,34 @@ $(function() {
self.updateFilterRegex = function() {
var filterRegexStr = self.activeFilters().join("|").trim();
if (filterRegexStr == "") {
self.filterRegex = undefined;
self.filterRegex(undefined);
} else {
self.filterRegex = new RegExp(filterRegexStr);
self.filterRegex(new RegExp(filterRegexStr));
}
self.updateOutput();
};
self.updateOutput = function() {
if (!self.log)
return;
var output = "";
for (var i = 0; i < self.log.length; i++) {
if (self.filterRegex !== undefined && self.log[i].match(self.filterRegex)) continue;
output += self.log[i] + "\n";
if (self.autoscrollEnabled()) {
self.scrollToEnd();
}
};
self.toggleAutoscroll = function() {
self.autoscrollEnabled(!self.autoscrollEnabled());
};
self.selectAll = function() {
var container = $("#terminal-output");
if (container.length) {
container.text(output);
container.selectText();
}
};
if (self.autoscrollEnabled()) {
container.scrollTop(container[0].scrollHeight - container.height())
}
self.scrollToEnd = function() {
var container = $("#terminal-output");
if (container.length) {
container.scrollTop(container[0].scrollHeight - container.height())
}
};
@ -155,10 +207,12 @@ $(function() {
};
self.onAfterTabChange = function(current, previous) {
if (current != "#terminal") {
if (current != "#term") {
return;
}
self.updateOutput();
if (self.autoscrollEnabled()) {
self.scrollToEnd();
}
};
}

View file

@ -4,9 +4,14 @@ $(function() {
self.loginState = parameters[0];
self.defaultFps = 25;
self.defaultPostRoll = 0;
self.defaultInterval = 10;
self.timelapseType = ko.observable(undefined);
self.timelapseTimedInterval = ko.observable(undefined);
self.timelapsePostRoll = ko.observable(undefined);
self.timelapseTimedInterval = ko.observable(self.defaultInterval);
self.timelapsePostRoll = ko.observable(self.defaultPostRoll);
self.timelapseFps = ko.observable(self.defaultFps);
self.persist = ko.observable(false);
self.isDirty = ko.observable(false);
@ -42,6 +47,9 @@ $(function() {
self.timelapsePostRoll.subscribe(function(newValue) {
self.isDirty(true);
});
self.timelapseFps.subscribe(function(newValue) {
self.isDirty(true);
});
// initialize list helper
self.listHelper = new ItemListHelper(
@ -91,15 +99,23 @@ $(function() {
self.listHelper.updateItems(response.files);
if (config.type == "timed") {
if (response.config.interval != undefined && response.config.interval > 0) {
self.timelapseTimedInterval(response.config.interval);
}
if (response.config.postRoll != undefined && response.config.postRoll >= 0) {
self.timelapsePostRoll(response.config.postRoll);
if (config.interval != undefined && config.interval > 0) {
self.timelapseTimedInterval(config.interval);
}
} else {
self.timelapseTimedInterval(undefined);
self.timelapsePostRoll(undefined);
self.timelapseTimedInterval(self.defaultInterval);
}
if (config.postRoll != undefined && config.postRoll >= 0) {
self.timelapsePostRoll(config.postRoll);
} else {
self.timelapsePostRoll(self.defaultPostRoll);
}
if (config.fps != undefined && config.fps > 0) {
self.timelapseFps(config.fps);
} else {
self.timelapseFps(self.defaultFps);
}
self.persist(false);
@ -137,6 +153,7 @@ $(function() {
var payload = {
"type": self.timelapseType(),
"postRoll": self.timelapsePostRoll(),
"fps": self.timelapseFps(),
"save": self.persist()
};
@ -167,4 +184,4 @@ $(function() {
["loginStateViewModel"],
"#timelapse"
]);
});
});

View file

@ -323,7 +323,7 @@ table {
}
#temp_newTemp, #temp_newBedTemp, #speed_innerWall, #speed_outerWall, #speed_fill, #speed_support,
#webcam_timelapse_interval, #webcam_timelapse_postRoll {
#webcam_timelapse_interval, #webcam_timelapse_postRoll, #webcam_timelapse_fps {
text-align: right;
}
@ -590,8 +590,17 @@ ul.dropdown-menu li a {
/** Terminal output */
#term {
#terminal-output {
min-height: 340px;
.terminal {
#terminal-output {
min-height: 340px;
margin-bottom: 5px;
}
margin-bottom: 30px;
}
#terminal-sendpanel {
text-align: right;
}
}

View file

@ -1,28 +1,34 @@
<form class="form-horizontal">
<div class="control-group">
<div class="control-group" title="{{ _('URL to embed into the UI for live viewing of the webcam stream') }}">
<label class="control-label" for="settings-webcamStreamUrl">{{ _('Stream URL') }}</label>
<div class="controls">
<input type="text" class="input-block-level" data-bind="value: webcam_streamUrl" id="settings-webcamStreamUrl">
</div>
</div>
<div class="control-group">
<label class="control-label" for="settings-webcamStreamUrl">{{ _('Snapshot URL') }}</label>
<div class="control-group" title="{{ _('URL to use for retrieving webcam snapshot images for timelapse creation') }}">
<label class="control-label" for="settings-webcamSnapshotUrl">{{ _('Snapshot URL') }}</label>
<div class="controls">
<input type="text" class="input-block-level" data-bind="value: webcam_snapshotUrl" id="settings-webcamSnapshotUrl">
</div>
</div>
<div class="control-group">
<label class="control-label" for="settings-webcamStreamUrl">{{ _('Path to FFMPEG') }}</label>
<div class="control-group" title="{{ _('Full path to the FFMPEG binary') }}">
<label class="control-label" for="settings-webcamFfmpegPath">{{ _('Path to FFMPEG') }}</label>
<div class="controls">
<input type="text" class="input-block-level" data-bind="value: webcam_ffmpegPath" id="settings-webcamFfmpegPath">
</div>
</div>
<div class="control-group">
<div class="control-group" title="{{ _('Bitrate to use for encoding the timelapse video') }}">
<label class="control-label" for="settings-webcamBitrate">{{ _('Timelapse bitrate') }}</label>
<div class="controls">
<input type="text" class="input-block-level" data-bind="value: webcam_bitrate" id="settings-webcamBitrate">
</div>
</div>
<div class="control-group" title="{{ _('Number of FFMPEG encoding threads') }}">
<label class="control-label" for="settings-webcam_ffmpegThreads">{{ _('FFMPEG threads') }}</label>
<div class="controls">
<input class="input-block-level" data-bind="value: webcam_ffmpegThreads" id="settings-webcamFfmpegThreads" type="number" step="1" min="1">
</div>
</div>
<div class="control-group">
<div class="controls">
<label class="checkbox">

View file

@ -60,6 +60,6 @@
<div class="bar" style="width: 0%"></div>
</div>
<div>
<small>{{ _('Hint: You can also drag and drop files on this page to upload them.') }}</small>
<small class="muted">{{ _('Hint: You can also drag and drop files on this page to upload them.') }}</small>
</div>
</div>

View file

@ -18,7 +18,7 @@
</div>
</div>
<div data-bind="visible: keycontrolPossible">
<small>{{ _("Hint: If you move your mouse over the picture, you enter keyboard control mode.") }}</small>
<small class="muted">{{ _("Hint: If you move your mouse over the picture, you enter keyboard control mode.") }}</small>
</div>
{% endif %}

View file

@ -1,14 +1,22 @@
<pre id="terminal-output" class="pre-scrollable"></pre>
<label class="checkbox">
<input type="checkbox" id="terminal-autoscroll" data-bind="checked: autoscrollEnabled"> {{ _('Autoscroll') }}
</label>
<div data-bind="foreach: filters">
<label class="checkbox">
<input type="checkbox" data-bind="attr: { value: regex }, checked: $parent.activeFilters"> <span data-bind="text: name"></span>
</label>
<div class="terminal">
<pre id="terminal-output" class="pre-scrollable" data-bind="foreach: displayedLines"><span data-bind="text: line, css: {muted: type == 'filtered'}"></span><br></pre>
<small class="pull-left"><button class="btn btn-mini" data-bind="click: toggleAutoscroll, css: {active: autoscrollEnabled}">{{ _('Autoscroll') }}</button> <span data-bind="text: lineCount"></span></small>
<small class="pull-right"><a href="#" data-bind="click: scrollToEnd">{{ _("Scroll to end") }}</a>&nbsp;|&nbsp;<a href="#" data-bind="click: selectAll">{{ _("Select all") }}</a></small>
</div>
<div class="input-append" style="display: none;" data-bind="visible: loginState.isUser">
<input type="text" id="terminal-command" data-bind="value: command, event: { keyup: function(d,e) { return handleKeyUp(e); }, keydown: function(d,e) { return handleKeyDown(e); } }, enable: isOperational() && loginState.isUser()">
<button class="btn" type="button" id="terminal-send" data-bind="click: sendCommand, enable: isOperational() && loginState.isUser()">{{ _('Send') }}</button>
<div class="row-fluid">
<div class="span6" id="termin-filterpanel">
<div data-bind="foreach: filters">
<label class="checkbox">
<input type="checkbox" data-bind="attr: { value: regex }, checked: $parent.activeFilters"> <span data-bind="text: name"></span>
</label>
</div>
</div>
<div class="span6" id="terminal-sendpanel" style="display: none;" data-bind="visible: loginState.isUser">
<div class="input-append">
<input type="text" id="terminal-command" data-bind="value: command, event: { keyup: function(d,e) { return handleKeyUp(e); }, keydown: function(d,e) { return handleKeyDown(e); } }, enable: isOperational() && loginState.isUser()">
<button class="btn" type="button" id="terminal-send" data-bind="click: sendCommand, enable: isOperational() && loginState.isUser()">{{ _('Send') }}</button>
</div>
<small class="muted">{{ _('Hint: Use the arrow up/down keys to recall commands sent previously') }}</small>
</div>
</div>

View file

@ -8,6 +8,12 @@
<option value="timed">{{ _('Timed') }}</option>
</select>
<label for="webcam_timelapse_fps">{{ _('Timelapse frame rate (in frames per second)') }}</label>
<div class="input-append">
<input type="text" class="input-mini" id="webcam_timelapse_fps" data-bind="value: timelapseFps, valueUpdate: 'afterkeydown', enable: isOperational() && !isPrinting() && loginState.isUser() && timelapseTypeSelected()">
<span class="add-on">{{ _('fps') }}</span>
</div>
<label for="webcam_timelapse_postRoll">{{ _('Timelapse post roll (in rendered seconds)') }}</label>
<div class="input-append">
<input type="text" class="input-mini" id="webcam_timelapse_postRoll" data-bind="value: timelapsePostRoll, valueUpdate: 'afterkeydown', enable: isOperational() && !isPrinting() && loginState.isUser() && timelapseTypeSelected()">

View file

@ -33,9 +33,9 @@ def getFinishedTimelapses():
statResult = os.stat(os.path.join(basedir, osFile))
files.append({
"name": osFile,
"size": util.getFormattedSize(statResult.st_size),
"size": util.get_formatted_size(statResult.st_size),
"bytes": statResult.st_size,
"date": util.getFormattedDateTime(datetime.datetime.fromtimestamp(statResult.st_ctime))
"date": util.get_formatted_datetime(datetime.datetime.fromtimestamp(statResult.st_ctime))
})
return files
@ -76,18 +76,22 @@ def configureTimelapse(config=None, persist=False):
type = config["type"]
postRoll = 0
if "postRoll" in config:
if "postRoll" in config and config["postRoll"] >= 0:
postRoll = config["postRoll"]
fps = 25
if "fps" in config and config["fps"] > 0:
fps = config["fps"]
if type is None or "off" == type:
current = None
elif "zchange" == type:
current = ZTimelapse(postRoll=postRoll)
current = ZTimelapse(postRoll=postRoll, fps=fps)
elif "timed" == type:
interval = 10
if "options" in config and "interval" in config["options"]:
if "options" in config and "interval" in config["options"] and config["options"]["interval"] > 0:
interval = config["options"]["interval"]
current = TimedTimelapse(postRoll=postRoll, interval=interval)
current = TimedTimelapse(postRoll=postRoll, interval=interval, fps=fps)
notifyCallbacks(current)
@ -97,7 +101,7 @@ def configureTimelapse(config=None, persist=False):
class Timelapse(object):
def __init__(self, postRoll=0):
def __init__(self, postRoll=0, fps=25):
self._logger = logging.getLogger(__name__)
self._imageNumber = None
self._inTimelapse = False
@ -110,8 +114,9 @@ class Timelapse(object):
self._captureDir = settings().getBaseFolder("timelapse_tmp")
self._movieDir = settings().getBaseFolder("timelapse")
self._snapshotUrl = settings().get(["webcam", "snapshot"])
self._ffmpegThreads = settings().get(["webcam", "ffmpegThreads"])
self._fps = 25
self._fps = fps
self._renderThread = None
self._captureMutex = threading.Lock()
@ -127,6 +132,9 @@ class Timelapse(object):
def postRoll(self):
return self._postRoll
def fps(self):
return self._fps
def unload(self):
if self._inTimelapse:
self.stopTimelapse(doCreateMovie=False)
@ -267,7 +275,7 @@ class Timelapse(object):
# prepare ffmpeg command
command = [
ffmpeg, '-loglevel', 'error', '-i', input, '-vcodec', 'mpeg2video', '-pix_fmt', 'yuv420p', '-r', str(self._fps), '-y', '-b:v', bitrate,
ffmpeg, '-framerate', str(self._fps), '-loglevel', 'error', '-i', input, '-vcodec', 'mpeg2video', '-threads', str(self._ffmpegThreads), '-pix_fmt', 'yuv420p', '-r', str(self._fps), '-y', '-b', bitrate,
'-f', 'vob']
filters = []
@ -335,8 +343,8 @@ class Timelapse(object):
class ZTimelapse(Timelapse):
def __init__(self, postRoll=0):
Timelapse.__init__(self, postRoll=postRoll)
def __init__(self, postRoll=0, fps=25):
Timelapse.__init__(self, postRoll=postRoll, fps=fps)
self._logger.debug("ZTimelapse initialized")
def eventSubscriptions(self):
@ -370,8 +378,8 @@ class ZTimelapse(Timelapse):
class TimedTimelapse(Timelapse):
def __init__(self, postRoll=0, interval=1):
Timelapse.__init__(self, postRoll=postRoll)
def __init__(self, postRoll=0, interval=1, fps=25):
Timelapse.__init__(self, postRoll=postRoll, fps=fps)
self._interval = interval
if self._interval < 1:
self._interval = 1 # force minimum interval of 1s

View file

@ -1,38 +1,135 @@
# coding=utf-8
"""
This module bundles commonly used utility methods or helper classes that are used in multiple places withing
OctoPrint's source code.
"""
__author__ = "Gina Häußge <osd@foosel.net>"
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
import os
import traceback
import sys
import time
import re
import tempfile
import logging
import shutil
from flask import make_response
from octoprint.settings import settings, default_settings
from functools import wraps
import warnings
logger = logging.getLogger(__name__)
def getFormattedSize(num):
def warning_decorator_factory(warning_type):
def specific_warning(message, stacklevel=1, since=None, includedoc=None):
def decorator(func):
@wraps(func)
def func_wrapper(*args, **kwargs):
# we need to increment the stacklevel by one because otherwise we'll get the location of our
# func_wrapper in the log, instead of our caller (which is the real caller of the wrapped function)
warnings.warn(message, warning_type, stacklevel=stacklevel + 1)
return func(*args, **kwargs)
if includedoc is not None and since is not None:
docstring = "\n.. deprecated:: {since}\n {message}\n\n".format(since=since, message=includedoc)
if hasattr(func_wrapper, "__doc__") and func_wrapper.__doc__ is not None:
docstring = func_wrapper.__doc__ + "\n" + docstring
func_wrapper.__doc__ = docstring
return func_wrapper
return decorator
return specific_warning
deprecated = warning_decorator_factory(DeprecationWarning)
"""
A decorator for deprecated methods. Logs a deprecation warning via Python's `:mod:`warnings` module including the
supplied ``message``. The call stack level used (for adding the source location of the offending call to the
warning) can be overridden using the optional ``stacklevel`` parameter. If both ``since`` and ``includedoc`` are
provided, a deprecation warning will also be added to the function's docstring by providing or extending its ``__doc__``
property.
Arguments:
message (string): The message to include in the deprecation warning.
stacklevel (int): Stack level for including the caller of the offending method in the logged warning. Defaults to 1,
meaning the direct caller of the method. It might make sense to increase this in case of the function call
happening dynamically from a fixed position to not shadow the real caller (e.g. in case of overridden
``getattr`` methods).
includedoc (string): Message about the deprecation to include in the wrapped function's docstring.
since (string): Version since when the function was deprecated, must be present for the docstring to get extended.
Returns:
function: The wrapped function with the deprecation warnings in place.
"""
pending_deprecation = warning_decorator_factory(PendingDeprecationWarning)
"""
A decorator for methods pending deprecation. Logs a pending deprecation warning via Python's `:mod:`warnings` module
including the supplied ``message``. The call stack level used (for adding the source location of the offending call to
the warning) can be overridden using the optional ``stacklevel`` parameter. If both ``since`` and ``includedoc`` are
provided, a deprecation warning will also be added to the function's docstring by providing or extending its ``__doc__``
property.
Arguments:
message (string): The message to include in the deprecation warning.
stacklevel (int): Stack level for including the caller of the offending method in the logged warning. Defaults to 1,
meaning the direct caller of the method. It might make sense to increase this in case of the function call
happening dynamically from a fixed position to not shadow the real caller (e.g. in case of overridden
``getattr`` methods).
includedoc (string): Message about the deprecation to include in the wrapped function's docstring.
since (string): Version since when the function was deprecated, must be present for the docstring to get extended.
Returns:
function: The wrapped function with the deprecation warnings in place.
"""
def get_formatted_size(num):
"""
Taken from http://stackoverflow.com/a/1094933/2028598
Formats the given byte count as a human readable rounded size expressed in the most pressing unit among B(ytes),
K(ilo)B(ytes), M(ega)B(ytes), G(iga)B(ytes) and T(era)B(ytes), with one decimal place.
Based on http://stackoverflow.com/a/1094933/2028598
Arguments:
num (int): The byte count to format
Returns:
string: The formatted byte count.
"""
for x in ["bytes","KB","MB","GB"]:
for x in ["B","KB","MB","GB"]:
if num < 1024.0:
return "%3.1f%s" % (num, x)
num /= 1024.0
return "%3.1f%s" % (num, "TB")
def isAllowedFile(filename, extensions):
return "." in filename and filename.rsplit(".", 1)[1] in extensions
def is_allowed_file(filename, extensions):
"""
Determines if the provided ``filename`` has one of the supplied ``extensions``. The check is done case-insensitive.
Arguments:
filename (string): The file name to check against the extensions.
extensions (list): The extensions to check against, a list of strings
Return:
boolean: True if the file name's extension matches one of the allowed extensions, False otherwise.
"""
return "." in filename and filename.rsplit(".", 1)[1].lower() in map(str.lower, extensions)
def getFormattedTimeDelta(d):
def get_formatted_timedelta(d):
"""
Formats a timedelta instance as "HH:MM:ss" and returns the resulting string.
Arguments:
d (datetime.timedelta): The timedelta instance to format
Returns:
string: The timedelta formatted as "HH:MM:ss"
"""
if d is None:
return None
hours = d.days * 24 + d.seconds // 3600
@ -41,17 +138,39 @@ def getFormattedTimeDelta(d):
return "%02d:%02d:%02d" % (hours, minutes, seconds)
def getFormattedDateTime(d):
def get_formatted_datetime(d):
"""
Formats a datetime instance as "YYYY-mm-dd HH:MM" and returns the resulting string.
Arguments:
d (datetime.datetime): The datetime instance to format
Returns:
string: The datetime formatted as "YYYY-mm-dd HH:MM"
"""
if d is None:
return None
return d.strftime("%Y-%m-%d %H:%M")
def getClass(name):
def get_class(name):
"""
Taken from http://stackoverflow.com/a/452981/2028598
Retrieves the class object for a given fully qualified class name.
Taken from http://stackoverflow.com/a/452981/2028598.
Arguments:
name (string): The fully qualified class name, including all modules separated by ``.``
Returns:
type: The class if it could be found.
Raises:
AttributeError: The class could not be found.
"""
parts = name.split(".")
module = ".".join(parts[:-1])
m = __import__(module)
@ -60,50 +179,34 @@ def getClass(name):
return m
def isDevVersion():
gitPath = os.path.abspath(os.path.join(os.path.split(os.path.abspath(__file__))[0], "../../../.git"))
return os.path.exists(gitPath)
def get_exception_string():
"""
Retrieves the exception info of the last raised exception and returns it as a string formatted as
``<exception type>: <exception message> @ <source file>:<function name>:<line number>'.
Returns:
string: The formatted exception information.
"""
def getExceptionString():
locationInfo = traceback.extract_tb(sys.exc_info()[2])[0]
return "%s: '%s' @ %s:%s:%d" % (str(sys.exc_info()[0].__name__), str(sys.exc_info()[1]), os.path.basename(locationInfo[0]), locationInfo[2], locationInfo[1])
def getGitInfo():
gitPath = os.path.abspath(os.path.join(os.path.split(os.path.abspath(__file__))[0], "../../../.git"))
if not os.path.exists(gitPath):
return (None, None)
headref = None
with open(os.path.join(gitPath, "HEAD"), "r") as f:
headref = f.readline().strip()
if headref is None:
return (None, None)
headref = headref[len("ref: "):]
branch = headref[headref.rfind("/") + 1:]
with open(os.path.join(gitPath, headref)) as f:
head = f.readline().strip()
return (branch, head)
def getNewTimeout(type):
now = time.time()
if type not in default_settings["serial"]["timeout"].keys():
# timeout immediately for unknown timeout type
return now
return now + settings().getFloat(["serial", "timeout", type])
def getFreeBytes(path):
def get_free_bytes(path):
"""
Retrieves the number of free bytes on the partition ``path`` is located at and returns it. Works on both Windows and
*nix.
Taken from http://stackoverflow.com/a/2372171/2028598
Arguments:
path (string): The path for which to check the remaining partition space.
Returns:
int: The amount of bytes still left on the partition.
"""
path = os.path.abspath(path)
if sys.platform == "win32":
import ctypes
freeBytes = ctypes.c_ulonglong(0)
@ -114,41 +217,115 @@ def getFreeBytes(path):
return st.f_bavail * st.f_frsize
def getRemoteAddress(request):
forwardedFor = request.headers.get("X-Forwarded-For", None)
if forwardedFor is not None:
return forwardedFor.split(",")[0]
return request.remote_addr
def get_dos_filename(origin, existing_filenames=None, extension=None, **kwargs):
"""
Converts the provided input filename to a 8.3 DOS compatible filename. If ``existing_filenames`` is provided, the
conversion result will be guaranteed not to collide with any of the filenames provided thus.
Uses :func:`find_collision_free_name` internally.
def getDosFilename(input, existingFilenames, extension=None):
if input is None:
Arguments:
input (string): The original filename incl. extension to convert to the 8.3 format.
existing_filenames (list): A list of existing filenames with which the generated 8.3 name must not collide.
Optional.
extension (string): The .3 file extension to use for the generated filename. If not provided, the extension of
the provided ``filename`` will simply be truncated to 3 characters.
kwargs (dict): Additional keyword arguments to provide to :func:`find_collision_free_name`.
Returns:
string: A 8.3 compatible translation of the original filename, not colliding with the optionally provided
``existing_filenames`` and with the provided ``extension`` or the original extension shortened to
a maximum of 3 characters.
Raises:
ValueError: No 8.3 compatible name could be found that doesn't collide with the provided ``existing_filenames``.
"""
if origin is None:
return None
if existing_filenames is None:
existing_filenames = []
filename, ext = os.path.splitext(origin)
if extension is None:
extension = "gco"
extension = ext
filename, ext = input.rsplit(".", 1)
return findCollisionfreeName(filename, extension, existingFilenames)
return find_collision_free_name(filename, extension, existing_filenames, **kwargs)
def findCollisionfreeName(input, extension, existingFilenames):
filename = re.sub(r"\s+", "_", input.lower().translate({ord(i):None for i in ".\"/\\[]:;=,"}))
def find_collision_free_name(filename, extension, existing_filenames, max_power=2):
"""
Tries to find a collision free translation of "<filename>.<extension>" to the 8.3 DOS compatible format,
preventing collisions with any of the ``existing_filenames``.
First strips all of ``."/\\[]:;=,`` from the filename and extensions, converts them to lower case and truncates
the ``extension`` to a maximum length of 3 characters.
If the filename is already equal or less than 8 characters in length after that procedure and "<filename>.<extension>"
are not contained in the ``existing_files``, that concatenation will be returned as the result.
If not, the following algorithm will be applied to try to find a collision free name::
set counter := power := 1
while counter < 10^max_power:
set truncated := substr(filename, 0, 6 - power + 1) + "~" + counter
set result := "<truncated>.<extension>"
if result is collision free:
return result
counter++
if counter >= 10 ** power:
power++
raise ValueError
This will basically -- for a given original filename of ``some_filename`` and an extension of ``gco`` -- iterate
through names of the format ``some_f~1.gco``, ``some_f~2.gco``, ..., ``some_~10.gco``, ``some_~11.gco``, ...,
``<prefix>~<n>.gco`` for ``n`` less than 10 ^ ``max_power``, returning as soon as one is found that is not colliding.
Arguments:
filename (string): The filename without the extension to convert to 8.3.
extension (string): The extension to convert to 8.3 -- will be truncated to 3 characters if it's longer than
that.
existing_filenames (list): A list of existing filenames to prevent name collisions with.
max_power (int): Limits the possible attempts of generating a collision free name to 10 ^ ``max_power``
variations. Defaults to 2, so the name generation will maximally reach ``<name>~99.<ext>`` before
aborting and raising an exception.
Returns:
string: A 8.3 representation of the provided original filename, ensured to not collide with the provided
``existing_filenames``
Raises:
ValueError: No collision free name could be found.
"""
# TODO unit test!
def make_valid(text):
return re.sub(r"\s+", "_", text.translate({ord(i):None for i in ".\"/\\[]:;=,"})).lower()
filename = make_valid(filename)
extension = make_valid(extension)
extension = extension[:3] if len(extension) > 3 else extension
if len(filename) <= 8 and not filename + "." + extension in existing_filenames:
# early exit
return filename + "." + extension
counter = 1
power = 1
while counter < (10 * power):
while counter < (10 ** max_power):
result = filename[:(6 - power + 1)] + "~" + str(counter) + "." + extension
if result not in existingFilenames:
if result not in existing_filenames:
return result
counter += 1
if counter == 10 * power:
if counter >= 10 ** power:
power += 1
raise ValueError("Can't create a collision free filename")
def safeRename(old, new, throw_error=False):
def safe_rename(old, new, throw_error=False):
"""
Safely renames a file.
@ -157,10 +334,16 @@ def safeRename(old, new, throw_error=False):
anything goes wrong during those steps, the backup (if already there) will be renamed to its old name and thus
the operation hopefully result in a no-op.
On other operating systems the atomic os.rename function will be used instead.
On other operating systems :func:`shutil.move` will be used instead.
@param old the path to the old file to be renamed
@param new the path to the new file to be created/replaced
Arguments:
old (string): The path to the old file to be renamed.
new (string): The path to the new file to be created/replaced.
throw_error (boolean): Whether to throw an error upon errors during the renaming procedure (True) or not
(False, default).
Raises:
OSError: One of the renaming steps on windows failed and ``throw_error`` was True
"""
if sys.platform == "win32":
@ -169,7 +352,7 @@ def safeRename(old, new, throw_error=False):
try:
if os.path.exists(new):
silentRemove(backup)
silent_remove(backup)
os.rename(new, backup)
os.rename(old, new)
os.remove(backup)
@ -177,7 +360,7 @@ def safeRename(old, new, throw_error=False):
# if anything went wrong, try to rename the backup file to its original name
logger.error("Could not perform safe rename, trying to revert")
if os.path.exists(backup):
silentRemove(new)
silent_remove(new)
os.rename(backup, new)
if throw_error:
raise e
@ -186,11 +369,12 @@ def safeRename(old, new, throw_error=False):
shutil.move(old, new)
def silentRemove(file):
def silent_remove(file):
"""
Silently removes a file. Does not raise an error if the file doesn't exist.
@param file the path of the file to be removed
Arguments:
file (string): The path of the file to be removed
"""
try:
@ -199,15 +383,19 @@ def silentRemove(file):
pass
def sanitizeAscii(line):
def sanitize_ascii(line):
return unicode(line, 'ascii', 'replace').encode('ascii', 'replace').rstrip()
def filterNonAscii(line):
def filter_non_ascii(line):
"""
Returns True if the line contains non-ascii characters, false otherwise
Filter predicate to test if a line contains non ASCII characters.
@param line the line to test
Arguments:
line (string): The line to test
Returns:
boolean: True if the line contains non ASCII characters, False otherwise.
"""
try:
@ -217,28 +405,19 @@ def filterNonAscii(line):
return True
def getJsonCommandFromRequest(request, valid_commands):
if not "application/json" in request.headers["Content-Type"]:
return None, None, make_response("Expected content-type JSON", 400)
data = request.json
if not "command" in data.keys() or not data["command"] in valid_commands.keys():
return None, None, make_response("Expected valid command", 400)
command = data["command"]
for parameter in valid_commands[command]:
if not parameter in data:
return None, None, make_response("Mandatory parameter %s missing for command %s" % (parameter, command), 400)
return command, data, None
def dict_merge(a, b):
'''recursively merges dict's. not just simple a['key'] = b['key'], if
both a and bhave a key who's value is a dict then dict_merge is called
on both values and the result stored in the returned dictionary.
"""
Recursively deep-merges two dictionaries.
Taken from https://www.xormedia.com/recursively-merge-dictionaries-in-python/'''
Taken from https://www.xormedia.com/recursively-merge-dictionaries-in-python/
Arguments:
a (dict): The dictionary to merge ``b`` into
b (dict): The dictionary to merge into ``a``
Returns:
dict: ``b`` deep-merged into ``a``
"""
from copy import deepcopy
@ -254,6 +433,17 @@ def dict_merge(a, b):
def dict_clean(a, b):
"""
Recursively deep-cleans ``b`` from ``a``, removing all keys and corresponding values from ``a`` that appear in
``b``.
Arguments:
a (dict): The dictionary to clean from ``b``.
b (dict): The dictionary to clean ``b`` from.
Results:
dict: A new dict based on ``a`` with all keys (and corresponding values) found in ``b`` removed.
"""
from copy import deepcopy
if not isinstance(b, dict):
@ -271,6 +461,24 @@ def dict_clean(a, b):
def dict_contains_keys(a, b):
"""
Recursively deep-checks if ``a`` contains all keys found in ``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)))
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)))
False
Arguments:
a (dict): The dictionary to check for the keys from ``b``.
b (dict): The dictionary whose keys to check ``a`` for.
Returns:
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):
return False
@ -283,11 +491,14 @@ def dict_contains_keys(a, b):
return True
class Object(object):
pass
def interface_addresses(family=None):
"""
Retrieves all of the host's network interface addresses.
"""
import netifaces
if not family:
family = netifaces.AF_INET
@ -303,6 +514,10 @@ def interface_addresses(family=None):
yield ifaddress["addr"]
def address_for_client(host, port):
"""
Determines the address of the network interface on this host needed to connect to the indicated client host and port.
"""
import socket
for address in interface_addresses():
@ -311,7 +526,7 @@ def address_for_client(host, port):
sock.bind((address, 0))
sock.connect((host, port))
return address
except Exception as e:
pass
except:
continue

View file

@ -20,11 +20,11 @@ from collections import deque
from octoprint.util.avr_isp import stk500v2
from octoprint.util.avr_isp import ispBase
from octoprint.settings import settings
from octoprint.settings import settings, default_settings
from octoprint.events import eventManager, Events
from octoprint.filemanager import valid_file_type
from octoprint.filemanager.destinations import FileDestinations
from octoprint.util import getExceptionString, getNewTimeout, sanitizeAscii, filterNonAscii
from octoprint.util import get_exception_string, sanitize_ascii, filter_non_ascii
from octoprint.util.virtual import VirtualPrinter
try:
@ -172,6 +172,7 @@ class MachineCom(object):
self._sdAvailable = False
self._sdFileList = False
self._sdFiles = []
self._sdFileToSelect = None
# print job
self._currentFile = None
@ -479,7 +480,7 @@ class MachineCom(object):
self._sendFromQueue(sendChecksum=True)
except:
self._logger.exception("Error while trying to start printing")
self._errorValue = getExceptionString()
self._errorValue = get_exception_string()
self._changeState(self.STATE_ERROR)
eventManager().fire(Events.ERROR, {"error": self.getErrorString()})
@ -503,6 +504,7 @@ class MachineCom(object):
if not self.isOperational():
# printer is not connected, can't use SD
return
self._sdFileToSelect = filename
self.sendCommand("M23 %s" % filename)
else:
self._currentFile = PrintingGcodeFileInformation(filename, self.getOffsets)
@ -728,10 +730,10 @@ class MachineCom(object):
self._changeState(self.STATE_CONNECTING)
#Start monitoring the serial port.
self._timeout = getNewTimeout("communication")
self._timeout = get_new_timeout("communication")
tempRequestTimeout = getNewTimeout("temperature")
sdStatusRequestTimeout = getNewTimeout("sdStatus")
tempRequestTimeout = get_new_timeout("temperature")
sdStatusRequestTimeout = get_new_timeout("sdStatus")
startSeen = not settings().getBoolean(["feature", "waitForStartOnConnect"])
swallowOk = False
@ -743,7 +745,7 @@ class MachineCom(object):
if line is None:
break
if line.strip() is not "":
self._timeout = getNewTimeout("communication")
self._timeout = get_new_timeout("communication")
##~~ debugging output handling
if line.startswith("//"):
@ -789,9 +791,12 @@ class MachineCom(object):
size = None
if valid_file_type(filename, "gcode"):
if filterNonAscii(filename):
if filter_non_ascii(filename):
self._logger.warn("Got a file from printer's SD that has a non-ascii filename (%s), that shouldn't happen according to the protocol" % filename)
else:
if not filename.startswith("/"):
# file from the root of the sd -- we'll prepend a /
filename = "/" + filename
self._sdFiles.append((filename, size))
continue
@ -861,7 +866,12 @@ class MachineCom(object):
elif 'File opened' in line:
# answer to M23, at least on Marlin, Repetier and Sprinter: "File opened:%s Size:%d"
match = self._regex_sdFileOpened.search(line)
self._currentFile = PrintingSdFileInformation(match.group(1), int(match.group(2)))
if self._sdFileToSelect:
name = self._sdFileToSelect
self._sdFileToSelect = None
else:
name = match.group(1)
self._currentFile = PrintingSdFileInformation(name, int(match.group(2)))
elif 'File selected' in line:
# final answer to M23, at least on Marlin, Repetier and Sprinter: "File selected"
if self._currentFile is not None:
@ -950,12 +960,12 @@ class MachineCom(object):
self._log("Trying baudrate: %d" % (baudrate))
self._baudrateDetectRetry = 5
self._baudrateDetectTestOk = 0
self._timeout = getNewTimeout("communication")
self._timeout = get_new_timeout("communication")
self._serial.write('\n')
self._sendCommand("M105")
self._testingBaudrate = True
except:
self._log("Unexpected error while setting baudrate: %d %s" % (baudrate, getExceptionString()))
self._log("Unexpected error while setting baudrate: %d %s" % (baudrate, get_exception_string()))
elif 'ok' in line and 'T:' in line:
self._baudrateDetectTestOk += 1
if self._baudrateDetectTestOk < 10:
@ -984,7 +994,7 @@ class MachineCom(object):
#Request the temperature on comm timeout (every 5 seconds) when we are not printing.
if line == "":
self.sendCommand("M105")
tempRequestTimeout = getNewTimeout("temperature")
tempRequestTimeout = get_new_timeout("temperature")
# if we still have commands to process, process them
elif "ok" in line:
@ -1018,11 +1028,11 @@ class MachineCom(object):
if self._heatupWaitStartTime is None:
if time.time() > tempRequestTimeout:
self.sendCommand("M105")
tempRequestTimeout = getNewTimeout("temperature")
tempRequestTimeout = get_new_timeout("temperature")
if self.isSdPrinting() and time.time() > sdStatusRequestTimeout:
self.sendCommand("M27")
sdStatusRequestTimeout = getNewTimeout("sdStatus")
sdStatusRequestTimeout = get_new_timeout("sdStatus")
if self._sendFromQueue(sendChecksum=True):
pass
@ -1074,7 +1084,7 @@ class MachineCom(object):
self._log("Error while connecting to %s: %s" % (p, str(e)))
pass
except:
self._log("Unexpected error while connecting to serial port: %s %s" % (p, getExceptionString()))
self._log("Unexpected error while connecting to serial port: %s %s" % (p, get_exception_string()))
programmer.close()
if self._serial is None:
self._log("Failed to autodetect serial port")
@ -1097,7 +1107,7 @@ class MachineCom(object):
self._serial.parity = serial.PARITY_NONE
self._serial.open()
except:
self._log("Unexpected error while connecting to serial port: %s %s" % (self._port, getExceptionString()))
self._log("Unexpected error while connecting to serial port: %s %s" % (self._port, get_exception_string()))
self._errorValue = "Failed to open serial port, permissions correct?"
self._changeState(self.STATE_ERROR)
eventManager().fire(Events.ERROR, {"error": self.getErrorString()})
@ -1136,14 +1146,14 @@ class MachineCom(object):
try:
ret = self._serial.readline()
except:
self._log("Unexpected error while reading serial port: %s" % (getExceptionString()))
self._errorValue = getExceptionString()
self._log("Unexpected error while reading serial port: %s" % (get_exception_string()))
self._errorValue = get_exception_string()
self.close(True)
return None
if ret == '':
#self._log("Recv: TIMEOUT")
return ''
self._log("Recv: %s" % sanitizeAscii(ret))
self._log("Recv: %s" % sanitize_ascii(ret))
return ret
def _getNext(self):
@ -1293,12 +1303,12 @@ class MachineCom(object):
try:
self._serial.write(cmd + '\n')
except:
self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
self._errorValue = getExceptionString()
self._log("Unexpected error while writing serial port: %s" % (get_exception_string()))
self._errorValue = get_exception_string()
self.close(True)
except:
self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
self._errorValue = getExceptionString()
self._log("Unexpected error while writing serial port: %s" % (get_exception_string()))
self._errorValue = get_exception_string()
self.close(True)
def _gcode_T(self, cmd):
@ -1403,7 +1413,7 @@ class MachineCom(object):
elif s_idx != -1:
# dwell time is specified in seconds
_timeout = int(cmd[s_idx+1:])
self._timeout = getNewTimeout("communication") + _timeout
self._timeout = get_new_timeout("communication") + _timeout
return cmd
### MachineCom callback ################################################################################################
@ -1642,3 +1652,13 @@ class StreamingGcodeFileInformation(PrintingGcodeFileInformation):
def getRemoteFilename(self):
return self._remoteFilename
def get_new_timeout(type):
now = time.time()
if type not in default_settings["serial"]["timeout"].keys():
# timeout immediately for unknown timeout type
return now
return now + settings().getFloat(["serial", "timeout", type])

View file

@ -0,0 +1,258 @@
# coding=utf-8
from __future__ import absolute_import
__author__ = "Gina Häußge <osd@foosel.net>"
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
__copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms of the AGPLv3 License"
import unittest
import mock
import warnings
from ddt import ddt, unpack, data
import octoprint.plugin
import octoprint.settings
@ddt
class SettingsTestCase(unittest.TestCase):
def setUp(self):
warnings.simplefilter("always")
self.plugin_key = "test_plugin"
self.settings = mock.create_autospec(octoprint.settings.Settings)
self.defaults = dict(
some_raw_key="some_raw_value",
some_int_key=1,
some_float_key=2.5,
some_boolean_key=True,
)
self.plugin_settings = octoprint.plugin.PluginSettings(self.settings, self.plugin_key, defaults=self.defaults)
@data(
("get", (["some_raw_key",],), dict(), "get"),
("get", (["some_raw_key",],), dict(merged=True), "get"),
("get", (["some_raw_key",],), dict(asdict=True), "get"),
("get", (["some_raw_key",],), dict(merged=True, asdict=True), "get"),
("get_int", (["some_int_key,"],), dict(), "getInt"),
("get_float", (["some_float_key"],), dict(), "getFloat"),
("get_boolean", (["some_boolean_key",],), dict(), "getBoolean"),
)
@unpack
def test_forwarded_getter(self, getter, getter_args, getter_kwargs, forwarded):
method_under_test = getattr(self.plugin_settings, getter)
self.assertTrue(callable(method_under_test))
method_under_test(*getter_args, **getter_kwargs)
forwarded_method = getattr(self.settings, forwarded)
forwarded_args = (["plugins", self.plugin_key] + getter_args[0],)
forwarded_kwargs = getter_kwargs
forwarded_kwargs.update(dict(defaults=dict(plugins=dict(test_plugin=self.defaults))))
forwarded_method.assert_called_once_with(*forwarded_args, **forwarded_kwargs)
@data(
("global_get", (["some_raw_key"],), dict(), "get"),
("global_get", (["some_raw_key"],), dict(merged=True), "get"),
("global_get", (["some_raw_key"],), dict(asdict=True), "get"),
("global_get", (["some_raw_key"],), dict(merged=True, asdict=True), "get"),
("global_get_int", (["some_int_key"],), dict(), "getInt"),
("global_get_float", (["some_float_key"],), dict(), "getFloat"),
("global_get_boolean", (["some_boolean_key"],), dict(), "getBoolean")
)
@unpack
def test_global_getter(self, getter, getter_args, getter_kwargs, forwarded):
method_under_test = getattr(self.plugin_settings, getter)
self.assertTrue(callable(method_under_test))
method_under_test(*getter_args, **getter_kwargs)
forwarded_method = getattr(self.settings, forwarded)
forwarded_method.assert_called_once_with(*getter_args, **getter_kwargs)
@data(
("getInt", "get_int", "getInt"),
("getFloat", "get_float", "getFloat"),
("getBoolean", "get_boolean", "getBoolean"),
)
@unpack
def test_deprecated_forwarded_getter(self, deprecated, current, forwarded):
with warnings.catch_warnings(record=True) as w:
called_method = getattr(self.settings, forwarded)
called_method.__name__ = forwarded
method = getattr(self.plugin_settings, deprecated)
self.assertTrue(callable(method))
method(["some_raw_key"])
called_method.assert_called_once_with(["plugins", self.plugin_key, "some_raw_key"], defaults=dict(plugins=dict(test_plugin=self.defaults)))
self.assertEquals(1, len(w))
self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
self.assertTrue("{old} has been renamed to {new}".format(old=deprecated, new=current) in (w[-1].message))
@data(
("globalGet", "global_get", "get"),
("globalGetInt", "global_get_int", "getInt"),
("globalGetFloat", "global_get_float", "getFloat"),
("globalGetBoolean", "global_get_boolean", "getBoolean")
)
@unpack
def test_deprecated_global_getter(self, deprecated, current, forwarded):
with warnings.catch_warnings(record=True) as w:
called_method = getattr(self.settings, forwarded)
called_method.__name__ = forwarded
method = getattr(self.plugin_settings, deprecated)
self.assertTrue(callable(method))
method(["some_raw_key"])
called_method.assert_called_once_with(["some_raw_key",])
self.assertEquals(1, len(w))
self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
self.assertTrue("{old} has been renamed to {new}".format(old=deprecated, new=current) in (w[-1].message))
@data(
("set", (["some_raw_key",], "some_value"), dict(), "set"),
("set", (["some_raw_key",], "some_value"), dict(force=True), "set"),
("set_int", (["some_int_key",], 23), dict(), "setInt"),
("set_int", (["some_int_key",], 23), dict(force=True), "setInt"),
("set_float", (["some_float_key",], 2.3), dict(), "setFloat"),
("set_float", (["some_float_key",], 2.3), dict(force=True), "setFloat"),
("set_boolean", (["some_boolean_key",], True), dict(), "setBoolean"),
("set_boolean", (["some_boolean_key",], True), dict(force=True), "setBoolean"),
)
@unpack
def test_forwarded_setter(self, setter, setter_args, setter_kwargs, forwarded):
method_under_test = getattr(self.plugin_settings, setter)
self.assertTrue(callable(method_under_test))
method_under_test(*setter_args, **setter_kwargs)
forwarded_method = getattr(self.settings, forwarded)
forwarded_args = (["plugins", self.plugin_key] + setter_args[0], setter_args[1])
forwarded_kwargs = setter_kwargs
forwarded_kwargs.update(dict(defaults=dict(plugins=dict(test_plugin=self.defaults))))
forwarded_method.assert_called_once_with(*forwarded_args, **forwarded_kwargs)
@data(
("global_set", (["some_raw_key",], "some_value"), dict(), "set"),
("global_set", (["some_raw_key",], "some_value"), dict(force=True), "set"),
("global_set_int", (["some_int_key",], 23), dict(), "setInt"),
("global_set_int", (["some_int_key",], 23), dict(force=True), "setInt"),
("global_set_float", (["some_float_key",], 2.3), dict(), "setFloat"),
("global_set_float", (["some_float_key",], 2.3), dict(force=True), "setFloat"),
("global_set_boolean", (["some_boolean_key",], True), dict(), "setBoolean"),
("global_set_boolean", (["some_boolean_key",], True), dict(force=True), "setBoolean"),
)
@unpack
def test_global_setter(self, setter, setter_args, setter_kwargs, forwarded):
method_under_test = getattr(self.plugin_settings, setter)
self.assertTrue(callable(method_under_test))
method_under_test(*setter_args, **setter_kwargs)
forwarded_method = getattr(self.settings, forwarded)
forwarded_method.assert_called_once_with(*setter_args, **setter_kwargs)
@data(
("setInt", "set_int", "setInt", 1),
("setFloat", "set_float", "setFloat", 2.5),
("setBoolean", "set_boolean", "setBoolean", True)
)
@unpack
def test_deprecated_forwarded_setter(self, deprecated, current, forwarded, value):
with warnings.catch_warnings(record=True) as w:
called_method = getattr(self.settings, forwarded)
called_method.__name__ = forwarded
method = getattr(self.plugin_settings, deprecated)
self.assertTrue(callable(method))
method(["some_raw_key"], value)
called_method.assert_called_once_with(["plugins", self.plugin_key, "some_raw_key"], value, defaults=dict(plugins=dict(test_plugin=self.defaults)))
self.assertEquals(1, len(w))
self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
self.assertTrue("{old} has been renamed to {new}".format(old=deprecated, new=current) in (w[-1].message))
@data(
("globalSet", "global_set", "set", "some_value"),
("globalSetInt", "global_set_int", "setInt", 1),
("globalSetFloat", "global_set_float", "setFloat", 2.5),
("globalSetBoolean", "global_set_boolean", "setBoolean", True)
)
@unpack
def test_deprecated_global_setter(self, deprecated, current, forwarded, value):
with warnings.catch_warnings(record=True) as w:
called_method = getattr(self.settings, forwarded)
called_method.__name__ = forwarded
method = getattr(self.plugin_settings, deprecated)
self.assertTrue(callable(method))
method(["some_raw_key"], value)
called_method.assert_called_once_with(["some_raw_key"], value)
self.assertEquals(1, len(w))
self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
self.assertTrue("{old} has been renamed to {new}".format(old=deprecated, new=current) in (w[-1].message))
def test_global_get_basefolder(self):
self.plugin_settings.global_get_basefolder("some_folder")
self.settings.getBaseFolder.assert_called_once_with("some_folder")
def test_deprecated_global_get_basefolder(self):
with warnings.catch_warnings(record=True) as w:
self.plugin_settings.globalGetBaseFolder("some_folder")
self.settings.getBaseFolder.assert_called_once_with("some_folder")
self.assertEquals(1, len(w))
self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
self.assertTrue("globalGetBaseFolder has been renamed to global_get_basefolder" in (w[-1].message))
def test_logfile_path(self):
import os
self.settings.getBaseFolder.return_value = "/some/folder"
path = self.plugin_settings.get_plugin_logfile_path()
self.settings.getBaseFolder.assert_called_once_with("logs")
self.assertEquals("/some/folder/plugin_{key}.log".format(key=self.plugin_key), path.replace(os.sep, "/"))
def test_logfile_path_with_postfix(self):
import os
self.settings.getBaseFolder.return_value = "/some/folder"
path = self.plugin_settings.get_plugin_logfile_path(postfix="mypostfix")
self.settings.getBaseFolder.assert_called_once_with("logs")
self.assertEquals("/some/folder/plugin_{key}_mypostfix.log".format(key=self.plugin_key), path.replace(os.sep, "/"))
def test_deprecated_logfile_path(self):
import os
with warnings.catch_warnings(record=True) as w:
self.settings.getBaseFolder.return_value = "/some/folder"
path = self.plugin_settings.getPluginLogfilePath()
self.settings.getBaseFolder.assert_called_once_with("logs")
self.assertEquals("/some/folder/plugin_{key}.log".format(key=self.plugin_key), path.replace(os.sep, "/"))
self.assertEquals(1, len(w))
self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
self.assertTrue("getPluginLogfilePath has been renamed to get_plugin_logfile_path" in (w[-1].message))
def test_unhandled_method(self):
try:
self.plugin_settings.some_method("some_parameter")
except AttributeError as e:
self.assertTrue("Mock object has no attribute 'some_method'" in str(e))