diff --git a/src/octoprint/cli/devel.py b/src/octoprint/cli/devel.py index c4625e6e..50155f28 100644 --- a/src/octoprint/cli/devel.py +++ b/src/octoprint/cli/devel.py @@ -52,6 +52,10 @@ class OctoPrintDevelCommands(click.MultiCommand): @contextlib.contextmanager def custom_cookiecutter_config(config): + """ + Allows overriding cookiecutter's user config with a custom dict + with fallback to the original data. + """ from octoprint.util import fallback_dict original_get_user_config = cookiecutter.main.get_user_config @@ -62,15 +66,92 @@ class OctoPrintDevelCommands(click.MultiCommand): finally: cookiecutter.main.get_user_config = original_get_user_config + @contextlib.contextmanager + def custom_cookiecutter_prompt(options): + """ + Custom cookiecutter prompter for the template config. + + If a setting is available in the provided options (read from the CLI) + that will be used, otherwise the user will be prompted for a value + via click. + """ + original_prompt_for_config = cookiecutter.main.prompt_for_config + + def custom_prompt_for_config(context, no_input=False): + import cookiecutter.prompt + + cookiecutter_dict = {} + env = cookiecutter.prompt.Environment() + + for key, raw in cookiecutter.prompt.iteritems(context['cookiecutter']): + if key in options: + val = options[key] + else: + raw = raw if cookiecutter.prompt.is_string(raw) else str(raw) + val = env.from_string(raw).render(cookiecutter=cookiecutter_dict) + + if not no_input: + new_val = click.prompt(key, default=val) + if new_val != "": + val = new_val + + cookiecutter_dict[key] = val + return cookiecutter_dict + + try: + cookiecutter.main.prompt_for_config = custom_prompt_for_config + yield + finally: + cookiecutter.main.prompt_for_config = original_prompt_for_config + @click.command("newplugin") - def command(): + @click.option("--name", "-n", help="The name of the plugin") + @click.option("--package", "-p", help="The plugin package") + @click.option("--author", "-a", help="The plugin author's name") + @click.option("--email", "-e", help="The plugin author's mail address") + @click.option("--license", "-l", help="The plugin's license") + @click.option("--description", "-d", help="The plugin's description") + @click.option("--homepage", help="The plugin's homepage URL") + @click.option("--source", "-s", help="The URL to the plugin's source") + @click.option("--installurl", "-i", help="The plugin's install URL") + @click.argument("identifier", required=False) + def command(name, package, author, email, description, license, homepage, source, installurl, identifier): """Creates a new plugin based on the OctoPrint Plugin cookiecutter template.""" from octoprint.util import tempdir - with tempdir() as path: + # deleting a git checkout folder might run into access errors due + # to write-protected sub folders, so we use a custom onerror handler + # that tries to fix such permissions + def onerror(func, path, exc_info): + """Originally from http://stackoverflow.com/a/2656405/2028598""" + import stat + import os + + if not os.access(path, os.W_OK): + os.chmod(path, stat.S_IWUSR) + func(path) + else: + raise + + with tempdir(onerror=onerror) as path: custom = dict(cookiecutters_dir=path) with custom_cookiecutter_config(custom): - cookiecutter.main.cookiecutter("gh:OctoPrint/cookiecutter-octoprint-plugin") + raw_options = dict( + plugin_identifier=identifier, + plugin_package=package, + plugin_name=name, + full_name=author, + email=email, + plugin_description=description, + plugin_license=license, + plugin_homepage=homepage, + plugin_source=source, + plugin_installurl=installurl + ) + options = dict((k, v) for k, v in raw_options.items() if v is not None) + + with custom_cookiecutter_prompt(options): + cookiecutter.main.cookiecutter("gh:OctoPrint/cookiecutter-octoprint-plugin") return command diff --git a/src/octoprint/util/__init__.py b/src/octoprint/util/__init__.py index 31cbf899..b8096ecf 100644 --- a/src/octoprint/util/__init__.py +++ b/src/octoprint/util/__init__.py @@ -625,7 +625,7 @@ def atomic_write(filename, mode="w+b", prefix="tmp", suffix=""): @contextlib.contextmanager -def tempdir(**kwargs): +def tempdir(ignore_errors=False, onerror=None, **kwargs): import tempfile import shutil @@ -633,7 +633,7 @@ def tempdir(**kwargs): try: yield dirpath finally: - shutil.rmtree(dirpath) + shutil.rmtree(dirpath, ignore_errors=ignore_errors, onerror=onerror) def bom_aware_open(filename, encoding="ascii", mode="r", **kwargs):