diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 1c09f76..489256c 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -7,7 +7,7 @@ jobs: fail-fast: false matrix: os: ["macos-latest"] - python-version: ["3.9", "3.10", "3.11"] + python-version: ["3.11"] runs-on: ${{ matrix.os }} steps: - name: Checkout code diff --git a/cruft/_commands/create.py b/cruft/_commands/create.py index 3d283c5..e7fe024 100644 --- a/cruft/_commands/create.py +++ b/cruft/_commands/create.py @@ -2,11 +2,14 @@ from typing import Any, Dict, List, Optional from cookiecutter.generate import generate_files +from cookiecutter.prompt import choose_nested_template from . import utils from .utils import example from .utils.clean import clean_context from .utils.iohelper import AltTemporaryDirectory +from .utils.nested import get_relative_path, is_nested_template +from .utils.validate import validate_cookiecutter @example("https://github.com/timothycrosley/cookiecutter-python/") @@ -48,6 +51,25 @@ def create( checkout, ) + if is_nested_template(context): + nested_template = choose_nested_template( + context, cookiecutter_template_dir_str, no_input + ) + return create( + template_git_url=template_git_url, + output_dir=output_dir, + config_file=config_file, + default_config=default_config, + extra_context=extra_context, + extra_context_file=extra_context_file, + no_input=no_input, + directory=get_relative_path(nested_template, cookiecutter_template_dir_str), + checkout=checkout, + skip=skip, + ) + + validate_cookiecutter(cookiecutter_template_dir) + project_dir = Path( generate_files( repo_dir=cookiecutter_template_dir, diff --git a/cruft/_commands/link.py b/cruft/_commands/link.py index 0628663..d1f94b5 100644 --- a/cruft/_commands/link.py +++ b/cruft/_commands/link.py @@ -7,6 +7,7 @@ from .utils import example from .utils.clean import clean_context from .utils.iohelper import AltTemporaryDirectory +from .utils.validate import validate_cookiecutter @example("https://github.com/timothycrosley/cookiecutter-python/") @@ -43,6 +44,9 @@ def link( project_dir, checkout, ) + + validate_cookiecutter(cookiecutter_template_dir) + if no_input: use_commit = last_commit else: diff --git a/cruft/_commands/utils/cookiecutter.py b/cruft/_commands/utils/cookiecutter.py index 909bbc6..e163073 100644 --- a/cruft/_commands/utils/cookiecutter.py +++ b/cruft/_commands/utils/cookiecutter.py @@ -9,7 +9,9 @@ from cookiecutter.prompt import prompt_for_config from git import GitCommandError, Repo -from cruft.exceptions import InvalidCookiecutterRepository, UnableToFindCookiecutterTemplate +from cruft.exceptions import InvalidCookiecutterRepository + +from .nested import is_nested_template CookiecutterContext = Dict[str, Any] @@ -62,18 +64,6 @@ def get_cookiecutter_repo( return repo -def _validate_cookiecutter(cookiecutter_template_dir: Path): - main_cookiecutter_directory: Optional[Path] = None - - for dir_item in cookiecutter_template_dir.glob("*cookiecutter.*"): - if dir_item.is_dir() and "{{" in dir_item.name and "}}" in dir_item.name: - main_cookiecutter_directory = dir_item - break - - if not main_cookiecutter_directory: - raise UnableToFindCookiecutterTemplate(cookiecutter_template_dir) - - def generate_cookiecutter_context( template_git_url: str, cookiecutter_template_dir: Path, @@ -84,8 +74,6 @@ def generate_cookiecutter_context( project_dir: Path = Path("."), checkout: Optional[str] = None, ) -> CookiecutterContext: - _validate_cookiecutter(cookiecutter_template_dir) - context_file = cookiecutter_template_dir / "cookiecutter.json" config_dict = get_user_config( config_file=str(config_file) if config_file else None, default_config=default_config @@ -97,6 +85,9 @@ def generate_cookiecutter_context( extra_context=extra_context, ) + if is_nested_template(context): + return context + # prompt the user to manually configure at the command line. # except when 'no-input' flag is set context["cookiecutter"] = prompt_for_config(context, no_input) diff --git a/cruft/_commands/utils/generate.py b/cruft/_commands/utils/generate.py index 6f3fc84..7696e2b 100644 --- a/cruft/_commands/utils/generate.py +++ b/cruft/_commands/utils/generate.py @@ -12,6 +12,7 @@ from .cookiecutter import CookiecutterContext, generate_cookiecutter_context from .cruft import CruftState from .iohelper import AltTemporaryDirectory +from .validate import validate_cookiecutter if not sys.version_info >= (3, 11): try: @@ -79,6 +80,8 @@ def _generate_output( no_input=not cookiecutter_input, ) + validate_cookiecutter(inner_dir) + # This generates the cookiecutter template. # Unfortunately, cookiecutter doesn't let us output the template in an # arbitrary directory. It insists on creating the initial project directory. diff --git a/cruft/_commands/utils/nested.py b/cruft/_commands/utils/nested.py new file mode 100644 index 0000000..57c9d8f --- /dev/null +++ b/cruft/_commands/utils/nested.py @@ -0,0 +1,7 @@ +def is_nested_template(context): + return bool({"template", "templates"} & set(context["cookiecutter"].keys())) + + +def get_relative_path(full_path_to_template, temporary_directory_root): + """Return the path of a nested template relative to the root of a given temporary directory.""" + return full_path_to_template.split(temporary_directory_root + "/")[-1] diff --git a/cruft/_commands/utils/validate.py b/cruft/_commands/utils/validate.py new file mode 100644 index 0000000..03a2b76 --- /dev/null +++ b/cruft/_commands/utils/validate.py @@ -0,0 +1,16 @@ +from pathlib import Path +from typing import Optional + +from cruft.exceptions import UnableToFindCookiecutterTemplate + + +def validate_cookiecutter(cookiecutter_template_dir: Path): + main_cookiecutter_directory: Optional[Path] = None + + for dir_item in cookiecutter_template_dir.glob("*cookiecutter.*"): + if dir_item.is_dir() and "{{" in dir_item.name and "}}" in dir_item.name: + main_cookiecutter_directory = dir_item + break + + if not main_cookiecutter_directory: + raise UnableToFindCookiecutterTemplate(cookiecutter_template_dir) diff --git a/poetry.lock b/poetry.lock index 1b1473e..e91d8c7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,10 +1,9 @@ -# This file is automatically @generated by Poetry and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "appnope" version = "0.1.3" description = "Disable App Nap on macOS >= 10.9" -category = "dev" optional = false python-versions = "*" files = [ @@ -16,7 +15,6 @@ files = [ name = "arrow" version = "1.2.3" description = "Better dates & times for Python" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -32,7 +30,6 @@ typing-extensions = {version = "*", markers = "python_version < \"3.8\""} name = "attrs" version = "22.2.0" description = "Classes Without Boilerplate" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -51,7 +48,6 @@ tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy name = "backcall" version = "0.2.0" description = "Specifications for callback functions passed in to an API" -category = "dev" optional = false python-versions = "*" files = [ @@ -63,7 +59,6 @@ files = [ name = "bandit" version = "1.7.4" description = "Security oriented static analyser for python code." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -86,7 +81,6 @@ yaml = ["PyYAML"] name = "binaryornot" version = "0.4.4" description = "Ultra-lightweight pure Python package to check if a file is binary or text." -category = "main" optional = false python-versions = "*" files = [ @@ -101,7 +95,6 @@ chardet = ">=3.0.2" name = "black" version = "22.12.0" description = "The uncompromising code formatter." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -138,7 +131,6 @@ uvloop = ["uvloop (>=0.15.2)"] name = "certifi" version = "2022.12.7" description = "Python package for providing Mozilla's CA Bundle." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -150,7 +142,6 @@ files = [ name = "chardet" version = "5.1.0" description = "Universal encoding detector for Python 3" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -162,7 +153,6 @@ files = [ name = "charset-normalizer" version = "2.0.12" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "main" optional = false python-versions = ">=3.5.0" files = [ @@ -177,7 +167,6 @@ unicode-backport = ["unicodedata2"] name = "click" version = "8.1.3" description = "Composable command line interface toolkit" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -193,7 +182,6 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -203,30 +191,29 @@ files = [ [[package]] name = "cookiecutter" -version = "2.1.1" +version = "2.6.0" description = "A command-line utility that creates projects from project templates, e.g. creating a Python package project from a Python package project template." -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "cookiecutter-2.1.1-py2.py3-none-any.whl", hash = "sha256:9f3ab027cec4f70916e28f03470bdb41e637a3ad354b4d65c765d93aad160022"}, - {file = "cookiecutter-2.1.1.tar.gz", hash = "sha256:f3982be8d9c53dac1261864013fdec7f83afd2e42ede6f6dd069c5e149c540d5"}, + {file = "cookiecutter-2.6.0-py3-none-any.whl", hash = "sha256:a54a8e37995e4ed963b3e82831072d1ad4b005af736bb17b99c2cbd9d41b6e2d"}, + {file = "cookiecutter-2.6.0.tar.gz", hash = "sha256:db21f8169ea4f4fdc2408d48ca44859349de2647fbe494a9d6c3edfc0542c21c"}, ] [package.dependencies] +arrow = "*" binaryornot = ">=0.4.4" click = ">=7.0,<9.0.0" Jinja2 = ">=2.7,<4.0.0" -jinja2-time = ">=0.2.0" python-slugify = ">=4.0.0" pyyaml = ">=5.3.1" requests = ">=2.23.0" +rich = "*" [[package]] name = "coverage" version = "7.0.1" description = "Code coverage measurement for Python" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -293,7 +280,6 @@ toml = ["tomli"] name = "decorator" version = "5.1.1" description = "Decorators for Humans" -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -305,7 +291,6 @@ files = [ name = "docstring-parser" version = "0.15" description = "Parse Python docstrings in reST, Google and Numpydoc format" -category = "dev" optional = false python-versions = ">=3.6,<4.0" files = [ @@ -317,7 +302,6 @@ files = [ name = "dparse" version = "0.6.2" description = "A parser for Python dependency files" -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -337,7 +321,6 @@ pipenv = ["pipenv"] name = "examples" version = "1.0.2" description = "Tests and Documentation Done by Example." -category = "dev" optional = false python-versions = ">=3.6,<4.0" files = [ @@ -352,7 +335,6 @@ pydantic = ">=0.32.2" name = "exceptiongroup" version = "1.1.0" description = "Backport of PEP 654 (exception groups)" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -367,7 +349,6 @@ test = ["pytest (>=6)"] name = "execnet" version = "1.9.0" description = "execnet: rapid multi-Python deployment" -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -382,7 +363,6 @@ testing = ["pre-commit"] name = "falcon" version = "2.0.0" description = "An unladen web framework for building APIs and app backends." -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -406,7 +386,6 @@ files = [ name = "flake8" version = "4.0.1" description = "the modular source code checker: pep8 pyflakes and co" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -424,7 +403,6 @@ pyflakes = ">=2.4.0,<2.5.0" name = "flake8-bugbear" version = "22.12.6" description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -443,7 +421,6 @@ dev = ["coverage", "hypothesis", "hypothesmith (>=0.2)", "pre-commit", "tox"] name = "ghp-import" version = "2.1.0" description = "Copy your docs directly to the gh-pages branch." -category = "dev" optional = false python-versions = "*" files = [ @@ -461,7 +438,6 @@ dev = ["flake8", "markdown", "twine", "wheel"] name = "gitdb" version = "4.0.10" description = "Git Object Database" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -476,7 +452,6 @@ smmap = ">=3.0.1,<6" name = "gitpython" version = "3.1.29" description = "GitPython is a python library used to interact with Git repositories" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -492,7 +467,6 @@ typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.8\"" name = "hug" version = "2.6.1" description = "A Python framework that makes developing APIs as simple as possible, but no simpler." -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -508,7 +482,6 @@ requests = "*" name = "idna" version = "3.4" description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -520,7 +493,6 @@ files = [ name = "importlib-metadata" version = "4.2.0" description = "Read metadata from Python packages" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -540,7 +512,6 @@ testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pep517", name = "iniconfig" version = "1.1.1" description = "iniconfig: brain-dead simple config-ini parsing" -category = "dev" optional = false python-versions = "*" files = [ @@ -552,7 +523,6 @@ files = [ name = "ipython" version = "7.34.0" description = "IPython: Productive Interactive Computing" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -589,7 +559,6 @@ test = ["ipykernel", "nbformat", "nose (>=0.10.1)", "numpy (>=1.17)", "pygments" name = "isort" version = "5.11.4" description = "A Python utility / library to sort Python imports." -category = "dev" optional = false python-versions = ">=3.7.0" files = [ @@ -607,7 +576,6 @@ requirements-deprecated-finder = ["pip-api", "pipreqs"] name = "jedi" version = "0.18.2" description = "An autocompletion tool for Python that can be used for text editors." -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -627,7 +595,6 @@ testing = ["Django (<3.1)", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] name = "jinja2" version = "3.1.2" description = "A very fast and expressive template engine." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -641,27 +608,10 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] -[[package]] -name = "jinja2-time" -version = "0.2.0" -description = "Jinja2 Extension for Dates and Times" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "jinja2-time-0.2.0.tar.gz", hash = "sha256:d14eaa4d315e7688daa4969f616f226614350c48730bfa1692d2caebd8c90d40"}, - {file = "jinja2_time-0.2.0-py2.py3-none-any.whl", hash = "sha256:d3eab6605e3ec8b7a0863df09cc1d23714908fa61aa6986a845c20ba488b4efa"}, -] - -[package.dependencies] -arrow = "*" -jinja2 = "*" - [[package]] name = "livereload" version = "2.6.3" description = "Python LiveReload is an awesome tool for web developers" -category = "dev" optional = false python-versions = "*" files = [ @@ -677,7 +627,6 @@ tornado = {version = "*", markers = "python_version > \"2.7\""} name = "mako" version = "1.2.4" description = "A super-fast templating language that borrows the best ideas from the existing templating languages." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -698,7 +647,6 @@ testing = ["pytest"] name = "markdown" version = "3.3.4" description = "Python implementation of Markdown." -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -712,11 +660,35 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [package.extras] testing = ["coverage", "pyyaml"] +[[package]] +name = "markdown-it-py" +version = "2.2.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.7" +files = [ + {file = "markdown-it-py-2.2.0.tar.gz", hash = "sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1"}, + {file = "markdown_it_py-2.2.0-py3-none-any.whl", hash = "sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" +typing_extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["attrs", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + [[package]] name = "markupsafe" version = "2.1.1" description = "Safely add untrusted strings to HTML/XML markup." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -766,7 +738,6 @@ files = [ name = "matplotlib-inline" version = "0.1.6" description = "Inline Matplotlib backend for Jupyter" -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -781,7 +752,6 @@ traitlets = "*" name = "mccabe" version = "0.6.1" description = "McCabe checker, plugin for flake8" -category = "dev" optional = false python-versions = "*" files = [ @@ -789,11 +759,21 @@ files = [ {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + [[package]] name = "mergedeep" version = "1.3.4" description = "A deep merge function for 🐍." -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -805,7 +785,6 @@ files = [ name = "mkdocs" version = "1.2.4" description = "Project documentation with Markdown." -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -832,7 +811,6 @@ i18n = ["babel (>=2.9.0)"] name = "mkdocs-material" version = "7.3.0" description = "A Material Design theme for MkDocs" -category = "dev" optional = false python-versions = "*" files = [ @@ -851,7 +829,6 @@ pymdown-extensions = ">=7.0" name = "mkdocs-material-extensions" version = "1.1.1" description = "Extension pack for Python Markdown and MkDocs Material." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -863,7 +840,6 @@ files = [ name = "mypy" version = "0.991" description = "Optional static typing for Python" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -915,7 +891,6 @@ reports = ["lxml"] name = "mypy-extensions" version = "0.4.3" description = "Experimental type system extensions for programs checked with the mypy typechecker." -category = "dev" optional = false python-versions = "*" files = [ @@ -927,7 +902,6 @@ files = [ name = "packaging" version = "21.3" description = "Core utilities for Python packages" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -942,7 +916,6 @@ pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" name = "parso" version = "0.8.3" description = "A Python Parser" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -958,7 +931,6 @@ testing = ["docopt", "pytest (<6.0.0)"] name = "pathspec" version = "0.10.3" description = "Utility library for gitignore style pattern matching of file paths." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -970,7 +942,6 @@ files = [ name = "pbr" version = "5.11.0" description = "Python Build Reasonableness" -category = "dev" optional = false python-versions = ">=2.6" files = [ @@ -982,7 +953,6 @@ files = [ name = "pdocs" version = "1.2.0" description = "A simple program and library to auto generate API documentation for Python modules." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1000,7 +970,6 @@ Markdown = ">=3.0.0" name = "pep8-naming" version = "0.13.2" description = "Check PEP-8 naming conventions, plugin for flake8" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1015,7 +984,6 @@ flake8 = ">=3.9.1" name = "pexpect" version = "4.8.0" description = "Pexpect allows easy control of interactive console applications." -category = "dev" optional = false python-versions = "*" files = [ @@ -1030,7 +998,6 @@ ptyprocess = ">=0.5" name = "pickleshare" version = "0.7.5" description = "Tiny 'shelve'-like database with concurrency support" -category = "dev" optional = false python-versions = "*" files = [ @@ -1042,7 +1009,6 @@ files = [ name = "platformdirs" version = "2.6.2" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1061,7 +1027,6 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2)", "pytest- name = "pluggy" version = "1.0.0" description = "plugin and hook calling mechanisms for python" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1080,7 +1045,6 @@ testing = ["pytest", "pytest-benchmark"] name = "portray" version = "1.7.0" description = "Your Project with Great Documentation" -category = "dev" optional = false python-versions = ">=3.6,<4.0" files = [ @@ -1103,7 +1067,6 @@ yaspin = ">=0.15.0,<0.16.0" name = "prompt-toolkit" version = "3.0.36" description = "Library for building powerful interactive command lines in Python" -category = "dev" optional = false python-versions = ">=3.6.2" files = [ @@ -1118,7 +1081,6 @@ wcwidth = "*" name = "ptyprocess" version = "0.7.0" description = "Run a subprocess in a pseudo terminal" -category = "dev" optional = false python-versions = "*" files = [ @@ -1130,7 +1092,6 @@ files = [ name = "pycodestyle" version = "2.8.0" description = "Python style guide checker" -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -1142,7 +1103,6 @@ files = [ name = "pydantic" version = "1.10.2" description = "Data validation and settings management using python type hints" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1195,7 +1155,6 @@ email = ["email-validator (>=1.0.3)"] name = "pyflakes" version = "2.4.0" description = "passive checker of Python programs" -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1207,7 +1166,6 @@ files = [ name = "pygments" version = "2.13.0" description = "Pygments is a syntax highlighting package written in Python." -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1222,7 +1180,6 @@ plugins = ["importlib-metadata"] name = "pymdown-extensions" version = "7.1" description = "Extension pack for Python Markdown." -category = "dev" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" files = [ @@ -1237,7 +1194,6 @@ Markdown = ">=3.2" name = "pyparsing" version = "3.0.9" description = "pyparsing module - Classes and methods to define and execute parsing grammars" -category = "dev" optional = false python-versions = ">=3.6.8" files = [ @@ -1252,7 +1208,6 @@ diagrams = ["jinja2", "railroad-diagrams"] name = "pytest" version = "7.2.0" description = "pytest: simple powerful testing with Python" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1277,7 +1232,6 @@ testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2. name = "pytest-cov" version = "4.0.0" description = "Pytest plugin for measuring coverage." -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1296,7 +1250,6 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale name = "pytest-mock" version = "3.10.0" description = "Thin-wrapper around the mock package for easier use with pytest" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1314,7 +1267,6 @@ dev = ["pre-commit", "pytest-asyncio", "tox"] name = "pytest-repeat" version = "0.9.1" description = "pytest plugin for repeating tests" -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -1329,7 +1281,6 @@ pytest = ">=3.6" name = "pytest-xdist" version = "3.1.0" description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1350,7 +1301,6 @@ testing = ["filelock"] name = "python-dateutil" version = "2.8.2" description = "Extensions to the standard Python datetime module" -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ @@ -1365,7 +1315,6 @@ six = ">=1.5" name = "python-slugify" version = "7.0.0" description = "A Python slugify application that also handles Unicode" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1383,7 +1332,6 @@ unidecode = ["Unidecode (>=1.1.1)"] name = "pyyaml" version = "6.0" description = "YAML parser and emitter for Python" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1433,7 +1381,6 @@ files = [ name = "pyyaml-env-tag" version = "0.1" description = "A custom YAML tag for referencing environment variables in YAML files. " -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1448,7 +1395,6 @@ pyyaml = "*" name = "requests" version = "2.27.1" description = "Python HTTP for Humans." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ @@ -1466,11 +1412,29 @@ urllib3 = ">=1.21.1,<1.27" socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] use-chardet-on-py3 = ["chardet (>=3.0.2,<5)"] +[[package]] +name = "rich" +version = "13.8.1" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "rich-13.8.1-py3-none-any.whl", hash = "sha256:1760a3c0848469b97b558fc61c85233e3dafb69c7a071b4d60c38099d3cd4c06"}, + {file = "rich-13.8.1.tar.gz", hash = "sha256:8260cda28e3db6bf04d2d1ef4dbc03ba80a824c88b0e7668a0f23126a424844a"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" +typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + [[package]] name = "ruamel-yaml" version = "0.17.21" description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" -category = "dev" optional = false python-versions = ">=3" files = [ @@ -1489,7 +1453,6 @@ jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] name = "ruamel-yaml-clib" version = "0.2.7" description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -1500,8 +1463,11 @@ files = [ {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-win32.whl", hash = "sha256:763d65baa3b952479c4e972669f679fe490eee058d5aa85da483ebae2009d231"}, {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:d000f258cf42fec2b1bbf2863c61d7b8918d31ffee905da62dede869254d3b8a"}, {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:045e0626baf1c52e5527bd5db361bc83180faaba2ff586e763d3d5982a876a9e"}, - {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-macosx_12_6_arm64.whl", hash = "sha256:721bc4ba4525f53f6a611ec0967bdcee61b31df5a56801281027a3a6d1c2daf5"}, + {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:1a6391a7cabb7641c32517539ca42cf84b87b667bad38b78d4d42dd23e957c81"}, + {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:9c7617df90c1365638916b98cdd9be833d31d337dbcd722485597b43c4a215bf"}, {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:41d0f1fa4c6830176eef5b276af04c89320ea616655d01327d5ce65e50575c94"}, + {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-win32.whl", hash = "sha256:f6d3d39611ac2e4f62c3128a9eed45f19a6608670c5a2f4f07f24e8de3441d38"}, + {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:da538167284de58a52109a9b89b8f6a53ff8437dd6dc26d33b57bf6699153122"}, {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4b3a93bb9bc662fc1f99c5c3ea8e623d8b23ad22f861eb6fce9377ac07ad6072"}, {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-macosx_12_0_arm64.whl", hash = "sha256:a234a20ae07e8469da311e182e70ef6b199d0fbeb6c6cc2901204dd87fb867e8"}, {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:15910ef4f3e537eea7fe45f8a5d19997479940d9196f357152a09031c5be59f3"}, @@ -1533,7 +1499,6 @@ files = [ name = "safety" version = "2.3.5" description = "Checks installed dependencies for known vulnerabilities and licenses." -category = "dev" optional = false python-versions = "*" files = [ @@ -1557,7 +1522,6 @@ gitlab = ["python-gitlab (>=1.3.0)"] name = "setuptools" version = "65.6.3" description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1574,7 +1538,6 @@ testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs ( name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -1586,7 +1549,6 @@ files = [ name = "smmap" version = "5.0.0" description = "A pure Python implementation of a sliding window memory map manager" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1598,7 +1560,6 @@ files = [ name = "stevedore" version = "3.5.2" description = "Manage dynamic plugins for Python applications" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1614,7 +1575,6 @@ pbr = ">=2.0.0,<2.1.0 || >2.1.0" name = "text-unidecode" version = "1.3" description = "The most basic Text::Unidecode port" -category = "main" optional = false python-versions = "*" files = [ @@ -1626,7 +1586,6 @@ files = [ name = "toml" version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" -category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -1638,7 +1597,6 @@ files = [ name = "tomli" version = "2.0.1" description = "A lil' TOML parser" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1650,7 +1608,6 @@ files = [ name = "tornado" version = "6.2" description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." -category = "dev" optional = false python-versions = ">= 3.7" files = [ @@ -1671,7 +1628,6 @@ files = [ name = "traitlets" version = "5.8.0" description = "Traitlets Python configuration system" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1687,7 +1643,6 @@ test = ["argcomplete (>=2.0)", "pre-commit", "pytest", "pytest-mock"] name = "typed-ast" version = "1.5.4" description = "a fork of Python 2 and 3 ast modules with type comment support" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1721,7 +1676,6 @@ files = [ name = "typer" version = "0.7.0" description = "Typer, build great CLIs. Easy to code. Based on Python type hints." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1742,7 +1696,6 @@ test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6. name = "types-toml" version = "0.10.8.1" description = "Typing stubs for toml" -category = "dev" optional = false python-versions = "*" files = [ @@ -1754,7 +1707,6 @@ files = [ name = "typing-extensions" version = "4.4.0" description = "Backported and Experimental Type Hints for Python 3.7+" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1766,7 +1718,6 @@ files = [ name = "urllib3" version = "1.26.13" description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ @@ -1783,7 +1734,6 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] name = "vulture" version = "2.6" description = "Find dead code" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1798,7 +1748,6 @@ toml = "*" name = "watchdog" version = "2.2.0" description = "Filesystem events monitoring" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1839,7 +1788,6 @@ watchmedo = ["PyYAML (>=3.10)"] name = "wcwidth" version = "0.2.5" description = "Measures the displayed width of unicode strings in a terminal" -category = "dev" optional = false python-versions = "*" files = [ @@ -1851,7 +1799,6 @@ files = [ name = "yaspin" version = "0.15.0" description = "Yet Another Terminal Spinner" -category = "dev" optional = false python-versions = "*" files = [ @@ -1863,7 +1810,6 @@ files = [ name = "zipp" version = "3.11.0" description = "Backport of pathlib-compatible object wrapper for zip files" -category = "main" optional = false python-versions = ">=3.7" files = [ diff --git a/tests/test_api.py b/tests/test_api.py index 5efc093..c2b6c09 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -4,6 +4,7 @@ import sys from pathlib import Path from subprocess import run +from unittest.mock import MagicMock import pytest from examples import verify_and_test_examples @@ -24,8 +25,20 @@ def test_invalid_cookiecutter_reference(tmpdir): cruft.create("https://github.com/cruft/cookiecutter-test", Path(tmpdir), checkout="DNE") -def test_no_cookiecutter_dir(tmpdir): +def test_no_cookiecutter_dir(tmpdir, mocker): with pytest.raises(exceptions.UnableToFindCookiecutterTemplate): + mock_repo_context_manager = MagicMock() + mock_repo_context_manager.__enter__.return_value.head.object.hexsha = "abc123" + mocker.patch( + "cruft._commands.utils.cookiecutter.get_cookiecutter_repo", + return_value=mock_repo_context_manager, + ) + + mocker.patch( + "cruft._commands.utils.cookiecutter.generate_cookiecutter_context", + return_value={"cookiecutter": {}}, + ) + cruft.create("https://github.com/cruft/cookiecutter-test", Path(tmpdir)) @@ -322,3 +335,30 @@ def test_diff_git_subdir(capfd, tmpdir): ) assert cruft.update(project_dir, checkout="updated") + + +def test_nested_template(mocker, tmpdir): + tmpdir.chdir() + + test_file_directory = os.path.dirname(__file__) + main_dir = f"{test_file_directory}/testdata/nested-templates" + + mocker.patch("cruft._commands.utils.cookiecutter.resolve_template_url", return_value="foo") + + mock_temp_dir_context_manager = MagicMock() + mock_temp_dir_context_manager.__enter__.return_value = main_dir + mocker.patch( + "cruft._commands.create.AltTemporaryDirectory", return_value=mock_temp_dir_context_manager + ) + + mock_repo_context_manager = MagicMock() + mock_repo_context_manager.__enter__.return_value.head.object.hexsha = "abc123" + mocker.patch( + "cruft._commands.utils.cookiecutter.get_cookiecutter_repo", + return_value=mock_repo_context_manager, + ) + + cruft.create("foo", tmpdir, no_input=True) + + with open(f"{tmpdir}/nested-app/.cruft.json", "r") as cruft_file: + assert json.load(cruft_file)["directory"] == "fake-project" diff --git a/tests/testdata/nested-templates/cookiecutter.json b/tests/testdata/nested-templates/cookiecutter.json new file mode 100644 index 0000000..2bb7cb9 --- /dev/null +++ b/tests/testdata/nested-templates/cookiecutter.json @@ -0,0 +1,9 @@ +{ + "templates": { + "fake-project": { + "path": "./fake-project", + "title": "A Fake Project", + "description": "A cookiecutter template for a project" + } + } + } \ No newline at end of file diff --git a/tests/testdata/nested-templates/fake-project/cookiecutter.json b/tests/testdata/nested-templates/fake-project/cookiecutter.json new file mode 100644 index 0000000..acfce9c --- /dev/null +++ b/tests/testdata/nested-templates/fake-project/cookiecutter.json @@ -0,0 +1,3 @@ +{ + "project_name": "nested-app" +} diff --git a/tests/testdata/nested-templates/fake-project/{{cookiecutter.project_name}}/_ b/tests/testdata/nested-templates/fake-project/{{cookiecutter.project_name}}/_ new file mode 100644 index 0000000..c9cdc63 --- /dev/null +++ b/tests/testdata/nested-templates/fake-project/{{cookiecutter.project_name}}/_ @@ -0,0 +1 @@ +_ \ No newline at end of file