More documentation

This commit is contained in:
Gina Häußge 2015-02-27 20:28:24 +01:00
parent 84c32a3cd9
commit cd3ead3f30
12 changed files with 712 additions and 75 deletions

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.

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

@ -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>"
@ -18,9 +33,41 @@ from octoprint.util import deprecated
_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:
@ -51,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:
@ -69,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
@ -119,39 +254,30 @@ class PluginSettings(object):
def global_get(self, path, **kwargs):
return self.settings.get(path, **kwargs)
globalGet = deprecated("globalGet has been renamed to global_get")(global_get)
def global_get_int(self, path, **kwargs):
return self.settings.getInt(path, **kwargs)
globalGetInt = deprecated("globalGetInt has been renamed to global_get_int")(global_get_int)
def global_get_float(self, path, **kwargs):
return self.settings.getFloat(path, **kwargs)
globalGetFloat = deprecated("globalGetFloat has been renamed to global_get_float")(global_get_float)
def global_get_boolean(self, path, **kwargs):
return self.settings.getBoolean(path, **kwargs)
globalGetBoolean = deprecated("globalGetBoolean has been renamed to global_get_boolean")(global_get_boolean)
def global_set(self, path, value, **kwargs):
self.settings.set(path, value, **kwargs)
globalSet = deprecated("globalSet has been renamed to global_set")(global_set)
def global_set_int(self, path, value, **kwargs):
self.settings.setInt(path, value, **kwargs)
globalSetInt = deprecated("globalSetInt has been renamed to global_set_int")(global_set_int)
def global_set_float(self, path, value, **kwargs):
self.settings.setFloat(path, value, **kwargs)
globalSetFloat = deprecated("globalSetFloat has been renamed to global_set_float")(global_set_float)
def global_set_boolean(self, path, value, **kwargs):
self.settings.setBoolean(path, value, **kwargs)
globalSetBoolean = deprecated("globalSetBoolean has been renamed to global_set_boolean")(global_set_boolean)
def global_get_basefolder(self, folder_type, **kwargs):
return self.settings.getBaseFolder(folder_type, **kwargs)
globalGetBaseFolder = deprecated("globalGetBaseFolder has been renamed to global_get_basefolder")(global_get_basefolder)
def get_plugin_logfile_path(self, postfix=None):
filename = "plugin_" + self.plugin_key
@ -159,7 +285,6 @@ class PluginSettings(object):
filename += "_" + postfix
filename += ".log"
return os.path.join(self.settings.getBaseFolder("logs"), filename)
getPluginLogfilePath = deprecated("getPluginLogfilePath has been renamed to get_plugin_logfile_path")(get_plugin_logfile_path)
def __getattr__(self, item):
all_access_methods = self.access_methods.keys() + self.deprecated_access_methods.keys()
@ -179,7 +304,43 @@ class PluginSettings(object):
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

@ -719,7 +719,7 @@ class Printer(comm.MachineComPrintCallback):
self.refreshSdFiles(blocking=True)
existingSdFiles = map(lambda x: x[0], self._comm.getSdFiles())
remoteName = util.get_dos_filename(filename, existingSdFiles)
remoteName = util.get_dos_filename(filename, existing_filenames=existingSdFiles, extension="gco")
self._timeEstimationData = TimeEstimationHelper()
self._comm.startFileTransfer(absolutePath, filename, "/" + remoteName)

View file

@ -533,23 +533,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

View file

@ -1,4 +1,9 @@
# 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'
@ -16,7 +21,7 @@ import warnings
logger = logging.getLogger(__name__)
def warning_decorator_factory(warning_type):
def specific_warning(message, stacklevel=1):
def specific_warning(message, stacklevel=1, since=None, includedoc=None):
def decorator(func):
@wraps(func)
def func_wrapper(*args, **kwargs):
@ -24,19 +29,75 @@ def warning_decorator_factory(warning_type):
# 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
@ -44,10 +105,31 @@ def get_formatted_size(num):
def is_allowed_file(filename, extensions):
return "." in filename and filename.rsplit(".", 1)[1] in 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 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
@ -57,6 +139,16 @@ def get_formatted_timedelta(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
@ -65,8 +157,20 @@ def get_formatted_datetime(d):
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)
@ -76,14 +180,33 @@ def get_class(name):
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.
"""
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 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)
@ -94,28 +217,109 @@ def get_free_bytes(path):
return st.f_bavail * st.f_frsize
def get_dos_filename(input, existingFilenames, extension=None):
if input is None:
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.
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 find_collision_free_name(filename, extension, existingFilenames)
return find_collision_free_name(filename, extension, existing_filenames, **kwargs)
def find_collision_free_name(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")
@ -132,8 +336,14 @@ def safe_rename(old, new, throw_error=False):
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":
@ -163,7 +373,8 @@ 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:
@ -178,9 +389,13 @@ def sanitize_ascii(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:
@ -191,11 +406,18 @@ def filter_non_ascii(line):
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
@ -211,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):
@ -228,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
@ -240,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
@ -260,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():