diff --git a/src/octoprint/cli/__init__.py b/src/octoprint/cli/__init__.py index 1ddf3b17..d7b2bab3 100644 --- a/src/octoprint/cli/__init__.py +++ b/src/octoprint/cli/__init__.py @@ -49,6 +49,7 @@ def hidden_option(*param_decls, **attrs): from .server import server_commands from .plugins import plugin_commands +from .devel import devel_commands def set_ctx_obj_option(ctx, param, value): """Helper for setting eager options on the context.""" @@ -60,7 +61,7 @@ def set_ctx_obj_option(ctx, param, value): @click.group(name="octoprint", invoke_without_command=True, cls=click.CommandCollection, - sources=[server_commands, plugin_commands]) + sources=[server_commands, plugin_commands, devel_commands]) @click.option("--basedir", "-b", type=click.Path(), callback=set_ctx_obj_option, is_eager=True, help="Specify the basedir to use for uploads, timelapses etc.") @click.option("--config", "-c", "configfile", type=click.Path(), callback=set_ctx_obj_option, is_eager=True, diff --git a/src/octoprint/cli/devel.py b/src/octoprint/cli/devel.py new file mode 100644 index 00000000..30e1c393 --- /dev/null +++ b/src/octoprint/cli/devel.py @@ -0,0 +1,75 @@ +# coding=utf-8 +from __future__ import absolute_import + +__author__ = "Gina Häußge " +__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html' +__copyright__ = "Copyright (C) 2015 The OctoPrint Project - Released under terms of the AGPLv3 License" + + +import click +import logging + + +class OctoPrintDevelCommands(click.MultiCommand): + + sep = ":" + + def list_commands(self, ctx): + result = [name for name in self._get_commands()] + result.sort() + return result + + def get_command(self, ctx, cmd_name): + commands = self._get_commands() + return commands.get(cmd_name, None) + + def _get_commands(self): + commands = dict() + + for name in [x for x in dir(self) if x.startswith("command_")]: + method = getattr(self, name) + + try: + result = method() + if result is not None: + commands["devel" + self.sep + result.name] = result + except: + logging.getLogger(__name__).exception("There was an error registering one of the devel commands ({})".format(name)) + + return commands + + def command_newplugin(self): + try: + import cookiecutter.main + except ImportError: + return None + + import contextlib + + @contextlib.contextmanager + def custom_cookiecutter_config(config): + from octoprint.util import fallback_dict + + original_get_user_config = cookiecutter.main.get_user_config + original_config = original_get_user_config() + try: + cookiecutter.main.get_user_config = lambda: fallback_dict(config, original_config) + yield + finally: + cookiecutter.main.get_user_config = original_get_user_config + + @click.command("newplugin") + def command(): + """Creates a new plugin based on the OctoPrint Plugin cookiecutter template.""" + from octoprint.util import tempdir + + with tempdir() as path: + custom = dict(cookiecutters_dir=path) + with custom_cookiecutter_config(custom): + cookiecutter.main.cookiecutter("gh:OctoPrint/cookiecutter-octoprint-plugin") + + return command + +@click.group(cls=OctoPrintDevelCommands) +def devel_commands(): + pass diff --git a/src/octoprint/util/__init__.py b/src/octoprint/util/__init__.py index d5066e48..31cbf899 100644 --- a/src/octoprint/util/__init__.py +++ b/src/octoprint/util/__init__.py @@ -547,6 +547,36 @@ def dict_contains_keys(keys, dictionary): return True + +class fallback_dict(dict): + def __init__(self, custom, *fallbacks): + self.custom = custom + self.fallbacks = fallbacks + + def __getitem__(self, item): + for dictionary in self._all(): + if item in dictionary: + return dictionary[item] + raise KeyError() + + def __setitem__(self, key, value): + self.custom[key] = value + + def __delitem__(self, key): + for dictionary in self._all(): + if key in dictionary: + del dictionary[key] + + def keys(self): + result = set() + for dictionary in self._all(): + result += dictionary.keys() + return result + + def _all(self): + return [self.custom] + list(self.fallbacks) + + class Object(object): pass @@ -594,6 +624,18 @@ def atomic_write(filename, mode="w+b", prefix="tmp", suffix=""): shutil.move(temp_config.name, filename) +@contextlib.contextmanager +def tempdir(**kwargs): + import tempfile + import shutil + + dirpath = tempfile.mkdtemp(**kwargs) + try: + yield dirpath + finally: + shutil.rmtree(dirpath) + + def bom_aware_open(filename, encoding="ascii", mode="r", **kwargs): import codecs