Source code for jupyter_starters.py_starters.cookiecutter

"""A starter that runs cookiecutter."""
# pylint: disable=cyclic-import,duplicate-code,broad-except

import re
import shutil
from copy import deepcopy
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import TYPE_CHECKING, Any, Dict, Text

from jupyter_server.utils import url_path_join as ujoin

from ..json_ import JsonSchemaException, json_validator
from ..types import Status

if TYPE_CHECKING:
    from ..manager import StarterManager  # noqa

try:
    import cookiecutter.main

    HAS_COOKIECUTTER = True
    HAS_DIRECTORY = cookiecutter.__version__ >= "1.7.1"
except ImportError:
    HAS_COOKIECUTTER = False
    HAS_DIRECTORY = False


GH = "https://github.com"
GITHUB_TOPIC = f"{GH}/topics/cookiecutter-template"
GITHUB_SEARCH = f"{GH}/search?utf8=%E2%9C%93&q=path%3A%2F+filename%3Acookiecutter.json"

JUPYTER_COOKIECUTTERS = {
    "Jupyter Docker Environments": [
        {
            "repo": f"{GH}/jupyter/cookiecutter-docker-stacks",
            "description": (
                "Cookiecutter for community-maintained Jupyter " "Docker images"
            ),
        },
    ],
    "Jupyter Widgets": [
        {
            "repo": f"{GH}/jupyter-widgets/widget-ts-cookiecutter",
            "description": (
                "A highly opinionated cookiecutter template for" "ipywidget extensions."
            ),
        },
        {
            "repo": f"{GH}/jupyter-widgets/widget-cookiecutter",
            "description": (
                "A cookiecutter template for creating a custom Jupyter"
                "widget project."
            ),
        },
    ],
    "JupyterLab Extensions": [
        {
            "repo": f"{GH}/jupyterlab/extension-cookiecutter-js",
            "description": "A cookiecutter recipe for building JupyterLab extensions.",
        },
        {
            "repo": f"{GH}/jupyterlab/extension-cookiecutter-ts",
            "description": (
                "A cookiecutter recipe for JupyterLab extensions in Typescript"
            ),
        },
        {
            "repo": f"{GH}/jupyterlab/mimerender-cookiecutter-ts",
            "description": (
                "Cookie cutter for JupyterLab mimerenderer"
                "extensions using TypeScript"
            ),
        },
    ],
    "JupyterLite Extensions": [
        {
            "repo": f"{GH}/jupyterlite/serverlite-cookiecutter-ts",
            "description": "A cookiecutter for a JupyterLite Kernel",
        },
    ],
    **(
        {}
        if not HAS_DIRECTORY
        else {
            "Jupyter Server Proxy": [
                {
                    "repo": f"{GH}/jupyterhub/jupyter-server-proxy",
                    "description": (
                        "Configure a jupyter-server-proxy "
                        "(use directory: `contrib/template`)"
                    ),
                }
            ],
        }
    ),
}


[docs]def cookiecutter_pantry(): """Try to load the pantry from the cookiecutter metadata.""" grouped = {**JUPYTER_COOKIECUTTERS} return [ { "title": name, "enum": [t["repo"] for t in templates], "enumNames": [ f"""{"/".join(t["repo"].split("/")[-2:])}: {t["description"]}""" for t in templates ], "default": sorted(templates, key=lambda t: t["repo"])[0]["repo"], } for name, templates in grouped.items() ]
[docs]def cookiecutter_starters(manager: "StarterManager"): """Try to find some cookiecutters.""" if not HAS_COOKIECUTTER: manager.log.debug( "🍪 install cookiecutter to enable the cookiecutter starter. yum!" ) return {} return { "cookiecutter": { "label": "Cookiecutter", "description": f"Cookiecutter {cookiecutter.__version__}", "type": "python", "callable": "jupyter_starters.py_starters.cookiecutter.start", "schema": { "type": "object", "required": ["template"], "properties": { "template": { "title": "Template", "type": "string", "description": ( "Directory or URL of template. " f" Find more on GitHub by [topic]({GITHUB_TOPIC}) " f" or [advanced search]({GITHUB_SEARCH})." ), "anyOf": [ {"type": "string", "title": "Enter URL"}, *cookiecutter_pantry(), ], }, "checkout": { "title": "Checkout", "description": "The branch, tag, or commit ID to use", "type": "string", "default": "HEAD", }, **{ "directory": { "title": "Directory", "description": ( "Relative path to a cookiecutter " "template in a repository." ), "type": "string", "default": "", } if HAS_DIRECTORY else {} }, }, }, "uiSchema": {"template": {"ui:autofocus": True}}, } }
[docs]def cookiecutter_to_schema(cookiecutter_json): """Convert a cookiecutter context to a JSON schema.""" bools = {"y": True, "n": False} schema = { "title": "Cookiecutter", "description": "Values to use in template variables", "type": "object", "properties": {}, } ui_schema = {} schema["properties"] = properties = {} for field, value in cookiecutter_json.items(): title = field.replace("_", " ").replace("-", " ").title() if isinstance(value, str): if value in bools: properties[field] = { "type": "string", "default": value, "title": title, "enum": [*bools.keys()], "enumNames": ["yes", "no"], } ui_schema[field] = {"ui:widget": "radio"} continue value_no_tmpl = re.sub(r"{[%{].*?[%}]}", "", value) properties[field] = { "type": "string", "description": f"default: {value}", "default": value_no_tmpl, "title": title, "minLength": 1, } continue if isinstance(value, dict): enum = list(value.keys()) properties[field] = {"enum": enum, "default": enum[0], "title": title} continue if isinstance(value, list): properties[field] = {"enum": value, "default": value[0], "title": title} continue schema["required"] = sorted(list(schema["properties"].keys())) return schema, ui_schema
[docs]async def start(name, starter, path, body, manager) -> Dict[Text, Any]: """Run cookiecutter.""" # pylint: disable=cyclic-import,broad-except,too-many-locals,unused-variable template = body["template"] checkout = body.get("checkout") manager.log.debug(f"🍪 body: {body}") config_dict = cookiecutter.main.get_user_config() repo_dir_kwargs = dict( template=template, abbreviations=config_dict["abbreviations"], clone_to_dir=config_dict["cookiecutters_dir"], checkout=checkout, no_input=True, password=None, ) if HAS_DIRECTORY: directory = body.get("directory") repo_dir_kwargs.update(directory=directory) repo_dir, cleanup = cookiecutter.main.determine_repo_dir(**repo_dir_kwargs) manager.log.debug(f"🍪 repo_dir: {repo_dir}") context_file = Path(repo_dir) / "cookiecutter.json" base_context = dict( cookiecutter.main.generate_context( context_file=str(context_file), default_context=config_dict["default_context"], extra_context={}, ) ) manager.log.debug(f"🍪 base_context: {base_context}") schema, ui_schema = cookiecutter_to_schema(base_context["cookiecutter"]) manager.log.debug(f"🍪 schema: {schema}") new_starter = deepcopy(starter) new_starter["schema"]["required"] += ["cookiecutter"] new_starter["schema"]["properties"]["cookiecutter"] = schema new_starter.setdefault("uiSchema", {})["cookiecutter"] = ui_schema validator = json_validator(new_starter["schema"]) valid = False try: validator(body) valid = True except JsonSchemaException as err: manager.log.debug(f"🍪 validator: {err}") if not valid: return { "body": body, "name": name, "path": path, "starter": new_starter, "status": Status.CONTINUING, } with TemporaryDirectory() as tmpd: final_context = {"cookiecutter": body["cookiecutter"]} final_context["cookiecutter"]["_template"] = template try: result = cookiecutter.main.generate_files( repo_dir=repo_dir, context=final_context, overwrite_if_exists=True, output_dir=tmpd, ) manager.log.debug(f"result {result}") roots = sorted(Path(tmpd).glob("*")) for root in roots: await manager.just_copy(root, path) if cleanup: shutil.rmtree(repo_dir) return { "body": body, "name": name, "path": ujoin(path, roots[0].name), "starter": new_starter, "status": Status.DONE, } except Exception as err: manager.log.exception("🍪 error") if cleanup: shutil.rmtree(repo_dir) return { "body": body, "name": name, "path": path, "starter": new_starter, "status": Status.CONTINUING, "errors": [str(err)], }