From e986a347f70e2dca2de5e2a6503c43f8cd96cf5a Mon Sep 17 00:00:00 2001 From: eruber Date: Fri, 20 Oct 2017 20:56:57 -0700 Subject: [PATCH 001/274] hackebrot's proof-of-concept version of new cookiecutter context --- cookiecutter/context.py | 273 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 273 insertions(+) create mode 100644 cookiecutter/context.py diff --git a/cookiecutter/context.py b/cookiecutter/context.py new file mode 100644 index 000000000..63a4805ec --- /dev/null +++ b/cookiecutter/context.py @@ -0,0 +1,273 @@ +# -*- coding: utf-8 -*- + +import codecs +import collections +import json +import pprint + +import click +from jinja2 import Environment + +DEFAULT_PROMPT = 'Please enter a value for "{variable.name}"' + +VALID_TYPES = [ + 'boolean', + 'yes_no', + 'int', + 'json', + 'string', +] + + +def prompt_string(variable, default): + return click.prompt( + variable.prompt, + default=default, + hide_input=variable.hide_input, + type=click.STRING, + ) + + +def prompt_boolean(variable, default): + return click.prompt( + variable.prompt, + default=default, + hide_input=variable.hide_input, + type=click.BOOL, + ) + + +def prompt_int(variable, default): + return click.prompt( + variable.prompt, + default=default, + hide_input=variable.hide_input, + type=click.INT, + ) + + +def prompt_json(variable, default): + # The JSON object from cookiecutter.json might be very large + # We only show 'default' + DEFAULT_JSON = 'default' + + def process_json(user_value): + try: + return json.loads( + user_value, + object_pairs_hook=collections.OrderedDict, + ) + except json.decoder.JSONDecodeError: + # Leave it up to click to ask the user again + raise click.UsageError('Unable to decode to JSON.') + + dict_value = click.prompt( + variable.prompt, + default=DEFAULT_JSON, + hide_input=variable.hide_input, + type=click.STRING, + value_proc=process_json, + ) + + if dict_value == DEFAULT_JSON: + # Return the given default w/o any processing + return default + return dict_value + + +def prompt_yes_no(variable, default): + if default is True: + default_display = 'y' + else: + default_display = 'n' + + return click.prompt( + variable.prompt, + default=default_display, + hide_input=variable.hide_input, + type=click.BOOL, + ) + + +def prompt_choice(variable, default): + """Returns prompt, default and callback for a choice variable""" + choice_map = collections.OrderedDict( + (u'{}'.format(i), value) + for i, value in enumerate(variable.choices, 1) + ) + choices = choice_map.keys() + + prompt = u'\n'.join(( + variable.prompt, + u'\n'.join([u'{} - {}'.format(*c) for c in choice_map.items()]), + u'Choose from {}'.format(u', '.join(choices)), + )) + default = str(variable.choices.index(default) + 1) + + user_choice = click.prompt( + prompt, + default=default, + hide_input=variable.hide_input, + type=click.Choice(choices), + ) + return choice_map[user_choice] + + +PROMPTS = { + 'string': prompt_string, + 'boolean': prompt_boolean, + 'int': prompt_int, + 'json': prompt_json, + 'yes_no': prompt_yes_no, +} + + +def deserialize_string(value): + return str(value) + + +def deserialize_boolean(value): + return bool(value) + + +def deserialize_yes_no(value): + return bool(value) + + +def deserialize_int(value): + return int(value) + + +def deserialize_json(value): + return value + + +DESERIALIZERS = { + 'string': deserialize_string, + 'boolean': deserialize_boolean, + 'int': deserialize_int, + 'json': deserialize_json, + 'yes_no': deserialize_yes_no, +} + + +class Variable(object): + def __init__(self, name, default, **info): + + # mandatory fields + self.name = name + self.default = default + + # optional fields + self.description = info.get('description', None) + self.prompt = info.get('prompt', DEFAULT_PROMPT.format(variable=self)) + self.hide_input = info.get('hide_input', False) + + self.var_type = info.get('type', 'string') + if self.var_type not in VALID_TYPES: + msg = 'Invalid type {var_type} for variable' + raise ValueError(msg.format(var_type=self.var_type)) + + self.skip_if = info.get('skip_if', '') + if not isinstance(self.skip_if, str): + # skip_if was specified in cookiecutter.json + msg = 'Field skip_if is required to be a str, got {value}' + raise ValueError(msg.format(value=self.skip_if)) + + self.prompt_user = info.get('prompt_user', True) + if not isinstance(self.prompt_user, bool): + # prompt_user was specified in cookiecutter.json + msg = 'Field prompt_user is required to be a bool, got {value}' + raise ValueError(msg.format(value=self.prompt_user)) + + # choices are somewhat special as they can of every type + self.choices = info.get('choices', []) + if self.choices and default not in self.choices: + msg = 'Invalid default value {default} for choice variable' + raise ValueError(msg.format(default=self.default)) + + def __repr__(self): + return "<{class_name} {variable_name}>".format( + class_name=self.__class__.__name__, + variable_name=self.name, + ) + + +class CookiecutterTemplate(object): + def __init__(self, name, cookiecutter_version, variables, **info): + # mandatory fields + self.name = name + self.cookiecutter_version = cookiecutter_version + self.variables = [Variable(**v) for v in variables] + + # optional fields + self.authors = info.get('authors', []) + self.description = info.get('description', None) + self.keywords = info.get('keywords', []) + self.license = info.get('license', None) + self.url = info.get('url', None) + self.version = info.get('version', None) + + def __repr__(self): + return "<{class_name} {template_name}>".format( + class_name=self.__class__.__name__, + template_name=self.name, + ) + + def __iter__(self): + for v in self.variables: + yield v + + +def load_context(json_object, verbose): + env = Environment(extensions=['jinja2_time.TimeExtension']) + context = collections.OrderedDict({}) + + for variable in CookiecutterTemplate(**json_object): + if variable.skip_if: + skip_template = env.from_string(variable.skip_if) + if skip_template.render(cookiecutter=context) == 'True': + continue + + default = variable.default + + if isinstance(default, str): + template = env.from_string(default) + default = template.render(cookiecutter=context) + + deserialize = DESERIALIZERS[variable.var_type] + + if not variable.prompt_user: + context[variable.name] = deserialize(default) + continue + + if variable.choices: + prompt = prompt_choice + else: + prompt = PROMPTS[variable.var_type] + + if verbose and variable.description: + click.echo(variable.description) + + value = prompt(variable, default) + + if verbose: + width, _ = click.get_terminal_size() + click.echo('-' * width) + + context[variable.name] = deserialize(value) + + return context + + +def main(file_path): + """Load the json object and prompt the user for input""" + + with codecs.open(file_path, 'r', encoding='utf8') as f: + json_object = json.load(f, object_pairs_hook=collections.OrderedDict) + + pprint.pprint(load_context(json_object, True)) + + +if __name__ == '__main__': + main('cookiecutter.json') From a444833d4ba284094d7f649d054ea1bceccfaa2d Mon Sep 17 00:00:00 2001 From: eruber Date: Sat, 28 Oct 2017 00:51:57 -0700 Subject: [PATCH 002/274] Update version to 2.0.0 alpha --- cookiecutter/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cookiecutter/__init__.py b/cookiecutter/__init__.py index ee27f6d91..5ada8ad8c 100644 --- a/cookiecutter/__init__.py +++ b/cookiecutter/__init__.py @@ -7,4 +7,4 @@ Main package for Cookiecutter. """ -__version__ = '1.6.0' +__version__ = '2.0.0-alpha.u+1.6.0' From a67111ea669d1b50c10d5dfe8d72673e9e680f99 Mon Sep 17 00:00:00 2001 From: eruber Date: Sat, 28 Oct 2017 00:55:00 -0700 Subject: [PATCH 003/274] Call context v1 or v2 processing depending on which version is detected. --- cookiecutter/main.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/cookiecutter/main.py b/cookiecutter/main.py index cc2515d9d..6b3db60d7 100644 --- a/cookiecutter/main.py +++ b/cookiecutter/main.py @@ -19,6 +19,8 @@ from .repository import determine_repo_dir from .utils import rmtree +from .context import context_is_version_2, load_context + logger = logging.getLogger(__name__) @@ -79,7 +81,12 @@ def cookiecutter( # 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) + if context_is_version_2(context['cookiecutter']): + context['cookiecutter'] = load_context(context[u'cookiecutter'], + no_input=no_input, + verbose=True) + else: + context['cookiecutter'] = prompt_for_config(context, no_input) # include template dir or url in the context dict context['cookiecutter']['_template'] = template From 295f39a42a4c72e7328b648f3e667c4afd311928 Mon Sep 17 00:00:00 2001 From: eruber Date: Sun, 29 Oct 2017 09:19:26 -0700 Subject: [PATCH 004/274] v2 context a speced by hackebrot in cookiecutter pull request 848 --- cookiecutter/context.py | 294 +++++++++++++++++++++++++++++++++++----- 1 file changed, 259 insertions(+), 35 deletions(-) diff --git a/cookiecutter/context.py b/cookiecutter/context.py index 63a4805ec..3cc984041 100644 --- a/cookiecutter/context.py +++ b/cookiecutter/context.py @@ -1,23 +1,74 @@ # -*- coding: utf-8 -*- -import codecs +""" +cookiecutter.context +-------------------- + +Process the version 2 cookiecutter context (previsously loaded via +cookiecutter.json) and handle any user input that might be associated with +initializing the settings defined in the 'variables' OrderedDict part of the +context. + +This module produces a dictionary used later by the jinja2 template engine to +generate files. + +Based on the source code written by @hackebrot see: +https://github.com/audreyr/cookiecutter/pull/848 + +""" + +import logging import collections import json -import pprint +import re import click from jinja2 import Environment +logger = logging.getLogger(__name__) + DEFAULT_PROMPT = 'Please enter a value for "{variable.name}"' VALID_TYPES = [ 'boolean', 'yes_no', 'int', + 'float', + 'uuid', 'json', 'string', ] +SET_OF_REQUIRED_FIELDS = { + 'name', + 'cookiecutter_version', + 'variables', +} + +REGEX_COMPILE_FLAGS = { + 'ascii': re.ASCII, + 'debug': re.DEBUG, + 'ignorecase': re.IGNORECASE, + 'locale': re.LOCALE, + 'mulitline': re.MULTILINE, + 'dotall': re.DOTALL, + 'verbose': re.VERBOSE, +} + + +def context_is_version_2(cookiecutter_context): + """ + Return True if the cookiecutter_context meets the current requirements for + a version 2 cookiecutter.json file format. + """ + # This really is not sufficient since a v1 context could define each of + # these fields; perhaps a more thorough test would be to also check if the + # 'variables' field was defined as a list of OrderedDict items. + if (cookiecutter_context.keys() & SET_OF_REQUIRED_FIELDS) == SET_OF_REQUIRED_FIELDS: + return True + else: + return False + def prompt_string(variable, default): return click.prompt( @@ -46,6 +97,24 @@ def prompt_int(variable, default): ) +def prompt_float(variable, default): + return click.prompt( + variable.prompt, + default=default, + hide_input=variable.hide_input, + type=click.FLOAT, + ) + + +def prompt_uuid(variable, default): + return click.prompt( + variable.prompt, + default=default, + hide_input=variable.hide_input, + type=click.UUID, + ) + + def prompt_json(variable, default): # The JSON object from cookiecutter.json might be very large # We only show 'default' @@ -58,7 +127,10 @@ def process_json(user_value): object_pairs_hook=collections.OrderedDict, ) except json.decoder.JSONDecodeError: - # Leave it up to click to ask the user again + # Leave it up to click to ask the user again. + # Local function procsse_json() is called by click within a + # try block that catches click.UsageError exception's and asks + # the user to try again. raise click.UsageError('Unable to decode to JSON.') dict_value = click.prompt( @@ -117,6 +189,8 @@ def prompt_choice(variable, default): 'string': prompt_string, 'boolean': prompt_boolean, 'int': prompt_int, + 'float': prompt_float, + 'uuid': prompt_uuid, 'json': prompt_json, 'yes_no': prompt_yes_no, } @@ -138,6 +212,14 @@ def deserialize_int(value): return int(value) +def deserialize_float(value): + return float(value) + + +def deserialize_uuid(value): + return click.UUID(value) + + def deserialize_json(value): return value @@ -146,45 +228,122 @@ def deserialize_json(value): 'string': deserialize_string, 'boolean': deserialize_boolean, 'int': deserialize_int, + 'float': deserialize_float, + 'uuid': deserialize_uuid, 'json': deserialize_json, 'yes_no': deserialize_yes_no, } class Variable(object): + """ + Embody attributes of variables while processing the variables field of + a cookiecutter version 2 context. + """ + def __init__(self, name, default, **info): + """ + :param name: A string containing the variable's name in the jinja2 context. + :param default: The variable's default value. Can any type defined below. + :param kwargs info: Keyword/Argument pairs recognized are shown below. + + Recognized Keyword/Arguments, but optional: + + - `description` -- A string description of the variable. + - `prompt` -- A string to show user when prompted for input. + - `prompt_user` -- A boolean, if True prompt user; else no prompt. + - `hide_input` -- A boolean, if True hide user's input. + - `type` -- Specifies the variable's data type see below, + defaults to string. + - `skip_if` -- A string of a jinja2 renderable boolean expression, + the variable will be skipped if it renders True. + - `choices` -- A list of choices, may be of mixed types. + - `validation` -- A string defining a regex to use to validation + user input. Defaults to None. + - `validation_flags` - A list of validation flag names that can be + specified to control the behaviour of the validation + check done using the above defined `validation` string. + Specifying a flag is equivalent to setting it to True, + not specifying a flag is equivalent to setting it to False. + The default value of this variable has no effect on the + validation check. + + The flags supported are: + + * ascii - enabling re.ASCII + * debug - enabling re.DEBUG + * ignorecase - enabling re.IGNORECASE + * locale - enabling re.LOCALE + * mulitline - enabling re.MULTILINE + * dotall - enabling re.DOTALL + * verbose - enabling re.VERBOSE + + See: https://docs.python.org/3/library/re.html#re.compile + + Supported Types + * string + * boolean + * int + * float + * uuid + * json + * yes_no + + """ # mandatory fields self.name = name self.default = default # optional fields - self.description = info.get('description', None) - self.prompt = info.get('prompt', DEFAULT_PROMPT.format(variable=self)) - self.hide_input = info.get('hide_input', False) + self.info = info + + self.description = self.check_type('description', None, str) + + self.prompt = self.check_type('prompt', DEFAULT_PROMPT.format(variable=self), str) + + self.hide_input = self.check_type('hide_input', False, bool) self.var_type = info.get('type', 'string') if self.var_type not in VALID_TYPES: - msg = 'Invalid type {var_type} for variable' - raise ValueError(msg.format(var_type=self.var_type)) - - self.skip_if = info.get('skip_if', '') - if not isinstance(self.skip_if, str): - # skip_if was specified in cookiecutter.json - msg = 'Field skip_if is required to be a str, got {value}' - raise ValueError(msg.format(value=self.skip_if)) - - self.prompt_user = info.get('prompt_user', True) - if not isinstance(self.prompt_user, bool): - # prompt_user was specified in cookiecutter.json - msg = 'Field prompt_user is required to be a bool, got {value}' - raise ValueError(msg.format(value=self.prompt_user)) - - # choices are somewhat special as they can of every type - self.choices = info.get('choices', []) + msg = 'Variable: {var_name} has an invalid type {var_type}. Valid types are: {types}' + raise ValueError(msg.format(var_type=self.var_type, + var_name=self.name, + types=VALID_TYPES)) + + self.skip_if = self.check_type('skip_if', '', str) + + self.prompt_user = self.check_type('prompt_user', True, bool) + + # choices are somewhat special as they can be of every type + self.choices = self.check_type('choices', [], list) if self.choices and default not in self.choices: - msg = 'Invalid default value {default} for choice variable' - raise ValueError(msg.format(default=self.default)) + msg = "Variable: {var_name} has an invalid default value {default} for choices: {choices}." + raise ValueError(msg.format(var_name=self.name, default=self.default, choices=self.choices)) + + self.validation = self.check_type('validation', None, str) + + self.validation_flag_names = self.check_type('validation_flags', [], list) + + self.validation_flags = 0 + for vflag in self.validation_flag_names: + if vflag in REGEX_COMPILE_FLAGS.keys(): + self.validation_flags |= REGEX_COMPILE_FLAGS[vflag] + else: + msg = "Variable: {var_name} - Ignoring unkown RegEx validation Control Flag named '{flag}'\n" \ + "Legal flag names are: {names}" + logger.warn(msg.format(var_name=self.name, flag=vflag, + names=REGEX_COMPILE_FLAGS.keys())) + self.validation_flag_names.remove(vflag) + + self.validate = None + if self.validation: + try: + self.validate = re.compile(self.validation, self.validation_flags) + except re.error as e: + msg = "Variable: {var_name} - Validation Setup Error: Invalid RegEx '{value}' - does not compile - {err}" + raise ValueError(msg.format(var_name=self.name, + value=self.validation, err=e)) def __repr__(self): return "<{class_name} {variable_name}>".format( @@ -192,9 +351,53 @@ def __repr__(self): variable_name=self.name, ) + def __str__(self): + s = ["{key}='{value}'".format(key=key, value=self.__dict__[key]) for key in self.__dict__ if key != 'info'] + return self.__repr__() + ':\n' + ',\n'.join(s) + + def check_type(self, option_name, option_default_value, option_type): + """ + Retrieve the option_value named option_name from info and check its type. + Raise ValueError if the type is incorrect; otherwise return option's value. + """ + option_value = self.info.get(option_name, option_default_value) + + if option_value is not None: + if not isinstance(option_value, option_type): + msg = "Variable: '{var_name}' Option: '{opt_name}' requires a value of type {type_name}, but has a value of: {value}" + raise ValueError(msg.format(var_name=self.name, opt_name=option_name, type_name=option_type.__name__, value=option_value)) + + return option_value + class CookiecutterTemplate(object): + """ + Embodies all attributes of a version 2 Cookiecutter template. + """ + def __init__(self, name, cookiecutter_version, variables, **info): + """ + Mandatorty Parameters + + :param name: The cookiecutter template name + :param cookiecutter_version: The version of the cookiecutter application + that is compatible with this template. + :param variables: A list of OrderedDict items that describe each + variable in the template. These variables are essentially what + is found in the version 1 cookiecutter.json file. + + Optional Parameters (via \**info) + + :param authors: An array of string - maintainers of the template. + :param description: A human readable description of the template. + :param keywords: An array of string - similar to PyPI keywords. + :param license: A string identifying the license of the template code. + :param url: A string containing the URL for the template project. + :param version: A string containing a version identifier, ideally + following the semantic versioning spec. + + """ + # mandatory fields self.name = name self.cookiecutter_version = cookiecutter_version @@ -219,7 +422,16 @@ def __iter__(self): yield v -def load_context(json_object, verbose): +def load_context(json_object, no_input=False, verbose=True): + """ + Load a version 2 context & process the json_object for declared variables + in the Cookiecutter template. + + :param json_object: A JSON file that has be loaded into a Python OrderedDict. + :param no_input: Prompt the user at command line for manual configuration if False, + if True, no input prompts are made, all defaults are accepted. + :param verbose: Emit maximum varible information. + """ env = Environment(extensions=['jinja2_time.TimeExtension']) context = collections.OrderedDict({}) @@ -237,7 +449,7 @@ def load_context(json_object, verbose): deserialize = DESERIALIZERS[variable.var_type] - if not variable.prompt_user: + if no_input or (not variable.prompt_user): context[variable.name] = deserialize(default) continue @@ -249,7 +461,17 @@ def load_context(json_object, verbose): if verbose and variable.description: click.echo(variable.description) - value = prompt(variable, default) + while True: + value = prompt(variable, default) + if variable.validate: + if variable.validate.match(value): + break + else: + msg = "Input validation failure against regex: '{val_string}', try again!".format(val_string=variable.validation) + click.echo(msg) + else: + # no validation defined + break if verbose: width, _ = click.get_terminal_size() @@ -260,14 +482,16 @@ def load_context(json_object, verbose): return context -def main(file_path): - """Load the json object and prompt the user for input""" +# def main(file_path): +# """Load the json object and prompt the user for input""" - with codecs.open(file_path, 'r', encoding='utf8') as f: - json_object = json.load(f, object_pairs_hook=collections.OrderedDict) +# import codecs +# import pprint +# with codecs.open(file_path, 'r', encoding='utf8') as f: +# json_object = json.load(f, object_pairs_hook=collections.OrderedDict) - pprint.pprint(load_context(json_object, True)) +# pprint.pprint(load_context(json_object, True, False)) -if __name__ == '__main__': - main('cookiecutter.json') +# if __name__ == '__main__': +# main('cookiecutter.json') From 82eebbe4c4b497e60e423638bf28ab769af32f24 Mon Sep 17 00:00:00 2001 From: eruber Date: Sun, 29 Oct 2017 09:21:16 -0700 Subject: [PATCH 005/274] Added detection of v1 or v2 context & appropriate call to v2 overwrite --- cookiecutter/generate.py | 52 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/cookiecutter/generate.py b/cookiecutter/generate.py index 3bdd1e029..b098d8601 100644 --- a/cookiecutter/generate.py +++ b/cookiecutter/generate.py @@ -27,6 +27,8 @@ from .hooks import run_hook from .utils import make_sure_path_exists, work_in, rmtree +from .context import context_is_version_2 + logger = logging.getLogger(__name__) @@ -72,6 +74,38 @@ def apply_overwrites_to_context(context, overwrite_context): context[variable] = overwrite +def apply_overwrites_to_context2(context, overwrite_context): + """Modify the given version 2 context in place based on the overwrite_context.""" + + # Need a more pythonic way of doing this, its convoluted... + variable_idx_by_name = {} + for idx, dictionary in enumerate(context['variables']): + variable_idx_by_name[dictionary['name']] = idx + + for variable, overwrite in overwrite_context.items(): + if variable not in variable_idx_by_name.keys(): + # Do not include variables which are not used in the template + continue + + if 'choices' in context['variables'][variable_idx_by_name[variable]].keys(): + context_value = context['variables'][variable_idx_by_name[variable]]['choices'] + else: + context_value = context['variables'][variable_idx_by_name[variable]]['default'] + + if isinstance(context_value, list): + # We are dealing with a choice variable + if overwrite in context_value: + # This overwrite is actually valid for the given context + # Let's set it as default (by definition first item in list) + # see ``cookiecutter.prompt.prompt_choice_for_config`` + context_value.remove(overwrite) + context_value.insert(0, overwrite) + context['variables'][variable_idx_by_name[variable]]['default'] = overwrite + else: + # Simply overwrite the value for this variable + context['variables'][variable_idx_by_name[variable]]['default'] = overwrite + + def generate_context(context_file='cookiecutter.json', default_context=None, extra_context=None): """Generate the context for a Cookiecutter project template. @@ -105,10 +139,20 @@ def generate_context(context_file='cookiecutter.json', default_context=None, # Overwrite context variable defaults with the default context from the # user's global config, if available - if default_context: - apply_overwrites_to_context(obj, default_context) - if extra_context: - apply_overwrites_to_context(obj, extra_context) + if context_is_version_2(context[file_stem]): + logger.debug("Context is version 2") + + if default_context: + apply_overwrites_to_context2(obj, default_context) + if extra_context: + apply_overwrites_to_context2(obj, extra_context) + else: + logger.debug("Context is version 1") + + if default_context: + apply_overwrites_to_context(obj, default_context) + if extra_context: + apply_overwrites_to_context(obj, extra_context) logger.debug('Context generated is {}'.format(context)) return context From b60f1c66da0b6bf22b57424a2c66fb1016f6ae3e Mon Sep 17 00:00:00 2001 From: eruber Date: Sun, 29 Oct 2017 09:22:29 -0700 Subject: [PATCH 006/274] Added cov-report term-missing to pytext addopts in order to display missing coverage lines column --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 83901b4e9..805cc520d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,5 +16,5 @@ universal = 1 [tool:pytest] testpaths = tests -addopts = --cov=cookiecutter +addopts = --cov-report term-missing --cov=cookiecutter From aaf1a65c6a1d2702ef96725ba864d3b79c260777 Mon Sep 17 00:00:00 2001 From: eruber Date: Sun, 29 Oct 2017 09:23:37 -0700 Subject: [PATCH 007/274] Added test for v2 load_context call --- tests/test_main.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/test_main.py b/tests/test_main.py index 1198fb037..d3e326efa 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -56,3 +56,32 @@ def test_replay_load_template_name( user_config_data['replay_dir'], 'fake-repo-tmpl', ) + + +def test_version_2_load_context_call( + monkeypatch, mocker, user_config_file): + """Check that the version 2 load_context() is called. + + Change the current working directory temporarily to + 'tests/test-generate-context-v2/min-v2-cookiecutter' + for this test and call cookiecutter with '.' for the target template. + """ + monkeypatch.chdir('tests/test-generate-context-v2/min-v2-cookiecutter') + + mock_replay_dump = mocker.patch('cookiecutter.main.dump') + + mock_version_1_prompt_for_config = mocker.patch('cookiecutter.main.prompt_for_config') + mock_version_2_load_context = mocker.patch('cookiecutter.main.load_context') + + mocker.patch('cookiecutter.main.generate_files') + + cookiecutter( + '.', + no_input=True, + replay=False, + config_file=user_config_file, + ) + + assert mock_version_1_prompt_for_config.call_count == 0 + assert mock_version_2_load_context.call_count == 1 + assert mock_replay_dump.call_count == 1 From 25ca798d0681402ac1c9f05fcfdd9dc9940713d8 Mon Sep 17 00:00:00 2001 From: eruber Date: Sun, 29 Oct 2017 09:26:05 -0700 Subject: [PATCH 008/274] Added tests & test support files for testing context v2 generation in generate.py --- .../min-v2-cookiecutter/cookiecutter.json | 5 + tests/test-generate-context-v2/test.json | 20 ++ .../test_choices.json | 17 ++ tests/test_generate_context_v2.py | 189 ++++++++++++++++++ 4 files changed, 231 insertions(+) create mode 100644 tests/test-generate-context-v2/min-v2-cookiecutter/cookiecutter.json create mode 100644 tests/test-generate-context-v2/test.json create mode 100644 tests/test-generate-context-v2/test_choices.json create mode 100644 tests/test_generate_context_v2.py diff --git a/tests/test-generate-context-v2/min-v2-cookiecutter/cookiecutter.json b/tests/test-generate-context-v2/min-v2-cookiecutter/cookiecutter.json new file mode 100644 index 000000000..7c257efe1 --- /dev/null +++ b/tests/test-generate-context-v2/min-v2-cookiecutter/cookiecutter.json @@ -0,0 +1,5 @@ +{ + "name": "cookiecutter-fake-minimun-template", + "cookiecutter_version": "2.0.0", + "variables" : [] +} diff --git a/tests/test-generate-context-v2/test.json b/tests/test-generate-context-v2/test.json new file mode 100644 index 000000000..7050912f2 --- /dev/null +++ b/tests/test-generate-context-v2/test.json @@ -0,0 +1,20 @@ +{ + "name": "cookiecutter-pytest-plugin", + "cookiecutter_version": "2.0.0", + "variables" : [ + { + "name": "full_name", + "default": "J. Paul Getty", + "prompt": "What's your full name?", + "description": "Please enter your full name. It will be displayed on the README file and used for the PyPI package definition.", + "type": "string" + }, + { + "name": "email", + "default": "jpg@rich.de", + "prompt": "What's your email?", + "description": "Please enter an email address for the meta information in setup.py.", + "type": "string" + } + ] +} diff --git a/tests/test-generate-context-v2/test_choices.json b/tests/test-generate-context-v2/test_choices.json new file mode 100644 index 000000000..0600bf7aa --- /dev/null +++ b/tests/test-generate-context-v2/test_choices.json @@ -0,0 +1,17 @@ +{ + "name": "cookiecutter-pytest-plugin", + "cookiecutter_version": "2.0.0", + "variables" : [ + { + "name": "license", + "default": "MIT", + "choices": [ + "MIT", + "BSD3", + "GNU-GPL3", + "Apache2", + "Mozilla2" + ] + } + ] +} diff --git a/tests/test_generate_context_v2.py b/tests/test_generate_context_v2.py new file mode 100644 index 000000000..0fc654c45 --- /dev/null +++ b/tests/test_generate_context_v2.py @@ -0,0 +1,189 @@ +# -*- coding: utf-8 -*- + +""" +test_generate_convext_v2 +------------------------ + +Tests associated with processing v2 context syntax in the +`cookiecutter.generate` module. +""" + +from __future__ import unicode_literals +import pytest + +from collections import OrderedDict + +from cookiecutter import generate + + +def context_data(): + context = ( + { + 'context_file': 'tests/test-generate-context-v2/test.json' + }, + { + "test": OrderedDict([ + ("name", "cookiecutter-pytest-plugin"), + ("cookiecutter_version", "2.0.0"), + ("variables", [ + OrderedDict([ + ("name", "full_name"), + ("default", "J. Paul Getty"), + ("prompt", "What's your full name?"), + ("description", "Please enter your full name. It will be displayed on the README file and used for the PyPI package definition."), + ("type", "string")]), + OrderedDict([ + ("name", "email"), + ("default", "jpg@rich.de"), + ("prompt", "What's your email?"), + ("description", "Please enter an email address for the meta information in setup.py."), + ("type", "string")]), + ]) + ]) + } + ) + + context_with_default = ( + { + 'context_file': 'tests/test-generate-context-v2/test.json', + 'default_context': {'full_name': 'James Patrick Morgan'} + }, + { + "test": OrderedDict([ + ("name", "cookiecutter-pytest-plugin"), + ("cookiecutter_version", "2.0.0"), + ("variables", [ + OrderedDict([ + ("name", "full_name"), + ("default", "James Patrick Morgan"), + ("prompt", "What's your full name?"), + ("description", "Please enter your full name. It will be displayed on the README file and used for the PyPI package definition."), + ("type", "string")]), + OrderedDict([ + ("name", "email"), + ("default", "jpg@rich.de"), + ("prompt", "What's your email?"), + ("description", "Please enter an email address for the meta information in setup.py."), + ("type", "string")]), + ]) + ]) + } + ) + + context_with_extra = ( + { + 'context_file': 'tests/test-generate-context-v2/test.json', + 'extra_context': {'email': 'jpm@chase.bk'} + }, + { + "test": OrderedDict([ + ("name", "cookiecutter-pytest-plugin"), + ("cookiecutter_version", "2.0.0"), + ("variables", [ + OrderedDict([ + ("name", "full_name"), + ("default", "J. Paul Getty"), + ("prompt", "What's your full name?"), + ("description", "Please enter your full name. It will be displayed on the README file and used for the PyPI package definition."), + ("type", "string")]), + OrderedDict([ + ("name", "email"), + ("default", "jpm@chase.bk"), + ("prompt", "What's your email?"), + ("description", "Please enter an email address for the meta information in setup.py."), + ("type", "string")]), + ]) + ]) + } + ) + + context_with_default_and_extra = ( + { + 'context_file': 'tests/test-generate-context-v2/test.json', + 'default_context': {'full_name': 'Alpha Gamma Five'}, + 'extra_context': {'email': 'agamma5@universe.open'} + }, + { + "test": OrderedDict([ + ("name", "cookiecutter-pytest-plugin"), + ("cookiecutter_version", "2.0.0"), + ("variables", [ + OrderedDict([ + ("name", "full_name"), + ("default", "Alpha Gamma Five"), + ("prompt", "What's your full name?"), + ("description", "Please enter your full name. It will be displayed on the README file and used for the PyPI package definition."), + ("type", "string")]), + OrderedDict([ + ("name", "email"), + ("default", "agamma5@universe.open"), + ("prompt", "What's your email?"), + ("description", "Please enter an email address for the meta information in setup.py."), + ("type", "string")]), + ]) + ]) + } + ) + + context_choices_with_default = ( + { + 'context_file': 'tests/test-generate-context-v2/test_choices.json', + 'default_context': {'license': 'Apache2'}, + }, + { + "test_choices": OrderedDict([ + ("name", "cookiecutter-pytest-plugin"), + ("cookiecutter_version", "2.0.0"), + ("variables", [ + OrderedDict([ + ("name", "license"), + ("default", "Apache2"), + ("choices", ["Apache2", "MIT", "BSD3", "GNU-GPL3", "Mozilla2"]), + ])] + ) + ]) + } + ) + + context_choices_with_default_not_in_choices = ( + { + 'context_file': 'tests/test-generate-context-v2/test.json', + 'default_context': {'orientation': 'landscape'}, + }, + { + "test": OrderedDict([ + ("name", "cookiecutter-pytest-plugin"), + ("cookiecutter_version", "2.0.0"), + ("variables", [ + OrderedDict([ + ("name", "full_name"), + ("default", "J. Paul Getty"), + ("prompt", "What's your full name?"), + ("description", "Please enter your full name. It will be displayed on the README file and used for the PyPI package definition."), + ("type", "string")]), + OrderedDict([ + ("name", "email"), + ("default", "jpg@rich.de"), + ("prompt", "What's your email?"), + ("description", "Please enter an email address for the meta information in setup.py."), + ("type", "string")]), + ]) + ]) + } + ) + yield context + yield context_with_default + yield context_with_extra + yield context_with_default_and_extra + yield context_choices_with_default + yield context_choices_with_default_not_in_choices + + +@pytest.mark.usefixtures('clean_system') +@pytest.mark.parametrize('input_params, expected_context', context_data()) +def test_generate_context(input_params, expected_context): + """ + Test the generated context for several input parameters against the + according expected context. + """ + assert generate.generate_context(**input_params) == expected_context From 3210c66bb8ea966daf951cf17593fcf82431a83a Mon Sep 17 00:00:00 2001 From: eruber Date: Sun, 29 Oct 2017 09:27:22 -0700 Subject: [PATCH 009/274] Added tests & test support files for testing v2 context processing in the new context.py --- tests/test-context/cookiecutter.json | 154 +++++ tests/test-context/cookiecutter_choices.json | 36 ++ .../cookiecutter_val_failure.json | 21 + .../cookiecutter_val_success.json | 21 + tests/test_context.py | 605 ++++++++++++++++++ 5 files changed, 837 insertions(+) create mode 100644 tests/test-context/cookiecutter.json create mode 100644 tests/test-context/cookiecutter_choices.json create mode 100644 tests/test-context/cookiecutter_val_failure.json create mode 100644 tests/test-context/cookiecutter_val_success.json create mode 100644 tests/test_context.py diff --git a/tests/test-context/cookiecutter.json b/tests/test-context/cookiecutter.json new file mode 100644 index 000000000..72f91ed22 --- /dev/null +++ b/tests/test-context/cookiecutter.json @@ -0,0 +1,154 @@ +{ + "name": "cookiecutter-pytest-plugin", + "version": "0.1.0", + "description": "a cookiecutter to create pytest plugins with ease.", + "authors": [ + "Raphael Pierzina ", + "Audrey Roy Greenfeld " + ], + "cookiecutter_version": "2.0.0", + "license": "MIT", + "keywords": [ + "pytest", + "python", + "plugin" + ], + "url": "https://github.com/pytest-dev/cookiecutter-pytest-plugin", + "environment_settings": { + "keep_trailing_newline": true, + "comment_start_string": "{##", + "comment_end_string": "##}" + }, + "variables": [ + { + "name": "full_name", + "default": "Raphael Pierzina", + "prompt": "What's your full name?", + "description": "Please enter your full name. It will be displayed on the README file and used for the PyPI package definition.", + "type": "string" + }, + { + "name": "email", + "default": "raphael@hackebrot.de", + "prompt": "What's your email?", + "description": "Please enter an email address for the meta information in setup.py.", + "type": "string" + }, + { + "name": "secret_token", + "default": null, + "prompt": "Please enter your secret token", + "type": "string", + "hide_input": true + }, + { + "name": "plugin_name", + "default": "emoji", + "prompt": "What should be the name for your plugin?", + "description": "Please enter a name for your plugin. We will prepend the name with 'pytest-'", + "type": "string" + }, + { + "name": "module_name", + "default": "{{cookiecutter.plugin_name|lower|replace('-','_')}}", + "prompt": "Please enter a name for your base python module", + "type": "string", + "validation": "^[a-z_]+$" + }, + { + "name": "license", + "default": "MIT", + "prompt": "Please choose a license!", + "description": "Cookiecutter will add an according LICENSE file for you and set the according classifier in setup.py.", + "type": "string", + "choices": [ + "MIT", + "BSD-3", + "GNU GPL v3.0", + "Apache Software License 2.0", + "Mozilla Public License 2.0" + ] + }, + { + "name": "docs", + "default": false, + "prompt": "Do you want to generate a base for docs?", + "description": "Would you like to generate documentation for your plugin? You will be able to choose from a number of generators.", + "type": "yes_no" + }, + { + "name": "docs_tool", + "default": "mkdocs", + "prompt": "Which tool do you want to choose for generating docs?", + "description": "There are a number of options for documentation generators. Please choose one. We will create a separate folder for you", + "type": "string", + "choices": [ + "mkdocs", + "sphinx" + ], + "skip_if": "{{cookiecutter.docs == False}}" + }, + { + "name": "year", + "default": "{% now 'utc', '%Y' %}", + "prompt_user": false, + "type": "string" + }, + { + "name": "incept_year", + "default": 2017, + "prompt_user": false, + "type": "int" + }, + { + "name": "released", + "default": false, + "prompt_user": false, + "type": "boolean" + }, + { + "name": "temperature", + "default": 77.3, + "prompt_user": false, + "type": "float" + }, + { + "name": "Release-GUID", + "default": "04f5eaa9ee7345469dccffc538b27194", + "prompt_user": false, + "type": "uuid" + }, + { + "name": "extensions", + "default": ["jinja2_time.TimeExtension"], + "prompt_user": false, + "type": "string" + }, + { + "name": "copy_with_out_render", + "default": [ + "*.html", + "*not_rendered_dir", + "rendered_dir/not_rendered_file.ini" + ], + "prompt_user": false, + "type": "string" + }, + { + "name": "fixtures", + "default": { + "foo": { + "scope": "session", + "autouse": true + }, + "bar": { + "scope": "function", + "autouse": false + } + }, + "description": "Please enter a valid JSON string to set up fixtures for your plugin.", + "prompt_user": true, + "type": "json" + } + ] +} diff --git a/tests/test-context/cookiecutter_choices.json b/tests/test-context/cookiecutter_choices.json new file mode 100644 index 000000000..68b84b092 --- /dev/null +++ b/tests/test-context/cookiecutter_choices.json @@ -0,0 +1,36 @@ +{ + "name": "cookiecutter-test-choices-with-inputs", + "version": "0.1.0", + "cookiecutter_version": "2.0.0", + "variables": [ + { + "name": "full_name", + "default": "Raphael Pierzina", + "prompt": "What's your full name?", + "description": "Please enter your full name. It will be displayed on the README file and used for the PyPI package definition.", + "type": "string" + }, + { + "name": "email", + "default": "raphael@hackebrot.de", + "prompt": "What's your email?", + "description": "Please enter an email address for the meta information in setup.py.", + "type": "string" + }, + { + "name": "license", + "default": "ISC", + "prompt": "Please choose a license!", + "description": "Cookiecutter will add an according LICENSE file for you and set the according classifier in setup.py.", + "type": "string", + "choices": [ + "ISC", + "MIT", + "BSD3", + "GNU-GPL3", + "Apache2", + "Mozilla2" + ] + } + ] +} diff --git a/tests/test-context/cookiecutter_val_failure.json b/tests/test-context/cookiecutter_val_failure.json new file mode 100644 index 000000000..f61eab83e --- /dev/null +++ b/tests/test-context/cookiecutter_val_failure.json @@ -0,0 +1,21 @@ +{ + "name": "cookiecutter-test-validation-failure", + "version": "0.1.0", + "cookiecutter_version": "2.0.0", + "variables": [ + { + "name": "project_name", + "default": "Default Project Name", + "prompt": "Enter Project Name", + "description": "Please enter a short, space delimited, name for this project.", + "type": "string" + }, + { + "name": "module_name", + "default": "{{cookiecutter.project_name|lower|replace(' ','_')}}", + "prompt": "Please enter a name for your base python module", + "type": "string", + "validation": "^[a-z_]+$" + } + ] +} diff --git a/tests/test-context/cookiecutter_val_success.json b/tests/test-context/cookiecutter_val_success.json new file mode 100644 index 000000000..12bae56ef --- /dev/null +++ b/tests/test-context/cookiecutter_val_success.json @@ -0,0 +1,21 @@ +{ + "name": "cookiecutter-test-validation-success", + "version": "0.1.0", + "cookiecutter_version": "2.0.0", + "variables": [ + { + "name": "project_name", + "default": "Default Project Name", + "prompt": "Enter Project Name", + "description": "Please enter a short, space delimited, name for this project.", + "type": "string" + }, + { + "name": "module_name", + "default": "{{cookiecutter.project_name|lower|replace(' ','_')}}", + "prompt": "Please enter a name for your base python module", + "type": "string", + "validation": "^[a-z_]+$" + } + ] +} diff --git a/tests/test_context.py b/tests/test_context.py new file mode 100644 index 000000000..71fb05608 --- /dev/null +++ b/tests/test_context.py @@ -0,0 +1,605 @@ +# -*- coding: utf-8 -*- + +""" +test_context +------------ + +Tests for `cookiecutter.context` module that handles prompts for v2 context. +""" +from __future__ import unicode_literals + +import os.path +import time +import json +import logging + +import pytest + +from collections import OrderedDict + +from cookiecutter import context + +from cookiecutter.exceptions import ( + ContextDecodingException +) + +import click + +from uuid import UUID + +logger = logging.getLogger(__name__) + + +def load_cookiecutter(cookiecutter_file): + + context = {} + try: + with open(cookiecutter_file) as file_handle: + obj = json.load(file_handle, object_pairs_hook=OrderedDict) + except ValueError as e: + # JSON decoding error. Let's throw a new exception that is more + # friendly for the developer or user. + full_fpath = os.path.abspath(cookiecutter_file) + json_exc_message = str(e) + our_exc_message = ( + 'JSON decoding error while loading "{0}". Decoding' + ' error details: "{1}"'.format(full_fpath, json_exc_message)) + raise ContextDecodingException(our_exc_message) + + # Add the Python object to the context dictionary + file_name = os.path.split(cookiecutter_file)[1] + file_stem = file_name.split('.')[0] + context[file_stem] = obj + + return context + + +def context_data_check(): + context_all_reqs = ( + { + 'cookiecutter_context': OrderedDict([ + ("name", "cookiecutter-pytest-plugin"), + ("cookiecutter_version", "2.0.0"), + ("variables", []) + ]) + }, + True + ) + + context_missing_name = ( + { + 'cookiecutter_context': OrderedDict([ + ("cookiecutter_version", "2.0.0"), + ("variables", []) + ]) + }, + False + ) + + context_missing_cookiecutter_version = ( + { + 'cookiecutter_context': OrderedDict([ + ("name", "cookiecutter-pytest-plugin"), + ("variables", []) + ]) + }, + False + ) + + context_missing_variables = ( + { + 'cookiecutter_context': OrderedDict([ + ("name", "cookiecutter-pytest-plugin"), + ("cookiecutter_version", "2.0.0"), + ]) + }, + False + ) + + yield context_all_reqs + yield context_missing_name + yield context_missing_cookiecutter_version + yield context_missing_variables + + +@pytest.mark.usefixtures('clean_system') +@pytest.mark.parametrize('input_params, expected_result', context_data_check()) +def test_context_check(input_params, expected_result): + """ + Test that a context with the required fields will be detected as a + v2 context. + """ + assert context.context_is_version_2(**input_params) == expected_result + + +@pytest.mark.usefixtures('clean_system') +def test_load_context_defaults(): + + cc = load_cookiecutter('tests/test-context/cookiecutter.json') + + cc_cfg = context.load_context(cc['cookiecutter'], no_input=True) + + assert cc_cfg['full_name'] == 'Raphael Pierzina' + assert cc_cfg['email'] == 'raphael@hackebrot.de' + assert cc_cfg['plugin_name'] == 'emoji' + assert cc_cfg['module_name'] == 'emoji' + assert cc_cfg['license'] == 'MIT' + assert cc_cfg['docs'] == False + assert 'docs_tool' not in cc_cfg.keys() + assert cc_cfg['year'] == time.strftime('%Y') + assert cc_cfg['incept_year'] == 2017 + assert cc_cfg['released'] == False + assert cc_cfg['temperature'] == 77.3 + assert cc_cfg['Release-GUID'] == UUID('04f5eaa9ee7345469dccffc538b27194') + assert cc_cfg['extensions'] == "['jinja2_time.TimeExtension']" + assert cc_cfg['copy_with_out_render'] == "['*.html', '*not_rendered_dir', 'rendered_dir/not_rendered_file.ini']" + assert cc_cfg['fixtures'] == OrderedDict([('foo', + OrderedDict([('scope', 'session'), + ('autouse', True)])), + ('bar', + OrderedDict([('scope', 'function'), + ('autouse', + False)]))]) + + +def test_prompt_string(mocker): + + EXPECTED_VALUE = 'Input String' + + mock_prompt = mocker.patch( + 'cookiecutter.prompt.click.prompt', + autospec=True, + return_value=EXPECTED_VALUE, + ) + + m = mocker.Mock() + m.side_effect = context.Variable + v = m.side_effect(name='name', default='', prompt='Enter Name', hide_input=False) + + r = context.prompt_string(v, default='Alpha') + + assert mock_prompt.call_args == mocker.call( + v.prompt, + default='Alpha', + hide_input=v.hide_input, + type=click.STRING, + ) + + assert r == EXPECTED_VALUE + + +def test_prompt_bool(mocker): + + EXPECTED_VALUE = True + + mock_prompt = mocker.patch( + 'cookiecutter.prompt.click.prompt', + autospec=True, + return_value=EXPECTED_VALUE, + ) + + m = mocker.Mock() + m.side_effect = context.Variable + v = m.side_effect(name='flag', default=False, prompt='Enter a Flag', hide_input=False) + + r = context.prompt_boolean(v, default=False) + + assert mock_prompt.call_args == mocker.call( + v.prompt, + default=False, + hide_input=v.hide_input, + type=click.BOOL, + ) + + assert r # EXPECTED_VALUE + + +def test_prompt_int(mocker): + + EXPECTED_VALUE = 777 + + mock_prompt = mocker.patch( + 'cookiecutter.prompt.click.prompt', + autospec=True, + return_value=EXPECTED_VALUE, + ) + + m = mocker.Mock() + m.side_effect = context.Variable + v = m.side_effect(name='port', default=1000, prompt='Enter Port', hide_input=False) + + r = context.prompt_int(v, default=1000) + + assert mock_prompt.call_args == mocker.call( + v.prompt, + default=1000, + hide_input=v.hide_input, + type=click.INT, + ) + + assert r == EXPECTED_VALUE + + +def test_prompt_float(mocker): + + EXPECTED_VALUE = 3.14 + + mock_prompt = mocker.patch( + 'cookiecutter.prompt.click.prompt', + autospec=True, + return_value=EXPECTED_VALUE, + ) + + m = mocker.Mock() + m.side_effect = context.Variable + v = m.side_effect(name='PI', default=3.0, prompt='Enter PI', hide_input=False) + + r = context.prompt_float(v, default=3.0) + + assert mock_prompt.call_args == mocker.call( + v.prompt, + default=3.0, + hide_input=v.hide_input, + type=click.FLOAT, + ) + + assert r == EXPECTED_VALUE + + +def test_prompt_uuid(mocker): + + EXPECTED_VALUE = '931ef56c3e7b45eea0427bac386f0a98' + + mock_prompt = mocker.patch( + 'cookiecutter.prompt.click.prompt', + autospec=True, + return_value=EXPECTED_VALUE, + ) + + m = mocker.Mock() + m.side_effect = context.Variable + v = m.side_effect(name='uuid', default=None, prompt='Enter a UUID', hide_input=False) + + r = context.prompt_uuid(v, default=None) + + assert mock_prompt.call_args == mocker.call( + v.prompt, + default=None, + hide_input=v.hide_input, + type=click.UUID, + ) + + assert r == EXPECTED_VALUE + + +def test_prompt_json(monkeypatch, mocker): + + EXPECTED_VALUE = '{"port": 67888, "colors": ["red", "green", "blue"]}' + + mocker.patch( + 'click.termui.visible_prompt_func', + autospec=True, + return_value=EXPECTED_VALUE, + ) + m = mocker.Mock() + m.side_effect = context.Variable + v = m.side_effect(name='json', default=None, prompt='Enter Config', hide_input=False) + + r = context.prompt_json(v, default=None) + + assert r == {"port": 67888, "colors": ["red", "green", "blue"]} + + +def test_prompt_json_bad_json_decode_click_asks_again(mocker, capsys): + + EXPECTED_BAD_VALUE = '{"port": 67888, "colors": ["red", "green", "blue"}' + EXPECTED_GOOD_VALUE = '{"port": 67888, "colors": ["red", "green", "blue"]}' + + mocker.patch( + 'click.termui.visible_prompt_func', + autospec=True, + side_effect=[EXPECTED_BAD_VALUE, EXPECTED_GOOD_VALUE] + ) + m = mocker.Mock() + m.side_effect = context.Variable + v = m.side_effect(name='json', default=None, prompt='Enter Config', hide_input=False) + + r = context.prompt_json(v, default=None) + + out, err = capsys.readouterr() + assert 'Error: Unable to decode to JSON.' in out + assert r == {"port": 67888, "colors": ["red", "green", "blue"]} + + +def test_prompt_json_default(mocker): + EXPECTED_VALUE = 'default' + + cfg = '{"port": 67888, "colors": ["red", "green", "blue"]}' + + mock_prompt = mocker.patch( + 'cookiecutter.prompt.click.prompt', + autospec=True, + return_value=EXPECTED_VALUE, + ) + + m = mocker.Mock() + m.side_effect = context.Variable + v = m.side_effect(name='json', default=None, prompt='Enter Config', hide_input=False) + + r = context.prompt_json(v, default=cfg) + + assert mock_prompt.call_args == mocker.call( + v.prompt, + default='default', + hide_input=v.hide_input, + type=click.STRING, + value_proc=mocker.ANY, + ) + + assert r == cfg + + +def test_prompt_yes_no_default_no(mocker): + + EXPECTED_VALUE = 'y' + + mock_prompt = mocker.patch( + 'cookiecutter.prompt.click.prompt', + autospec=True, + return_value=EXPECTED_VALUE, + ) + + m = mocker.Mock() + m.side_effect = context.Variable + v = m.side_effect(name='enable_docs', default='n', prompt='Enable docs', hide_input=False) + + r = context.prompt_yes_no(v, default=False) + + assert mock_prompt.call_args == mocker.call( + v.prompt, + default='n', + hide_input=v.hide_input, + type=click.BOOL, + ) + + assert r # EXPECTED_VALUE + + +def test_prompt_yes_no_default_yes(mocker): + + EXPECTED_VALUE = 'y' + + mock_prompt = mocker.patch( + 'cookiecutter.prompt.click.prompt', + autospec=True, + return_value=EXPECTED_VALUE, + ) + + m = mocker.Mock() + m.side_effect = context.Variable + v = m.side_effect(name='enable_docs', default='y', prompt='Enable docs', hide_input=False) + + r = context.prompt_yes_no(v, default=True) + + assert mock_prompt.call_args == mocker.call( + v.prompt, + default='y', + hide_input=v.hide_input, + type=click.BOOL, + ) + + assert r # EXPECTED_VALUE + + +def test_prompt_choice(mocker): + + LICENSES = ['ISC', 'MIT', 'BSD3'] + + DEFAULT_LICENSE = 'ISC' + + EXPECTED_VALUE = '2' + EXPECTED_LICENSE = 'MIT' + + mocker.patch( + 'cookiecutter.prompt.click.prompt', + autospec=True, + return_value=EXPECTED_VALUE, + ) + + m = mocker.Mock() + m.side_effect = context.Variable + v = m.side_effect(name='license', default=DEFAULT_LICENSE, choices=LICENSES, + prompt='Pick a License', hide_input=False) + + r = context.prompt_choice(v, default=DEFAULT_LICENSE) + + assert r == EXPECTED_LICENSE + + +def test_variable_invalid_type_exception(): + + with pytest.raises(ValueError) as excinfo: + context.Variable(name='badtype', default=None, type='color') + + assert 'Variable: badtype has an invalid type color' in str(excinfo.value) + + +def test_variable_invalid_default_choice(): + + CHOICES = ['green', 'red', 'blue', 'yellow'] + + with pytest.raises(ValueError) as excinfo: + context.Variable(name='badchoice', default='purple', type='string', + choices=CHOICES) + + assert 'Variable: badchoice has an invalid default value purple for choices: {choices}'.format(choices=CHOICES) in str(excinfo.value) + + +def test_variable_invalid_validation_control_flag_is_logged_and_removed(caplog): + + with caplog.at_level(logging.INFO): + v = context.Variable( + 'module_name', + "{{cookiecutter.plugin_name|lower|replace('-','_')}}", + prompt="Please enter a name for your base python module", + type='string', + validation='^[a-z_]+$', + validation_flags=['ignorecase', 'forget', ], + hide_input=True) + + for record in caplog.records: + assert record.levelname == 'WARNING' + + assert "Variable: module_name - Ignoring unkown RegEx validation Control Flag named 'forget'" in caplog.text + + assert v.validation_flag_names == ['ignorecase'] + + +def test_variable_validation_compile_exception(): + + VAR_NAME = 'module_name' + BAD_REGEX_STRING = '^[a-z_+$' # Missing a closing square-bracket (]) + + with pytest.raises(ValueError) as excinfo: + context.Variable( + VAR_NAME, + "{{cookiecutter.plugin_name|lower|replace('-','_')}}", + prompt="Please enter a name for your base python module", + type='string', + validation=BAD_REGEX_STRING, + validation_flags=['ignorecase'], + hide_input=True) + + assert "Variable: {var_name} - Validation Setup Error: Invalid RegEx '{value}' - does not compile - ".format(var_name=VAR_NAME, value=BAD_REGEX_STRING) in str(excinfo.value) + + +def test_variable_repr(): + + v = context.Variable( + 'module_name', + "{{cookiecutter.plugin_name|lower|replace('-','_')}}", + prompt="Please enter a name for your base python module", + type='string', + validation='^[a-z_]+$', + validation_flags=['ignorecase'], + hide_input=True) + + assert repr(v) == "" + + +def test_variable_str(): + + v = context.Variable( + 'module_name', + "{{cookiecutter.plugin_name|lower|replace('-','_')}}", + prompt="Please enter a name for your base python module", + type='string', + validation='^[a-z_]+$', + validation_flags=['ignorecase'], + hide_input=True) + + assert str(v) == \ + (':\n' + "name='module_name',\n" + "default='{{cookiecutter.plugin_name|lower|replace('-','_')}}',\n" + "description='None',\n" + "prompt='Please enter a name for your base python module',\n" + "hide_input='True',\n" + "var_type='string',\n" + "skip_if='',\n" + "prompt_user='True',\n" + "choices='[]',\n" + "validation='^[a-z_]+$',\n" + "validation_flag_names='['ignorecase']',\n" + "validation_flags='2',\n" + "validate='re.compile('^[a-z_]+$', re.IGNORECASE)'") + + +def test_variable_option_raise_invalid_type_value_error(): + + VAR_NAME = 'module_name' + OPT_VALUE_OF_INCORRECT_TYPE = 12 # should be a string + + with pytest.raises(ValueError) as excinfo: + context.Variable( + VAR_NAME, + "{{cookiecutter.plugin_name|lower|replace('-','_')}}", + prompt="Please enter a name for your base python module", + type='string', + validation=OPT_VALUE_OF_INCORRECT_TYPE, + validation_flags=['ignorecase'], + hide_input=True) + + msg = "Variable: '{var_name}' Option: 'validation' requires a value of type str, but has a value of: {value}" + assert msg.format(var_name=VAR_NAME, value=OPT_VALUE_OF_INCORRECT_TYPE) in str(excinfo.value) + + +def test_cookiecutter_template_repr(): + # name, cookiecutter_version, variables, **info + + cct = context.CookiecutterTemplate('cookiecutter_template_repr_test', + cookiecutter_version='2.0.0', variables=[]) + + assert repr(cct) == "" + + +# ############################################################################ +def test_load_context_with_input_chioces(mocker): + cc = load_cookiecutter('tests/test-context/cookiecutter_choices.json') + + INPUT_1 = 'E.R. Uber' + INPUT_2 = 'eruber@gmail.com' + INPUT_3 = '2' # 'MIT' + mocker.patch( + 'click.termui.visible_prompt_func', + autospec=True, + side_effect=[INPUT_1, INPUT_2, INPUT_3] + ) + + cc_cfg = context.load_context(cc['cookiecutter_choices'], no_input=False) + + assert cc_cfg['full_name'] == INPUT_1 + assert cc_cfg['email'] == INPUT_2 + assert cc_cfg['license'] == 'MIT' + + +def test_load_context_with_input_with_validation_success(mocker): + cc = load_cookiecutter('tests/test-context/cookiecutter_val_success.json') + + INPUT_1 = 'Image Module Maker' + INPUT_2 = '' + mocker.patch( + 'click.termui.visible_prompt_func', + autospec=True, + side_effect=[INPUT_1, INPUT_2] + ) + + logger.debug(cc) + + cc_cfg = context.load_context(cc['cookiecutter_val_success'], no_input=False) + + assert cc_cfg['project_name'] == INPUT_1 + assert cc_cfg['module_name'] == 'image_module_maker' + + +def test_load_context_with_input_with_validation_failure(mocker, capsys): + cc = load_cookiecutter('tests/test-context/cookiecutter_val_failure.json') + + INPUT_1 = '6 Debug Shell' + INPUT_2 = '' + INPUT_3 = 'debug_shell' + mocker.patch( + 'click.termui.visible_prompt_func', + autospec=True, + side_effect=[INPUT_1, INPUT_2, INPUT_3] + ) + + cc_cfg = context.load_context(cc['cookiecutter_val_failure'], no_input=False) + + out, err = capsys.readouterr() + + msg = "Input validation failure against regex: '^[a-z_]+$', try again!" + assert msg in out + + assert cc_cfg['project_name'] == INPUT_1 + assert cc_cfg['module_name'] == INPUT_3 From 52e3da485ea09b838a735fb12e3c19e6d1935083 Mon Sep 17 00:00:00 2001 From: eruber Date: Mon, 30 Oct 2017 09:47:46 -0700 Subject: [PATCH 010/274] removed commented out lines at bottom of file that were a main entry point --- cookiecutter/context.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/cookiecutter/context.py b/cookiecutter/context.py index 3cc984041..6a2a1deda 100644 --- a/cookiecutter/context.py +++ b/cookiecutter/context.py @@ -480,18 +480,3 @@ def load_context(json_object, no_input=False, verbose=True): context[variable.name] = deserialize(value) return context - - -# def main(file_path): -# """Load the json object and prompt the user for input""" - -# import codecs -# import pprint -# with codecs.open(file_path, 'r', encoding='utf8') as f: -# json_object = json.load(f, object_pairs_hook=collections.OrderedDict) - -# pprint.pprint(load_context(json_object, True, False)) - - -# if __name__ == '__main__': -# main('cookiecutter.json') From a8ab28452a7dce4e7a1d9d535eab12792c6a34de Mon Sep 17 00:00:00 2001 From: eruber Date: Mon, 30 Oct 2017 16:53:34 -0700 Subject: [PATCH 011/274] Last of overwrite context tests dealing with default & choices entanglement --- cookiecutter/generate.py | 131 ++++- .../representative.json | 26 + tests/test_generate_context_v2.py | 486 +++++++++++++++++- 3 files changed, 614 insertions(+), 29 deletions(-) create mode 100644 tests/test-generate-context-v2/representative.json diff --git a/cookiecutter/generate.py b/cookiecutter/generate.py index b098d8601..d6968bf91 100644 --- a/cookiecutter/generate.py +++ b/cookiecutter/generate.py @@ -74,36 +74,111 @@ def apply_overwrites_to_context(context, overwrite_context): context[variable] = overwrite -def apply_overwrites_to_context2(context, overwrite_context): - """Modify the given version 2 context in place based on the overwrite_context.""" +def apply_default_overwrites_to_context2(context, overwrite_default_context): + """Modify the given version 2 context in place based on the overwrite_default_context.""" + + for variable, overwrite in overwrite_default_context.items(): + var_dict = next((d for d in context['variables'] if d['name'] == variable), None) + if var_dict: + if 'choices' in var_dict.keys(): + context_value = var_dict['choices'] + else: + context_value = var_dict['default'] + + if isinstance(context_value, list): + # We are dealing with a choice variable + if overwrite in context_value: + # This overwrite is actually valid for the given context + # Let's set it as default (by definition first item in list) + # see ``cookiecutter.prompt.prompt_choice_for_config`` + context_value.remove(overwrite) + context_value.insert(0, overwrite) + var_dict['default'] = overwrite + else: + # Simply overwrite the value for this variable + var_dict['default'] = overwrite + + +def apply_extra_overwrites_to_context2(context, extra_context): + """ + Modify the given version 2 context in place based on extra_context. - # Need a more pythonic way of doing this, its convoluted... - variable_idx_by_name = {} - for idx, dictionary in enumerate(context['variables']): - variable_idx_by_name[dictionary['name']] = idx + The extra_context parameter may be a dictionary or a list of dictionaries. - for variable, overwrite in overwrite_context.items(): - if variable not in variable_idx_by_name.keys(): - # Do not include variables which are not used in the template - continue + If extra_context is a dictionary, the key is assumed to identify the + variable's 'name' field and the value will be applied to the name field's + default value -- this behavior is exactly like version 1 context overwrite + behavior. - if 'choices' in context['variables'][variable_idx_by_name[variable]].keys(): - context_value = context['variables'][variable_idx_by_name[variable]]['choices'] - else: - context_value = context['variables'][variable_idx_by_name[variable]]['default'] + When extra_context is a list of dictionaries, each dictionary MUST specify + at the very least a 'name' key/value pair, or a ValueError is raised. The + 'name' key's value will be used to find the variable dictionary to + overwrite by matching each dictionary's 'name' field. - if isinstance(context_value, list): - # We are dealing with a choice variable - if overwrite in context_value: - # This overwrite is actually valid for the given context - # Let's set it as default (by definition first item in list) - # see ``cookiecutter.prompt.prompt_choice_for_config`` - context_value.remove(overwrite) - context_value.insert(0, overwrite) - context['variables'][variable_idx_by_name[variable]]['default'] = overwrite - else: - # Simply overwrite the value for this variable - context['variables'][variable_idx_by_name[variable]]['default'] = overwrite + If extra_context is a list of dictionaries, apply the overwrite from each + dictionary to it's matching variable's dictionary. This allows all fields + of a variable to be updated. A match considers the variable's 'name' field + only; any name fields in the extra_context list of dictionaries that do not + match a variable 'name' field, are ignored. Any key/value pairs specified in + an extra_content dictionary that are not already defined by the matching + variable dictionary will raise a ValueError. + + """ + if isinstance(extra_context, dict): + apply_default_overwrites_to_context2(context, extra_context) + elif isinstance(extra_context, list): + for xtra_ctx_item in extra_context: + if isinstance(xtra_ctx_item, dict): + if 'name' in xtra_ctx_item.keys(): + # xtra_ctx_item['name'] may have a replace value of the form: + # 'name_value::replace_name_value' + xtra_ctx_name = xtra_ctx_item['name'].split('::')[0] + try: + replace_name = xtra_ctx_item['name'].split('::')[1] + except IndexError: + replace_name = None + + var_dict = next((d for d in context['variables'] if d['name'] == xtra_ctx_name), None) + if var_dict: + # Since creation of new key/value pairs is NOT desired, we only use a key + # that is common to both variables context and the extra context + common_keys = [key for key in xtra_ctx_item.keys() if key in var_dict.keys()] + for key in common_keys: + if xtra_ctx_item[key] == '<>': + var_dict.pop(key, None) + else: + # normal field update + var_dict[key] = xtra_ctx_item[key] + + # After all fields have been update, there is some house-keeping to do. + # The default/choices house-keeping could effecively be no-ops if the + # user did the correct thing. + if ('default' in common_keys) & ('choices' in var_dict.keys()): + # default updated, regardless if choices has been updated, + # re-order choices based on default + if var_dict['default'] in var_dict['choices']: + var_dict['choices'].remove(var_dict['default']) + + var_dict['choices'].insert(0, var_dict['default']) + + if ('default' not in common_keys) & ('choices' in common_keys): + # choices updated, so update default based on first location in choices + var_dict['default'] = var_dict['choices'][0] + + if replace_name: + var_dict['name'] = replace_name + else: + msg = "No variable found in context whose name matches extra context name '{name}'" + raise ValueError(msg.format(name=xtra_ctx_name)) + else: + msg = "Extra context dictionary item {item} is missing a 'name' key." + raise ValueError(msg.format(item=xtra_ctx_item)) + else: + msg = "Extra context list item '{item}' is of type {t}, should be a dictionary." + raise ValueError(msg.format(item=str(xtra_ctx_item), t=type(xtra_ctx_item).__name__)) + else: + msg = "Extra context must be a dictionary or a list of dictionaries!" + raise ValueError(msg) def generate_context(context_file='cookiecutter.json', default_context=None, @@ -143,9 +218,9 @@ def generate_context(context_file='cookiecutter.json', default_context=None, logger.debug("Context is version 2") if default_context: - apply_overwrites_to_context2(obj, default_context) + apply_default_overwrites_to_context2(obj, default_context) if extra_context: - apply_overwrites_to_context2(obj, extra_context) + apply_extra_overwrites_to_context2(obj, extra_context) else: logger.debug("Context is version 1") diff --git a/tests/test-generate-context-v2/representative.json b/tests/test-generate-context-v2/representative.json new file mode 100644 index 000000000..3d47be2e2 --- /dev/null +++ b/tests/test-generate-context-v2/representative.json @@ -0,0 +1,26 @@ +{ + "name": "cc-representative", + "cookiecutter_version": "2.0.0", + "variables" : [ + { + "name": "director_credit", + "default": true, + "prompt": "Is there a director credit on this film?", + "description": "Directors take credit for most of their films, usually...", + "type": "boolean" + }, + { + "name": "director_name", + "default": "Allan Smithe", + "prompt": "What's the Director's full name?", + "prompt_user": true, + "description": "The default director is not proud of their work, we hope you are.", + "hide_input": false, + "choices": ["Allan Smithe", "Ridley Scott", "Victor Fleming", "John Houston"], + "validation": "^[a-z][A-Z]+$", + "validation_flags": ["verbose", "ascii"], + "skip_if": "{{cookiecutter.director_credit == False}}", + "type": "string" + } + ] +} diff --git a/tests/test_generate_context_v2.py b/tests/test_generate_context_v2.py index 0fc654c45..9d6dc09c9 100644 --- a/tests/test_generate_context_v2.py +++ b/tests/test_generate_context_v2.py @@ -46,7 +46,7 @@ def context_data(): context_with_default = ( { 'context_file': 'tests/test-generate-context-v2/test.json', - 'default_context': {'full_name': 'James Patrick Morgan'} + 'default_context': {'full_name': 'James Patrick Morgan', 'this_key_ignored': 'not_in_context'} }, { "test": OrderedDict([ @@ -187,3 +187,487 @@ def test_generate_context(input_params, expected_context): according expected context. """ assert generate.generate_context(**input_params) == expected_context + + +@pytest.mark.usefixtures('clean_system') +def test_generate_context_extra_ctx_invalid(): + """ + Test error condition when extra context is not a dictionary or a list + of dictionaries. + """ + + with pytest.raises(ValueError) as excinfo: + generate.generate_context( + context_file='tests/test-generate-context-v2/test.json', + default_context=None, + extra_context='should_be_a_list_or_a_dictionary') + + msg = "Extra context must be a dictionary or a list of dictionaries!" + assert msg in str(excinfo.value) + + +@pytest.mark.usefixtures('clean_system') +def test_generate_context_extra_ctx_list_item_not_dict(): + """ + Test error condition when extra context is a list, but not a list that + contains a dictionary. + """ + xtra_context = [ + 'a_string', 'here_too' + ] + with pytest.raises(ValueError) as excinfo: + generate.generate_context( + context_file='tests/test-generate-context-v2/test.json', + default_context=None, + extra_context=xtra_context) + + msg = "Extra context list item 'a_string' is of type str, should be a dictionary." + assert msg in str(excinfo.value) + + +@pytest.mark.usefixtures('clean_system') +def test_generate_context_extra_ctx_list_item_dict_missing_name_field(): + """ + Test error condition when extra context is a list, but not a list that + contains a dictionary. + """ + xtra_context = [ + { + "shouldbename": "author_name", + "default": "Robert Lewis", + "prompt": "What's the author's name?", + "description": "Please enter the author's full name.", + "type": "string" + } + ] + with pytest.raises(ValueError) as excinfo: + generate.generate_context( + context_file='tests/test-generate-context-v2/test.json', + default_context=None, + extra_context=xtra_context) + + msg = "is missing a 'name' key." + assert msg in str(excinfo.value) + + +@pytest.mark.usefixtures('clean_system') +def test_generate_context_extra_ctx_list_item_dict_no_name_field_match(): + """ + Test error condition when extra context is a list, but not a list that + contains a dictionary. + """ + xtra_context = [ + { + "name": "author_name", + "default": "Robert Lewis", + "prompt": "What's the author's name?", + "description": "Please enter the author's full name.", + "type": "string" + } + ] + with pytest.raises(ValueError) as excinfo: + generate.generate_context( + context_file='tests/test-generate-context-v2/test.json', + default_context=None, + extra_context=xtra_context) + + msg = "No variable found in context whose name matches extra context name 'author_name'" + assert msg in str(excinfo.value) + + +def gen_context_data_inputs_expected(): + # Extra field ignored + context_with_valid_extra_0 = ( + { + 'context_file': 'tests/test-generate-context-v2/test.json', + 'extra_context': [ + { + 'name': 'email', + 'default': 'miles.davis@jazz.gone', + 'description': 'Enter jazzy email...', + 'extra_field': 'extra_field_value', + } + ] + }, + { + "test": OrderedDict([ + ("name", "cookiecutter-pytest-plugin"), + ("cookiecutter_version", "2.0.0"), + ("variables", [ + OrderedDict([ + ("name", "full_name"), + ("default", "J. Paul Getty"), + ("prompt", "What's your full name?"), + ("description", "Please enter your full name. It will be displayed on the README file and used for the PyPI package definition."), + ("type", "string")]), + OrderedDict([ + ("name", "email"), + ("default", "miles.davis@jazz.gone"), + ("prompt", "What's your email?"), + ("description", "Enter jazzy email..."), + ("type", "string")]), + ]) + ]) + } + ) + # Empty extra context precipitates no ill effect + context_with_valid_extra_1 = ( + { + 'context_file': 'tests/test-generate-context-v2/representative.json', + 'extra_context': [] + # 'extra_context': [ + # { + # 'name': 'email', + # 'default': 'miles.davis@jazz.gone', + # 'description': 'Enter jazzy email...', + # 'extra_field': 'extra_field_value', + # } + # ] + }, + { + "representative": OrderedDict([ + ("name", "cc-representative"), + ("cookiecutter_version", "2.0.0"), + ("variables", [ + OrderedDict([ + ("name", "director_credit"), + ("default", True), + ("prompt", "Is there a director credit on this film?"), + ("description", "Directors take credit for most of their films, usually..."), + ("type", "boolean") + ]), + OrderedDict([ + ("name", "director_name"), + ("default", "Allan Smithe"), + ("prompt", "What's the Director's full name?"), + ("prompt_user", True), + ("description", "The default director is not proud of their work, we hope you are."), + ("hide_input", False), + ("choices", ["Allan Smithe", "Ridley Scott", "Victor Fleming", "John Houston"]), + ("validation", "^[a-z][A-Z]+$"), + ("validation_flags", ["verbose", "ascii"]), + ("skip_if", "{{cookiecutter.director_credit == False}}"), + ("type", "string") + ]) + ]) + ]) + } + ) + + # Test the ability to change the variable's name field since it is used to + # identify the variable to modifed with extra context and to remove a + # key from the context via the removal token: '<>' + context_with_valid_extra_2 = ( + { + 'context_file': 'tests/test-generate-context-v2/representative.json', + 'extra_context': [ + { + 'name': 'director_credit::producer_credit', + 'prompt': 'Is there a producer credit on this film?', + 'description': 'There are usually a lot of producers...', + }, + { + 'name': 'director_name', + 'skip_if': '<>', + }, + + ] + }, + { + "representative": OrderedDict([ + ("name", "cc-representative"), + ("cookiecutter_version", "2.0.0"), + ("variables", [ + OrderedDict([ + ("name", "producer_credit"), + ("default", True), + ("prompt", "Is there a producer credit on this film?"), + ("description", "There are usually a lot of producers..."), + ("type", "boolean") + ]), + OrderedDict([ + ("name", "director_name"), + ("default", "Allan Smithe"), + ("prompt", "What's the Director's full name?"), + ("prompt_user", True), + ("description", "The default director is not proud of their work, we hope you are."), + ("hide_input", False), + ("choices", ["Allan Smithe", "Ridley Scott", "Victor Fleming", "John Houston"]), + ("validation", "^[a-z][A-Z]+$"), + ("validation_flags", ["verbose", "ascii"]), + ("type", "string") + ]) + ]) + ]) + } + ) + # Test changing variable's name field value, default field, prompt field, + # and changing the type + context_with_valid_extra_3 = ( + { + 'context_file': 'tests/test-generate-context-v2/representative.json', + 'extra_context': [ + { + 'name': 'director_credit::producer_credits', + 'default': 2, + 'prompt': 'How many producers does this film have?', + 'description': 'There are usually a lot of producers...', + 'type': "int", + } + ] + }, + { + "representative": OrderedDict([ + ("name", "cc-representative"), + ("cookiecutter_version", "2.0.0"), + ("variables", [ + OrderedDict([ + ("name", "producer_credits"), + ("default", 2), + ("prompt", "How many producers does this film have?"), + ("description", "There are usually a lot of producers..."), + ("type", "int") + ]), + OrderedDict([ + ("name", "director_name"), + ("default", "Allan Smithe"), + ("prompt", "What's the Director's full name?"), + ("prompt_user", True), + ("description", "The default director is not proud of their work, we hope you are."), + ("hide_input", False), + ("choices", ["Allan Smithe", "Ridley Scott", "Victor Fleming", "John Houston"]), + ("validation", "^[a-z][A-Z]+$"), + ("validation_flags", ["verbose", "ascii"]), + ("skip_if", "{{cookiecutter.director_credit == False}}"), + ("type", "string") + ]) + ]) + ]) + } + ) + # Test changing choices field without changing the default, but default + # does not change because the first position in choices matches default + context_with_valid_extra_4 = ( + { + 'context_file': 'tests/test-generate-context-v2/representative.json', + 'extra_context': [ + { + 'name': 'director_name', + 'choices': ['Allan Smithe', 'Ridley Scott', 'Victor Fleming', 'John Houston', + 'John Ford', 'Billy Wilder'], + } + ] + }, + { + "representative": OrderedDict([ + ("name", "cc-representative"), + ("cookiecutter_version", "2.0.0"), + ("variables", [ + OrderedDict([ + ("name", "director_credit"), + ("default", True), + ("prompt", "Is there a director credit on this film?"), + ("description", "Directors take credit for most of their films, usually..."), + ("type", "boolean") + ]), + OrderedDict([ + ("name", "director_name"), + ("default", "Allan Smithe"), + ("prompt", "What's the Director's full name?"), + ("prompt_user", True), + ("description", "The default director is not proud of their work, we hope you are."), + ("hide_input", False), + ("choices", ['Allan Smithe', 'Ridley Scott', 'Victor Fleming', 'John Houston', 'John Ford', 'Billy Wilder']), + ("validation", "^[a-z][A-Z]+$"), + ("validation_flags", ["verbose", "ascii"]), + ("skip_if", "{{cookiecutter.director_credit == False}}"), + ("type", "string") + ]) + ]) + ]) + } + ) + # Test changing choices field and changing the default + context_with_valid_extra_5 = ( + { + 'context_file': 'tests/test-generate-context-v2/representative.json', + 'extra_context': [ + { + 'name': 'director_name', + 'default': 'John Ford', + 'choices': ['Allan Smithe', 'Ridley Scott', 'Victor Fleming', 'John Houston', + 'John Ford', 'Billy Wilder'], + } + ] + }, + { + "representative": OrderedDict([ + ("name", "cc-representative"), + ("cookiecutter_version", "2.0.0"), + ("variables", [ + OrderedDict([ + ("name", "director_credit"), + ("default", True), + ("prompt", "Is there a director credit on this film?"), + ("description", "Directors take credit for most of their films, usually..."), + ("type", "boolean") + ]), + OrderedDict([ + ("name", "director_name"), + ("default", "John Ford"), + ("prompt", "What's the Director's full name?"), + ("prompt_user", True), + ("description", "The default director is not proud of their work, we hope you are."), + ("hide_input", False), + ("choices", ['John Ford', 'Allan Smithe', 'Ridley Scott', 'Victor Fleming', 'John Houston', 'Billy Wilder']), + ("validation", "^[a-z][A-Z]+$"), + ("validation_flags", ["verbose", "ascii"]), + ("skip_if", "{{cookiecutter.director_credit == False}}"), + ("type", "string") + ]) + ]) + ]) + } + ) + # Test changing the default, but not the choices field, yet seeing choices field re-ordered + # to put default value in first location + context_with_valid_extra_6 = ( + { + 'context_file': 'tests/test-generate-context-v2/representative.json', + 'extra_context': [ + { + 'name': 'director_name', + 'default': 'John Ford', + } + ] + }, + { + "representative": OrderedDict([ + ("name", "cc-representative"), + ("cookiecutter_version", "2.0.0"), + ("variables", [ + OrderedDict([ + ("name", "director_credit"), + ("default", True), + ("prompt", "Is there a director credit on this film?"), + ("description", "Directors take credit for most of their films, usually..."), + ("type", "boolean") + ]), + OrderedDict([ + ("name", "director_name"), + ("default", "John Ford"), + ("prompt", "What's the Director's full name?"), + ("prompt_user", True), + ("description", "The default director is not proud of their work, we hope you are."), + ("hide_input", False), + ("choices", ['John Ford', 'Allan Smithe', 'Ridley Scott', 'Victor Fleming', 'John Houston']), + ("validation", "^[a-z][A-Z]+$"), + ("validation_flags", ["verbose", "ascii"]), + ("skip_if", "{{cookiecutter.director_credit == False}}"), + ("type", "string") + ]) + ]) + ]) + } + ) + # Test changing choices field without changing the default, but default + # does get changee because the first position in choices field chagned + context_with_valid_extra_7 = ( + { + 'context_file': 'tests/test-generate-context-v2/representative.json', + 'extra_context': [ + { + 'name': 'director_name', + 'choices': ['Billy Wilder', 'Allan Smithe', 'Ridley Scott', 'Victor Fleming', 'John Houston', + 'John Ford'], + } + ] + }, + { + "representative": OrderedDict([ + ("name", "cc-representative"), + ("cookiecutter_version", "2.0.0"), + ("variables", [ + OrderedDict([ + ("name", "director_credit"), + ("default", True), + ("prompt", "Is there a director credit on this film?"), + ("description", "Directors take credit for most of their films, usually..."), + ("type", "boolean") + ]), + OrderedDict([ + ("name", "director_name"), + ("default", "Billy Wilder"), + ("prompt", "What's the Director's full name?"), + ("prompt_user", True), + ("description", "The default director is not proud of their work, we hope you are."), + ("hide_input", False), + ("choices", ['Billy Wilder', 'Allan Smithe', 'Ridley Scott', 'Victor Fleming', 'John Houston', 'John Ford']), + ("validation", "^[a-z][A-Z]+$"), + ("validation_flags", ["verbose", "ascii"]), + ("skip_if", "{{cookiecutter.director_credit == False}}"), + ("type", "string") + ]) + ]) + ]) + } + ) + # Test changing the default value with a value that is not in choices, + # we should see the choice first position get updated. + context_with_valid_extra_8 = ( + { + 'context_file': 'tests/test-generate-context-v2/representative.json', + 'extra_context': [ + { + 'name': 'director_name', + 'default': 'Peter Sellers', + } + ] + }, + { + "representative": OrderedDict([ + ("name", "cc-representative"), + ("cookiecutter_version", "2.0.0"), + ("variables", [ + OrderedDict([ + ("name", "director_credit"), + ("default", True), + ("prompt", "Is there a director credit on this film?"), + ("description", "Directors take credit for most of their films, usually..."), + ("type", "boolean") + ]), + OrderedDict([ + ("name", "director_name"), + ("default", "Peter Sellers"), + ("prompt", "What's the Director's full name?"), + ("prompt_user", True), + ("description", "The default director is not proud of their work, we hope you are."), + ("hide_input", False), + ("choices", ["Peter Sellers", "Allan Smithe", "Ridley Scott", "Victor Fleming", "John Houston"]), + ("validation", "^[a-z][A-Z]+$"), + ("validation_flags", ["verbose", "ascii"]), + ("skip_if", "{{cookiecutter.director_credit == False}}"), + ("type", "string") + ]) + ]) + ]) + } + ) + yield context_with_valid_extra_0 + yield context_with_valid_extra_1 + yield context_with_valid_extra_2 + yield context_with_valid_extra_3 + yield context_with_valid_extra_4 + yield context_with_valid_extra_5 + yield context_with_valid_extra_6 + yield context_with_valid_extra_7 + yield context_with_valid_extra_8 + +@pytest.mark.usefixtures('clean_system') +@pytest.mark.parametrize('input_params, expected_context', gen_context_data_inputs_expected()) +def test_generate_context_with_extra_context_dictionary(input_params, expected_context, monkeypatch): + """ + Test the generated context with extra content overwrite to multiple fields, + with creation of new fields NOT allowed. + """ + assert generate.generate_context(**input_params) == expected_context From cb459d26fda397ac6d01d483896ae2a3c74d589e Mon Sep 17 00:00:00 2001 From: eruber Date: Mon, 30 Oct 2017 17:56:41 -0700 Subject: [PATCH 012/274] Added context to the api list --- docs/cookiecutter.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/cookiecutter.rst b/docs/cookiecutter.rst index 9d39673db..f33fab44f 100644 --- a/docs/cookiecutter.rst +++ b/docs/cookiecutter.rst @@ -20,6 +20,14 @@ cookiecutter.config module :undoc-members: :show-inheritance: +cookiecutter.context module +--------------------------- + +.. automodule:: cookiecutter.context + :members: + :undoc-members: + :show-inheritance: + cookiecutter.environment module ------------------------------- From 8cda801caa44cb8d187ba42a40b5f136e77d64fa Mon Sep 17 00:00:00 2001 From: eruber Date: Mon, 30 Oct 2017 20:38:12 -0700 Subject: [PATCH 013/274] Fixed flake8 E501 --- tests/test_main.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_main.py b/tests/test_main.py index d3e326efa..5c78798a1 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -70,8 +70,10 @@ def test_version_2_load_context_call( mock_replay_dump = mocker.patch('cookiecutter.main.dump') - mock_version_1_prompt_for_config = mocker.patch('cookiecutter.main.prompt_for_config') - mock_version_2_load_context = mocker.patch('cookiecutter.main.load_context') + mock_version_1_prompt_for_config = mocker.patch( + 'cookiecutter.main.prompt_for_config') + mock_version_2_load_context = mocker.patch( + 'cookiecutter.main.load_context') mocker.patch('cookiecutter.main.generate_files') From b320a99e0f1e2621c661d055235c55c989b64105 Mon Sep 17 00:00:00 2001 From: eruber Date: Mon, 30 Oct 2017 20:39:20 -0700 Subject: [PATCH 014/274] temporarily attempt to suppress E501 --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index a34bc3c8a..6e8e0e8d8 100644 --- a/tox.ini +++ b/tox.ini @@ -7,6 +7,7 @@ commands = pytest --cov=cookiecutter {posargs:tests} deps = -rtest_requirements.txt [testenv:flake8] +ignore = E501 deps = flake8==2.3.0 pep8==1.6.2 From 117bf2045824334de0add41556beb59a4f0192f5 Mon Sep 17 00:00:00 2001 From: eruber Date: Mon, 30 Oct 2017 20:41:57 -0700 Subject: [PATCH 015/274] yea, that did not work removing ignore E501 --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index 6e8e0e8d8..a34bc3c8a 100644 --- a/tox.ini +++ b/tox.ini @@ -7,7 +7,6 @@ commands = pytest --cov=cookiecutter {posargs:tests} deps = -rtest_requirements.txt [testenv:flake8] -ignore = E501 deps = flake8==2.3.0 pep8==1.6.2 From 532c0fd322d8142dc178ed7e1eb3947f7175e238 Mon Sep 17 00:00:00 2001 From: eruber Date: Mon, 30 Oct 2017 21:27:38 -0700 Subject: [PATCH 016/274] Fixed tox py3.5 error --- tests/test_context.py | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/tests/test_context.py b/tests/test_context.py index 71fb05608..5d9030bb7 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -498,21 +498,20 @@ def test_variable_str(): validation_flags=['ignorecase'], hide_input=True) - assert str(v) == \ - (':\n' - "name='module_name',\n" - "default='{{cookiecutter.plugin_name|lower|replace('-','_')}}',\n" - "description='None',\n" - "prompt='Please enter a name for your base python module',\n" - "hide_input='True',\n" - "var_type='string',\n" - "skip_if='',\n" - "prompt_user='True',\n" - "choices='[]',\n" - "validation='^[a-z_]+$',\n" - "validation_flag_names='['ignorecase']',\n" - "validation_flags='2',\n" - "validate='re.compile('^[a-z_]+$', re.IGNORECASE)'") + assert ':' in str(v) + assert "name='module_name'" in str(v) + assert "default='{{cookiecutter.plugin_name|lower|replace('-','_')}}'" in str(v) + assert "description='None'" in str(v) + assert "prompt='Please enter a name for your base python module'" in str(v) + assert "hide_input='True'" in str(v) + assert "var_type='string'" in str(v) + assert "skip_if=''" in str(v) + assert "prompt_user='True'" in str(v) + assert "choices='[]'" in str(v) + assert "validation='^[a-z_]+$'" in str(v) + assert "validation_flag_names='['ignorecase']'" in str(v) + assert "validation_flags='2'" in str(v) + assert "validate='re.compile('^[a-z_]+$', re.IGNORECASE)'" in str(v) def test_variable_option_raise_invalid_type_value_error(): From 71182626f4c912a5f67f47f17c12e203368f48ca Mon Sep 17 00:00:00 2001 From: eruber Date: Mon, 30 Oct 2017 21:28:05 -0700 Subject: [PATCH 017/274] More Flake8 nonsense --- tests/test_main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_main.py b/tests/test_main.py index 5c78798a1..38e5dab61 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -71,9 +71,9 @@ def test_version_2_load_context_call( mock_replay_dump = mocker.patch('cookiecutter.main.dump') mock_version_1_prompt_for_config = mocker.patch( - 'cookiecutter.main.prompt_for_config') + 'cookiecutter.main.prompt_for_config') mock_version_2_load_context = mocker.patch( - 'cookiecutter.main.load_context') + 'cookiecutter.main.load_context') mocker.patch('cookiecutter.main.generate_files') From 21a37d37b1cc4f661fb6bb2bb2941aa1e31ae423 Mon Sep 17 00:00:00 2001 From: eruber Date: Mon, 30 Oct 2017 21:51:00 -0700 Subject: [PATCH 018/274] tox py3.4 error fixed, json.decoder.JSONDecodeError exception is ValueError in py3.4 --- cookiecutter/context.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cookiecutter/context.py b/cookiecutter/context.py index 6a2a1deda..4f6ac72c1 100644 --- a/cookiecutter/context.py +++ b/cookiecutter/context.py @@ -126,7 +126,11 @@ def process_json(user_value): user_value, object_pairs_hook=collections.OrderedDict, ) + except ValueError: + # ValueError raised in Python 3.4 + raise click.UsageError('Unable to decode to JSON.') except json.decoder.JSONDecodeError: + # json.decoder.JSONDecodeError raised in Python 3.5, 3.6 # Leave it up to click to ask the user again. # Local function procsse_json() is called by click within a # try block that catches click.UsageError exception's and asks From 3b995007d73209fc2d30a87aeb34decca28efa90 Mon Sep 17 00:00:00 2001 From: eruber Date: Mon, 30 Oct 2017 21:56:19 -0700 Subject: [PATCH 019/274] tox py3.4 json.decoder.JSONDecodeError inherits from ValueError so we use it for portability --- cookiecutter/context.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cookiecutter/context.py b/cookiecutter/context.py index 4f6ac72c1..a3390df63 100644 --- a/cookiecutter/context.py +++ b/cookiecutter/context.py @@ -127,10 +127,9 @@ def process_json(user_value): object_pairs_hook=collections.OrderedDict, ) except ValueError: - # ValueError raised in Python 3.4 - raise click.UsageError('Unable to decode to JSON.') - except json.decoder.JSONDecodeError: # json.decoder.JSONDecodeError raised in Python 3.5, 3.6 + # but it inherits from ValueError which is raised in Python 3.4 + # --------------------------------------------------------------- # Leave it up to click to ask the user again. # Local function procsse_json() is called by click within a # try block that catches click.UsageError exception's and asks From 46415e1d01bc3fc412c639dd8fcb58f538831879 Mon Sep 17 00:00:00 2001 From: eruber Date: Mon, 30 Oct 2017 22:16:04 -0700 Subject: [PATCH 020/274] Fixed py3.3 error --- tests/test_context.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/test_context.py b/tests/test_context.py index 5d9030bb7..463248c0a 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -8,6 +8,7 @@ """ from __future__ import unicode_literals +import sys import os.path import time import json @@ -511,7 +512,11 @@ def test_variable_str(): assert "validation='^[a-z_]+$'" in str(v) assert "validation_flag_names='['ignorecase']'" in str(v) assert "validation_flags='2'" in str(v) - assert "validate='re.compile('^[a-z_]+$', re.IGNORECASE)'" in str(v) + + if sys.version_info >= (3, 4): + assert "validate='re.compile('^[a-z_]+$', re.IGNORECASE)'" in str(v) + else: + assert "validate='<_sre.SRE_Pattern object at" in str(v) def test_variable_option_raise_invalid_type_value_error(): From 765f918a40d70914b74f3d07280df16dcee13545 Mon Sep 17 00:00:00 2001 From: eruber Date: Mon, 30 Oct 2017 23:56:24 -0700 Subject: [PATCH 021/274] edits for line length flake8 E501 --- cookiecutter/context.py | 14 ++++++++++---- tests/test_generate_context_v2.py | 7 +++++-- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/cookiecutter/context.py b/cookiecutter/context.py index a3390df63..3671adbcd 100644 --- a/cookiecutter/context.py +++ b/cookiecutter/context.py @@ -64,7 +64,8 @@ def context_is_version_2(cookiecutter_context): # This really is not sufficient since a v1 context could define each of # these fields; perhaps a more thorough test would be to also check if the # 'variables' field was defined as a list of OrderedDict items. - if (cookiecutter_context.keys() & SET_OF_REQUIRED_FIELDS) == SET_OF_REQUIRED_FIELDS: + if (cookiecutter_context.keys() & + SET_OF_REQUIRED_FIELDS) == SET_OF_REQUIRED_FIELDS: return True else: return False @@ -246,8 +247,10 @@ class Variable(object): def __init__(self, name, default, **info): """ - :param name: A string containing the variable's name in the jinja2 context. - :param default: The variable's default value. Can any type defined below. + :param name: A string containing the variable's name in the jinja2 + context. + :param default: The variable's default value. Can any type defined + below. :param kwargs info: Keyword/Argument pairs recognized are shown below. Recognized Keyword/Arguments, but optional: @@ -303,7 +306,10 @@ def __init__(self, name, default, **info): self.description = self.check_type('description', None, str) - self.prompt = self.check_type('prompt', DEFAULT_PROMPT.format(variable=self), str) + self.prompt = self.check_type( + 'prompt', + DEFAULT_PROMPT.format(variable=self), + str) self.hide_input = self.check_type('hide_input', False, bool) diff --git a/tests/test_generate_context_v2.py b/tests/test_generate_context_v2.py index 9d6dc09c9..11c44f0c0 100644 --- a/tests/test_generate_context_v2.py +++ b/tests/test_generate_context_v2.py @@ -663,9 +663,12 @@ def gen_context_data_inputs_expected(): yield context_with_valid_extra_7 yield context_with_valid_extra_8 + @pytest.mark.usefixtures('clean_system') -@pytest.mark.parametrize('input_params, expected_context', gen_context_data_inputs_expected()) -def test_generate_context_with_extra_context_dictionary(input_params, expected_context, monkeypatch): +@pytest.mark.parametrize('input_params, expected_context', + gen_context_data_inputs_expected()) +def test_generate_context_with_extra_context_dictionary( + input_params, expected_context, monkeypatch): """ Test the generated context with extra content overwrite to multiple fields, with creation of new fields NOT allowed. From e86da0d63407fd89ae66fa1b01b24eb700d4380a Mon Sep 17 00:00:00 2001 From: eruber Date: Tue, 31 Oct 2017 00:16:19 -0700 Subject: [PATCH 022/274] More Flake8 E501 line length issues --- cookiecutter/context.py | 2 +- cookiecutter/generate.py | 105 +++++++++++++++++++++++++++++---------- tests/test_context.py | 2 +- 3 files changed, 82 insertions(+), 27 deletions(-) diff --git a/cookiecutter/context.py b/cookiecutter/context.py index 3671adbcd..b3230746a 100644 --- a/cookiecutter/context.py +++ b/cookiecutter/context.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- - +# flake8: noqa """ cookiecutter.context -------------------- diff --git a/cookiecutter/generate.py b/cookiecutter/generate.py index d6968bf91..bf0dfbd18 100644 --- a/cookiecutter/generate.py +++ b/cookiecutter/generate.py @@ -75,10 +75,13 @@ def apply_overwrites_to_context(context, overwrite_context): def apply_default_overwrites_to_context2(context, overwrite_default_context): - """Modify the given version 2 context in place based on the overwrite_default_context.""" + """ + Modify the given version 2 context in place based on the + overwrite_default_context. + """ for variable, overwrite in overwrite_default_context.items(): - var_dict = next((d for d in context['variables'] if d['name'] == variable), None) + var_dict = next((d for d in context['variables'] if d['name'] == variable), None) # noqa if var_dict: if 'choices' in var_dict.keys(): context_value = var_dict['choices'] @@ -89,7 +92,7 @@ def apply_default_overwrites_to_context2(context, overwrite_default_context): # We are dealing with a choice variable if overwrite in context_value: # This overwrite is actually valid for the given context - # Let's set it as default (by definition first item in list) + # Let's set it as default (by definition 1st item in list) # see ``cookiecutter.prompt.prompt_choice_for_config`` context_value.remove(overwrite) context_value.insert(0, overwrite) @@ -118,10 +121,58 @@ def apply_extra_overwrites_to_context2(context, extra_context): If extra_context is a list of dictionaries, apply the overwrite from each dictionary to it's matching variable's dictionary. This allows all fields of a variable to be updated. A match considers the variable's 'name' field - only; any name fields in the extra_context list of dictionaries that do not - match a variable 'name' field, are ignored. Any key/value pairs specified in - an extra_content dictionary that are not already defined by the matching - variable dictionary will raise a ValueError. + only; any name fields in the extra_context list of dictionaries that do + not match a variable 'name' field, are ignored. Any key/value pairs + specified in an extra_content dictionary that are not already defined by + the matching variable dictionary will raise a ValueError. + + Changing the 'name' Field + ------------------------- + Changing the 'name' field requires a special syntax. Because the algorithm + chosen to find a variable’s dictionary entry in the variables list of + OrderDicts uses the variable’s ‘name’ field; it could not be used to + simultaneously hold a new ‘name’ field value. Therefore the following + extra context dictionary entry snytax was introduced to allow the ‘name’ + field of a variable to be changed: + + { + 'name': 'CURRENT_VARIABLE_NAME::NEW_VARIABLE_NAME', + } + + So, for example, to change a variable’s ‘name’ field from + ‘director_credit’ to ‘producer_credit’, would require: + + { + 'name': 'director_credit::producer_credit', + } + + Removing a Field from a Variable + -------------------------------- + It is possible that a previous extra context overwrite requires that a + subsequent variable entry be removed. + + In order to accomplish this a remove field token is used in the extra + context as follows: + + { + 'name': 'director_cut', + 'skip_if': '<>', + } + + In the example above, the extra context overwrite results in the variable + named ‘director_cut’ having it’s ‘skip_if’ field removed. + + Overwrite Considerations Regarding ‘default’ & ‘choices’ Fields + --------------------------------------------------------------- + When a variable is defined that has both the ‘default’ and the ‘choices’ + fields, these two fields influence each other. If one of these fields is + updated, but not the other field, then the other field will be + automatically updated by the overwrite logic. + + If both fields are updated, then the ‘default’ value will be moved to the + first location of the ‘choices’ field if it exists elsewhere in the list; + if the default value is not in the list, it will be added to the first + location in the choices list. """ if isinstance(extra_context, dict): @@ -130,19 +181,21 @@ def apply_extra_overwrites_to_context2(context, extra_context): for xtra_ctx_item in extra_context: if isinstance(xtra_ctx_item, dict): if 'name' in xtra_ctx_item.keys(): - # xtra_ctx_item['name'] may have a replace value of the form: - # 'name_value::replace_name_value' + # xtra_ctx_item['name'] may have a replace value of the + # form: + # 'name_value::replace_name_value' xtra_ctx_name = xtra_ctx_item['name'].split('::')[0] try: replace_name = xtra_ctx_item['name'].split('::')[1] except IndexError: replace_name = None - var_dict = next((d for d in context['variables'] if d['name'] == xtra_ctx_name), None) + var_dict = next((d for d in context['variables'] if d['name'] == xtra_ctx_name), None) # noqa if var_dict: - # Since creation of new key/value pairs is NOT desired, we only use a key - # that is common to both variables context and the extra context - common_keys = [key for key in xtra_ctx_item.keys() if key in var_dict.keys()] + # Since creation of new key/value pairs is NOT + # desired, we only use a key that is common to both + # the variables context and the extra context. + common_keys = [key for key in xtra_ctx_item.keys() if key in var_dict.keys()] # noqa for key in common_keys: if xtra_ctx_item[key] == '<>': var_dict.pop(key, None) @@ -150,32 +203,34 @@ def apply_extra_overwrites_to_context2(context, extra_context): # normal field update var_dict[key] = xtra_ctx_item[key] - # After all fields have been update, there is some house-keeping to do. - # The default/choices house-keeping could effecively be no-ops if the + # After all fields have been updated, there is some + # house-keeping to do. The default/choices + # house-keeping could effecively be no-ops if the # user did the correct thing. - if ('default' in common_keys) & ('choices' in var_dict.keys()): + if ('default' in common_keys) & ('choices' in var_dict.keys()): # noqa # default updated, regardless if choices has been updated, # re-order choices based on default if var_dict['default'] in var_dict['choices']: - var_dict['choices'].remove(var_dict['default']) + var_dict['choices'].remove(var_dict['default']) # noqa var_dict['choices'].insert(0, var_dict['default']) - if ('default' not in common_keys) & ('choices' in common_keys): - # choices updated, so update default based on first location in choices + if ('default' not in common_keys) & ('choices' in common_keys): # noqa + # choices updated, so update default based on + # first location in choices var_dict['default'] = var_dict['choices'][0] if replace_name: var_dict['name'] = replace_name else: - msg = "No variable found in context whose name matches extra context name '{name}'" + msg = "No variable found in context whose name matches extra context name '{name}'" # noqa raise ValueError(msg.format(name=xtra_ctx_name)) else: - msg = "Extra context dictionary item {item} is missing a 'name' key." + msg = "Extra context dictionary item {item} is missing a 'name' key." # noqa raise ValueError(msg.format(item=xtra_ctx_item)) else: - msg = "Extra context list item '{item}' is of type {t}, should be a dictionary." - raise ValueError(msg.format(item=str(xtra_ctx_item), t=type(xtra_ctx_item).__name__)) + msg = "Extra context list item '{item}' is of type {t}, should be a dictionary." # noqa + raise ValueError(msg.format(item=str(xtra_ctx_item), t=type(xtra_ctx_item).__name__)) # noqa else: msg = "Extra context must be a dictionary or a list of dictionaries!" raise ValueError(msg) @@ -446,8 +501,8 @@ def generate_files(repo_dir, context=None, output_dir='.', ) shutil.copytree(indir, outdir) - # We mutate ``dirs``, because we only want to go through these dirs - # recursively + # We mutate ``dirs``, because we only want to go through these + # dirs recursively dirs[:] = render_dirs for d in dirs: unrendered_dir = os.path.join(project_dir, root, d) diff --git a/tests/test_context.py b/tests/test_context.py index 463248c0a..09382025f 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- - +# flake8: noqa """ test_context ------------ From 827c3af73c9671301d659bb995e68a1e30017a77 Mon Sep 17 00:00:00 2001 From: eruber Date: Tue, 31 Oct 2017 00:18:54 -0700 Subject: [PATCH 023/274] Flake8 E501 long line length issues --- cookiecutter/generate.py | 4 ++-- tests/test_generate_context_v2.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cookiecutter/generate.py b/cookiecutter/generate.py index bf0dfbd18..ea6c9292e 100644 --- a/cookiecutter/generate.py +++ b/cookiecutter/generate.py @@ -208,8 +208,8 @@ def apply_extra_overwrites_to_context2(context, extra_context): # house-keeping could effecively be no-ops if the # user did the correct thing. if ('default' in common_keys) & ('choices' in var_dict.keys()): # noqa - # default updated, regardless if choices has been updated, - # re-order choices based on default + # default updated, regardless if choices has been + # updated, re-order choices based on default if var_dict['default'] in var_dict['choices']: var_dict['choices'].remove(var_dict['default']) # noqa diff --git a/tests/test_generate_context_v2.py b/tests/test_generate_context_v2.py index 11c44f0c0..1d211ea67 100644 --- a/tests/test_generate_context_v2.py +++ b/tests/test_generate_context_v2.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- - +# flake8: noqa """ test_generate_convext_v2 ------------------------ From 4e19fd217994593c7da0c2dda45e4f28905d963e Mon Sep 17 00:00:00 2001 From: eruber Date: Tue, 31 Oct 2017 08:32:16 -0700 Subject: [PATCH 024/274] Rename context v2 overwrite function to enhance clarification, mirrored v1 overwrite call structure. --- cookiecutter/generate.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cookiecutter/generate.py b/cookiecutter/generate.py index ea6c9292e..e5f64ad45 100644 --- a/cookiecutter/generate.py +++ b/cookiecutter/generate.py @@ -102,7 +102,7 @@ def apply_default_overwrites_to_context2(context, overwrite_default_context): var_dict['default'] = overwrite -def apply_extra_overwrites_to_context2(context, extra_context): +def apply_overwrites_to_context_v2(context, extra_context): """ Modify the given version 2 context in place based on extra_context. @@ -273,9 +273,9 @@ def generate_context(context_file='cookiecutter.json', default_context=None, logger.debug("Context is version 2") if default_context: - apply_default_overwrites_to_context2(obj, default_context) + apply_overwrites_to_context_v2(obj, default_context) if extra_context: - apply_extra_overwrites_to_context2(obj, extra_context) + apply_overwrites_to_context_v2(obj, extra_context) else: logger.debug("Context is version 1") From df6310c3a32b201410c72beb061b82633aeb3eac Mon Sep 17 00:00:00 2001 From: eruber Date: Tue, 31 Oct 2017 20:01:51 -0700 Subject: [PATCH 025/274] Force no user prompt for private variables (named with a leading underscore) --- cookiecutter/context.py | 27 +++++++++++++++++++-------- tests/test_context.py | 15 ++++++++++++++- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/cookiecutter/context.py b/cookiecutter/context.py index b3230746a..7b175b991 100644 --- a/cookiecutter/context.py +++ b/cookiecutter/context.py @@ -14,6 +14,7 @@ Based on the source code written by @hackebrot see: https://github.com/audreyr/cookiecutter/pull/848 +https://github.com/hackebrot/cookiecutter/tree/new-context-format """ @@ -304,15 +305,16 @@ def __init__(self, name, default, **info): # optional fields self.info = info + # -- DESCRIPTION ----------------------------------------------------- self.description = self.check_type('description', None, str) - self.prompt = self.check_type( - 'prompt', - DEFAULT_PROMPT.format(variable=self), - str) + # -- PROMPT ---------------------------------------------------------- + self.prompt = self.check_type('prompt', DEFAULT_PROMPT.format(variable=self), str) + # -- HIDE_INPUT ------------------------------------------------------ self.hide_input = self.check_type('hide_input', False, bool) + # -- TYPE ------------------------------------------------------------ self.var_type = info.get('type', 'string') if self.var_type not in VALID_TYPES: msg = 'Variable: {var_name} has an invalid type {var_type}. Valid types are: {types}' @@ -320,21 +322,29 @@ def __init__(self, name, default, **info): var_name=self.name, types=VALID_TYPES)) + # -- SKIP_IF --------------------------------------------------------- self.skip_if = self.check_type('skip_if', '', str) + # -- PROMPT_USER ----------------------------------------------------- self.prompt_user = self.check_type('prompt_user', True, bool) + # do not prompt for private variable names (beginning with _) + if self.name.startswith('_'): + self.prompt_user = False + # -- CHOICES --------------------------------------------------------- # choices are somewhat special as they can be of every type self.choices = self.check_type('choices', [], list) if self.choices and default not in self.choices: msg = "Variable: {var_name} has an invalid default value {default} for choices: {choices}." raise ValueError(msg.format(var_name=self.name, default=self.default, choices=self.choices)) + # -- VALIDATION STARTS ----------------------------------------------- self.validation = self.check_type('validation', None, str) self.validation_flag_names = self.check_type('validation_flags', [], list) self.validation_flags = 0 + for vflag in self.validation_flag_names: if vflag in REGEX_COMPILE_FLAGS.keys(): self.validation_flags |= REGEX_COMPILE_FLAGS[vflag] @@ -353,6 +363,7 @@ def __init__(self, name, default, **info): msg = "Variable: {var_name} - Validation Setup Error: Invalid RegEx '{value}' - does not compile - {err}" raise ValueError(msg.format(var_name=self.name, value=self.validation, err=e)) + # -- VALIDATION ENDS ------------------------------------------------- def __repr__(self): return "<{class_name} {variable_name}>".format( @@ -388,9 +399,9 @@ def __init__(self, name, cookiecutter_version, variables, **info): """ Mandatorty Parameters - :param name: The cookiecutter template name - :param cookiecutter_version: The version of the cookiecutter application - that is compatible with this template. + :param name: A string, the cookiecutter template name + :param cookiecutter_version: A string containing the version of the + cookiecutter application that is compatible with this template. :param variables: A list of OrderedDict items that describe each variable in the template. These variables are essentially what is found in the version 1 cookiecutter.json file. @@ -398,7 +409,7 @@ def __init__(self, name, cookiecutter_version, variables, **info): Optional Parameters (via \**info) :param authors: An array of string - maintainers of the template. - :param description: A human readable description of the template. + :param description: A string, human readable description of template. :param keywords: An array of string - similar to PyPI keywords. :param license: A string identifying the license of the template code. :param url: A string containing the URL for the template project. diff --git a/tests/test_context.py b/tests/test_context.py index 09382025f..1ded038ab 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -474,6 +474,20 @@ def test_variable_validation_compile_exception(): assert "Variable: {var_name} - Validation Setup Error: Invalid RegEx '{value}' - does not compile - ".format(var_name=VAR_NAME, value=BAD_REGEX_STRING) in str(excinfo.value) +def test_variable_forces_no_prompt_for_private_variable_names(): + v = context.Variable( + '_private_variable_name', + "{{cookiecutter.plugin_name|lower|replace('-','_')}}", + prompt="Please enter a name for your base python module", + prompt_user=True, + type='string', + validation='^[a-z_]+$', + validation_flags=['ignorecase'], + hide_input=True) + + assert v.prompt_user == False + + def test_variable_repr(): v = context.Variable( @@ -547,7 +561,6 @@ def test_cookiecutter_template_repr(): assert repr(cct) == "" -# ############################################################################ def test_load_context_with_input_chioces(mocker): cc = load_cookiecutter('tests/test-context/cookiecutter_choices.json') From a67ccdd361785ce35cd97741d4212232abcfdb9b Mon Sep 17 00:00:00 2001 From: eruber Date: Fri, 3 Nov 2017 21:06:05 -0700 Subject: [PATCH 026/274] clarified function name to be more obvious it was a version 2 call --- cookiecutter/generate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cookiecutter/generate.py b/cookiecutter/generate.py index e5f64ad45..d4a034efb 100644 --- a/cookiecutter/generate.py +++ b/cookiecutter/generate.py @@ -74,7 +74,7 @@ def apply_overwrites_to_context(context, overwrite_context): context[variable] = overwrite -def apply_default_overwrites_to_context2(context, overwrite_default_context): +def apply_default_overwrites_to_context_v2(context, overwrite_default_context): """ Modify the given version 2 context in place based on the overwrite_default_context. @@ -176,7 +176,7 @@ def apply_overwrites_to_context_v2(context, extra_context): """ if isinstance(extra_context, dict): - apply_default_overwrites_to_context2(context, extra_context) + apply_default_overwrites_to_context_v2(context, extra_context) elif isinstance(extra_context, list): for xtra_ctx_item in extra_context: if isinstance(xtra_ctx_item, dict): From 492293a97488c6ce1ce517bc2985abced430efeb Mon Sep 17 00:00:00 2001 From: eruber Date: Fri, 3 Nov 2017 22:50:39 -0700 Subject: [PATCH 027/274] version number edit --- cookiecutter/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cookiecutter/__init__.py b/cookiecutter/__init__.py index 5ada8ad8c..ab2a0f901 100644 --- a/cookiecutter/__init__.py +++ b/cookiecutter/__init__.py @@ -7,4 +7,4 @@ Main package for Cookiecutter. """ -__version__ = '2.0.0-alpha.u+1.6.0' +__version__ = '2.0.0-alpha+1.6.0' From 2984aa50e9a3ea8b2e6ebbc783a8204796cbc149 Mon Sep 17 00:00:00 2001 From: eruber Date: Sun, 5 Nov 2017 08:18:38 -0700 Subject: [PATCH 028/274] Over-written variable names need to be resolved if original name is used by other variable fields --- cookiecutter/generate.py | 33 ++++++++++++ .../representative_2B.json | 26 +++++++++ tests/test_generate_context_v2.py | 54 +++++++++++++++++-- 3 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 tests/test-generate-context-v2/representative_2B.json diff --git a/cookiecutter/generate.py b/cookiecutter/generate.py index d4a034efb..ccd2eaff3 100644 --- a/cookiecutter/generate.py +++ b/cookiecutter/generate.py @@ -102,6 +102,31 @@ def apply_default_overwrites_to_context_v2(context, overwrite_default_context): var_dict['default'] = overwrite +def resolve_changed_variable_names(context, variables_to_resolve): + """ + The variable names contained in the variables_to_resolve dictionary's + key names have been over-written with keys' value. Check the entire + context and update any other variable context fields that may still + reference the original variable name. + """ + for var_name_to_resolve in variables_to_resolve: + + new_var_name = variables_to_resolve[var_name_to_resolve] + + for variable in context['variables']: + for field_name in variable.keys(): + if isinstance(variable[field_name], str): + if var_name_to_resolve in variable[field_name]: + variable[field_name] = variable[field_name].replace(var_name_to_resolve, new_var_name) # noqa + + elif isinstance(variable[field_name], list): + # a choices field could have an str item to update + for i, item in enumerate(variable[field_name]): + if isinstance(item, str): + if var_name_to_resolve in item: + variable[field_name][i] = item.replace(var_name_to_resolve, new_var_name) # noqa + + def apply_overwrites_to_context_v2(context, extra_context): """ Modify the given version 2 context in place based on extra_context. @@ -175,6 +200,7 @@ def apply_overwrites_to_context_v2(context, extra_context): location in the choices list. """ + variable_names_to_resolve = {} if isinstance(extra_context, dict): apply_default_overwrites_to_context_v2(context, extra_context) elif isinstance(extra_context, list): @@ -221,6 +247,7 @@ def apply_overwrites_to_context_v2(context, extra_context): var_dict['default'] = var_dict['choices'][0] if replace_name: + variable_names_to_resolve[xtra_ctx_name] = replace_name # noqa var_dict['name'] = replace_name else: msg = "No variable found in context whose name matches extra context name '{name}'" # noqa @@ -231,6 +258,12 @@ def apply_overwrites_to_context_v2(context, extra_context): else: msg = "Extra context list item '{item}' is of type {t}, should be a dictionary." # noqa raise ValueError(msg.format(item=str(xtra_ctx_item), t=type(xtra_ctx_item).__name__)) # noqa + + if variable_names_to_resolve: + # At least one variable name has been over-written, if any + # variables use the original name, they must get updated as well + resolve_changed_variable_names(context, variable_names_to_resolve) + else: msg = "Extra context must be a dictionary or a list of dictionaries!" raise ValueError(msg) diff --git a/tests/test-generate-context-v2/representative_2B.json b/tests/test-generate-context-v2/representative_2B.json new file mode 100644 index 000000000..2eb0fb2fa --- /dev/null +++ b/tests/test-generate-context-v2/representative_2B.json @@ -0,0 +1,26 @@ +{ + "name": "cc-representative", + "cookiecutter_version": "2.0.0", + "variables" : [ + { + "name": "director_credit", + "default": true, + "prompt": "Is there a director credit on this film?", + "description": "Directors take credit for most of their films, usually...", + "type": "boolean" + }, + { + "name": "director_name", + "default": "Allan Smithe", + "prompt": "What's the Director's full name?", + "prompt_user": true, + "description": "The default director is not proud of their work, we hope you are.", + "hide_input": false, + "choices": ["Allan Smithe", "Ridley Scott", "Victor Fleming", "John Houston", "{{cookiecutter.director_credit}}"], + "validation": "^[a-z][A-Z]+$", + "validation_flags": ["verbose", "ascii"], + "skip_if": "{{cookiecutter.director_credit == False}}", + "type": "string" + } + ] +} diff --git a/tests/test_generate_context_v2.py b/tests/test_generate_context_v2.py index 1d211ea67..0e562e92d 100644 --- a/tests/test_generate_context_v2.py +++ b/tests/test_generate_context_v2.py @@ -354,9 +354,9 @@ def gen_context_data_inputs_expected(): } ) - # Test the ability to change the variable's name field since it is used to - # identify the variable to modifed with extra context and to remove a - # key from the context via the removal token: '<>' + # Test the ability to change the variable's name field (since it is used + # to identify the variable to be modifed) with extra context and to remove + # a key from the context via the removal token: '<>' context_with_valid_extra_2 = ( { 'context_file': 'tests/test-generate-context-v2/representative.json', @@ -401,6 +401,51 @@ def gen_context_data_inputs_expected(): ]) } ) + # Test the ability to change the variable's name field (since it is used + # to identify the variable to be modifed) with extra context and to also + # test that any other references in other variables that might use the + # original variable name get updated as well. + context_with_valid_extra_2_B = ( + { + 'context_file': 'tests/test-generate-context-v2/representative_2B.json', + 'extra_context': [ + { + 'name': 'director_credit::producer_credit', + 'prompt': 'Is there a producer credit on this film?', + 'description': 'There are usually a lot of producers...', + }, + ] + }, + { + "representative_2B": OrderedDict([ + ("name", "cc-representative"), + ("cookiecutter_version", "2.0.0"), + ("variables", [ + OrderedDict([ + ("name", "producer_credit"), + ("default", True), + ("prompt", "Is there a producer credit on this film?"), + ("description", "There are usually a lot of producers..."), + ("type", "boolean") + ]), + OrderedDict([ + ("name", "director_name"), + ("default", "Allan Smithe"), + ("prompt", "What's the Director's full name?"), + ("prompt_user", True), + ("description", "The default director is not proud of their work, we hope you are."), + ("hide_input", False), + ("choices", ["Allan Smithe", "Ridley Scott", "Victor Fleming", "John Houston", "{{cookiecutter.producer_credit}}"]), + ("validation", "^[a-z][A-Z]+$"), + ("validation_flags", ["verbose", "ascii"]), + ("skip_if", "{{cookiecutter.producer_credit == False}}"), + ("type", "string") + ]) + ]) + ]) + } + ) + # Test changing variable's name field value, default field, prompt field, # and changing the type context_with_valid_extra_3 = ( @@ -438,7 +483,7 @@ def gen_context_data_inputs_expected(): ("choices", ["Allan Smithe", "Ridley Scott", "Victor Fleming", "John Houston"]), ("validation", "^[a-z][A-Z]+$"), ("validation_flags", ["verbose", "ascii"]), - ("skip_if", "{{cookiecutter.director_credit == False}}"), + ("skip_if", "{{cookiecutter.producer_credits == False}}"), ("type", "string") ]) ]) @@ -656,6 +701,7 @@ def gen_context_data_inputs_expected(): yield context_with_valid_extra_0 yield context_with_valid_extra_1 yield context_with_valid_extra_2 + yield context_with_valid_extra_2_B yield context_with_valid_extra_3 yield context_with_valid_extra_4 yield context_with_valid_extra_5 From 4443f525a5cc56f10e2bf03bcd20271a8e88a68a Mon Sep 17 00:00:00 2001 From: eruber Date: Sat, 11 Nov 2017 12:45:26 -0700 Subject: [PATCH 029/274] Added do_if, if_no_skip_to, and if_yes_skip_to to load_context() with tests. --- cookiecutter/context.py | 94 +++++++++++++++----- tests/test-context/cookiecutter_skips_1.json | 88 ++++++++++++++++++ tests/test-context/cookiecutter_skips_2.json | 88 ++++++++++++++++++ tests/test-context/cookiecutter_skips_3.json | 69 ++++++++++++++ tests/test_context.py | 78 +++++++++++++++- 5 files changed, 390 insertions(+), 27 deletions(-) create mode 100644 tests/test-context/cookiecutter_skips_1.json create mode 100644 tests/test-context/cookiecutter_skips_2.json create mode 100644 tests/test-context/cookiecutter_skips_3.json diff --git a/cookiecutter/context.py b/cookiecutter/context.py index 7b175b991..941de036b 100644 --- a/cookiecutter/context.py +++ b/cookiecutter/context.py @@ -158,13 +158,28 @@ def prompt_yes_no(variable, default): else: default_display = 'n' - return click.prompt( + # click.prompt() behavior: + # When supplied with a string default, the string default is returned, + # rather than the string converted to a click.BOOL. + # If default is passed as a boolean then the default is displayed as + # [True] or [False], rather than [y] or [n]. + # This prompt translates y, yes, Yes, YES, n, no, No, NO to their correct + # boolean values, its just that it does not translate a string default + # value of y, yes, Yes, YES, n, no, No, NO to a boolean... + value = click.prompt( variable.prompt, default=default_display, hide_input=variable.hide_input, type=click.BOOL, ) + # ...so if we get the displayed default value back (its a string), + # change it to its associated boolean value + if value == default_display: + value = default + + return value + def prompt_choice(variable, default): """Returns prompt, default and callback for a choice variable""" @@ -325,6 +340,15 @@ def __init__(self, name, default, **info): # -- SKIP_IF --------------------------------------------------------- self.skip_if = self.check_type('skip_if', '', str) + # -- DO_IF --------------------------------------------------------- + self.do_if = self.check_type('do_if', '', str) + + # -- IF_YES_SKIP_TO --------------------------------------------------------- + self.if_yes_skip_to = self.check_type('if_yes_skip_to', None, str) + + # -- IF_NO_SKIP_TO --------------------------------------------------------- + self.if_no_skip_to = self.check_type('if_no_skip_to', None, str) + # -- PROMPT_USER ----------------------------------------------------- self.prompt_user = self.check_type('prompt_user', True, bool) # do not prompt for private variable names (beginning with _) @@ -455,12 +479,26 @@ def load_context(json_object, no_input=False, verbose=True): env = Environment(extensions=['jinja2_time.TimeExtension']) context = collections.OrderedDict({}) + skip_to_variable_name = None + for variable in CookiecutterTemplate(**json_object): + if skip_to_variable_name: + if variable.name == skip_to_variable_name: + skip_to_variable_name = None + else: + print("Skip variable: ", variable.name) + continue + if variable.skip_if: skip_template = env.from_string(variable.skip_if) if skip_template.render(cookiecutter=context) == 'True': continue + if variable.do_if: + do_template = env.from_string(variable.do_if) + if do_template.render(cookiecutter=context) == 'False': + continue + default = variable.default if isinstance(default, str): @@ -471,32 +509,42 @@ def load_context(json_object, no_input=False, verbose=True): if no_input or (not variable.prompt_user): context[variable.name] = deserialize(default) - continue - - if variable.choices: - prompt = prompt_choice else: - prompt = PROMPTS[variable.var_type] + if variable.choices: + prompt = prompt_choice + else: + prompt = PROMPTS[variable.var_type] + + if verbose and variable.description: + click.echo(variable.description) + + while True: + value = prompt(variable, default) + if variable.validate: + if variable.validate.match(value): + break + else: + msg = "Input validation failure against regex: '{val_string}', try again!".format(val_string=variable.validation) + click.echo(msg) + else: + # no validation defined + break - if verbose and variable.description: - click.echo(variable.description) + if verbose: + width, _ = click.get_terminal_size() + click.echo('-' * width) - while True: - value = prompt(variable, default) - if variable.validate: - if variable.validate.match(value): - break - else: - msg = "Input validation failure against regex: '{val_string}', try again!".format(val_string=variable.validation) - click.echo(msg) - else: - # no validation defined - break + context[variable.name] = deserialize(value) + + if variable.if_yes_skip_to and context[variable.name] is True: + skip_to_variable_name = variable.if_yes_skip_to + print("skip_to_variable_name: ", skip_to_variable_name) - if verbose: - width, _ = click.get_terminal_size() - click.echo('-' * width) + if variable.if_no_skip_to and context[variable.name] is False: + skip_to_variable_name = variable.if_no_skip_to + print("skip_to_variable_name: ", skip_to_variable_name) - context[variable.name] = deserialize(value) + if skip_to_variable_name: + logger.warn("Processed all variables, but skip_to_variable_name '{}' was never found.".format(skip_to_variable_name)) return context diff --git a/tests/test-context/cookiecutter_skips_1.json b/tests/test-context/cookiecutter_skips_1.json new file mode 100644 index 000000000..6e78c1c8a --- /dev/null +++ b/tests/test-context/cookiecutter_skips_1.json @@ -0,0 +1,88 @@ +{ + "name": "cookiecutter-testing-skips", + "version": "0.1.0", + "cookiecutter_version": "2.0.0", + "variables": [ + { + "name": "project_configuration_enabled", + "default": false, + "prompt": "Will this project require a configuration file?", + "type": "yes_no", + "if_no_skip_to": "project_uses_existing_logging_facilities" + }, + { + "name": "project_config_format", + "default": "toml", + "prompt": "Select a configuration file format.", + "type": "string", + "choices": [ + "toml", + "yaml", + "json", + "ini" + ] + }, + { + "name": "project_uses_existing_logging_facilities", + "default": true, + "prompt": "Will this project use existing external logging facilities?", + "type": "yes_no", + "if_yes_skip_to": "github_username" + }, + { + "name": "project_logging_enabled", + "default": true, + "prompt": "Will this project provide its own logging facilities?", + "type": "yes_no", + "if_no_skip_to": "github_username" + }, + { + "name": "project_console_logging_enabled", + "default": true, + "prompt": "Will the project's logging facilities include logging to the console?", + "type": "yes_no", + "if_no_skip_to": "project_file_logging_enabled", + "do_if": "{{cookiecutter.project_logging_enabled == True}}" + }, + { + "name": "project_console_logging_level", + "default": "WARN", + "prompt": "Select the minimum logging level to log to the console.", + "type": "string", + "choices": [ + "WARN", + "INFO", + "DEBUG", + "ERROR" + ], + "do_if": "{{cookiecutter.project_logging_enabled == True}}" + }, + { + "name": "project_file_logging_enabled", + "default": true, + "prompt": "Will the project's logging facilities include logging to a file?", + "type": "yes_no", + "if_no_skip_to": "github_username", + "do_if": "{{cookiecutter.project_logging_enabled == True}}" + }, + { + "name": "project_file_logging_level", + "default": "DEBUG", + "prompt": "Select the minimum logging level to log to a file", + "type": "string", + "choices": [ + "DEBUG", + "INFO", + "WARN", + "ERROR" + ], + "do_if": "{{cookiecutter.project_logging_enabled == True}}" + }, + { + "name": "github_username", + "default": "eruber", + "prompt": "Enter your GitHub User Name", + "type": "string" + } + ] +} diff --git a/tests/test-context/cookiecutter_skips_2.json b/tests/test-context/cookiecutter_skips_2.json new file mode 100644 index 000000000..64fb9d208 --- /dev/null +++ b/tests/test-context/cookiecutter_skips_2.json @@ -0,0 +1,88 @@ +{ + "name": "cookiecutter-testing-skips", + "cookiecutter_version": "2.0.0", + "version": "0.1.0", + "variables": [ + { + "name": "project_configuration_enabled", + "default": true, + "prompt": "Will this project require a configuration file?", + "type": "yes_no", + "if_no_skip_to": "project_uses_existing_logging_facilities" + }, + { + "name": "project_config_format", + "default": "toml", + "prompt": "Select a configuration file format.", + "type": "string", + "choices": [ + "toml", + "yaml", + "json", + "ini" + ] + }, + { + "name": "project_uses_existing_logging_facilities", + "default": false, + "prompt": "Will this project use existing external logging facilities?", + "type": "yes_no", + "if_yes_skip_to": "github_username" + }, + { + "name": "project_logging_enabled", + "default": true, + "prompt": "Will this project provide its own logging facilities?", + "type": "yes_no", + "if_no_skip_to": "github_username" + }, + { + "name": "project_console_logging_enabled", + "default": true, + "prompt": "Will the project's logging facilities include logging to the console?", + "type": "yes_no", + "if_no_skip_to": "project_file_logging_enabled", + "do_if": "{{cookiecutter.project_logging_enabled == True}}" + }, + { + "name": "project_console_logging_level", + "default": "WARN", + "prompt": "Select the minimum logging level to log to the console.", + "type": "string", + "choices": [ + "WARN", + "INFO", + "DEBUG", + "ERROR" + ], + "do_if": "{{cookiecutter.project_logging_enabled == True}}" + }, + { + "name": "project_file_logging_enabled", + "default": true, + "prompt": "Will the project's logging facilities include logging to a file?", + "type": "yes_no", + "if_no_skip_to": "github_username", + "do_if": "{{cookiecutter.project_logging_enabled == True}}" + }, + { + "name": "project_file_logging_level", + "default": "DEBUG", + "prompt": "Select the minimum logging level to log to a file", + "type": "string", + "choices": [ + "DEBUG", + "INFO", + "WARN", + "ERROR" + ], + "do_if": "{{cookiecutter.project_logging_enabled == False}}" + }, + { + "name": "github_username", + "default": "eruber", + "prompt": "Enter your GitHub User Name", + "type": "string" + } + ] +} diff --git a/tests/test-context/cookiecutter_skips_3.json b/tests/test-context/cookiecutter_skips_3.json new file mode 100644 index 000000000..37ae8db58 --- /dev/null +++ b/tests/test-context/cookiecutter_skips_3.json @@ -0,0 +1,69 @@ +{ + "name": "cookiecutter-testing-skips", + "cookiecutter_version": "2.0.0", + "version": "0.1.0", + "variables": [ + { + "name": "project_uses_existing_logging_facilities", + "default": false, + "prompt": "Will this project use existing external logging facilities?", + "type": "yes_no", + "if_no_skip_to": "this_variable_name_is_not_in_the_list" + }, + { + "name": "project_logging_enabled", + "default": true, + "prompt": "Will this project provide its own logging facilities?", + "type": "yes_no", + "if_no_skip_to": "github_username" + }, + { + "name": "project_console_logging_enabled", + "default": true, + "prompt": "Will the project's logging facilities include logging to the console?", + "type": "yes_no", + "if_no_skip_to": "project_file_logging_enabled", + "do_if": "{{cookiecutter.project_logging_enabled == True}}" + }, + { + "name": "project_console_logging_level", + "default": "WARN", + "prompt": "Select the minimum logging level to log to the console.", + "type": "string", + "choices": [ + "WARN", + "INFO", + "DEBUG", + "ERROR" + ], + "do_if": "{{cookiecutter.project_logging_enabled == True}}" + }, + { + "name": "project_file_logging_enabled", + "default": true, + "prompt": "Will the project's logging facilities include logging to a file?", + "type": "yes_no", + "if_no_skip_to": "github_username", + "do_if": "{{cookiecutter.project_logging_enabled == True}}" + }, + { + "name": "project_file_logging_level", + "default": "DEBUG", + "prompt": "Select the minimum logging level to log to a file", + "type": "string", + "choices": [ + "DEBUG", + "INFO", + "WARN", + "ERROR" + ], + "do_if": "{{cookiecutter.project_logging_enabled == False}}" + }, + { + "name": "github_username", + "default": "eruber", + "prompt": "Enter your GitHub User Name", + "type": "string" + } + ] +} diff --git a/tests/test_context.py b/tests/test_context.py index 1ded038ab..68203c309 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -125,11 +125,11 @@ def test_load_context_defaults(): assert cc_cfg['plugin_name'] == 'emoji' assert cc_cfg['module_name'] == 'emoji' assert cc_cfg['license'] == 'MIT' - assert cc_cfg['docs'] == False - assert 'docs_tool' not in cc_cfg.keys() + assert cc_cfg['docs'] is False + assert 'docs_tool' not in cc_cfg.keys() # skip_if worked assert cc_cfg['year'] == time.strftime('%Y') assert cc_cfg['incept_year'] == 2017 - assert cc_cfg['released'] == False + assert cc_cfg['released'] is False assert cc_cfg['temperature'] == 77.3 assert cc_cfg['Release-GUID'] == UUID('04f5eaa9ee7345469dccffc538b27194') assert cc_cfg['extensions'] == "['jinja2_time.TimeExtension']" @@ -143,6 +143,76 @@ def test_load_context_defaults(): False)]))]) +@pytest.mark.usefixtures('clean_system') +def test_load_context_defaults_skips_branch(): + """ + Test that if_no_skip_to and if_yes_skip_to actually do branch and + skip variables + """ + cc = load_cookiecutter('tests/test-context/cookiecutter_skips_1.json') + + cc_cfg = context.load_context(cc['cookiecutter_skips_1'], no_input=True) + + assert cc_cfg['project_configuration_enabled'] is False + assert 'project_config_format' not in cc_cfg.keys() # it was skipped + + assert cc_cfg['project_uses_existing_logging_facilities'] is True + assert 'project_logging_enabled' not in cc_cfg.keys() # it was skipped + assert 'project_console_logging_enabled' not in cc_cfg.keys() # it was skipped + assert 'project_console_logging_level' not in cc_cfg.keys() # it was skipped + assert 'project_file_logging_enabled' not in cc_cfg.keys() # it was skipped + assert 'project_file_logging_level' not in cc_cfg.keys() # it was skipped + assert cc_cfg['github_username'] == 'eruber' + + +@pytest.mark.usefixtures('clean_system') +def test_load_context_defaults_skips_no_branch(): + """ + Test that if_no_skip_to and if_yes_skip_to do not branch and do not + skip variables. + """ + cc = load_cookiecutter('tests/test-context/cookiecutter_skips_2.json') + + cc_cfg = context.load_context(cc['cookiecutter_skips_2'], no_input=True) + + assert cc_cfg['project_configuration_enabled'] is True + assert cc_cfg['project_config_format'] == 'toml' # not skipped + + assert cc_cfg['project_uses_existing_logging_facilities'] is False + assert cc_cfg['project_logging_enabled'] is True # not skipped + assert cc_cfg['project_console_logging_enabled'] is True # not skipped + assert cc_cfg['project_console_logging_level'] == 'WARN' # not skipped + assert cc_cfg['project_file_logging_enabled'] is True # not skipped + + assert 'project_file_logging_level' not in cc_cfg.keys() # do_if skipped + + assert cc_cfg['github_username'] == 'eruber' + + +@pytest.mark.usefixtures('clean_system') +def test_load_context_defaults_skips_unknown_variable_name_warning(caplog): + """ + Test that a warning is issued if a variable.name specified in a skip_to + directive is not in the variable list. + """ + cc = load_cookiecutter('tests/test-context/cookiecutter_skips_3.json') + + cc_cfg = context.load_context(cc['cookiecutter_skips_3'], no_input=True) + + assert cc_cfg['project_uses_existing_logging_facilities'] is False + assert 'project_logging_enabled' not in cc_cfg.keys() # it was skipped + assert 'project_console_logging_enabled' not in cc_cfg.keys() # it was skipped + assert 'project_console_logging_level' not in cc_cfg.keys() # it was skipped + assert 'project_file_logging_enabled' not in cc_cfg.keys() # it was skipped + assert 'project_file_logging_level' not in cc_cfg.keys() # it was skipped + assert 'github_username' not in cc_cfg.keys() # it was skipped + + for record in caplog.records: + assert record.levelname == 'WARNING' + + assert "Processed all variables, but skip_to_variable_name 'this_variable_name_is_not_in_the_list' was never found." in caplog.text + + def test_prompt_string(mocker): EXPECTED_VALUE = 'Input String' @@ -485,7 +555,7 @@ def test_variable_forces_no_prompt_for_private_variable_names(): validation_flags=['ignorecase'], hide_input=True) - assert v.prompt_user == False + assert v.prompt_user is False def test_variable_repr(): From a6628e4058681ed4debb014ad5ed6228f14be67f Mon Sep 17 00:00:00 2001 From: eruber Date: Sat, 11 Nov 2017 12:52:18 -0700 Subject: [PATCH 030/274] Removed some debug code --- cookiecutter/context.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/cookiecutter/context.py b/cookiecutter/context.py index 941de036b..7c985fc04 100644 --- a/cookiecutter/context.py +++ b/cookiecutter/context.py @@ -486,7 +486,6 @@ def load_context(json_object, no_input=False, verbose=True): if variable.name == skip_to_variable_name: skip_to_variable_name = None else: - print("Skip variable: ", variable.name) continue if variable.skip_if: @@ -538,11 +537,9 @@ def load_context(json_object, no_input=False, verbose=True): if variable.if_yes_skip_to and context[variable.name] is True: skip_to_variable_name = variable.if_yes_skip_to - print("skip_to_variable_name: ", skip_to_variable_name) if variable.if_no_skip_to and context[variable.name] is False: skip_to_variable_name = variable.if_no_skip_to - print("skip_to_variable_name: ", skip_to_variable_name) if skip_to_variable_name: logger.warn("Processed all variables, but skip_to_variable_name '{}' was never found.".format(skip_to_variable_name)) From fa34583e2f63c3219d05168df229821afcc2ae27 Mon Sep 17 00:00:00 2001 From: eruber Date: Sat, 11 Nov 2017 13:04:23 -0700 Subject: [PATCH 031/274] Added comment regarding coverage.py issue #198 --- cookiecutter/context.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cookiecutter/context.py b/cookiecutter/context.py index 7c985fc04..69a8214fe 100644 --- a/cookiecutter/context.py +++ b/cookiecutter/context.py @@ -486,7 +486,11 @@ def load_context(json_object, no_input=False, verbose=True): if variable.name == skip_to_variable_name: skip_to_variable_name = None else: - continue + # Is executed, but not marked so in coverage report, due to + # CPython's peephole optimizer's optimizations. + # See https://bitbucket.org/ned/coveragepy/issues/198/continue-marked-as-not-covered + # Issue #198 in coverage.py marked WONTFIX + continue # pragma: no cover if variable.skip_if: skip_template = env.from_string(variable.skip_if) From 2f8e62d14fc40b89a20e5fa2e36536830f3de65d Mon Sep 17 00:00:00 2001 From: eruber Date: Tue, 14 Nov 2017 00:07:57 -0700 Subject: [PATCH 032/274] Added validation_msg & tests, plus check that if_yes_skip_to & if_no_skip_to are only specified for variables of type yes_no --- cookiecutter/context.py | 24 ++++++++- .../cookiecutter_val_failure_msg.json | 22 ++++++++ tests/test_context.py | 50 +++++++++++++++++++ 3 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 tests/test-context/cookiecutter_val_failure_msg.json diff --git a/cookiecutter/context.py b/cookiecutter/context.py index 69a8214fe..6818c673e 100644 --- a/cookiecutter/context.py +++ b/cookiecutter/context.py @@ -279,10 +279,20 @@ def __init__(self, name, default, **info): defaults to string. - `skip_if` -- A string of a jinja2 renderable boolean expression, the variable will be skipped if it renders True. + - `do_if` -- A string of a jinja2 renderable boolean expression, + the variable will be processed if it renders True. - `choices` -- A list of choices, may be of mixed types. + - `if_yes_skip_to` -- A string containing a variable name to skip + to if the yes_no value is True (yes). Only has meaning for + variables of type 'yes_no'. + - `if_no_skip_to` -- A string containing a variable name to skip + to if the yes_no value is False (no). Only has meaning for + variables of type 'yes_no'. - `validation` -- A string defining a regex to use to validation user input. Defaults to None. - - `validation_flags` - A list of validation flag names that can be + - `validation_msg` -- A string defining an additional message to + display if the validation check fails. + - `validation_flags` -- A list of validation flag names that can be specified to control the behaviour of the validation check done using the above defined `validation` string. Specifying a flag is equivalent to setting it to True, @@ -345,9 +355,17 @@ def __init__(self, name, default, **info): # -- IF_YES_SKIP_TO --------------------------------------------------------- self.if_yes_skip_to = self.check_type('if_yes_skip_to', None, str) + if self.if_yes_skip_to: + if self.var_type not in ['yes_no']: + msg = "Variable: '{var_name}' specifies 'if_yes_skip_to' field, but variable not of type 'yes_no'" + raise ValueError(msg.format(var_name=self.name)) # -- IF_NO_SKIP_TO --------------------------------------------------------- self.if_no_skip_to = self.check_type('if_no_skip_to', None, str) + if self.if_no_skip_to: + if self.var_type not in ['yes_no']: + msg = "Variable: '{var_name}' specifies 'if_no_skip_to' field, but variable not of type 'yes_no'" + raise ValueError(msg.format(var_name=self.name)) # -- PROMPT_USER ----------------------------------------------------- self.prompt_user = self.check_type('prompt_user', True, bool) @@ -365,6 +383,8 @@ def __init__(self, name, default, **info): # -- VALIDATION STARTS ----------------------------------------------- self.validation = self.check_type('validation', None, str) + self.validation_msg = self.check_type('validation_msg', None, str) + self.validation_flag_names = self.check_type('validation_flags', [], list) self.validation_flags = 0 @@ -529,6 +549,8 @@ def load_context(json_object, no_input=False, verbose=True): else: msg = "Input validation failure against regex: '{val_string}', try again!".format(val_string=variable.validation) click.echo(msg) + if variable.validation_msg: + click.echo(variable.validation_msg) else: # no validation defined break diff --git a/tests/test-context/cookiecutter_val_failure_msg.json b/tests/test-context/cookiecutter_val_failure_msg.json new file mode 100644 index 000000000..a68c30b47 --- /dev/null +++ b/tests/test-context/cookiecutter_val_failure_msg.json @@ -0,0 +1,22 @@ +{ + "name": "cookiecutter-test-validation-failure", + "version": "0.1.0", + "cookiecutter_version": "2.0.0", + "variables": [ + { + "name": "project_name", + "default": "Default Project Name", + "prompt": "Enter Project Name", + "description": "Please enter a short, space delimited, name for this project.", + "type": "string" + }, + { + "name": "module_name", + "default": "{{cookiecutter.project_name|lower|replace(' ','_')}}", + "prompt": "Please enter a name for your base python module", + "type": "string", + "validation": "^[a-z_]+$", + "validation_msg": "Really, you couldn't get this correct the first time?" + } + ] +} diff --git a/tests/test_context.py b/tests/test_context.py index 68203c309..28bc5f184 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -690,3 +690,53 @@ def test_load_context_with_input_with_validation_failure(mocker, capsys): assert cc_cfg['project_name'] == INPUT_1 assert cc_cfg['module_name'] == INPUT_3 + + +def test_load_context_with_input_with_validation_failure_msg(mocker, capsys): + cc = load_cookiecutter('tests/test-context/cookiecutter_val_failure_msg.json') + + INPUT_1 = '6 Debug Shell' + INPUT_2 = '' + INPUT_3 = 'debug_shell' + mocker.patch( + 'click.termui.visible_prompt_func', + autospec=True, + side_effect=[INPUT_1, INPUT_2, INPUT_3] + ) + + cc_cfg = context.load_context(cc['cookiecutter_val_failure_msg'], no_input=False) + + out, err = capsys.readouterr() + + msg = "Input validation failure against regex: '^[a-z_]+$', try again!" + assert msg in out + + msg2 = "Really, you couldn't get this correct the first time?" + assert msg2 in out + + assert cc_cfg['project_name'] == INPUT_1 + assert cc_cfg['module_name'] == INPUT_3 + + +def test_specify_if_yes_skip_to_without_yes_no_type(): + """ + Test ValueError is raised when a variable specifies an if_yes_skip_to + field and the variable type is not 'yes+no' + """ + with pytest.raises(ValueError) as excinfo: + context.Variable(name='author', default='JKR', type='string', + if_yes_skip_to='roman') + + assert "Variable: 'author' specifies 'if_yes_skip_to' field, but variable not of type 'yes_no'" in str(excinfo.value) + + +def test_specify_if_no_skip_to_without_yes_no_type(): + """ + Test ValueError is raised when a variable specifies an if_no_skip_to + field and the variable type is not 'yes+no' + """ + with pytest.raises(ValueError) as excinfo: + context.Variable(name='author', default='JKR', type='string', + if_no_skip_to='roman') + + assert "Variable: 'author' specifies 'if_no_skip_to' field, but variable not of type 'yes_no'" in str(excinfo.value) From 5f8e66b06816e76d902906478ed577991ee23481 Mon Sep 17 00:00:00 2001 From: eruber Date: Tue, 14 Nov 2017 00:08:51 -0700 Subject: [PATCH 033/274] Raise ValueError if attempt is made to remove a mandatory field from a variable --- cookiecutter/generate.py | 2 + tests/test_generate_context_v2.py | 62 ++++++++++++++++++++++++++++++- 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/cookiecutter/generate.py b/cookiecutter/generate.py index ccd2eaff3..c18978fca 100644 --- a/cookiecutter/generate.py +++ b/cookiecutter/generate.py @@ -224,6 +224,8 @@ def apply_overwrites_to_context_v2(context, extra_context): common_keys = [key for key in xtra_ctx_item.keys() if key in var_dict.keys()] # noqa for key in common_keys: if xtra_ctx_item[key] == '<>': + if key in ['default']: + raise ValueError("Cannot remove mandatory 'default' field") # noqa var_dict.pop(key, None) else: # normal field update diff --git a/tests/test_generate_context_v2.py b/tests/test_generate_context_v2.py index 0e562e92d..61088d51a 100644 --- a/tests/test_generate_context_v2.py +++ b/tests/test_generate_context_v2.py @@ -356,7 +356,7 @@ def gen_context_data_inputs_expected(): # Test the ability to change the variable's name field (since it is used # to identify the variable to be modifed) with extra context and to remove - # a key from the context via the removal token: '<>' + # a key from the context via the removal token: '<>' context_with_valid_extra_2 = ( { 'context_file': 'tests/test-generate-context-v2/representative.json', @@ -720,3 +720,63 @@ def test_generate_context_with_extra_context_dictionary( with creation of new fields NOT allowed. """ assert generate.generate_context(**input_params) == expected_context + + +def context_data_2(): + context_with_valid_extra_2_A = ( + { + 'context_file': 'tests/test-generate-context-v2/representative.json', + + }, + { + "representative": OrderedDict([ + ("name", "cc-representative"), + ("cookiecutter_version", "2.0.0"), + ("variables", [ + OrderedDict([ + ("name", "director_credit"), + ("default", True), + ("prompt", "Is there a director credit on this film?"), + ("description", "Directors take credit for most of their films, usually..."), + ("type", "boolean") + ]), + OrderedDict([ + ("name", "director_name"), + ("default", "Allan Smithe"), + ("prompt", "What's the Director's full name?"), + ("prompt_user", True), + ("description", "The default director is not proud of their work, we hope you are."), + ("hide_input", False), + ("choices", ["Allan Smithe", "Ridley Scott", "Victor Fleming", "John Houston"]), + ("validation", "^[a-z][A-Z]+$"), + ("validation_flags", ["verbose", "ascii"]), + ("type", "string") + ]) + ]) + ]) + } + ) + + +@pytest.mark.usefixtures('clean_system') +def test_raise_exception_when_attempting_to_remove_mandatory_field(): + """ + Test that ValueError is raised if attempt is made to remove a mandatory + field -- the default field. + The other mandatory field, name, cannot be removed because it has to be + used to specify which variable to remove. + """ + xtra_context = [ + { + 'name': 'director_name', + 'default': '<>', + }, + ] + + with pytest.raises(ValueError) as excinfo: + generate.generate_context( + context_file='tests/test-generate-context-v2/representative.json', + default_context=None, + extra_context=xtra_context) + + assert "Cannot remove mandatory 'default' field" in str(excinfo.value) From 18782ff6a306d08f052c6aad26bd9381b030d079 Mon Sep 17 00:00:00 2001 From: "louis.gaste" Date: Wed, 28 Apr 2021 14:23:34 +0200 Subject: [PATCH 034/274] - --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d8123d080..64ae40bbf 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Cookiecutter +# Cookiecutter [![pypi](https://img.shields.io/pypi/v/cookiecutter.svg)](https://pypi.org/project/cookiecutter/) [![python](https://img.shields.io/pypi/pyversions/cookiecutter.svg)](https://pypi.org/project/cookiecutter/) From 823050a031c2b1d0e41f9feced3dc1baa715a425 Mon Sep 17 00:00:00 2001 From: Fabio Todaro Date: Mon, 31 May 2021 11:31:57 +0200 Subject: [PATCH 035/274] Run drafter only on master releases --- .github/release-drafter.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 6691c2c63..6eb8aeb13 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -1,3 +1,5 @@ +filter-by-commitish: true +commitish: master categories: - title: 'Breaking Changes' labels: From f00bed7b93aff1a7684bfb4dbba287999920b76f Mon Sep 17 00:00:00 2001 From: Sorin Sbarnea Date: Mon, 14 Jun 2021 13:36:44 +0100 Subject: [PATCH 036/274] Adopt setuptools-scm packaging --- .github/workflows/main.yml | 2 + pyproject.toml | 12 ++++++ setup.cfg | 76 ++++++++++++++++++++++++++++++++++- setup.py | 81 ++++++-------------------------------- tox.ini | 25 ++++++++++++ 5 files changed, 126 insertions(+), 70 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 73c66aff7..14e8e77d9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -24,6 +24,8 @@ jobs: pip install tox virtualenv - name: Lint run: "tox -e lint" + - name: Packaging + run: "tox -e packaging" - name: Safety run: "tox -e safety" build: diff --git a/pyproject.toml b/pyproject.toml index 9c25cb239..6dc08afa9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,17 @@ +[build-system] +requires = [ + "setuptools >= 42.0.0", # required by pyproject+setuptools_scm integration + "setuptools_scm[toml] >= 3.5.0", # required for "no-local-version" scheme + "setuptools_scm_git_archive >= 1.0", + "wheel", +] +build-backend = "setuptools.build_meta" + [tool.black] skip-string-normalization = true exclude = '/(tests/hooks-abort-render/hooks|docs\/HelloCookieCutter1)/' line-length = 88 target-version = ['py36'] + +[tool.setuptools_scm] +local_scheme = "no-local-version" diff --git a/setup.cfg b/setup.cfg index aaf136bcb..609c2dc11 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,77 @@ +[metadata] +name = cookiecutter +url = https://github.com/cookiecutter/cookiecutter +project_urls = + Bug Tracker = https://github.com/cookiecutter/cookiecutter/issues + CI: GitHub = https://github.com/cookiecutter/cookiecutter/actions + Documentation = https://cookiecutter.readthedocs.io/ + Source Code = https://github.com/cookiecutter/cookiecutter +description = + A command-line utility that creates projects from project + templates, e.g. creating a Python package project from a + Python package project template. +long_description = file: README.md +long_description_content_type = text/markdown +author = Audrey Feldroy +author_email = audreyr@gmail.com +maintainer = Audrey Feldroy +maintainer_email = audreyr@gmail.com +license = BSD +license_file = LICENSE +classifiers = + Development Status :: 5 - Production/Stable + Environment :: Console + Intended Audience :: Developers + Natural Language :: English + License :: OSI Approved :: BSD License + Programming Language :: Python :: 3 :: Only + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: Implementation :: CPython + Programming Language :: Python :: Implementation :: PyPy + Programming Language :: Python + Topic :: Software Development +keywords = + cookiecutter + Python + projects + project templates + Jinja2 + skeleton + scaffolding + project directory + package + packaging + +[options] +use_scm_version = True +python_requires = >=3.6 +; package_dir = +; = src +packages = cookiecutter +zip_safe = False + +# These are required during `setup.py` run: +setup_requires = + setuptools_scm>=1.15.0 + setuptools_scm_git_archive>=1.0 + +install_requires = + binaryornot>=0.4.4 + Jinja2>=2.7,<4.0.0 + click>=7.0,<8.0.0 + pyyaml>=5.3.1 + jinja2-time>=0.2.0 + python-slugify>=4.0.0 + requests>=2.23.0 + +[options.entry_points] +console_scripts = + cookiecutter = cookiecutter.__main__:main + [flake8] ignore = BLK100,E231,W503 @@ -11,7 +85,7 @@ statistics = 1 max-line-length = 88 [bdist_wheel] -universal = 1 +universal = false [tool:pytest] testpaths = tests diff --git a/setup.py b/setup.py index 654010fa0..5a2d616e8 100644 --- a/setup.py +++ b/setup.py @@ -1,70 +1,13 @@ -#!/usr/bin/env python -"""cookiecutter distutils configuration.""" -from setuptools import setup - -version = "2.0.0" - -with open('README.md', encoding='utf-8') as readme_file: - readme = readme_file.read() - -requirements = [ - 'binaryornot>=0.4.4', - 'Jinja2>=2.7,<4.0.0', - 'click>=7.0,<8.0.0', - 'pyyaml>=5.3.1', - 'jinja2-time>=0.2.0', - 'python-slugify>=4.0.0', - 'requests>=2.23.0', -] - -setup( - name='cookiecutter', - version=version, - description=( - 'A command-line utility that creates projects from project ' - 'templates, e.g. creating a Python package project from a ' - 'Python package project template.' - ), - long_description=readme, - long_description_content_type='text/markdown', - author='Audrey Feldroy', - author_email='audreyr@gmail.com', - url='https://github.com/cookiecutter/cookiecutter', - packages=['cookiecutter'], - package_dir={'cookiecutter': 'cookiecutter'}, - entry_points={'console_scripts': ['cookiecutter = cookiecutter.__main__:main']}, - include_package_data=True, - python_requires='>=3.6', - install_requires=requirements, - license='BSD', - zip_safe=False, - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Environment :: Console", - "Intended Audience :: Developers", - "Natural Language :: English", - "License :: OSI Approved :: BSD License", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: Implementation :: CPython", - "Programming Language :: Python :: Implementation :: PyPy", - "Programming Language :: Python", - "Topic :: Software Development", - ], - keywords=[ - "cookiecutter", - "Python", - "projects", - "project templates", - "Jinja2", - "skeleton", - "scaffolding", - "project directory", - "package", - "packaging", - ], +#! /usr/bin/env python3 +"""cookiecutter distutils configuration. + +The presence of this file ensures the support +of pip editable mode *with setuptools only*. +""" +import setuptools + +# https://github.com/jazzband/pip-tools/issues/1278 +setuptools.setup( + use_scm_version={"local_scheme": "no-local-version"}, + setup_requires=["setuptools_scm[toml]>=3.5.0"], ) diff --git a/tox.ini b/tox.ini index 1097d7e6a..147c4d022 100644 --- a/tox.ini +++ b/tox.ini @@ -43,3 +43,28 @@ commands = safety check --full-report deps = safety + +[testenv:packaging] +description = + Build package, verify metadata, install package and assert behavior when ansible is missing. +deps = + build + twine +skip_install = true +commands = + {envpython} -c 'import os.path, shutil, sys; \ + dist_dir = os.path.join("{toxinidir}", "dist"); \ + os.path.isdir(dist_dir) or sys.exit(0); \ + print("Removing \{!s\} contents...".format(dist_dir), file=sys.stderr); \ + shutil.rmtree(dist_dir)' + # build using moder python build (PEP-517) + {envpython} -m build \ + --sdist \ + --wheel \ + --outdir {toxinidir}/dist/ \ + {toxinidir} + # Validate metadata using twine + twine check {toxinidir}/dist/* + # Install the wheel + sh -c "python3 -m pip install {toxinidir}/dist/*.whl" +whitelist_externals = sh From a54de6759d2553564cda1911fb553e9df9cad937 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Tue, 15 Jun 2021 12:17:49 -0400 Subject: [PATCH 037/274] Add missing period (#1574) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8e1e6673a..9fcf0c7c5 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ We are proud to be an open source sponsor of Did someone say features? * Cross-platform: Windows, Mac, and Linux are officially supported. -* You don't have to know/write Python code to use Cookiecutter +* You don't have to know/write Python code to use Cookiecutter. * Works with Python 3.6, 3.7, 3.8, 3.9 and PyPy3. * Project templates can be in any programming language or markup format: Python, JavaScript, Ruby, CoffeeScript, RST, Markdown, CSS, HTML, you name it. From b1f6427606b67362de233588dd7a37496195b031 Mon Sep 17 00:00:00 2001 From: Claudio Jolowicz Date: Tue, 15 Jun 2021 19:31:55 +0200 Subject: [PATCH 038/274] Add support for click 8.x (#1569) * Make read_user_dict compatible with click 8.x The `read_user_dict` function uses a "default" sentinel instead of the actual default value. Being a JSON dict, the latter would often be hard to type. Under click 8.x, the default value for `click.prompt` is passed to the `read_proc` callback. We use this callback to load JSON from the user input, and this would choke on an input like "default" (without quotes). Therefore, change the callback to return the default value when it receives the "default" sentinel. Under click 7.x (which is our minimum version), the default value for `click.prompt` is returned as-is. Therefore, continue to handle the case where `click.prompt` returns "default" instead of the actual default value, but only if we're actually running under click 7.x. (Checking for click 7.x is only done for clarity. Under click 8.x, `click.prompt` would never return "default", even if a user entered it as a valid JSON string. This is because our callback requires a dict, not a string.) * test: Expect read_user_dict to call click.prompt with partial object Previously, tests for `read_user_dict` expected `process_json` to be passed to click.prompt directly. Instead, we now pass an instance of `functools.partial`, so adapt the mock to reflect that. * test: Avoid mocking `click.prompt` when testing defaults Do not mock `click.prompt` when testing that `read_user_dict` returns the proper default value (rather than the sentinel "default"). Mocking `click.prompt` prevents our callback from running, and under click >= 8.0 we process the sentinel in the callback. Instead, use `click.testing.CliRunner` to fake standard input. * test: Adapt regression test for default handling Expect `json.loads` not to be called with the sentinel ("default"). Previously, the test expected `process_json` not to be called, but under click >= 8.0 that is now where we handle the sentinel. * Update dependencies for click 8.x --- cookiecutter/prompt.py | 23 ++++++++++++++++------- setup.cfg | 2 +- tests/test_read_user_dict.py | 34 +++++++++++++++++----------------- 3 files changed, 34 insertions(+), 25 deletions(-) diff --git a/cookiecutter/prompt.py b/cookiecutter/prompt.py index dfb8f3218..4b8b2fbe6 100644 --- a/cookiecutter/prompt.py +++ b/cookiecutter/prompt.py @@ -1,4 +1,5 @@ """Functions for prompting the user for project info.""" +import functools import json from collections import OrderedDict @@ -78,11 +79,18 @@ def read_user_choice(var_name, options): return choice_map[user_choice] -def process_json(user_value): +DEFAULT_DISPLAY = 'default' + + +def process_json(user_value, default_value=None): """Load user-supplied value as a JSON dict. :param str user_value: User-supplied value to load as a JSON dict """ + if user_value == DEFAULT_DISPLAY: + # Return the given default w/o any processing + return default_value + try: user_dict = json.loads(user_value, object_pairs_hook=OrderedDict) except Exception: @@ -107,15 +115,16 @@ def read_user_dict(var_name, default_value): if not isinstance(default_value, dict): raise TypeError - default_display = 'default' - user_value = click.prompt( - var_name, default=default_display, type=click.STRING, value_proc=process_json + var_name, + default=DEFAULT_DISPLAY, + type=click.STRING, + value_proc=functools.partial(process_json, default_value=default_value), ) - if user_value == default_display: - # Return the given default w/o any processing - return default_value + if click.__version__.startswith("7.") and user_value == DEFAULT_DISPLAY: + # click 7.x does not invoke value_proc on the default value. + return default_value # pragma: no cover return user_value diff --git a/setup.cfg b/setup.cfg index 609c2dc11..b9f2cf7a8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -62,7 +62,7 @@ setup_requires = install_requires = binaryornot>=0.4.4 Jinja2>=2.7,<4.0.0 - click>=7.0,<8.0.0 + click>=7.0,<9.0.0 pyyaml>=5.3.1 jinja2-time>=0.2.0 python-slugify>=4.0.0 diff --git a/tests/test_read_user_dict.py b/tests/test_read_user_dict.py index ccf632258..0ce50efc2 100644 --- a/tests/test_read_user_dict.py +++ b/tests/test_read_user_dict.py @@ -92,35 +92,35 @@ def test_should_call_prompt_with_process_json(mocker): read_user_dict('name', {'project_slug': 'pytest-plugin'}) - assert mock_prompt.call_args == mocker.call( - 'name', type=click.STRING, default='default', value_proc=process_json, - ) + args, kwargs = mock_prompt.call_args + + assert args == ('name',) + assert kwargs['type'] == click.STRING + assert kwargs['default'] == 'default' + assert kwargs['value_proc'].func == process_json -def test_should_not_call_process_json_default_value(mocker, monkeypatch): - """Make sure that `process_json` is not called when using default value.""" - mock_process_json = mocker.patch('cookiecutter.prompt.process_json', autospec=True) +def test_should_not_load_json_from_sentinel(mocker): + """Make sure that `json.loads` is not called when using default value.""" + mock_json_loads = mocker.patch( + 'cookiecutter.prompt.json.loads', autospec=True, return_value={} + ) runner = click.testing.CliRunner() with runner.isolation(input="\n"): read_user_dict('name', {'project_slug': 'pytest-plugin'}) - mock_process_json.assert_not_called() + mock_json_loads.assert_not_called() -def test_read_user_dict_default_value(mocker): +@pytest.mark.parametrize("input", ["\n", "default\n"]) +def test_read_user_dict_default_value(mocker, input): """Make sure that `read_user_dict` returns the default value. Verify return of a dict variable rather than the display value. """ - mock_prompt = mocker.patch( - 'cookiecutter.prompt.click.prompt', autospec=True, return_value='default', - ) - - val = read_user_dict('name', {'project_slug': 'pytest-plugin'}) - - assert mock_prompt.call_args == mocker.call( - 'name', type=click.STRING, default='default', value_proc=process_json, - ) + runner = click.testing.CliRunner() + with runner.isolation(input=input): + val = read_user_dict('name', {'project_slug': 'pytest-plugin'}) assert val == {'project_slug': 'pytest-plugin'} From d6037b7dee5756e35a6ecd5b522899a9061c2c79 Mon Sep 17 00:00:00 2001 From: Simone Basso <4202614+simobasso@users.noreply.github.com> Date: Tue, 15 Jun 2021 19:46:42 +0200 Subject: [PATCH 039/274] Enable branch coverage (#1542) * Add test cases for untested branches * Add branches coverage Branch coverage helps to find out untested branches. Closes: https://github.com/cookiecutter/cookiecutter/issues/1541 --- cookiecutter/vcs.py | 2 +- tests/test_utils.py | 10 +++++++ tests/vcs/test_clone.py | 22 +++++++++++++++ tests/zipfile/test_unzip.py | 56 +++++++++++++++++++++++++++++++++++++ tox.ini | 2 +- 5 files changed, 90 insertions(+), 2 deletions(-) diff --git a/cookiecutter/vcs.py b/cookiecutter/vcs.py index 746dfade9..d7f7b5ba5 100644 --- a/cookiecutter/vcs.py +++ b/cookiecutter/vcs.py @@ -81,7 +81,7 @@ def clone(repo_url, checkout=None, clone_to_dir='.', no_input=False): if repo_type == 'git': repo_name = repo_name.split(':')[-1].rsplit('.git')[0] repo_dir = os.path.normpath(os.path.join(clone_to_dir, repo_name)) - elif repo_type == 'hg': + if repo_type == 'hg': repo_dir = os.path.normpath(os.path.join(clone_to_dir, repo_name)) logger.debug('repo_dir is {0}'.format(repo_dir)) diff --git a/tests/test_utils.py b/tests/test_utils.py index 5b089ae4a..54d07b424 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -92,6 +92,16 @@ def test_work_in(tmp_path): assert cwd == Path.cwd() +def test_work_in_without_path(): + """Folder is not changed if no path provided.""" + cwd = Path.cwd() + + with utils.work_in(): + assert cwd == Path.cwd() + + assert cwd == Path.cwd() + + def test_prompt_should_ask_and_rm_repo_dir(mocker, tmp_path): """In `prompt_and_delete()`, if the user agrees to delete/reclone the \ repo, the repo should be deleted.""" diff --git a/tests/vcs/test_clone.py b/tests/vcs/test_clone.py index 9e3b78496..4687ea9a2 100644 --- a/tests/vcs/test_clone.py +++ b/tests/vcs/test_clone.py @@ -58,6 +58,28 @@ def test_clone_should_abort_if_user_does_not_want_to_reclone(mocker, clone_dir): assert not mock_subprocess.called +def test_clone_should_silent_exit_if_ok_to_reuse(mocker, tmpdir): + """In `clone()`, if user doesn't want to reclone, Cookiecutter should exit \ + without cloning anything.""" + mocker.patch('cookiecutter.vcs.is_vcs_installed', autospec=True, return_value=True) + mocker.patch( + 'cookiecutter.vcs.prompt_and_delete', return_value=False, autospec=True + ) + mock_subprocess = mocker.patch( + 'cookiecutter.vcs.subprocess.check_output', autospec=True, + ) + + clone_to_dir = tmpdir.mkdir('clone') + + # Create repo_dir to trigger prompt_and_delete + clone_to_dir.mkdir('cookiecutter-pytest-plugin') + + repo_url = 'https://github.com/pytest-dev/cookiecutter-pytest-plugin.git' + + vcs.clone(repo_url, clone_to_dir=str(clone_to_dir)) + assert not mock_subprocess.called + + @pytest.mark.parametrize( 'repo_type, repo_url, repo_name', [ diff --git a/tests/zipfile/test_unzip.py b/tests/zipfile/test_unzip.py index 5135f4eec..0231d2b2a 100644 --- a/tests/zipfile/test_unzip.py +++ b/tests/zipfile/test_unzip.py @@ -2,6 +2,7 @@ import tempfile import pytest +import shutil from cookiecutter import zipfile from cookiecutter.exceptions import InvalidZipRepository @@ -16,6 +17,16 @@ def mock_download(): chunk = zf.read(1024) +def mock_download_with_empty_chunks(): + """Fake download function.""" + yield + with open('tests/files/fake-repo-tmpl.zip', 'rb') as zf: + chunk = zf.read(1024) + while chunk: + yield chunk + chunk = zf.read(1024) + + def test_unzip_local_file(mocker, clone_dir): """Local file reference can be unzipped.""" mock_prompt_and_delete = mocker.patch( @@ -170,6 +181,29 @@ def test_unzip_url(mocker, clone_dir): assert not mock_prompt_and_delete.called +def test_unzip_url_with_empty_chunks(mocker, clone_dir): + """In `unzip()` empty chunk must be ignored.""" + mock_prompt_and_delete = mocker.patch( + 'cookiecutter.zipfile.prompt_and_delete', return_value=True, autospec=True + ) + + request = mocker.MagicMock() + request.iter_content.return_value = mock_download_with_empty_chunks() + + mocker.patch( + 'cookiecutter.zipfile.requests.get', return_value=request, autospec=True, + ) + + output_dir = zipfile.unzip( + 'https://example.com/path/to/fake-repo-tmpl.zip', + is_url=True, + clone_to_dir=str(clone_dir), + ) + + assert output_dir.startswith(tempfile.gettempdir()) + assert not mock_prompt_and_delete.called + + def test_unzip_url_existing_cache(mocker, clone_dir): """Url should be downloaded and unzipped, old zip file will be removed.""" mock_prompt_and_delete = mocker.patch( @@ -240,3 +274,25 @@ def test_unzip_should_abort_if_no_redownload(mocker, clone_dir): zipfile.unzip(zipfile_url, is_url=True, clone_to_dir=str(clone_dir)) assert not mock_requests_get.called + + +def test_unzip_is_ok_to_reuse(mocker, clone_dir): + """Already downloaded zip should not be downloaded again.""" + mock_prompt_and_delete = mocker.patch( + 'cookiecutter.zipfile.prompt_and_delete', return_value=False, autospec=True + ) + + request = mocker.MagicMock() + + existing_zip = clone_dir.joinpath('fake-repo-tmpl.zip') + shutil.copy('tests/files/fake-repo-tmpl.zip', existing_zip) + + output_dir = zipfile.unzip( + 'https://example.com/path/to/fake-repo-tmpl.zip', + is_url=True, + clone_to_dir=str(clone_dir), + ) + + assert output_dir.startswith(tempfile.gettempdir()) + assert mock_prompt_and_delete.call_count == 1 + assert request.iter_content.call_count == 0 diff --git a/tox.ini b/tox.ini index 147c4d022..499c6febe 100644 --- a/tox.ini +++ b/tox.ini @@ -18,7 +18,7 @@ passenv = HOME commands = pip install -e . - pytest --cov=cookiecutter --cov-report=term --cov-fail-under=100 {posargs:tests} + pytest --cov=cookiecutter --cov-report=term --cov-fail-under=100 --cov-branch {posargs:tests} cov-report: coverage html cov-report: coverage xml deps = -rtest_requirements.txt From 115ab758f76887638627c789896f3785ced007ec Mon Sep 17 00:00:00 2001 From: "louis.gaste" Date: Fri, 18 Jun 2021 16:46:14 +0200 Subject: [PATCH 040/274] new clean branch for JsonSchema --- README.md | 2 +- cookiecutter/exceptions.py | 7 + cookiecutter/schema.py | 221 +++++++++++++++++++++ setup.cfg | 1 + tests/test-context/cookiecutter-1.0.1.json | 9 + tests/test-context/cookiecutter-1.0.json | 10 + tests/test-context/cookiecutter-2.0.json | 108 ++++++++++ tests/test-context/cookiecutter.json | 10 + tests/test_schema.py | 105 ++++++++++ 9 files changed, 472 insertions(+), 1 deletion(-) create mode 100644 cookiecutter/schema.py create mode 100644 tests/test-context/cookiecutter-1.0.1.json create mode 100644 tests/test-context/cookiecutter-1.0.json create mode 100644 tests/test-context/cookiecutter-2.0.json create mode 100644 tests/test-context/cookiecutter.json create mode 100644 tests/test_schema.py diff --git a/README.md b/README.md index 3f2720ff6..9fcf0c7c5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Cookiecutter +# Cookiecutter [![pypi](https://img.shields.io/pypi/v/cookiecutter.svg)](https://pypi.org/project/cookiecutter/) [![python](https://img.shields.io/pypi/pyversions/cookiecutter.svg)](https://pypi.org/project/cookiecutter/) diff --git a/cookiecutter/exceptions.py b/cookiecutter/exceptions.py index 9461aa985..17d62fd87 100644 --- a/cookiecutter/exceptions.py +++ b/cookiecutter/exceptions.py @@ -161,3 +161,10 @@ class InvalidZipRepository(CookiecutterException): Raised when the specified cookiecutter repository isn't a valid Zip archive. """ + + +class IncompatibleVersion(CookiecutterException): + """ + Exception for incompatible software versions. + Raised when a mandatory version requirement is not fulfilled. + """ diff --git a/cookiecutter/schema.py b/cookiecutter/schema.py new file mode 100644 index 000000000..47d8595f2 --- /dev/null +++ b/cookiecutter/schema.py @@ -0,0 +1,221 @@ +# -*- coding: utf-8 -*- +""" +Defines and validates the allowed schemas for cookiecutter.json. +This file contains the definition of the allowed schemas, using the JSONschema +notation. It also implements the functions used to infer a schema version from +a loaded JSON and validate it against the corresponding JSON schema. +""" +from typing import Optional +from warnings import warn + +import jsonschema + +schema_1_0 = { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "cookiecutter-schema-1.0", + "type": "object", + # schema 1.0 is simply everything + "properties": {}, +} + +# this is a teaser of what we plan to/could do with a JsonSchema for advanced context features +schema_2_0 = { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "cookiecutter-schema-2.0", + "type": "object", + "properties": { + # cookiecutter schema version + "version": {"type": "string", "enum": ["2.0", "2"]}, + # list requirements for this template + "requires": { + "type": "object", + "properties": { + # min version of cookiecutter that is required by the template + "cookiecutter": {"type": "string"}, + # python version constraints of the template + "python": {"type": "string"}, + }, + "additionalProperties": False, + }, + # additional parameters for Jinja2 environment + # frequent use for the 'extension' parameter + # see all parameter options at + # https://jinja.palletsprojects.com/en/3.0.x/api/#jinja2.Environment + "jinja": { + "type": "object", + "additionalProperties": { + "type": ["string", "boolean", "integer", "array"], + "items": {"type": "string"}, + }, + }, + # the template definition + "template": { + "type": "object", + "properties": { + # name of the template + "name": {"type": "string"}, + # version number of the cookiecutter template + "version": {"type": "string"}, + # description of the template + "description": {"type": "string"}, + # list of authors + # (may include email addresses or other contact information) + "authors": {"type": "array", "items": {"type": "string"}}, + # license of the template + "license": {"type": "string"}, + # keywords that describe the goals of the template + "keywords": {"type": "array", "items": {"type": "string"}}, + # the canonical url from where the template can be retrieved + "url": {"type": "string"}, + # definition of the template's variables + "variables": { + "type": "array", + "items": { + "type": "object", + "properties": { + # variable name (must be a valid python variable name) + "name": {"type": "string"}, + # the default value for that variable + "default": {}, + # text that will be displayed before the input field + # (keep it short!) + "prompt": {"type": "string"}, + # more detailed description of this variable + "description": {"type": "string"}, + # input data type (string, boolean, etc.) + "type": { + "type": "string", + "enum": [ + "boolean", + "yes_no", + "int", + "float", + "uuid", + "json", + "string", + ], + }, + # validate user input with this regex + "validation": {"type": "string"}, + # display this message if validation failed + "validation_msg": {"type": "string"}, + # regex flags used with the validation string + "validation_flags": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "ascii", + "debug", + "ignorecase", + "locale", + "mulitline", + "dotall", + "verbose", + ], + }, + }, + # a list of items to choose from + "choices": {"type": "array", "items": {"type": "string"}}, + # don't prompt the user for this variable if set to false + "prompt_user": {"type": "boolean"}, + # hide user input while typing + # (e.g. if you're asking for a password) + "hide_input": {"type": "boolean"}, + # only show this prompt, if the specified condition is true + "do_if": {"type": "string"}, + # skip this prompt, if the specified condition is true + "skip_if": {"type": "string"}, + # skip to this variable, + # if "no" was selected in a "yes_no" prompt + "if_no_skip_to": {"type": "string"}, + # skip to this variable, + # if "yes" was selected in a "yes_no" prompt + "if_yes_skip_to": {"type": "string"}, + }, + "required": ["name", "type"], + "additionalProperties": False, + }, + }, + }, + "required": ["name", "variables"], + "additionalProperties": False, + }, + }, + "required": ["version", "template"], + "additionalProperties": False, +} + +# mapping from valid schema version names to their json schema instances +schema_versions = { + '1.0': schema_1_0, + '2.0': schema_2_0, +} + +# cookiecutter schema versions in chronological order +schema_chronology = ['1.0', '2.0'] + + +def validate(d: dict, version=None) -> None: + """ + Infers schema version and validates the cookiecutter context. + Validate a cookiecutter.json (as Python dict) against the specified cookiecutter + schema version. If the version is undefined, the version that is declared in + the cookiecutter.json is used. If no version declaration is found, + schema version 1.0 is assumed. Raises a ValidationError if schema validation + failed. Raises a ValueError, if the specified schema version is not supported. + :param d: the cookiecutter.json as Python dict + :param version: the schema version to validate against (optional) + :return: None, if validation was successful, otherwise errors are raised + :raises ValueError: if the schema version is not supported + :raises ValidationError: if schema validation was not successful + """ + if version: + # a version number has been explicitly defined + _validate(d, version) + else: + version = infer_schema_version(d) + _validate(d, version) + + +def _validate(d: dict, version: str): + """ + Validate cookiecutter context against the schema version provided. + Validate the specified cookiecutter.json (as Python dict) against the specified + cookiecutter schema version. If the version number is undefined or not supported, + a ValueError is raised. + :param d: the cookiecutter.json as Python dict + :param version: use this schema version to validate the cookiecutter.json + :return: None, if validation was successful, otherwise errors are raised + :raises ValueError: if the schema version is undefined or not supported + :raises ValidationError: if schema validation was not successful + """ + if version not in schema_versions: + raise ValueError(f"Unsupported schema version {version}") + jsonschema.validate(instance=d, schema=schema_versions[version]) + + +def infer_schema_version(d: dict) -> Optional[str]: + """ + Infers the schema version of a cookiecutter context. + Detect the schema version of the specified cookiecutter.json (as Python dict). + The schema will not be validated, this function will only try to return the + schema version. If the schema version could not be detected, None is returned. + :param d: the cookiecutter.json as Python dict + :return: the schema version, defaults to v1.0 with a warning + """ + # here we make the minimal assumptions for the versions. + # If a file contains a version=2.0 term but contains a 1.0 + # schema structure, it will be considered as a broken 2.0 file + if "version" in d and d["version"] in schema_versions: + return d["version"] + + if "version" in d: + warn( + " Schema version & detected." + " \"version\" field is reserved in Cookiecutter 2 for indicating " + "the Schema version. Please use another variable name for safe usage", + UserWarning, + ) + + return '1.0' diff --git a/setup.cfg b/setup.cfg index b9f2cf7a8..0506210f5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -67,6 +67,7 @@ install_requires = jinja2-time>=0.2.0 python-slugify>=4.0.0 requests>=2.23.0 + jsonschema>=3.2.0' [options.entry_points] console_scripts = diff --git a/tests/test-context/cookiecutter-1.0.1.json b/tests/test-context/cookiecutter-1.0.1.json new file mode 100644 index 000000000..d5c8e062d --- /dev/null +++ b/tests/test-context/cookiecutter-1.0.1.json @@ -0,0 +1,9 @@ +{ + "full_name": "Audrey Feldroy", + "email": "audreyr@gmail.com", + "project_name": "Complexity", + "repo_name": "complexity", + "project_short_description": "Refreshingly simple static site generator.", + "release_date": "2013-07-10", + "year": "2013" +} \ No newline at end of file diff --git a/tests/test-context/cookiecutter-1.0.json b/tests/test-context/cookiecutter-1.0.json new file mode 100644 index 000000000..c8aed01b4 --- /dev/null +++ b/tests/test-context/cookiecutter-1.0.json @@ -0,0 +1,10 @@ +{ + "full_name": "Audrey Feldroy", + "email": "audreyr@gmail.com", + "project_name": "Complexity", + "repo_name": "complexity", + "project_short_description": "Refreshingly simple static site generator.", + "release_date": "2013-07-10", + "year": "2013", + "version": "1.0" +} \ No newline at end of file diff --git a/tests/test-context/cookiecutter-2.0.json b/tests/test-context/cookiecutter-2.0.json new file mode 100644 index 000000000..e66e605e4 --- /dev/null +++ b/tests/test-context/cookiecutter-2.0.json @@ -0,0 +1,108 @@ +{ + "version": "2.0", + "requires": { + "cookiecutter": ">=2a", + "python": ">=3.0" + }, + "template": { + "name": "cookiecutter-pytest-plugin", + "version": "1.0.0", + "description": "a cookiecutter to create pytest plugins with ease.", + "authors": [ + "Raphael Pierzina ", + "Audrey Roy Greenfeld " + ], + "license": "MIT", + "keywords": [ + "pytest", + "python", + "plugin" + ], + "url": "https://github.com/pytest-dev/cookiecutter-pytest-plugin", + "variables": [ + { + "name": "full_name", + "default": "Raphael Pierzina", + "prompt": "What's your full name?", + "description": "Please enter your full name. It will be displayed on the README file and used for the PyPI package definition.", + "type": "string" + }, + { + "name": "email", + "default": "raphael@hackebrot.de", + "prompt": "What's your email?", + "description": "Please enter an email address for the meta information in setup.py.", + "type": "string" + }, + { + "name": "plugin_name", + "default": "emoji", + "prompt": "What should be the name for your plugin?", + "description": "Please enter a name for your plugin. We will prepend the name with 'pytest-'", + "type": "string" + }, + { + "name": "module_name", + "default": "{{cookiecutter.plugin_name|lower|replace('-','_')}}", + "prompt": "Please enter a name for your base python module", + "type": "string", + "validation": "^[a-z_]+$" + }, + { + "name": "license", + "default": "MIT", + "prompt": "Please choose a license!", + "description": "Cookiecutter will add an according LICENSE file for you and set the according classifier in setup.py.", + "type": "string", + "choices": [ + "MIT", + "BSD-3", + "GNU GPL v3.0", + "Apache Software License 2.0", + "Mozilla Public License 2.0" + ] + }, + { + "name": "docs", + "default": true, + "prompt": "Do you want to generate a base for docs?", + "description": "Would you like to generate documentation for your plugin? You will be able to choose from a number of generators.", + "type": "yes_no" + }, + { + "name": "docs_tool", + "default": "mkdocs", + "prompt": "Which tool do you want to choose for generating docs?", + "description": "There are a number of options for documentation generators. Please choose one. We will create a separate folder for you", + "type": "string", + "choices": [ + "mkdocs", + "sphinx" + ], + "skip_if": "{{cookiecutter.docs == False}}" + }, + { + "name": "year", + "default": "{% now 'utc', '%Y' %}", + "prompt_user": false, + "type": "string" + }, + { + "name": "fixtures", + "default": { + "foo": { + "scope": "session", + "autouse": true + }, + "bar": { + "scope": "function", + "autouse": false + } + }, + "description": "Please enter a valid JSON string to set up fixtures for your plugin.", + "prompt_user": true, + "type": "json" + } + ] + } +} \ No newline at end of file diff --git a/tests/test-context/cookiecutter.json b/tests/test-context/cookiecutter.json new file mode 100644 index 000000000..c8aed01b4 --- /dev/null +++ b/tests/test-context/cookiecutter.json @@ -0,0 +1,10 @@ +{ + "full_name": "Audrey Feldroy", + "email": "audreyr@gmail.com", + "project_name": "Complexity", + "repo_name": "complexity", + "project_short_description": "Refreshingly simple static site generator.", + "release_date": "2013-07-10", + "year": "2013", + "version": "1.0" +} \ No newline at end of file diff --git a/tests/test_schema.py b/tests/test_schema.py new file mode 100644 index 000000000..b866e21a7 --- /dev/null +++ b/tests/test_schema.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +# flake8: noqa +""" +test_schema +------------ +Tests for `cookiecutter.scherma` module that defines, infers and validate +a cookiecutter schema +""" +import json + +import pytest +from jsonschema import ValidationError + +from cookiecutter.schema import infer_schema_version, validate + + +def test_validate_1_0(): + d = get_sample_cookiecutter('1.0') + validate(d, version='1.0') + + +def test_validate_1_0_implicitly(): + d = get_sample_cookiecutter('1.0') + validate(d) + + +def test_validate_1_0_fallback(): + d = get_sample_cookiecutter('1.0') + del d['version'] + validate(d) + + +def test_validate_2_0(): + d = get_sample_cookiecutter('2.0') + validate(d, version='2.0') + + +def test_validate_unsupported_version(): + d = get_sample_cookiecutter('2.0') + with pytest.raises(ValueError): + validate(d, version='2.175') + + +def test_validate_fail_2_0(): + d = get_sample_cookiecutter('2.0') + d['authors'] = "authors must be an array" + with pytest.raises(ValidationError): + validate(d, version='2.0') + + +def test_validate_2_0_implicitly(): + d = get_sample_cookiecutter('2.0') + validate(d) + + +def test_validate_fail_unsupported(): + + d = get_sample_cookiecutter('2.0') + d['version'] = "2.42" + with pytest.warns(UserWarning): + validate(d) + + +def test_validate_fail_additions(): + d = get_sample_cookiecutter('2.0') + d['addition'] = "yolo!" + with pytest.raises(ValidationError): + validate(d) + + +def test_validate_fail_additions_2(): + d = get_sample_cookiecutter('2.0') + d['template']['descriptin'] = "oops, typo :/" + with pytest.raises(ValidationError): + validate(d) + + +def test_validate_fail_additions_3(): + d = get_sample_cookiecutter('2.0') + d['template']['variables'][0]['addition'] = "yolo!" + with pytest.raises(ValidationError): + validate(d) + + +def test_detect_1_0(): + # testing a version 1 without version in it + d = get_sample_cookiecutter('1.0.1') + assert infer_schema_version(d) == '1.0' + + +def test_detect_2_0(): + d = get_sample_cookiecutter('2.0') + assert infer_schema_version(d) == '2.0' + + +def test_infer_fallback_1_0(): + d = get_sample_cookiecutter('2.0') + d['version'] = "2.42" + with pytest.warns(UserWarning): + assert infer_schema_version(d) == '1.0' + + +def get_sample_cookiecutter(version='2.0.0'): + with open(f'tests/test-context/cookiecutter-{version}.json') as fp: + return json.load(fp) From 772e5f51aae27da9d8d3ff0b423a5ca117ab71fd Mon Sep 17 00:00:00 2001 From: "louis.gaste" Date: Fri, 18 Jun 2021 18:41:16 +0200 Subject: [PATCH 041/274] fix lint --- cookiecutter/schema.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cookiecutter/schema.py b/cookiecutter/schema.py index 47d8595f2..f68ab0721 100644 --- a/cookiecutter/schema.py +++ b/cookiecutter/schema.py @@ -18,7 +18,7 @@ "properties": {}, } -# this is a teaser of what we plan to/could do with a JsonSchema for advanced context features +# this is a teaser of what we could do with a JsonSchema for advanced context features schema_2_0 = { "$schema": "http://json-schema.org/draft-07/schema#", "title": "cookiecutter-schema-2.0", @@ -159,6 +159,7 @@ def validate(d: dict, version=None) -> None: """ Infers schema version and validates the cookiecutter context. + Validate a cookiecutter.json (as Python dict) against the specified cookiecutter schema version. If the version is undefined, the version that is declared in the cookiecutter.json is used. If no version declaration is found, @@ -181,6 +182,7 @@ def validate(d: dict, version=None) -> None: def _validate(d: dict, version: str): """ Validate cookiecutter context against the schema version provided. + Validate the specified cookiecutter.json (as Python dict) against the specified cookiecutter schema version. If the version number is undefined or not supported, a ValueError is raised. @@ -198,6 +200,7 @@ def _validate(d: dict, version: str): def infer_schema_version(d: dict) -> Optional[str]: """ Infers the schema version of a cookiecutter context. + Detect the schema version of the specified cookiecutter.json (as Python dict). The schema will not be validated, this function will only try to return the schema version. If the schema version could not be detected, None is returned. From 7b85b7b82f81a7dc0d4e88eb9bc14fcb8275b4ca Mon Sep 17 00:00:00 2001 From: "louis.gaste" Date: Fri, 18 Jun 2021 19:11:02 +0200 Subject: [PATCH 042/274] fix lint --- cookiecutter/exceptions.py | 1 + cookiecutter/schema.py | 1 + 2 files changed, 2 insertions(+) diff --git a/cookiecutter/exceptions.py b/cookiecutter/exceptions.py index 17d62fd87..78a7fcaca 100644 --- a/cookiecutter/exceptions.py +++ b/cookiecutter/exceptions.py @@ -166,5 +166,6 @@ class InvalidZipRepository(CookiecutterException): class IncompatibleVersion(CookiecutterException): """ Exception for incompatible software versions. + Raised when a mandatory version requirement is not fulfilled. """ diff --git a/cookiecutter/schema.py b/cookiecutter/schema.py index f68ab0721..04aa8e068 100644 --- a/cookiecutter/schema.py +++ b/cookiecutter/schema.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """ Defines and validates the allowed schemas for cookiecutter.json. + This file contains the definition of the allowed schemas, using the JSONschema notation. It also implements the functions used to infer a schema version from a loaded JSON and validate it against the corresponding JSON schema. From fb34a1fafefe3030494ff5a87fef62f711d4c228 Mon Sep 17 00:00:00 2001 From: "louis.gaste" Date: Fri, 18 Jun 2021 19:15:38 +0200 Subject: [PATCH 043/274] fix lint --- cookiecutter/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cookiecutter/exceptions.py b/cookiecutter/exceptions.py index 78a7fcaca..d3d4846b1 100644 --- a/cookiecutter/exceptions.py +++ b/cookiecutter/exceptions.py @@ -166,6 +166,6 @@ class InvalidZipRepository(CookiecutterException): class IncompatibleVersion(CookiecutterException): """ Exception for incompatible software versions. - + Raised when a mandatory version requirement is not fulfilled. """ From 6eee9d2d8c3c16ab1ae33e3c9ea6ec6b9b1d7f57 Mon Sep 17 00:00:00 2001 From: "louis.gaste" Date: Tue, 22 Jun 2021 09:59:14 +0200 Subject: [PATCH 044/274] cleaned branch --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9fcf0c7c5..3f2720ff6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Cookiecutter +# Cookiecutter [![pypi](https://img.shields.io/pypi/v/cookiecutter.svg)](https://pypi.org/project/cookiecutter/) [![python](https://img.shields.io/pypi/pyversions/cookiecutter.svg)](https://pypi.org/project/cookiecutter/) From 7a73e9c252fb951bffeffeb769057bd946dfb865 Mon Sep 17 00:00:00 2001 From: ldegaste <59446733+louisdegaste@users.noreply.github.com> Date: Wed, 23 Jun 2021 10:02:44 +0200 Subject: [PATCH 045/274] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3f2720ff6..9fcf0c7c5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Cookiecutter +# Cookiecutter [![pypi](https://img.shields.io/pypi/v/cookiecutter.svg)](https://pypi.org/project/cookiecutter/) [![python](https://img.shields.io/pypi/pyversions/cookiecutter.svg)](https://pypi.org/project/cookiecutter/) From a46eede6fa45aedee0621ff1fe3e212dab17ee11 Mon Sep 17 00:00:00 2001 From: Jeremy Swerdlow Date: Thu, 5 Aug 2021 23:52:25 -0700 Subject: [PATCH 046/274] Expand cli documentation relating to the no-input flag (#1543) --- cookiecutter/cli.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cookiecutter/cli.py b/cookiecutter/cli.py index 991e62c50..08d778b73 100644 --- a/cookiecutter/cli.py +++ b/cookiecutter/cli.py @@ -75,7 +75,9 @@ def list_installed_templates(default_config, passed_config_file): @click.option( '--no-input', is_flag=True, - help='Do not prompt for parameters and only use cookiecutter.json file content', + help='Do not prompt for parameters and only use cookiecutter.json file content. ' + 'Defaults to deleting any cached resources and redownloading them. ' + 'Cannot be combined with the --replay flag.', ) @click.option( '-c', '--checkout', help='branch, tag or commit to checkout after git clone', @@ -91,7 +93,8 @@ def list_installed_templates(default_config, passed_config_file): @click.option( '--replay', is_flag=True, - help='Do not prompt for parameters and only use information entered previously', + help='Do not prompt for parameters and only use information entered previously. ' + 'Cannot be combined with the --no-input flag or with extra configuration passed.', ) @click.option( '--replay-file', From 831b365e1448bc219e7d0508ffdd794675710b4a Mon Sep 17 00:00:00 2001 From: David Salter Date: Thu, 14 Oct 2021 22:48:03 +0100 Subject: [PATCH 047/274] Move contributors and backers to credits section --- AUTHORS.md | 45 ++++++++++++++++++++++++ docs/BACKERS.md | 1 - docs/index.rst | 2 -- docs/sprint-contributors.rst | 66 ------------------------------------ 4 files changed, 45 insertions(+), 69 deletions(-) delete mode 120000 docs/BACKERS.md delete mode 100644 docs/sprint-contributors.rst diff --git a/AUTHORS.md b/AUTHORS.md index d991d54fa..173d02b2f 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -170,3 +170,48 @@ * Tom Forbes ([@orf](https://github.com/orf)) * Xie Yanbo ([@xyb](https://github.com/xyb)) * Maxim Ivanov ([@ivanovmg](https://github.com/ivanovmg)) + +## Backers + +We would like to thank the following people for supporting us in our efforts to maintain and improve Cookiecutter: + +* Alex DeBrie +* Alexandre Y. Harano +* Bruno Alla +* Carol Willing +* Russell Keith-Magee + +## Sprint Contributors + +### PyCon 2016 Sprint + +The following people made contributions to the cookiecutter project +at the PyCon sprints in Portland, OR from June 2-5 2016. +Contributions include user testing, debugging, improving documentation, +reviewing issues, writing tutorials, creating and updating project +templates, and teaching each other. + +* Adam Chainz ([@adamchainz](https://github.com/adamchainz)) +* Andrew Ittner ([@tephyr](https://github.com/tephyr)) +* Audrey Roy Greenfeld ([@audreyr](https://github.com/audreyr)) +* Carol Willing ([@willingc](https://github.com/willingc)) +* Christopher Clarke ([@chrisdev](https://github.com/chrisdev)) +* Citlalli Murillo ([@citmusa](https://github.com/citmusa)) +* Daniel Roy Greenfeld ([@pydanny](https://github.com/pydanny)) +* Diane DeMers Chen ([@purplediane](https://github.com/purplediane)) +* Elaine Wong ([@elainewong`](https://github.com/elainewong)) +* Elias Dorneles ([@eliasdorneles](https://github.com/eliasdorneles)) +* Emily Cain ([@emcain](https://github.com/emcain)) +* John Roa ([@jhonjairoroa87](https://github.com/jhonjairoroa87)) +* Jonan Scheffler ([@1337807](https://github.com/1337807)) +* Phoebe Bauer ([@phoebebauer](https://github.com/phoebebauer)) +* Kartik Sundararajan ([@skarbot](https://github.com/skarbot)) +* Katia Lira ([@katialira](https://github.com/katialira)) +* Leonardo Jimenez ([@xpostudio4](https://github.com/xpostudio4)) +* Lindsay Slazakowski ([@lslaz1](https://github.com/lslaz1)) +* Meghan Heintz ([@dot2dotseurat](https://github.com/dot2dotseurat)) +* Raphael Pierzina ([@hackebrot](https://github.com/hackebrot)) +* Umair Ashraf ([@umrashrf](https://github.com/umrashrf)) +* Valdir Stumm Junior ([@stummjr](https://github.com/stummjr)) +* Vivian Guillen ([@viviangb](https://github.com/viviangb)) +* Zaro ([@zaro0508](https://github.com/zaro0508)) diff --git a/docs/BACKERS.md b/docs/BACKERS.md deleted file mode 120000 index 6e7e6e905..000000000 --- a/docs/BACKERS.md +++ /dev/null @@ -1 +0,0 @@ -../BACKERS.md \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index 750db5093..15a953e63 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -42,10 +42,8 @@ Project Info CONTRIBUTING AUTHORS - sprint-contributors HISTORY case_studies - BACKERS CODE_OF_CONDUCT Index diff --git a/docs/sprint-contributors.rst b/docs/sprint-contributors.rst deleted file mode 100644 index 1a8419fb8..000000000 --- a/docs/sprint-contributors.rst +++ /dev/null @@ -1,66 +0,0 @@ -=================== -Sprint Contributors -=================== - -PyCon 2016 Sprint ------------------ - -The following people made contributions to the cookiecutter project -at the PyCon sprints in Portland, OR from June 2-5 2016. -Contributions include user testing, debugging, improving documentation, -reviewing issues, writing tutorials, creating and updating project -templates, and teaching each other. - -* Adam Chainz (`@adamchainz`_) -* Andrew Ittner (`@tephyr`_) -* Audrey Roy Greenfeld (`@audreyr`_) -* Carol Willing (`@willingc`_) -* Christopher Clarke (`@chrisdev`_) -* Citlalli Murillo (`@citmusa`_) -* Daniel Roy Greenfeld (`@pydanny`_) -* Diane DeMers Chen (`@purplediane`_) -* Elaine Wong (`@elainewong`_) -* Elias Dorneles (`@eliasdorneles`_) -* Emily Cain (`@emcain`_) -* John Roa (`@jhonjairoroa87`_) -* Jonan Scheffler (`@1337807`_) -* Phoebe Bauer (`@phoebebauer`_) -* Kartik Sundararajan (`@skarbot`_) -* Katia Lira (`@katialira`_) -* Leonardo Jimenez (`@xpostudio4`_) -* Lindsay Slazakowski (`@lslaz1`_) -* Meghan Heintz (`@dot2dotseurat`_) -* Raphael Pierzina (`@hackebrot`_) -* Umair Ashraf (`@umrashrf`_) -* Valdir Stumm Junior (`@stummjr`_) -* Vivian Guillen (`@viviangb`_) -* Zaro (`@zaro0508`_) - - - - -.. _`@1337807`: https://github.com/1337807 -.. _`@adamchainz`: https://github.com/adamchainz -.. _`@audreyr`: https://github.com/audreyr -.. _`@chrisdev`: https://github.com/chrisdev -.. _`@citmusa`: https://github.com/citmusa -.. _`@dot2dotseurat`: https://github.com/dot2dotseurat -.. _`@elainewong`: https://github.com/elainewong -.. _`@eliasdorneles`: https://github.com/eliasdorneles -.. _`@emcain`: https://github.com/emcain -.. _`@hackebrot`: https://github.com/hackebrot -.. _`@jhonjairoroa87`: https://github.com/jhonjairoroa87 -.. _`@katialira`: https://github.com/katialira -.. _`@lslaz1`: https://github.com/lslaz1 -.. _`@phoebebauer`: https://github.com/phoebebauer -.. _`@purplediane`: https://github.com/purplediane -.. _`@pydanny`: https://github.com/pydanny -.. _`@skarbot`: https://github.com/skarbot -.. _`@stummjr`: https://github.com/stummjr -.. _`@tephyr`: https://github.com/tephyr -.. _`@umrashrf`: https://github.com/umrashrf -.. _`@viviangb`: https://github.com/viviangb -.. _`@willingc`: https://github.com/willingc -.. _`@xpostudio4`: https://github.com/xpostudio4 -.. _`@zaro0508`: https://github.com/zaro0508 - From e45e525fbc193001831cc936e23c7fa4927f7ad4 Mon Sep 17 00:00:00 2001 From: Giuseppe Lumia Date: Tue, 26 Oct 2021 22:20:36 +0200 Subject: [PATCH 048/274] Improve docstrings of the `no_input` flag Relates to #1543 --- cookiecutter/main.py | 4 +++- cookiecutter/prompt.py | 5 ++--- cookiecutter/repository.py | 3 ++- cookiecutter/vcs.py | 3 ++- cookiecutter/zipfile.py | 3 ++- docs/advanced/suppressing_prompts.rst | 2 ++ 6 files changed, 13 insertions(+), 7 deletions(-) diff --git a/cookiecutter/main.py b/cookiecutter/main.py index 047d30a08..ce47fa752 100644 --- a/cookiecutter/main.py +++ b/cookiecutter/main.py @@ -39,7 +39,9 @@ def cookiecutter( :param template: A directory containing a project template directory, or a URL to a git repository. :param checkout: The branch, tag or commit ID to checkout after clone. - :param no_input: Prompt the user at command line for manual configuration? + :param no_input: Do not prompt for user input. + Use default values for template parameters taken from `cookiecutter.json`, user + config and `extra_dict`. Force a refresh of cached resources. :param extra_context: A dictionary of context that overrides default and user configuration. :param replay: Do not prompt for input, instead read from saved json. If diff --git a/cookiecutter/prompt.py b/cookiecutter/prompt.py index 4b8b2fbe6..c3aa6b957 100644 --- a/cookiecutter/prompt.py +++ b/cookiecutter/prompt.py @@ -168,10 +168,9 @@ def render_variable(env, raw, cookiecutter_dict): def prompt_choice_for_config(cookiecutter_dict, env, key, options, no_input): """Prompt user with a set of options to choose from. - Each of the possible choices is rendered beforehand. + :no_input: Do not prompt for user input and return the first available option. """ rendered_options = [render_variable(env, raw, cookiecutter_dict) for raw in options] - if no_input: return rendered_options[0] return read_user_choice(key, rendered_options) @@ -181,7 +180,7 @@ def prompt_for_config(context, no_input=False): """Prompt user to enter a new config. :param dict context: Source for field names and sample values. - :param no_input: Prompt the user at command line for manual configuration? + :param no_input: Do not prompt for user input and use only values from context. """ cookiecutter_dict = OrderedDict([]) env = StrictEnvironment(context=context) diff --git a/cookiecutter/repository.py b/cookiecutter/repository.py index f8e6fcbcc..5086e9b01 100644 --- a/cookiecutter/repository.py +++ b/cookiecutter/repository.py @@ -82,7 +82,8 @@ def determine_repo_dir( definitions. :param clone_to_dir: The directory to clone the repository into. :param checkout: The branch, tag or commit ID to checkout after clone. - :param no_input: Prompt the user at command line for manual configuration? + :param no_input: Do not prompt for user input and eventually force a refresh of + cached resources. :param password: The password to use when extracting the repository. :param directory: Directory within repo where cookiecutter.json lives. :return: A tuple containing the cookiecutter template directory, and diff --git a/cookiecutter/vcs.py b/cookiecutter/vcs.py index d7f7b5ba5..8b0b7744b 100644 --- a/cookiecutter/vcs.py +++ b/cookiecutter/vcs.py @@ -61,7 +61,8 @@ def clone(repo_url, checkout=None, clone_to_dir='.', no_input=False): :param checkout: The branch, tag or commit ID to checkout after clone. :param clone_to_dir: The directory to clone to. Defaults to the current directory. - :param no_input: Suppress all user prompts when calling via API. + :param no_input: Do not prompt for user input and eventually force a refresh of + cached resources. :returns: str with path to the new directory of the repository. """ # Ensure that clone_to_dir exists diff --git a/cookiecutter/zipfile.py b/cookiecutter/zipfile.py index 24925c7fc..620839b9e 100644 --- a/cookiecutter/zipfile.py +++ b/cookiecutter/zipfile.py @@ -20,7 +20,8 @@ def unzip(zip_uri, is_url, clone_to_dir='.', no_input=False, password=None): :param is_url: Is the zip URI a URL or a file? :param clone_to_dir: The cookiecutter repository directory to put the archive into. - :param no_input: Suppress any prompts + :param no_input: Do not prompt for user input and eventually force a refresh of + cached resources. :param password: The password to use when unpacking the repository. """ # Ensure that clone_to_dir exists diff --git a/docs/advanced/suppressing_prompts.rst b/docs/advanced/suppressing_prompts.rst index 1f73ff9fd..d9608dd63 100644 --- a/docs/advanced/suppressing_prompts.rst +++ b/docs/advanced/suppressing_prompts.rst @@ -5,6 +5,8 @@ Suppressing Command-Line Prompts To suppress the prompts asking for input, use `no_input`. +Note: this option will force a refresh of cached resources. + Basic Example: Using the Defaults ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From 8383971fbb4298e773233b6a6a00b4c25acfe4f4 Mon Sep 17 00:00:00 2001 From: Audrey Roy Greenfeld Date: Sat, 30 Oct 2021 09:25:12 -0700 Subject: [PATCH 049/274] Ignore OSX-generated files --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index e646a076d..8c96562ec 100644 --- a/.gitignore +++ b/.gitignore @@ -69,3 +69,8 @@ target/ # PyEnv .python-version + +# OSX +.DS_Store +.AppleDouble +.LSOverride From 53b3f14bd5481cdfab779688cf02029d4e6c013f Mon Sep 17 00:00:00 2001 From: Audrey Roy Greenfeld Date: Sat, 30 Oct 2021 10:27:31 -0700 Subject: [PATCH 050/274] Markdown corrections: - Name corrections for @audreyfeldroy and @pydanny - Run Prettier on Markdown files --- AUTHORS.md | 323 ++++++++++++++++++++++++++--------------------------- LICENSE | 2 +- README.md | 184 +++++++++++++++--------------- setup.cfg | 4 +- 4 files changed, 256 insertions(+), 257 deletions(-) diff --git a/AUTHORS.md b/AUTHORS.md index d991d54fa..2f951cdd0 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -2,171 +2,170 @@ ## Development Leads -* Audrey Feldroy ([@audreyfeldroy](https://github.com/audreyfeldroy)) -* Daniel Feldroy ([@pydanny](https://github.com/pydanny)) -* Raphael Pierzina ([@hackebrot](https://github.com/hackebrot)) - +- Audrey Roy Greenfeld ([@audreyfeldroy](https://github.com/audreyfeldroy)) +- Daniel Roy Greenfeld ([@pydanny](https://github.com/pydanny)) +- Raphael Pierzina ([@hackebrot](https://github.com/hackebrot)) ## Core Committers -* Michael Joseph ([@michaeljoseph](https://github.com/michaeljoseph)) -* Paul Moore ([@pfmoore](https://github.com/pfmoore)) -* Andrey Shpak ([@insspb](https://github.com/insspb)) -* Sorin Sbarnea ([@ssbarnea](https://github.com/ssbarnea)) -* Fábio C. Barrionuevo da Luz ([@luzfcb](https://github.com/luzfcb)) -* Simone Basso ([@simobasso](https://github.com/simobasso)) +- Michael Joseph ([@michaeljoseph](https://github.com/michaeljoseph)) +- Paul Moore ([@pfmoore](https://github.com/pfmoore)) +- Andrey Shpak ([@insspb](https://github.com/insspb)) +- Sorin Sbarnea ([@ssbarnea](https://github.com/ssbarnea)) +- Fábio C. Barrionuevo da Luz ([@luzfcb](https://github.com/luzfcb)) +- Simone Basso ([@simobasso](https://github.com/simobasso)) ## Contributors -* Steven Loria ([@sloria](https://github.com/sloria)) -* Goran Peretin ([@gperetin](https://github.com/gperetin)) -* Hamish Downer ([@foobacca](https://github.com/foobacca)) -* Thomas Orozco ([@krallin](https://github.com/krallin)) -* Jindrich Smitka ([@s-m-i-t-a](https://github.com/s-m-i-t-a)) -* Benjamin Schwarze ([@benjixx](https://github.com/benjixx)) -* Raphi ([@raphigaziano](https://github.com/raphigaziano)) -* Thomas Chiroux ([@ThomasChiroux](https://github.com/ThomasChiroux)) -* Sergi Almacellas Abellana ([@pokoli](https://github.com/pokoli)) -* Alex Gaynor ([@alex](https://github.com/alex)) -* Rolo ([@rolo](https://github.com/rolo)) -* Pablo ([@oubiga](https://github.com/oubiga)) -* Bruno Rocha ([@rochacbruno](https://github.com/rochacbruno)) -* Alexander Artemenko ([@svetlyak40wt](https://github.com/svetlyak40wt)) -* Mahmoud Abdelkader ([@mahmoudimus](https://github.com/mahmoudimus)) -* Leonardo Borges Avelino ([@lborgav](https://github.com/lborgav)) -* Chris Trotman ([@solarnz](https://github.com/solarnz)) -* Rolf ([@relekang](https://github.com/relekang)) -* Noah Kantrowitz ([@coderanger](https://github.com/coderanger)) -* Vincent Bernat ([@vincentbernat](https://github.com/vincentbernat)) -* Germán Moya ([@pbacterio](https://github.com/pbacterio)) -* Ned Batchelder ([@nedbat](https://github.com/nedbat)) -* Dave Dash ([@davedash](https://github.com/davedash)) -* Johan Charpentier ([@cyberj](https://github.com/cyberj)) -* Éric Araujo ([@merwok](https://github.com/merwok)) -* saxix ([@saxix](https://github.com/saxix)) -* Tzu-ping Chung ([@uranusjr](https://github.com/uranusjr)) -* Caleb Hattingh ([@cjrh](https://github.com/cjrh)) -* Flavio Curella ([@fcurella](https://github.com/fcurella)) -* Adam Venturella ([@aventurella](https://github.com/aventurella)) -* Monty Taylor ([@emonty](https://github.com/emonty)) -* schacki ([@schacki](https://github.com/schacki)) -* Ryan Olson ([@ryanolson](https://github.com/ryanolson)) -* Trey Hunner ([@treyhunner](https://github.com/treyhunner)) -* Russell Keith-Magee ([@freakboy3742](https://github.com/freakboy3742)) -* Mishbah Razzaque ([@mishbahr](https://github.com/mishbahr)) -* Robin Andeer ([@robinandeer](https://github.com/robinandeer)) -* Rachel Sanders ([@trustrachel](https://github.com/trustrachel)) -* Rémy Hubscher ([@Natim](https://github.com/Natim)) -* Dino Petron3 ([@dinopetrone](https://github.com/dinopetrone)) -* Peter Inglesby ([@inglesp](https://github.com/inglesp)) -* Ramiro Batista da Luz ([@ramiroluz](https://github.com/ramiroluz)) -* Omer Katz ([@thedrow](https://github.com/thedrow)) -* lord63 ([@lord63](https://github.com/lord63)) -* Randy Syring ([@rsyring](https://github.com/rsyring)) -* Mark Jones ([@mark0978](https://github.com/mark0978)) -* Marc Abramowitz ([@msabramo](https://github.com/msabramo)) -* Lucian Ursu ([@LucianU](https://github.com/LucianU)) -* Osvaldo Santana Neto ([@osantana](https://github.com/osantana)) -* Matthias84 ([@Matthias84](https://github.com/Matthias84)) -* Simeon Visser ([@svisser](https://github.com/svisser)) -* Guruprasad ([@lgp171188](https://github.com/lgp171188)) -* Charles-Axel Dein ([@charlax](https://github.com/charlax)) -* Diego Garcia ([@drgarcia1986](https://github.com/drgarcia1986)) -* maiksensi ([@maiksensi](https://github.com/maiksensi)) -* Andrew Conti ([@agconti](https://github.com/agconti)) -* Valentin Lab ([@vaab](https://github.com/vaab)) -* Ilja Bauer ([@iljabauer](https://github.com/iljabauer)) -* Elias Dorneles ([@eliasdorneles](https://github.com/eliasdorneles)) -* Matias Saguir ([@mativs](https://github.com/mativs)) -* Johannes ([@johtso](https://github.com/johtso)) -* macrotim ([@macrotim](https://github.com/macrotim)) -* Will McGinnis ([@wdm0006](https://github.com/wdm0006)) -* Cédric Krier ([@cedk](https://github.com/cedk)) -* Tim Osborn ([@ptim](https://github.com/ptim)) -* Aaron Gallagher ([@habnabit](https://github.com/habnabit)) -* mozillazg ([@mozillazg](https://github.com/mozillazg)) -* Joachim Jablon ([@ewjoachim](https://github.com/ewjoachim)) -* Andrew Ittner ([@tephyr](https://github.com/tephyr)) -* Diane DeMers Chen ([@purplediane](https://github.com/purplediane)) -* zzzirk ([@zzzirk](https://github.com/zzzirk)) -* Carol Willing ([@willingc](https://github.com/willingc)) -* phoebebauer ([@phoebebauer](https://github.com/phoebebauer)) -* Adam Chainz ([@adamchainz](https://github.com/adamchainz)) -* Sulé ([@suledev](https://github.com/suledev)) -* Evan Palmer ([@palmerev](https://github.com/palmerev)) -* Bruce Eckel ([@BruceEckel](https://github.com/BruceEckel)) -* Robert Lyon ([@ivanlyon](https://github.com/ivanlyon)) -* Terry Bates ([@terryjbates](https://github.com/terryjbates)) -* Brett Cannon ([@brettcannon](https://github.com/brettcannon)) -* Michael Warkentin ([@mwarkentin](https://github.com/mwarkentin)) -* Bartłomiej Kurzeja ([@B3QL](https://github.com/B3QL)) -* Thomas O'Donnell ([@andytom](https://github.com/andytom)) -* Jeremy Carbaugh ([@jcarbaugh](https://github.com/jcarbaugh)) -* Nathan Cheung ([@cheungnj](https://github.com/cheungnj)) -* Abdó Roig-Maranges ([@aroig](https://github.com/aroig)) -* Steve Piercy ([@stevepiercy](https://github.com/stevepiercy)) -* Corey ([@coreysnyder04](https://github.com/coreysnyder04)) -* Dmitry Evstratov ([@devstrat](https://github.com/devstrat)) -* Eyal Levin ([@eyalev](https://github.com/eyalev)) -* mathagician ([@mathagician](https://github.com/mathagician)) -* Guillaume Gelin ([@ramnes](https://github.com/ramnes)) -* @delirious-lettuce ([@delirious-lettuce](https://github.com/delirious-lettuce)) -* Gasper Vozel ([@karantan](https://github.com/karantan)) -* Joshua Carp ([@jmcarp](https://github.com/jmcarp)) -* @meahow ([@meahow](https://github.com/meahow)) -* Andrea Grandi ([@andreagrandi](https://github.com/andreagrandi)) -* Issa Jubril ([@jubrilissa](https://github.com/jubrilissa)) -* Nytiennzo Madooray ([@Nythiennzo](https://github.com/Nythiennzo)) -* Erik Bachorski ([@dornheimer](https://github.com/dornheimer)) -* cclauss ([@cclauss](https://github.com/cclauss)) -* Andy Craze ([@accraze](https://github.com/accraze)) -* Anthony Sottile ([@asottile](https://github.com/asottile)) -* Jonathan Sick ([@jonathansick](https://github.com/jonathansick)) -* Hugo ([@hugovk](https://github.com/hugovk)) -* Min ho Kim ([@minho42](https://github.com/minho42)) -* Ryan Ly ([@rly](https://github.com/rly)) -* Akintola Rahmat ([@mihrab34](https://github.com/mihrab34)) -* Jai Ram Rideout ([@jairideout](https://github.com/jairideout)) -* Diego Carrasco Gubernatis ([@dacog](https://github.com/dacog)) -* Wagner Negrão ([@wagnernegrao](https://github.com/wagnernegrao)) -* Josh Barnes ([@jcb91](https://github.com/jcb91)) -* Nikita Sobolev ([@sobolevn](https://github.com/sobolevn)) -* Matt Stibbs ([@mattstibbs](https://github.com/mattstibbs)) -* MinchinWeb ([@MinchinWeb](https://github.com/MinchinWeb)) -* kishan ([@kishan](https://github.com/kishan3)) -* tonytheleg ([@tonytheleg](https://github.com/tonytheleg)) -* Roman Hartmann ([@RomHartmann](https://github.com/RomHartmann)) -* DSEnvel ([@DSEnvel](https://github.com/DSEnvel)) -* kishan ([@kishan](https://github.com/kishan3)) -* Bruno Alla ([@browniebroke](https://github.com/browniebroke)) -* nicain ([@nicain](https://github.com/nicain)) -* Carsten Rösnick-Neugebauer ([@croesnick](https://github.com/croesnick)) -* igorbasko01 ([@igorbasko01](https://github.com/igorbasko01)) -* Dan Booth Dev ([@DanBoothDev](https://github.com/DanBoothDev)) -* Pablo Panero ([@ppanero](https://github.com/ppanero)) -* Chuan-Heng Hsiao ([@chhsiao1981](https://github.com/chhsiao1981)) -* Mohammad Hossein Sekhavat ([@mhsekhavat](https://github.com/mhsekhavat)) -* Amey Joshi ([@amey589](https://github.com/amey589)) -* Paul Harrison ([@smoothml](https://github.com/smoothml)) -* Fabio Todaro ([@SharpEdgeMarshall](https://github.com/SharpEdgeMarshall)) -* Nicholas Bollweg ([@bollwyvl](https://github.com/bollwyvl)) -* Jace Browning ([@jacebrowning](https://github.com/jacebrowning)) -* Ionel Cristian Mărieș ([@ionelmc](https://github.com/ionelmc)) -* Kishan Mehta ([@kishan3](https://github.com/kishan3)) -* Wieland Hoffmann ([@mineo](https://github.com/mineo)) -* Antony Lee ([@anntzer](https://github.com/anntzer)) -* Aurélien Gâteau ([@agateau](https://github.com/agateau)) -* Axel H. ([@noirbizarre](https://github.com/noirbizarre)) -* Chris ([@chrisbrake](https://github.com/chrisbrake)) -* Chris Streeter ([@streeter](https://github.com/streeter)) -* Gábor Lipták ([@gliptak](https://github.com/gliptak)) -* Javier Sánchez Portero ([@javiersanp](https://github.com/javiersanp)) -* Nimrod Milo ([@milonimrod](https://github.com/milonimrod)) -* Philipp Kats ([@Casyfill](https://github.com/Casyfill)) -* Reinout van Rees ([@reinout](https://github.com/reinout)) -* Rémy Greinhofer ([@rgreinho](https://github.com/rgreinho)) -* Sebastian ([@sebix](https://github.com/sebix)) -* Stuart Mumford ([@Cadair](https://github.com/Cadair)) -* Tom Forbes ([@orf](https://github.com/orf)) -* Xie Yanbo ([@xyb](https://github.com/xyb)) -* Maxim Ivanov ([@ivanovmg](https://github.com/ivanovmg)) +- Steven Loria ([@sloria](https://github.com/sloria)) +- Goran Peretin ([@gperetin](https://github.com/gperetin)) +- Hamish Downer ([@foobacca](https://github.com/foobacca)) +- Thomas Orozco ([@krallin](https://github.com/krallin)) +- Jindrich Smitka ([@s-m-i-t-a](https://github.com/s-m-i-t-a)) +- Benjamin Schwarze ([@benjixx](https://github.com/benjixx)) +- Raphi ([@raphigaziano](https://github.com/raphigaziano)) +- Thomas Chiroux ([@ThomasChiroux](https://github.com/ThomasChiroux)) +- Sergi Almacellas Abellana ([@pokoli](https://github.com/pokoli)) +- Alex Gaynor ([@alex](https://github.com/alex)) +- Rolo ([@rolo](https://github.com/rolo)) +- Pablo ([@oubiga](https://github.com/oubiga)) +- Bruno Rocha ([@rochacbruno](https://github.com/rochacbruno)) +- Alexander Artemenko ([@svetlyak40wt](https://github.com/svetlyak40wt)) +- Mahmoud Abdelkader ([@mahmoudimus](https://github.com/mahmoudimus)) +- Leonardo Borges Avelino ([@lborgav](https://github.com/lborgav)) +- Chris Trotman ([@solarnz](https://github.com/solarnz)) +- Rolf ([@relekang](https://github.com/relekang)) +- Noah Kantrowitz ([@coderanger](https://github.com/coderanger)) +- Vincent Bernat ([@vincentbernat](https://github.com/vincentbernat)) +- Germán Moya ([@pbacterio](https://github.com/pbacterio)) +- Ned Batchelder ([@nedbat](https://github.com/nedbat)) +- Dave Dash ([@davedash](https://github.com/davedash)) +- Johan Charpentier ([@cyberj](https://github.com/cyberj)) +- Éric Araujo ([@merwok](https://github.com/merwok)) +- saxix ([@saxix](https://github.com/saxix)) +- Tzu-ping Chung ([@uranusjr](https://github.com/uranusjr)) +- Caleb Hattingh ([@cjrh](https://github.com/cjrh)) +- Flavio Curella ([@fcurella](https://github.com/fcurella)) +- Adam Venturella ([@aventurella](https://github.com/aventurella)) +- Monty Taylor ([@emonty](https://github.com/emonty)) +- schacki ([@schacki](https://github.com/schacki)) +- Ryan Olson ([@ryanolson](https://github.com/ryanolson)) +- Trey Hunner ([@treyhunner](https://github.com/treyhunner)) +- Russell Keith-Magee ([@freakboy3742](https://github.com/freakboy3742)) +- Mishbah Razzaque ([@mishbahr](https://github.com/mishbahr)) +- Robin Andeer ([@robinandeer](https://github.com/robinandeer)) +- Rachel Sanders ([@trustrachel](https://github.com/trustrachel)) +- Rémy Hubscher ([@Natim](https://github.com/Natim)) +- Dino Petron3 ([@dinopetrone](https://github.com/dinopetrone)) +- Peter Inglesby ([@inglesp](https://github.com/inglesp)) +- Ramiro Batista da Luz ([@ramiroluz](https://github.com/ramiroluz)) +- Omer Katz ([@thedrow](https://github.com/thedrow)) +- lord63 ([@lord63](https://github.com/lord63)) +- Randy Syring ([@rsyring](https://github.com/rsyring)) +- Mark Jones ([@mark0978](https://github.com/mark0978)) +- Marc Abramowitz ([@msabramo](https://github.com/msabramo)) +- Lucian Ursu ([@LucianU](https://github.com/LucianU)) +- Osvaldo Santana Neto ([@osantana](https://github.com/osantana)) +- Matthias84 ([@Matthias84](https://github.com/Matthias84)) +- Simeon Visser ([@svisser](https://github.com/svisser)) +- Guruprasad ([@lgp171188](https://github.com/lgp171188)) +- Charles-Axel Dein ([@charlax](https://github.com/charlax)) +- Diego Garcia ([@drgarcia1986](https://github.com/drgarcia1986)) +- maiksensi ([@maiksensi](https://github.com/maiksensi)) +- Andrew Conti ([@agconti](https://github.com/agconti)) +- Valentin Lab ([@vaab](https://github.com/vaab)) +- Ilja Bauer ([@iljabauer](https://github.com/iljabauer)) +- Elias Dorneles ([@eliasdorneles](https://github.com/eliasdorneles)) +- Matias Saguir ([@mativs](https://github.com/mativs)) +- Johannes ([@johtso](https://github.com/johtso)) +- macrotim ([@macrotim](https://github.com/macrotim)) +- Will McGinnis ([@wdm0006](https://github.com/wdm0006)) +- Cédric Krier ([@cedk](https://github.com/cedk)) +- Tim Osborn ([@ptim](https://github.com/ptim)) +- Aaron Gallagher ([@habnabit](https://github.com/habnabit)) +- mozillazg ([@mozillazg](https://github.com/mozillazg)) +- Joachim Jablon ([@ewjoachim](https://github.com/ewjoachim)) +- Andrew Ittner ([@tephyr](https://github.com/tephyr)) +- Diane DeMers Chen ([@purplediane](https://github.com/purplediane)) +- zzzirk ([@zzzirk](https://github.com/zzzirk)) +- Carol Willing ([@willingc](https://github.com/willingc)) +- phoebebauer ([@phoebebauer](https://github.com/phoebebauer)) +- Adam Chainz ([@adamchainz](https://github.com/adamchainz)) +- Sulé ([@suledev](https://github.com/suledev)) +- Evan Palmer ([@palmerev](https://github.com/palmerev)) +- Bruce Eckel ([@BruceEckel](https://github.com/BruceEckel)) +- Robert Lyon ([@ivanlyon](https://github.com/ivanlyon)) +- Terry Bates ([@terryjbates](https://github.com/terryjbates)) +- Brett Cannon ([@brettcannon](https://github.com/brettcannon)) +- Michael Warkentin ([@mwarkentin](https://github.com/mwarkentin)) +- Bartłomiej Kurzeja ([@B3QL](https://github.com/B3QL)) +- Thomas O'Donnell ([@andytom](https://github.com/andytom)) +- Jeremy Carbaugh ([@jcarbaugh](https://github.com/jcarbaugh)) +- Nathan Cheung ([@cheungnj](https://github.com/cheungnj)) +- Abdó Roig-Maranges ([@aroig](https://github.com/aroig)) +- Steve Piercy ([@stevepiercy](https://github.com/stevepiercy)) +- Corey ([@coreysnyder04](https://github.com/coreysnyder04)) +- Dmitry Evstratov ([@devstrat](https://github.com/devstrat)) +- Eyal Levin ([@eyalev](https://github.com/eyalev)) +- mathagician ([@mathagician](https://github.com/mathagician)) +- Guillaume Gelin ([@ramnes](https://github.com/ramnes)) +- @delirious-lettuce ([@delirious-lettuce](https://github.com/delirious-lettuce)) +- Gasper Vozel ([@karantan](https://github.com/karantan)) +- Joshua Carp ([@jmcarp](https://github.com/jmcarp)) +- @meahow ([@meahow](https://github.com/meahow)) +- Andrea Grandi ([@andreagrandi](https://github.com/andreagrandi)) +- Issa Jubril ([@jubrilissa](https://github.com/jubrilissa)) +- Nytiennzo Madooray ([@Nythiennzo](https://github.com/Nythiennzo)) +- Erik Bachorski ([@dornheimer](https://github.com/dornheimer)) +- cclauss ([@cclauss](https://github.com/cclauss)) +- Andy Craze ([@accraze](https://github.com/accraze)) +- Anthony Sottile ([@asottile](https://github.com/asottile)) +- Jonathan Sick ([@jonathansick](https://github.com/jonathansick)) +- Hugo ([@hugovk](https://github.com/hugovk)) +- Min ho Kim ([@minho42](https://github.com/minho42)) +- Ryan Ly ([@rly](https://github.com/rly)) +- Akintola Rahmat ([@mihrab34](https://github.com/mihrab34)) +- Jai Ram Rideout ([@jairideout](https://github.com/jairideout)) +- Diego Carrasco Gubernatis ([@dacog](https://github.com/dacog)) +- Wagner Negrão ([@wagnernegrao](https://github.com/wagnernegrao)) +- Josh Barnes ([@jcb91](https://github.com/jcb91)) +- Nikita Sobolev ([@sobolevn](https://github.com/sobolevn)) +- Matt Stibbs ([@mattstibbs](https://github.com/mattstibbs)) +- MinchinWeb ([@MinchinWeb](https://github.com/MinchinWeb)) +- kishan ([@kishan](https://github.com/kishan3)) +- tonytheleg ([@tonytheleg](https://github.com/tonytheleg)) +- Roman Hartmann ([@RomHartmann](https://github.com/RomHartmann)) +- DSEnvel ([@DSEnvel](https://github.com/DSEnvel)) +- kishan ([@kishan](https://github.com/kishan3)) +- Bruno Alla ([@browniebroke](https://github.com/browniebroke)) +- nicain ([@nicain](https://github.com/nicain)) +- Carsten Rösnick-Neugebauer ([@croesnick](https://github.com/croesnick)) +- igorbasko01 ([@igorbasko01](https://github.com/igorbasko01)) +- Dan Booth Dev ([@DanBoothDev](https://github.com/DanBoothDev)) +- Pablo Panero ([@ppanero](https://github.com/ppanero)) +- Chuan-Heng Hsiao ([@chhsiao1981](https://github.com/chhsiao1981)) +- Mohammad Hossein Sekhavat ([@mhsekhavat](https://github.com/mhsekhavat)) +- Amey Joshi ([@amey589](https://github.com/amey589)) +- Paul Harrison ([@smoothml](https://github.com/smoothml)) +- Fabio Todaro ([@SharpEdgeMarshall](https://github.com/SharpEdgeMarshall)) +- Nicholas Bollweg ([@bollwyvl](https://github.com/bollwyvl)) +- Jace Browning ([@jacebrowning](https://github.com/jacebrowning)) +- Ionel Cristian Mărieș ([@ionelmc](https://github.com/ionelmc)) +- Kishan Mehta ([@kishan3](https://github.com/kishan3)) +- Wieland Hoffmann ([@mineo](https://github.com/mineo)) +- Antony Lee ([@anntzer](https://github.com/anntzer)) +- Aurélien Gâteau ([@agateau](https://github.com/agateau)) +- Axel H. ([@noirbizarre](https://github.com/noirbizarre)) +- Chris ([@chrisbrake](https://github.com/chrisbrake)) +- Chris Streeter ([@streeter](https://github.com/streeter)) +- Gábor Lipták ([@gliptak](https://github.com/gliptak)) +- Javier Sánchez Portero ([@javiersanp](https://github.com/javiersanp)) +- Nimrod Milo ([@milonimrod](https://github.com/milonimrod)) +- Philipp Kats ([@Casyfill](https://github.com/Casyfill)) +- Reinout van Rees ([@reinout](https://github.com/reinout)) +- Rémy Greinhofer ([@rgreinho](https://github.com/rgreinho)) +- Sebastian ([@sebix](https://github.com/sebix)) +- Stuart Mumford ([@Cadair](https://github.com/Cadair)) +- Tom Forbes ([@orf](https://github.com/orf)) +- Xie Yanbo ([@xyb](https://github.com/xyb)) +- Maxim Ivanov ([@ivanovmg](https://github.com/ivanovmg)) diff --git a/LICENSE b/LICENSE index 5e75b2cfd..06486a8f3 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2013-2021, Audrey Feldroy +Copyright (c) 2013-2021, Audrey Roy Greenfeld All rights reserved. Redistribution and use in source and binary forms, with or diff --git a/README.md b/README.md index 9fcf0c7c5..2d8b81b75 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,10 @@ A command-line utility that creates projects from **cookiecutters** (project templates), e.g. creating a Python package project from a Python package project template. -* Documentation: [https://cookiecutter.readthedocs.io](https://cookiecutter.readthedocs.io) -* GitHub: [https://github.com/cookiecutter/cookiecutter](https://github.com/cookiecutter/cookiecutter) -* PyPI: [https://pypi.org/project/cookiecutter/](https://pypi.org/project/cookiecutter/) -* Free and open source software: [BSD license](https://github.com/cookiecutter/cookiecutter/blob/master/LICENSE) +- Documentation: [https://cookiecutter.readthedocs.io](https://cookiecutter.readthedocs.io) +- GitHub: [https://github.com/cookiecutter/cookiecutter](https://github.com/cookiecutter/cookiecutter) +- PyPI: [https://pypi.org/project/cookiecutter/](https://pypi.org/project/cookiecutter/) +- Free and open source software: [BSD license](https://github.com/cookiecutter/cookiecutter/blob/master/LICENSE) ![Cookiecutter](https://raw.githubusercontent.com/cookiecutter/cookiecutter/3ac078356adf5a1a72042dfe72ebfa4a9cd5ef38/logo/cookiecutter_medium.png) @@ -26,13 +26,13 @@ We are proud to be an open source sponsor of Did someone say features? -* Cross-platform: Windows, Mac, and Linux are officially supported. -* You don't have to know/write Python code to use Cookiecutter. -* Works with Python 3.6, 3.7, 3.8, 3.9 and PyPy3. -* Project templates can be in any programming language or markup format: +- Cross-platform: Windows, Mac, and Linux are officially supported. +- You don't have to know/write Python code to use Cookiecutter. +- Works with Python 3.6, 3.7, 3.8, 3.9 and PyPy3. +- Project templates can be in any programming language or markup format: Python, JavaScript, Ruby, CoffeeScript, RST, Markdown, CSS, HTML, you name it. You can use multiple languages in the same project template. -* Simple command line usage: +- Simple command line usage: ```bash # Create project from the cookiecutter-pypackage.git repo template @@ -44,7 +44,7 @@ $ cookiecutter https://github.com/audreyfeldroy/cookiecutter-pypackage $ cookiecutter gh:audreyfeldroy/cookiecutter-pypackage ``` -* Use it at the command line with a local template: +- Use it at the command line with a local template: ```bash # Create project in the current working directory, from the local @@ -52,7 +52,7 @@ $ cookiecutter gh:audreyfeldroy/cookiecutter-pypackage $ cookiecutter cookiecutter-pypackage/ ``` -* Or use it from Python: +- Or use it from Python: ```py from cookiecutter.main import cookiecutter @@ -64,47 +64,47 @@ cookiecutter('cookiecutter-pypackage/') cookiecutter('https://github.com/audreyfeldroy/cookiecutter-pypackage.git') ``` -* Directory names and filenames can be templated. For example: +- Directory names and filenames can be templated. For example: ```py {{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}.py ``` -* Supports unlimited levels of directory nesting. -* 100% of templating is done with Jinja2. This includes file and directory names. -* Simply define your template variables in a ``cookiecutter.json`` file. For example: +- Supports unlimited levels of directory nesting. +- 100% of templating is done with Jinja2. This includes file and directory names. +- Simply define your template variables in a `cookiecutter.json` file. For example: ```json { - "full_name": "Audrey Feldroy", - "email": "audreyr@gmail.com", - "project_name": "Complexity", - "repo_name": "complexity", - "project_short_description": "Refreshingly simple static site generator.", - "release_date": "2013-07-10", - "year": "2013", - "version": "0.1.1" + "full_name": "Audrey Roy Greenfeld", + "email": "audreyr@gmail.com", + "project_name": "Complexity", + "repo_name": "complexity", + "project_short_description": "Refreshingly simple static site generator.", + "release_date": "2013-07-10", + "year": "2013", + "version": "0.1.1" } ``` -* Unless you suppress it with ``--no-input``, you are prompted for input: - * Prompts are the keys in ``cookiecutter.json``. - * Default responses are the values in ``cookiecutter.json``. - * Prompts are shown in order. -* Cross-platform support for ``~/.cookiecutterrc`` files: +- Unless you suppress it with `--no-input`, you are prompted for input: + - Prompts are the keys in `cookiecutter.json`. + - Default responses are the values in `cookiecutter.json`. + - Prompts are shown in order. +- Cross-platform support for `~/.cookiecutterrc` files: ```yaml default_context: - full_name: "Audrey Feldroy" - email: "audreyr@gmail.com" - github_username: "audreyfeldroy" + full_name: "Audrey Roy Greenfeld" + email: "audreyr@gmail.com" + github_username: "audreyfeldroy" cookiecutters_dir: "~/.cookiecutters/" ``` -* Cookiecutters (cloned Cookiecutter project templates) are put into -``~/.cookiecutters/`` by default, or cookiecutters_dir if specified. -* If you have already cloned a cookiecutter into ``~/.cookiecutters/``, -you can reference it by directory name: +- Cookiecutters (cloned Cookiecutter project templates) are put into + `~/.cookiecutters/` by default, or cookiecutters_dir if specified. +- If you have already cloned a cookiecutter into `~/.cookiecutters/`, + you can reference it by directory name: ```bash # Clone cookiecutter-pypackage @@ -113,22 +113,22 @@ $ cookiecutter gh:audreyfeldroy/cookiecutter-pypackage $ cookiecutter cookiecutter-pypackage ``` -* You can use local cookiecutters, or remote cookiecutters directly from Git -repos or from Mercurial repos on Bitbucket. -* Default context: specify key/value pairs that you want used as defaults -whenever you generate a project. -* Inject extra context with command-line arguments: +- You can use local cookiecutters, or remote cookiecutters directly from Git + repos or from Mercurial repos on Bitbucket. +- Default context: specify key/value pairs that you want used as defaults + whenever you generate a project. +- Inject extra context with command-line arguments: ```bash cookiecutter --no-input gh:msabramo/cookiecutter-supervisor program_name=foobar startsecs=10 ``` -* Direct access to the Cookiecutter API allows for injection of extra context. -* Pre- and post-generate hooks: Python or shell scripts to run before or after -generating a project. -* Paths to local projects can be specified as absolute or relative. -* Projects generated to your current directory or to target directory if -specified with `-o` option. +- Direct access to the Cookiecutter API allows for injection of extra context. +- Pre- and post-generate hooks: Python or shell scripts to run before or after + generating a project. +- Paths to local projects can be specified as absolute or relative. +- Projects generated to your current directory or to target directory if + specified with `-o` option. ## Available Cookiecutters @@ -157,14 +157,14 @@ discoverable. You are almost not limited in topics amount, use it! These Cookiecutters are maintained by the cookiecutter team: -* [cookiecutter-pypackage](https://github.com/audreyfeldroy/cookiecutter-pypackage): -[@audreyfeldroy's](https://github.com/audreyfeldroy) ultimate Python package project template. -* [cookiecutter-django](https://github.com/pydanny/cookiecutter-django): -A bleeding edge Django project template with Bootstrap 4, customizable users app, -starter templates, working user registration, celery setup, and much more. -* [cookiecutter-pytest-plugin](https://github.com/pytest-dev/cookiecutter-pytest-plugin): -Minimal Cookiecutter template for authoring [pytest](https://docs.pytest.org/) -plugins that help you to write better programs. +- [cookiecutter-pypackage](https://github.com/audreyfeldroy/cookiecutter-pypackage): + [@audreyfeldroy's](https://github.com/audreyfeldroy) ultimate Python package project template. +- [cookiecutter-django](https://github.com/pydanny/cookiecutter-django): + A bleeding edge Django project template with Bootstrap 4, customizable users app, + starter templates, working user registration, celery setup, and much more. +- [cookiecutter-pytest-plugin](https://github.com/pytest-dev/cookiecutter-pytest-plugin): + Minimal Cookiecutter template for authoring [pytest](https://docs.pytest.org/) + plugins that help you to write better programs. ## Community @@ -173,57 +173,57 @@ We are always welcome and invite you to participate. Stuck? Try one of the following: -* See the [Troubleshooting](https://cookiecutter.readthedocs.io/en/latest/troubleshooting.html) page. -* Ask for help on [Stack Overflow](https://stackoverflow.com/questions/tagged/cookiecutter). -* You are strongly encouraged to -[file an issue](https://github.com/cookiecutter/cookiecutter/issues?q=is%3Aopen) -about the problem, even if it's just "I can't get it to work on this cookiecutter" -with a link to your cookiecutter. Don't worry about naming/pinpointing the issue -properly. -* Ask for help on [Slack](https://join.slack.com/t/cookie-cutter/shared_invite/enQtNzI0Mzg5NjE5Nzk5LTRlYWI2YTZhYmQ4YmU1Y2Q2NmE1ZjkwOGM0NDQyNTIwY2M4ZTgyNDVkNjMxMDdhZGI5ZGE5YmJjM2M3ODJlY2U) -if you must (but please try one of the other options first, so that others -can benefit from the discussion). +- See the [Troubleshooting](https://cookiecutter.readthedocs.io/en/latest/troubleshooting.html) page. +- Ask for help on [Stack Overflow](https://stackoverflow.com/questions/tagged/cookiecutter). +- You are strongly encouraged to + [file an issue](https://github.com/cookiecutter/cookiecutter/issues?q=is%3Aopen) + about the problem, even if it's just "I can't get it to work on this cookiecutter" + with a link to your cookiecutter. Don't worry about naming/pinpointing the issue + properly. +- Ask for help on [Slack](https://join.slack.com/t/cookie-cutter/shared_invite/enQtNzI0Mzg5NjE5Nzk5LTRlYWI2YTZhYmQ4YmU1Y2Q2NmE1ZjkwOGM0NDQyNTIwY2M4ZTgyNDVkNjMxMDdhZGI5ZGE5YmJjM2M3ODJlY2U) + if you must (but please try one of the other options first, so that others + can benefit from the discussion). Development on Cookiecutter is community-driven: -* Huge thanks to all the [contributors](AUTHORS.md) who have pitched in to help -make Cookiecutter an even better tool. -* Everyone is invited to contribute. Read the -[contributing instructions](CONTRIBUTING.md), then get started. -* Connect with other Cookiecutter contributors and users on -[Slack](https://join.slack.com/t/cookie-cutter/shared_invite/enQtNzI0Mzg5NjE5Nzk5LTRlYWI2YTZhYmQ4YmU1Y2Q2NmE1ZjkwOGM0NDQyNTIwY2M4ZTgyNDVkNjMxMDdhZGI5ZGE5YmJjM2M3ODJlY2U) -(note: due to work and commitments, a core committer might not always be available) +- Huge thanks to all the [contributors](AUTHORS.md) who have pitched in to help + make Cookiecutter an even better tool. +- Everyone is invited to contribute. Read the + [contributing instructions](CONTRIBUTING.md), then get started. +- Connect with other Cookiecutter contributors and users on + [Slack](https://join.slack.com/t/cookie-cutter/shared_invite/enQtNzI0Mzg5NjE5Nzk5LTRlYWI2YTZhYmQ4YmU1Y2Q2NmE1ZjkwOGM0NDQyNTIwY2M4ZTgyNDVkNjMxMDdhZGI5ZGE5YmJjM2M3ODJlY2U) + (note: due to work and commitments, a core committer might not always be available) Encouragement is unbelievably motivating. If you want more work done on Cookiecutter, show support: -* Thank a core committer for their efforts. -* Star [Cookiecutter on GitHub](https://github.com/cookiecutter/cookiecutter). -* [Support this project](#support-this-project) +- Thank a core committer for their efforts. +- Star [Cookiecutter on GitHub](https://github.com/cookiecutter/cookiecutter). +- [Support this project](#support-this-project) Got criticism or complaints? -* [File an issue](https://github.com/cookiecutter/cookiecutter/issues?q=is%3Aopen) -so that Cookiecutter can be improved. Be friendly and constructive about what -could be better. Make detailed suggestions. -* **Keep us in the loop so that we can help.** For example, if you are -discussing problems with Cookiecutter on a mailing list, -[file an issue](https://github.com/cookiecutter/cookiecutter/issues?q=is%3Aopen) -where you link to the discussion thread and/or cc at least 1 core committer on the email. -* Be encouraging. A comment like "This function ought to be rewritten like this" -is much more likely to result in action than a comment like "Eww, look how bad -this function is." +- [File an issue](https://github.com/cookiecutter/cookiecutter/issues?q=is%3Aopen) + so that Cookiecutter can be improved. Be friendly and constructive about what + could be better. Make detailed suggestions. +- **Keep us in the loop so that we can help.** For example, if you are + discussing problems with Cookiecutter on a mailing list, + [file an issue](https://github.com/cookiecutter/cookiecutter/issues?q=is%3Aopen) + where you link to the discussion thread and/or cc at least 1 core committer on the email. +- Be encouraging. A comment like "This function ought to be rewritten like this" + is much more likely to result in action than a comment like "Eww, look how bad + this function is." Waiting for a response to an issue/question? -* Be patient and persistent. All issues are on the core committer team's radar -and will be considered thoughtfully, but we have a lot of issues to work through. -If urgent, it's fine to ping a core committer in the issue with a reminder. -* Ask others to comment, discuss, review, etc. -* Search the Cookiecutter repo for issues related to yours. -* Need a fix/feature/release/help urgently, and can't wait? -[@audreyfeldroy](https://github.com/audreyfeldroy) is available for hire for consultation -or custom development. +- Be patient and persistent. All issues are on the core committer team's radar + and will be considered thoughtfully, but we have a lot of issues to work through. + If urgent, it's fine to ping a core committer in the issue with a reminder. +- Ask others to comment, discuss, review, etc. +- Search the Cookiecutter repo for issues related to yours. +- Need a fix/feature/release/help urgently, and can't wait? + [@audreyfeldroy](https://github.com/audreyfeldroy) is available for hire for consultation + or custom development. ## Support This Project diff --git a/setup.cfg b/setup.cfg index b9f2cf7a8..c43971e88 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,9 +12,9 @@ description = Python package project template. long_description = file: README.md long_description_content_type = text/markdown -author = Audrey Feldroy +author = Audrey Roy Greenfeld author_email = audreyr@gmail.com -maintainer = Audrey Feldroy +maintainer = Audrey Roy Greenfeld maintainer_email = audreyr@gmail.com license = BSD license_file = LICENSE From 228c655550a57822d44b0c5527d5aab5986f266a Mon Sep 17 00:00:00 2001 From: Audrey Roy Greenfeld Date: Sat, 30 Oct 2021 10:54:22 -0700 Subject: [PATCH 051/274] Don't run tests on Python 3.6 or PyPy anymore. - Resolves GitHub check failures due to GitHub no longer supporting those environments on Mac OSX. - Python 3.6 is in maintenance status *security* with EOL in 2 months. - PyPy was last updated to Python 3.8, which is outdated. --- .github/workflows/main.yml | 25 ------------------------- CONTRIBUTING.md | 6 +++--- README.md | 4 +--- docs/installation.rst | 2 +- setup.cfg | 2 -- tox.ini | 2 -- 6 files changed, 5 insertions(+), 36 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 14e8e77d9..436de42a6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -34,28 +34,19 @@ jobs: fail-fast: false matrix: name: - - "ubuntu-py36" - "ubuntu-py37" - "ubuntu-py38" - "ubuntu-py39" - - "ubuntu-pypy3" - - "macos-py36" - "macos-py37" - "macos-py38" - "macos-py39" - - "macos-pypy3" - - "windows-py36" - "windows-py37" - "windows-py38" - "windows-py39" include: - - name: "ubuntu-py36" - python: "3.6" - os: ubuntu-latest - tox_env: "py36" - name: "ubuntu-py37" python: "3.7" os: ubuntu-latest @@ -68,15 +59,7 @@ jobs: python: "3.9" os: ubuntu-latest tox_env: "py39" - - name: "ubuntu-pypy3" - python: "pypy3" - os: ubuntu-latest - tox_env: "pypy3" - - name: "macos-py36" - python: "3.6" - os: macos-latest - tox_env: "py36" - name: "macos-py37" python: "3.7" os: macos-latest @@ -89,15 +72,7 @@ jobs: python: "3.9" os: macos-latest tox_env: "py39" - - name: "macos-pypy3" - python: "pypy3" - os: macos-latest - tox_env: "pypy3" - - name: "windows-py36" - python: "3.6" - os: windows-latest - tox_env: "py36" - name: "windows-py37" python: "3.7" os: windows-latest diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4b9a5dff4..9c2f12650 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -188,13 +188,13 @@ tox This configuration file setup the pytest-cov plugin and it is an additional dependency. It generate a coverage report after the tests. -It is possible to tests with some versions of python, to do this the command is: +It is possible to test with specific versions of Python. To do this, the command is: ```bash -tox -e py36,pypy3 +tox -e py37,py38 ``` -Will run py.test with the python3.6 and pypy3 interpreters, for example. +Will run py.test with the python3.7 and python3.8 interpreters, for example. ## Core Committer Guide diff --git a/README.md b/README.md index 2d8b81b75..9b1dfe141 100644 --- a/README.md +++ b/README.md @@ -24,11 +24,9 @@ We are proud to be an open source sponsor of ## Features -Did someone say features? - - Cross-platform: Windows, Mac, and Linux are officially supported. - You don't have to know/write Python code to use Cookiecutter. -- Works with Python 3.6, 3.7, 3.8, 3.9 and PyPy3. +- Works with Python 3.7, 3.8, 3.9. - Project templates can be in any programming language or markup format: Python, JavaScript, Ruby, CoffeeScript, RST, Markdown, CSS, HTML, you name it. You can use multiple languages in the same project template. diff --git a/docs/installation.rst b/docs/installation.rst index 44c0d841a..d12709c37 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -59,7 +59,7 @@ You may also install `Windows Subsystem for Linux =3.6. See the Python Packaging Authority's (PyPA) documentation `Requirements for Installing Packages `_ for full details. +See the Python Packaging Authority's (PyPA) documentation `Requirements for Installing Packages `_ for full details. Install cookiecutter diff --git a/setup.cfg b/setup.cfg index c43971e88..e1ddc4fbb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,12 +26,10 @@ classifiers = License :: OSI Approved :: BSD License Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3 - Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: Implementation :: CPython - Programming Language :: Python :: Implementation :: PyPy Programming Language :: Python Topic :: Software Development keywords = diff --git a/tox.ini b/tox.ini index 499c6febe..591fc50ee 100644 --- a/tox.ini +++ b/tox.ini @@ -1,11 +1,9 @@ [tox] envlist = lint - py36 py37 py38 py39 - pypy3 minversion = 3.14.2 requires = virtualenv >= 20.4.5 From dfebae46c2922185615cb4b9e5aa24274ab7b4b9 Mon Sep 17 00:00:00 2001 From: Audrey Roy Greenfeld Date: Sat, 30 Oct 2021 10:57:26 -0700 Subject: [PATCH 052/274] Update Black target version from py36 to py39 Co-authored-by: Daniel Roy Greenfeld --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6dc08afa9..26bea3f21 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ build-backend = "setuptools.build_meta" skip-string-normalization = true exclude = '/(tests/hooks-abort-render/hooks|docs\/HelloCookieCutter1)/' line-length = 88 -target-version = ['py36'] +target-version = ['py39'] [tool.setuptools_scm] local_scheme = "no-local-version" From 120c8fa2794a244a8ea3de6ecc3994e2a7e3ba13 Mon Sep 17 00:00:00 2001 From: Audrey Roy Greenfeld Date: Sat, 30 Oct 2021 11:09:24 -0700 Subject: [PATCH 053/274] Correct linting - Update Black version - Use Black python target of Python 3.9 Co-authored-by: Daniel Roy Greenfeld --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c1f2a0886..f183b3f18 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,8 +10,8 @@ repos: language: python files: \.rst$ require_serial: true - - repo: https://github.com/python/black.git - rev: 19.10b0 + - repo: https://github.com/psf/black.git + rev: 21.9b0 hooks: - id: black language_version: python3 From 7189ca0e1438bb755401b5012e1de7a8137373f5 Mon Sep 17 00:00:00 2001 From: Audrey Roy Greenfeld Date: Wed, 3 Nov 2021 20:27:10 -0700 Subject: [PATCH 054/274] Forced re-run of PR CI --- .pre-commit-config.yaml | 2 +- CONTRIBUTING.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f183b3f18..8b6b8eb0f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ repos: hooks: - id: doc8 name: doc8 - description: This hook runs doc8 for linting docs + description: This hook runs doc8 for linting docs. entry: python -m doc8 language: python files: \.rst$ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9c2f12650..0d484ed03 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -72,7 +72,7 @@ git clone git@github.com:your_name_here/cookiecutter.git 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development: ```bash -mkvirtualenv cookiecutter + cd cookiecutter/ python setup.py develop ``` From 81f0195252bfe72fcef3d0037874ce763337bc60 Mon Sep 17 00:00:00 2001 From: Audrey Roy Greenfeld Date: Wed, 3 Nov 2021 20:36:23 -0700 Subject: [PATCH 055/274] Comment out doc8 to see if it is the issue with GHCI --- .pre-commit-config.yaml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8b6b8eb0f..229ec0f18 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,15 +1,15 @@ --- repos: - - repo: https://github.com/PyCQA/doc8 - rev: 0.8.1 - hooks: - - id: doc8 - name: doc8 - description: This hook runs doc8 for linting docs. - entry: python -m doc8 - language: python - files: \.rst$ - require_serial: true + # - repo: https://github.com/PyCQA/doc8 + # rev: 0.8.1 + # hooks: + # - id: doc8 + # name: doc8 + # description: This hook runs doc8 for linting docs. + # entry: python -m doc8 + # language: python + # files: \.rst$ + # require_serial: true - repo: https://github.com/psf/black.git rev: 21.9b0 hooks: From 5987775cfd54c07ba8ba8e3794bb69fda1242f16 Mon Sep 17 00:00:00 2001 From: Audrey Roy Greenfeld Date: Wed, 3 Nov 2021 20:39:06 -0700 Subject: [PATCH 056/274] Test to see if Black is failing --- .pre-commit-config.yaml | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 229ec0f18..ba22afc71 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,21 +1,21 @@ --- repos: - # - repo: https://github.com/PyCQA/doc8 - # rev: 0.8.1 - # hooks: - # - id: doc8 - # name: doc8 - # description: This hook runs doc8 for linting docs. - # entry: python -m doc8 - # language: python - # files: \.rst$ - # require_serial: true - - repo: https://github.com/psf/black.git - rev: 21.9b0 + - repo: https://github.com/PyCQA/doc8 + rev: 0.8.1 hooks: - - id: black - language_version: python3 - exclude: ^(tests\/hooks-abort-render\/hooks|docs\/HelloCookieCutter1) + - id: doc8 + name: doc8 + description: This hook runs doc8 for linting docs. + entry: python -m doc8 + language: python + files: \.rst$ + require_serial: true + # - repo: https://github.com/psf/black.git + # rev: 21.9b0 + # hooks: + # - id: black + # language_version: python3 + # exclude: ^(tests\/hooks-abort-render\/hooks|docs\/HelloCookieCutter1) - repo: https://github.com/pre-commit/pre-commit-hooks rev: v2.4.0 hooks: From c767bd91786c884774dabb5686660c6f2b26d51f Mon Sep 17 00:00:00 2001 From: Audrey Roy Greenfeld Date: Wed, 3 Nov 2021 20:43:56 -0700 Subject: [PATCH 057/274] Add no-verify agument to pre-commit hooks --- .pre-commit-config.yaml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ba22afc71..246050ae4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,17 +10,17 @@ repos: language: python files: \.rst$ require_serial: true - # - repo: https://github.com/psf/black.git - # rev: 21.9b0 - # hooks: - # - id: black - # language_version: python3 - # exclude: ^(tests\/hooks-abort-render\/hooks|docs\/HelloCookieCutter1) + - repo: https://github.com/psf/black.git + rev: 21.9b0 + hooks: + - id: black + language_version: python3 + exclude: ^(tests\/hooks-abort-render\/hooks|docs\/HelloCookieCutter1) - repo: https://github.com/pre-commit/pre-commit-hooks rev: v2.4.0 hooks: - id: trailing-whitespace - args: [--markdown-linebreak-ext=md] + args: [--markdown-linebreak-ext=md, --no-verify] - id: mixed-line-ending - id: check-byte-order-marker - id: check-executables-have-shebangs From def2bb64de18621db88d3529654f856cbbe7456a Mon Sep 17 00:00:00 2001 From: Audrey Roy Greenfeld Date: Wed, 3 Nov 2021 20:45:52 -0700 Subject: [PATCH 058/274] Upgrade black --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 246050ae4..ce7d43cfc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ repos: files: \.rst$ require_serial: true - repo: https://github.com/psf/black.git - rev: 21.9b0 + rev: 21.10b0 hooks: - id: black language_version: python3 @@ -20,7 +20,7 @@ repos: rev: v2.4.0 hooks: - id: trailing-whitespace - args: [--markdown-linebreak-ext=md, --no-verify] + args: [--markdown-linebreak-ext=md] - id: mixed-line-ending - id: check-byte-order-marker - id: check-executables-have-shebangs From 3b5a4340c6178cfe7d9652520736eba680092cfc Mon Sep 17 00:00:00 2001 From: Audrey Roy Greenfeld Date: Wed, 3 Nov 2021 20:47:44 -0700 Subject: [PATCH 059/274] Comment out black so the PR can proceed --- .pre-commit-config.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ce7d43cfc..d4f0cf881 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,12 +10,12 @@ repos: language: python files: \.rst$ require_serial: true - - repo: https://github.com/psf/black.git - rev: 21.10b0 - hooks: - - id: black - language_version: python3 - exclude: ^(tests\/hooks-abort-render\/hooks|docs\/HelloCookieCutter1) + # - repo: https://github.com/psf/black.git + # rev: 21.10b0 + # hooks: + # - id: black + # language_version: python3 + # exclude: ^(tests\/hooks-abort-render\/hooks|docs\/HelloCookieCutter1) - repo: https://github.com/pre-commit/pre-commit-hooks rev: v2.4.0 hooks: From c9e36a42382959a4b1ce796d2379bf83646df764 Mon Sep 17 00:00:00 2001 From: Audrey Roy Greenfeld Date: Fri, 5 Nov 2021 10:55:01 -0700 Subject: [PATCH 060/274] Run pre-commit linting - Run tox twice, which lints the code base via pre-commit --- .pre-commit-config.yaml | 12 +++++----- cookiecutter/cli.py | 4 +++- cookiecutter/main.py | 3 ++- tests/test_cli.py | 42 +++++++++++++++++++++++++++------- tests/test_generate_context.py | 8 ++++++- tests/test_get_config.py | 6 ++++- tests/test_get_user_config.py | 6 ++++- tests/test_main.py | 23 ++++++++++++++----- tests/test_prompt.py | 3 ++- tests/test_utils.py | 3 ++- tests/vcs/test_clone.py | 12 ++++++---- tests/zipfile/test_unzip.py | 19 +++++++++++---- 12 files changed, 105 insertions(+), 36 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d4f0cf881..ce7d43cfc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,12 +10,12 @@ repos: language: python files: \.rst$ require_serial: true - # - repo: https://github.com/psf/black.git - # rev: 21.10b0 - # hooks: - # - id: black - # language_version: python3 - # exclude: ^(tests\/hooks-abort-render\/hooks|docs\/HelloCookieCutter1) + - repo: https://github.com/psf/black.git + rev: 21.10b0 + hooks: + - id: black + language_version: python3 + exclude: ^(tests\/hooks-abort-render\/hooks|docs\/HelloCookieCutter1) - repo: https://github.com/pre-commit/pre-commit-hooks rev: v2.4.0 hooks: diff --git a/cookiecutter/cli.py b/cookiecutter/cli.py index 991e62c50..84241ac2a 100644 --- a/cookiecutter/cli.py +++ b/cookiecutter/cli.py @@ -78,7 +78,9 @@ def list_installed_templates(default_config, passed_config_file): help='Do not prompt for parameters and only use cookiecutter.json file content', ) @click.option( - '-c', '--checkout', help='branch, tag or commit to checkout after git clone', + '-c', + '--checkout', + help='branch, tag or commit to checkout after git clone', ) @click.option( '--directory', diff --git a/cookiecutter/main.py b/cookiecutter/main.py index 047d30a08..7c9eef731 100644 --- a/cookiecutter/main.py +++ b/cookiecutter/main.py @@ -60,7 +60,8 @@ def cookiecutter( raise InvalidModeException(err_msg) config_dict = get_user_config( - config_file=config_file, default_config=default_config, + config_file=config_file, + default_config=default_config, ) repo_dir, cleanup = determine_repo_dir( diff --git a/tests/test_cli.py b/tests/test_cli.py index e357fef2e..623945e8b 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -311,7 +311,10 @@ def test_default_user_config_overwrite(mocker, cli_runner, user_config_path): template_path = 'tests/fake-repo-pre/' result = cli_runner( - template_path, '--config-file', user_config_path, '--default-config', + template_path, + '--config-file', + user_config_path, + '--default-config', ) assert result.exit_code == 0 @@ -362,7 +365,11 @@ def test_echo_undefined_variable_error(output_dir, cli_runner): template_path = 'tests/undefined-variable/file-name/' result = cli_runner( - '--no-input', '--default-config', '--output-dir', output_dir, template_path, + '--no-input', + '--default-config', + '--output-dir', + output_dir, + template_path, ) assert result.exit_code == 1 @@ -392,7 +399,11 @@ def test_echo_unknown_extension_error(output_dir, cli_runner): template_path = 'tests/test-extensions/unknown/' result = cli_runner( - '--no-input', '--default-config', '--output-dir', output_dir, template_path, + '--no-input', + '--default-config', + '--output-dir', + output_dir, + template_path, ) assert result.exit_code == 1 @@ -404,7 +415,10 @@ def test_echo_unknown_extension_error(output_dir, cli_runner): def test_cli_extra_context(cli_runner): """Cli invocation replace content if called with replacement pairs.""" result = cli_runner( - 'tests/fake-repo-pre/', '--no-input', '-v', 'project_name=Awesomez', + 'tests/fake-repo-pre/', + '--no-input', + '-v', + 'project_name=Awesomez', ) assert result.exit_code == 0 assert os.path.isdir('fake-project') @@ -416,7 +430,10 @@ def test_cli_extra_context(cli_runner): def test_cli_extra_context_invalid_format(cli_runner): """Cli invocation raise error if called with unknown argument.""" result = cli_runner( - 'tests/fake-repo-pre/', '--no-input', '-v', 'ExtraContextWithNoEqualsSoInvalid', + 'tests/fake-repo-pre/', + '--no-input', + '-v', + 'ExtraContextWithNoEqualsSoInvalid', ) assert result.exit_code == 2 assert "Error: Invalid value for '[EXTRA_CONTEXT]...'" in result.output @@ -438,7 +455,10 @@ def test_debug_file_non_verbose(cli_runner, debug_file): assert not debug_file.exists() result = cli_runner( - '--no-input', '--debug-file', str(debug_file), 'tests/fake-repo-pre/', + '--no-input', + '--debug-file', + str(debug_file), + 'tests/fake-repo-pre/', ) assert result.exit_code == 0 @@ -493,7 +513,10 @@ def test_debug_list_installed_templates(cli_runner, debug_file, user_config_path open(os.path.join('fake-project', 'cookiecutter.json'), 'w').write('{}') result = cli_runner( - '--list-installed', '--config-file', user_config_path, str(debug_file), + '--list-installed', + '--config-file', + user_config_path, + str(debug_file), ) assert "1 installed templates:" in result.output @@ -520,7 +543,10 @@ def test_debug_list_installed_templates_failure( def test_directory_repo(cli_runner): """Test cli invocation works with `directory` option.""" result = cli_runner( - 'tests/fake-repo-dir/', '--no-input', '-v', '--directory=my-dir', + 'tests/fake-repo-dir/', + '--no-input', + '-v', + '--directory=my-dir', ) assert result.exit_code == 0 assert os.path.isdir("fake-project") diff --git a/tests/test_generate_context.py b/tests/test_generate_context.py index 2a2bc06dd..44ab99194 100644 --- a/tests/test_generate_context.py +++ b/tests/test_generate_context.py @@ -110,7 +110,13 @@ def test_default_context_replacement_in_generate_context(): def test_generate_context_decodes_non_ascii_chars(): """Verify `generate_context` correctly decodes non-ascii chars.""" - expected_context = {'non_ascii': OrderedDict([('full_name', 'éèà'),])} + expected_context = { + 'non_ascii': OrderedDict( + [ + ('full_name', 'éèà'), + ] + ) + } generated_context = generate.generate_context( context_file='tests/test-generate-context/non_ascii.json' diff --git a/tests/test_get_config.py b/tests/test_get_config.py index 760db3cdb..a37317413 100644 --- a/tests/test_get_config.py +++ b/tests/test_get_config.py @@ -62,7 +62,11 @@ def test_get_config(): 'github_username': 'example', 'project': { 'description': 'description', - 'tags': ['first', 'second', 'third',], + 'tags': [ + 'first', + 'second', + 'third', + ], }, }, 'abbreviations': { diff --git a/tests/test_get_user_config.py b/tests/test_get_user_config.py index 560c1d873..551502c5c 100644 --- a/tests/test_get_user_config.py +++ b/tests/test_get_user_config.py @@ -48,7 +48,11 @@ def custom_config(): 'github_username': 'example', 'project': { 'description': 'description', - 'tags': ['first', 'second', 'third',], + 'tags': [ + 'first', + 'second', + 'third', + ], }, }, 'cookiecutters_dir': '/home/example/some-path-to-templates', diff --git a/tests/test_main.py b/tests/test_main.py index 46b7ff9a8..ee0f738f9 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -22,11 +22,16 @@ def test_replay_dump_template_name( mocker.patch('cookiecutter.main.generate_files') cookiecutter( - '.', no_input=True, replay=False, config_file=user_config_file, + '.', + no_input=True, + replay=False, + config_file=user_config_file, ) mock_replay_dump.assert_called_once_with( - user_config_data['replay_dir'], 'fake-repo-tmpl', mocker.ANY, + user_config_data['replay_dir'], + 'fake-repo-tmpl', + mocker.ANY, ) @@ -46,11 +51,14 @@ def test_replay_load_template_name( mocker.patch('cookiecutter.main.generate_files') cookiecutter( - '.', replay=True, config_file=user_config_file, + '.', + replay=True, + config_file=user_config_file, ) mock_replay_load.assert_called_once_with( - user_config_data['replay_dir'], 'fake-repo-tmpl', + user_config_data['replay_dir'], + 'fake-repo-tmpl', ) @@ -62,9 +70,12 @@ def test_custom_replay_file(monkeypatch, mocker, user_config_file): mocker.patch('cookiecutter.main.generate_files') cookiecutter( - '.', replay='./custom-replay-file', config_file=user_config_file, + '.', + replay='./custom-replay-file', + config_file=user_config_file, ) mock_replay_load.assert_called_once_with( - '.', 'custom-replay-file', + '.', + 'custom-replay-file', ) diff --git a/tests/test_prompt.py b/tests/test_prompt.py index 0932ab573..8187cb283 100644 --- a/tests/test_prompt.py +++ b/tests/test_prompt.py @@ -80,7 +80,8 @@ class TestPrompt(object): def test_prompt_for_config(self, monkeypatch, context): """Verify `prompt_for_config` call `read_user_variable` on text request.""" monkeypatch.setattr( - 'cookiecutter.prompt.read_user_variable', lambda var, default: default, + 'cookiecutter.prompt.read_user_variable', + lambda var, default: default, ) cookiecutter_dict = prompt.prompt_for_config(context) diff --git a/tests/test_utils.py b/tests/test_utils.py index 54d07b424..988fc3887 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -122,7 +122,8 @@ def test_prompt_should_ask_and_exit_on_user_no_answer(mocker, tmp_path): """In `prompt_and_delete()`, if the user decline to delete/reclone the \ repo, cookiecutter should exit.""" mock_read_user = mocker.patch( - 'cookiecutter.utils.read_user_yes_no', return_value=False, + 'cookiecutter.utils.read_user_yes_no', + return_value=False, ) mock_sys_exit = mocker.patch('sys.exit', return_value=True) repo_dir = Path(tmp_path, 'repo') diff --git a/tests/vcs/test_clone.py b/tests/vcs/test_clone.py index 4687ea9a2..cd4ac13d7 100644 --- a/tests/vcs/test_clone.py +++ b/tests/vcs/test_clone.py @@ -24,7 +24,8 @@ def test_clone_should_rstrip_trailing_slash_in_repo_url(mocker, clone_dir): mocker.patch('cookiecutter.vcs.is_vcs_installed', autospec=True, return_value=True) mock_subprocess = mocker.patch( - 'cookiecutter.vcs.subprocess.check_output', autospec=True, + 'cookiecutter.vcs.subprocess.check_output', + autospec=True, ) vcs.clone('https://github.com/foo/bar/', clone_to_dir=str(clone_dir), no_input=True) @@ -44,7 +45,8 @@ def test_clone_should_abort_if_user_does_not_want_to_reclone(mocker, clone_dir): 'cookiecutter.vcs.prompt_and_delete', side_effect=SystemExit, autospec=True ) mock_subprocess = mocker.patch( - 'cookiecutter.vcs.subprocess.check_output', autospec=True, + 'cookiecutter.vcs.subprocess.check_output', + autospec=True, ) # Create repo_dir to trigger prompt_and_delete @@ -66,7 +68,8 @@ def test_clone_should_silent_exit_if_ok_to_reuse(mocker, tmpdir): 'cookiecutter.vcs.prompt_and_delete', return_value=False, autospec=True ) mock_subprocess = mocker.patch( - 'cookiecutter.vcs.subprocess.check_output', autospec=True, + 'cookiecutter.vcs.subprocess.check_output', + autospec=True, ) clone_to_dir = tmpdir.mkdir('clone') @@ -103,7 +106,8 @@ def test_clone_should_invoke_vcs_command( mocker.patch('cookiecutter.vcs.is_vcs_installed', autospec=True, return_value=True) mock_subprocess = mocker.patch( - 'cookiecutter.vcs.subprocess.check_output', autospec=True, + 'cookiecutter.vcs.subprocess.check_output', + autospec=True, ) expected_repo_dir = os.path.normpath(os.path.join(clone_dir, repo_name)) diff --git a/tests/zipfile/test_unzip.py b/tests/zipfile/test_unzip.py index 0231d2b2a..2bf58b00f 100644 --- a/tests/zipfile/test_unzip.py +++ b/tests/zipfile/test_unzip.py @@ -168,7 +168,9 @@ def test_unzip_url(mocker, clone_dir): request.iter_content.return_value = mock_download() mocker.patch( - 'cookiecutter.zipfile.requests.get', return_value=request, autospec=True, + 'cookiecutter.zipfile.requests.get', + return_value=request, + autospec=True, ) output_dir = zipfile.unzip( @@ -191,7 +193,9 @@ def test_unzip_url_with_empty_chunks(mocker, clone_dir): request.iter_content.return_value = mock_download_with_empty_chunks() mocker.patch( - 'cookiecutter.zipfile.requests.get', return_value=request, autospec=True, + 'cookiecutter.zipfile.requests.get', + return_value=request, + autospec=True, ) output_dir = zipfile.unzip( @@ -214,7 +218,9 @@ def test_unzip_url_existing_cache(mocker, clone_dir): request.iter_content.return_value = mock_download() mocker.patch( - 'cookiecutter.zipfile.requests.get', return_value=request, autospec=True, + 'cookiecutter.zipfile.requests.get', + return_value=request, + autospec=True, ) # Create an existing cache of the zipfile @@ -237,7 +243,9 @@ def test_unzip_url_existing_cache_no_input(mocker, clone_dir): request.iter_content.return_value = mock_download() mocker.patch( - 'cookiecutter.zipfile.requests.get', return_value=request, autospec=True, + 'cookiecutter.zipfile.requests.get', + return_value=request, + autospec=True, ) # Create an existing cache of the zipfile @@ -261,7 +269,8 @@ def test_unzip_should_abort_if_no_redownload(mocker, clone_dir): ) mock_requests_get = mocker.patch( - 'cookiecutter.zipfile.requests.get', autospec=True, + 'cookiecutter.zipfile.requests.get', + autospec=True, ) # Create an existing cache of the zipfile From 3db109d9815a21088429ce24d3ec982c4ae8f32f Mon Sep 17 00:00:00 2001 From: Audrey Roy Greenfeld Date: Wed, 10 Nov 2021 09:08:03 -0800 Subject: [PATCH 061/274] Update CONTRIBUTING.md Co-authored-by: Michael Joseph --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0d484ed03..c17a916c4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -194,7 +194,7 @@ It is possible to test with specific versions of Python. To do this, the command tox -e py37,py38 ``` -Will run py.test with the python3.7 and python3.8 interpreters, for example. +This will run `py.test` with the `python3.7` and `python3.8` interpreters. ## Core Committer Guide From 04aff1e5a0f4a8b594e486ab104b9ac5c40af4ff Mon Sep 17 00:00:00 2001 From: Christo Date: Mon, 15 Nov 2021 09:36:43 -0500 Subject: [PATCH 062/274] Hacks --- .python-version | 1 + cookiecutter/hooks.py | 16 +++++++++++++--- cookiecutter/main.py | 2 ++ cookiecutter/zipfile.py | 2 +- 4 files changed, 17 insertions(+), 4 deletions(-) create mode 100644 .python-version diff --git a/.python-version b/.python-version new file mode 100644 index 000000000..c8e988bc2 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +cookiecutter diff --git a/cookiecutter/hooks.py b/cookiecutter/hooks.py index b6a31a1e0..62c8d485c 100644 --- a/cookiecutter/hooks.py +++ b/cookiecutter/hooks.py @@ -105,13 +105,23 @@ def run_script_with_context(script_path, cwd, context): with open(script_path, 'r', encoding='utf-8') as file: contents = file.read() + temp_name = None with tempfile.NamedTemporaryFile(delete=False, mode='wb', suffix=extension) as temp: env = StrictEnvironment(context=context, keep_trailing_newline=True) template = env.from_string(contents) output = template.render(**context) - temp.write(output.encode('utf-8')) - - run_script(temp.name, cwd) + if os.getenv('COOKIECUTTER_DEBUG_HOOKS', None): + import pathlib + temp = tempfile.NamedTemporaryFile(delete=False, mode='wb', suffix=extension, dir='/tmp', prefix=os.path.basename(_)+'+') + temp = pathlib.Path(temp.name) + temp = pathlib.Path(os.path.join(temp.parent, temp.stem.split('+')[0]+temp.suffix)) + temp.write_text(output, encoding='utf-8') + temp_name = str(temp) + else: + temp.write(output.encode('utf-8')) + temp_name = temp.name + + run_script(temp_name, cwd) def run_hook(hook_name, project_dir, context): diff --git a/cookiecutter/main.py b/cookiecutter/main.py index 047d30a08..24ce296e1 100644 --- a/cookiecutter/main.py +++ b/cookiecutter/main.py @@ -103,6 +103,8 @@ def cookiecutter( dump(config_dict['replay_dir'], template_name, context) + from cookiecutter import __version__ as cookiecutter__version__ + context['__version__'] = cookiecutter__version__ # Create project from local context and project template. result = generate_files( repo_dir=repo_dir, diff --git a/cookiecutter/zipfile.py b/cookiecutter/zipfile.py index 24925c7fc..ab7a374c5 100644 --- a/cookiecutter/zipfile.py +++ b/cookiecutter/zipfile.py @@ -1,7 +1,7 @@ """Utility functions for handling and fetching repo archives in zip format.""" import os import tempfile -from zipfile import BadZipFile, ZipFile +#from zipfile import BadZipFile, ZipFile import requests From 509a9fadbb482b0f4eb1b4947e87833f39cac4ff Mon Sep 17 00:00:00 2001 From: Christo Date: Mon, 15 Nov 2021 09:41:45 -0500 Subject: [PATCH 063/274] wheel --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5a2d616e8..e19534797 100644 --- a/setup.py +++ b/setup.py @@ -9,5 +9,5 @@ # https://github.com/jazzband/pip-tools/issues/1278 setuptools.setup( use_scm_version={"local_scheme": "no-local-version"}, - setup_requires=["setuptools_scm[toml]>=3.5.0"], + setup_requires=["setuptools_scm[toml]>=3.5.0", "wheel"], ) From 8de299ebe5e97ecd909a059d3fcd2bc8ce862bdc Mon Sep 17 00:00:00 2001 From: Prathamesh Desai Date: Sat, 11 Dec 2021 22:00:06 +0530 Subject: [PATCH 064/274] Get python version from version_info tuple Co-authored-by: Sahil --- cookiecutter/cli.py | 2 +- tests/test_cli.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/cookiecutter/cli.py b/cookiecutter/cli.py index 84241ac2a..6ce8f0ada 100644 --- a/cookiecutter/cli.py +++ b/cookiecutter/cli.py @@ -25,7 +25,7 @@ def version_msg(): """Return the Cookiecutter version, location and Python powering it.""" - python_version = sys.version[:3] + python_version = str(sys.version_info[0]) + '.' + str(sys.version_info[1]) location = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) message = 'Cookiecutter %(version)s from {} (Python {})' return message.format(location, python_version) diff --git a/tests/test_cli.py b/tests/test_cli.py index 623945e8b..b5c8d3c78 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -53,6 +53,8 @@ def test_cli_version(cli_runner, version_cli_flag): assert result.exit_code == 0 assert result.output.startswith('Cookiecutter') +def test_version_msg(): + print("to do") @pytest.mark.usefixtures('make_fake_project_dir', 'remove_fake_project_dir') def test_cli_error_on_existing_output_directory(cli_runner): From 3fa6b0e5071ea337950c1df6867073e6c45f9e06 Mon Sep 17 00:00:00 2001 From: Audrey Roy Greenfeld Date: Sat, 11 Dec 2021 10:43:58 -0800 Subject: [PATCH 065/274] Update HISTORY.md --- HISTORY.md | 94 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 90 insertions(+), 4 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 132a1e028..0760c1d2b 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -2,10 +2,96 @@ History is important, but our current roadmap can be found [here](https://github.com/cookiecutter/cookiecutter/projects) -## 1.8.0 (Current master, in development) - -* Do not modify this file, since 1.7.1 Changes are generated on Pull request -title and will be added before release. +## 2.0.1 (2021-12-11) +### Breaking Changes + +* Release preparation for 2.0.1rc1 (#1608) @audreyfeldroy +* Replace poyo with pyyaml. (#1489) @dHannasch +* Added: Path templates will be rendered when copy_without_render used (#839) @noirbizarre +* Added: End of line detection and configuration. (#1407) @insspb +* Remove support for python2.7 (#1386) @ssbarnea + +### Minor Changes + +* Adopt setuptools-scm packaging (#1577) @ssbarnea +* Log the error message when git clone fails, not just the return code (#1505) @logworthy +* allow jinja 3.0.0 (#1548) @wouterdb +* Added uuid extension to be able to generate uuids (#1493) @jonaswre +* Alert user if choice is invalid (#1496) @dHannasch +* Replace poyo with pyyaml. (#1489) @dHannasch +* update AUTHOR lead (#1532) @HosamAlmoghraby +* Add Python 3.9 (#1478) @gliptak +* Added: --list-installed cli option, listing already downloaded cookiecutter packages (#1096) @chrisbrake +* Added: Jinja2 Environment extension on files generation stage (#1419) @insspb +* Added: --replay-file cli option, for replay file distributing (#906) @Cadair +* Added: _output_dir to cookiecutter context (#1034) @Casyfill +* Added: CLI option to ignore hooks (#992) @rgreinho +* Changed: Generated projects can use multiple type hooks at same time. (sh + py) (#974) @milonimrod +* Added: Path templates will be rendered when copy_without_render used (#839) @noirbizarre +* Added: End of line detection and configuration. (#1407) @insspb +* Making code python 3 only: Remove python2 u' sign, fix some strings (#1402) @insspb +* py3: remove futures, six and encoding (#1401) @insspb +* Render variables starting with an underscore. (#1339) @smoothml +* Tests refactoring: test_utils write issues fixed #1405 (#1406) @insspb + +### CI/CD and QA changes + +* enable branch coverage (#1542) @simobasso +* Make release-drafter diff only between master releases (#1568) @SharpEdgeMarshall +* ensure filesystem isolation during tests execution (#1564) @simobasso +* add safety ci step (#1560) @simobasso +* pre-commit: add bandit hook (#1559) @simobasso +* Replace tmpdir in favour of tmp_path (#1545) @SharpEdgeMarshall +* Fix linting in CI (#1546) @SharpEdgeMarshall +* Coverage 100% (#1526) @SharpEdgeMarshall +* Run coverage with matrix (#1521) @SharpEdgeMarshall +* Lint rst files (#1443) @ssbarnea +* Python3: Changed io.open to build-in open (PEP3116) (#1408) @insspb +* Making code python 3 only: Remove python2 u' sign, fix some strings (#1402) @insspb +* py3: remove futures, six and encoding (#1401) @insspb +* Removed: Bumpversion, setup.py arguments. (#1404) @insspb +* Tests refactoring: test_utils write issues fixed #1405 (#1406) @insspb +* Added: Automatic PyPI deploy on tag creation (#1400) @insspb +* Changed: Restored coverage reporter (#1399) @insspb + +### Documentation updates + +* Fix pull requests checklist reference (#1537) @glumia +* Fix author name (#1544) @HosamAlmoghraby +* Add missing contributors (#1535) @glumia +* Update CONTRIBUTING.md (#1529) @glumia +* Update LICENSE (#1519) @simobasso +* docs: rewrite the conditional files / directories example description. (#1437) @lyz-code +* Fix incorrect years in release history (#1473) @graue70 +* Add slugify in the default extensions list (#1470) @oncleben31 +* Renamed cookiecutter.package to API (#1442) @grrlic +* Fixed wording detail (#1427) @steltenpower +* Changed: CLI Commands documentation engine (#1418) @insspb +* Added: Example for conditional files / directories in hooks (#1397) @xyb +* Changed: README.md PyPI URLs changed to the modern PyPI last version (#1391) @brettcannon +* Fixed: Comma in README.md (#1390) @Cy-dev-tex +* Fixed: Replaced no longer maintained pipsi by pipx (#1395) @ndclt + +### Bugfixes + +* Add support for click 8.x (#1569) @cjolowicz +* Force click<8.0.0 (#1562) @SharpEdgeMarshall +* Remove direct dependency on markupsafe (#1549) @ssbarnea +* fixes prompting private rendered dicts (#1504) @juhuebner +* User's JSON parse error causes ugly Python exception #809 (#1468) @noone234 +* config: set default on missing default_context key (#1516) @simobasso +* Fixed: Values encoding on Windows (#1414) @agateau +* Fixed: Fail with gitolite repositories (#1144) @javiersanp +* MANIFEST: Fix file name extensions (#1387) @sebix + +### Deprecations + +* Removed: Bumpversion, setup.py arguments. (#1404) @insspb +* Removed support for Python 3.6 and PyPy (#1608) @audreyfeldroy + +### This release was made possible by our wonderful contributors: + +@Cadair, @Casyfill, @Cy-dev-tex, @HosamAlmoghraby, @SharpEdgeMarshall, @agateau, @audreyfeldroy, @brettcannon, @chrisbrake, @cjolowicz, @dHannasch, @gliptak, @glumia, @graue70, @grrlic, @insspb, @javiersanp, @jonaswre, @jsoref, @Jthevos, @juhuebner, @logworthy, @lyz-code, @milonimrod, @ndclt, @noirbizarre, @noone234, @oncleben31, @ozer619, @rgreinho, @sebix, @Sahil-101, @simobasso, @smoothml, @ssbarnea, @steltenpower, @wouterdb, @xyb, Christopher Wolfe and Hosam Almoghraby ( RIAG Digital ) ## 1.7.2 (2020-04-21) From 4741a0575cbe75dcf618e88ef8c3e0e3b2225e66 Mon Sep 17 00:00:00 2001 From: Audrey Roy Greenfeld Date: Sat, 11 Dec 2021 10:54:51 -0800 Subject: [PATCH 066/274] Bump version number to 2.0.1 Co-authored-by: Sahil Co-authored-by: Prathamesh Co-authored-by: John-Anthony --- cookiecutter/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cookiecutter/__init__.py b/cookiecutter/__init__.py index 774de19bf..fd2954f2e 100644 --- a/cookiecutter/__init__.py +++ b/cookiecutter/__init__.py @@ -1,2 +1,2 @@ """Main package for Cookiecutter.""" -__version__ = "2.0.0" +__version__ = "2.0.1" From b1403ead3bcac644eb2619642dee4b325f7b8b08 Mon Sep 17 00:00:00 2001 From: Prathamesh Desai Date: Sun, 12 Dec 2021 13:18:30 +0530 Subject: [PATCH 067/274] Added test for version_msg Co-authored-by: Sahil --- .github/workflows/main.yml | 5 +++++ tests/test_cli.py | 22 ++++++++++++++++++++-- tox.ini | 1 + 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 436de42a6..b4e49ae73 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -37,6 +37,7 @@ jobs: - "ubuntu-py37" - "ubuntu-py38" - "ubuntu-py39" + - "ubuntu-py310" - "macos-py37" - "macos-py38" @@ -59,6 +60,10 @@ jobs: python: "3.9" os: ubuntu-latest tox_env: "py39" + - name: "ubuntu-py310" + python: "3.10" + os: ubuntu-latest + tox_env: "py310" - name: "macos-py37" python: "3.7" diff --git a/tests/test_cli.py b/tests/test_cli.py index b5c8d3c78..12ca5f5bb 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -4,6 +4,9 @@ import os import re +import sys + + import pytest from click.testing import CliRunner @@ -53,8 +56,23 @@ def test_cli_version(cli_runner, version_cli_flag): assert result.exit_code == 0 assert result.output.startswith('Cookiecutter') -def test_version_msg(): - print("to do") + +def test_version_msg(cli_runner, version_cli_flag): + """Verify correct output for Cookiecutter Python version.""" + result = cli_runner(version_cli_flag) + python_major_number = sys.version_info.major + python_minor_number = sys.version_info.minor + version_output_string = result.output[:-1] + version_output_string = version_output_string[-5:-1] + if version_output_string[0] == ' ': + version_output_string = version_output_string[1:] + if version_output_string[-1] == ')': + version_output_string = version_output_string[:-1] + assert ( + str(python_major_number) + '.' + str(python_minor_number) + == version_output_string + ) + @pytest.mark.usefixtures('make_fake_project_dir', 'remove_fake_project_dir') def test_cli_error_on_existing_output_directory(cli_runner): diff --git a/tox.ini b/tox.ini index 591fc50ee..b8dd5c5e1 100644 --- a/tox.ini +++ b/tox.ini @@ -4,6 +4,7 @@ envlist = py37 py38 py39 + py310 minversion = 3.14.2 requires = virtualenv >= 20.4.5 From e5e0e96a0212219e9b80210f85b464b6226c83bc Mon Sep 17 00:00:00 2001 From: Prathamesh Desai Date: Sun, 12 Dec 2021 22:21:56 +0530 Subject: [PATCH 068/274] Use f strings and add Python 3.10 CI environments Co-authored-by: Sahil --- .github/workflows/main.yml | 10 ++++++++++ cookiecutter/cli.py | 2 +- tests/test_cli.py | 13 ++++--------- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b4e49ae73..12dce4e24 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -42,10 +42,12 @@ jobs: - "macos-py37" - "macos-py38" - "macos-py39" + - "macos-py310" - "windows-py37" - "windows-py38" - "windows-py39" + - "windows-py310" include: - name: "ubuntu-py37" @@ -77,6 +79,10 @@ jobs: python: "3.9" os: macos-latest tox_env: "py39" + - name: "macos-py310" + python: "3.10" + os: macos-latest + tox_env: "py310" - name: "windows-py37" python: "3.7" @@ -90,6 +96,10 @@ jobs: python: "3.9" os: windows-latest tox_env: "py39" + - name: "windows-py310" + python: "3.10" + os: windows-latest + tox_env: "py310" steps: - uses: actions/checkout@v2 diff --git a/cookiecutter/cli.py b/cookiecutter/cli.py index 6ce8f0ada..385e024c5 100644 --- a/cookiecutter/cli.py +++ b/cookiecutter/cli.py @@ -25,7 +25,7 @@ def version_msg(): """Return the Cookiecutter version, location and Python powering it.""" - python_version = str(sys.version_info[0]) + '.' + str(sys.version_info[1]) + python_version = f'{sys.version_info.major}.{sys.version_info.minor}' location = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) message = 'Cookiecutter %(version)s from {} (Python {})' return message.format(location, python_version) diff --git a/tests/test_cli.py b/tests/test_cli.py index 12ca5f5bb..180314373 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -51,14 +51,14 @@ def version_cli_flag(request): def test_cli_version(cli_runner, version_cli_flag): - """Verify correct version output by `cookiecutter` on cli invocation.""" + """Verify Cookiecutter version output by `cookiecutter` on cli invocation.""" result = cli_runner(version_cli_flag) assert result.exit_code == 0 assert result.output.startswith('Cookiecutter') -def test_version_msg(cli_runner, version_cli_flag): - """Verify correct output for Cookiecutter Python version.""" +def test_cli_version_python(cli_runner, version_cli_flag): + """Verify correct Python version output by `cookiecutter` on cli invocation.""" result = cli_runner(version_cli_flag) python_major_number = sys.version_info.major python_minor_number = sys.version_info.minor @@ -66,12 +66,7 @@ def test_version_msg(cli_runner, version_cli_flag): version_output_string = version_output_string[-5:-1] if version_output_string[0] == ' ': version_output_string = version_output_string[1:] - if version_output_string[-1] == ')': - version_output_string = version_output_string[:-1] - assert ( - str(python_major_number) + '.' + str(python_minor_number) - == version_output_string - ) + assert f'{python_major_number}.{python_minor_number}' == version_output_string @pytest.mark.usefixtures('make_fake_project_dir', 'remove_fake_project_dir') From 8a7a6f51688940a67c397de0396d51a696dbcece Mon Sep 17 00:00:00 2001 From: Prathamesh Desai Date: Sat, 18 Dec 2021 22:56:09 +0530 Subject: [PATCH 069/274] Prints the whole sys.version string Co-authored-by: Sahil --- cookiecutter/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cookiecutter/cli.py b/cookiecutter/cli.py index 385e024c5..6b3c583ad 100644 --- a/cookiecutter/cli.py +++ b/cookiecutter/cli.py @@ -25,7 +25,7 @@ def version_msg(): """Return the Cookiecutter version, location and Python powering it.""" - python_version = f'{sys.version_info.major}.{sys.version_info.minor}' + python_version = sys.version location = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) message = 'Cookiecutter %(version)s from {} (Python {})' return message.format(location, python_version) From cfef5740e47ec5be22be6edb51b4d919a5a724ad Mon Sep 17 00:00:00 2001 From: Prathamesh Desai Date: Sat, 18 Dec 2021 23:11:23 +0530 Subject: [PATCH 070/274] Removed python_version_test Co-authored-by: Sahil --- tests/test_cli.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 180314373..6bf093522 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -57,18 +57,6 @@ def test_cli_version(cli_runner, version_cli_flag): assert result.output.startswith('Cookiecutter') -def test_cli_version_python(cli_runner, version_cli_flag): - """Verify correct Python version output by `cookiecutter` on cli invocation.""" - result = cli_runner(version_cli_flag) - python_major_number = sys.version_info.major - python_minor_number = sys.version_info.minor - version_output_string = result.output[:-1] - version_output_string = version_output_string[-5:-1] - if version_output_string[0] == ' ': - version_output_string = version_output_string[1:] - assert f'{python_major_number}.{python_minor_number}' == version_output_string - - @pytest.mark.usefixtures('make_fake_project_dir', 'remove_fake_project_dir') def test_cli_error_on_existing_output_directory(cli_runner): """Test cli invocation without `overwrite-if-exists` fail if dir exist.""" From 11743af681e37a998748f69d646661b7a2043ff0 Mon Sep 17 00:00:00 2001 From: Prathamesh Desai Date: Sat, 18 Dec 2021 23:17:36 +0530 Subject: [PATCH 071/274] Fixed linting errors Co-authored-by: Sahil --- tests/test_cli.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 6bf093522..19740ef26 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -4,8 +4,6 @@ import os import re -import sys - import pytest from click.testing import CliRunner From db5fce2ee25b5ece417c12dddfc712beaace61fe Mon Sep 17 00:00:00 2001 From: Prathamesh Desai Date: Mon, 27 Dec 2021 23:16:19 +0530 Subject: [PATCH 072/274] Removed changes related to setuptools_scm --- .github/workflows/main.yml | 2 - pyproject.toml | 12 ------ setup.cfg | 77 +---------------------------------- setup.py | 83 ++++++++++++++++++++++++++++++++------ tox.ini | 25 ------------ 5 files changed, 71 insertions(+), 128 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 12dce4e24..8fb12c2b4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -24,8 +24,6 @@ jobs: pip install tox virtualenv - name: Lint run: "tox -e lint" - - name: Packaging - run: "tox -e packaging" - name: Safety run: "tox -e safety" build: diff --git a/pyproject.toml b/pyproject.toml index 26bea3f21..2240e4443 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,17 +1,5 @@ -[build-system] -requires = [ - "setuptools >= 42.0.0", # required by pyproject+setuptools_scm integration - "setuptools_scm[toml] >= 3.5.0", # required for "no-local-version" scheme - "setuptools_scm_git_archive >= 1.0", - "wheel", -] -build-backend = "setuptools.build_meta" - [tool.black] skip-string-normalization = true exclude = '/(tests/hooks-abort-render/hooks|docs\/HelloCookieCutter1)/' line-length = 88 target-version = ['py39'] - -[tool.setuptools_scm] -local_scheme = "no-local-version" diff --git a/setup.cfg b/setup.cfg index e1ddc4fbb..6d65158d3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,78 +1,3 @@ -[metadata] -name = cookiecutter -url = https://github.com/cookiecutter/cookiecutter -project_urls = - Bug Tracker = https://github.com/cookiecutter/cookiecutter/issues - CI: GitHub = https://github.com/cookiecutter/cookiecutter/actions - Documentation = https://cookiecutter.readthedocs.io/ - Source Code = https://github.com/cookiecutter/cookiecutter -description = - A command-line utility that creates projects from project - templates, e.g. creating a Python package project from a - Python package project template. -long_description = file: README.md -long_description_content_type = text/markdown -author = Audrey Roy Greenfeld -author_email = audreyr@gmail.com -maintainer = Audrey Roy Greenfeld -maintainer_email = audreyr@gmail.com -license = BSD -license_file = LICENSE -classifiers = - Development Status :: 5 - Production/Stable - Environment :: Console - Intended Audience :: Developers - Natural Language :: English - License :: OSI Approved :: BSD License - Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: Implementation :: CPython - Programming Language :: Python - Topic :: Software Development -keywords = - cookiecutter - Python - projects - project templates - Jinja2 - skeleton - scaffolding - project directory - package - packaging - -[options] -use_scm_version = True -python_requires = >=3.6 -; package_dir = -; = src -packages = cookiecutter -zip_safe = False - -# These are required during `setup.py` run: -setup_requires = - setuptools_scm>=1.15.0 - setuptools_scm_git_archive>=1.0 - -install_requires = - binaryornot>=0.4.4 - Jinja2>=2.7,<4.0.0 - click>=7.0,<9.0.0 - pyyaml>=5.3.1 - jinja2-time>=0.2.0 - python-slugify>=4.0.0 - requests>=2.23.0 - -[options.entry_points] -console_scripts = - cookiecutter = cookiecutter.__main__:main - -[flake8] -ignore = BLK100,E231,W503 - # Excludes due to known issues or incompatibilities with black: # BLK100: Black would make changes. https://pypi.org/project/flake8-black/ # W503: https://github.com/psf/black/search?q=W503&unscoped_q=W503 @@ -83,7 +8,7 @@ statistics = 1 max-line-length = 88 [bdist_wheel] -universal = false +universal = 1 [tool:pytest] testpaths = tests diff --git a/setup.py b/setup.py index 5a2d616e8..c2db2f187 100644 --- a/setup.py +++ b/setup.py @@ -1,13 +1,70 @@ -#! /usr/bin/env python3 -"""cookiecutter distutils configuration. - -The presence of this file ensures the support -of pip editable mode *with setuptools only*. -""" -import setuptools - -# https://github.com/jazzband/pip-tools/issues/1278 -setuptools.setup( - use_scm_version={"local_scheme": "no-local-version"}, - setup_requires=["setuptools_scm[toml]>=3.5.0"], -) +#!/usr/bin/env python +"""cookiecutter distutils configuration.""" +from setuptools import setup + +version = "2.0.0" + +with open('README.md', encoding='utf-8') as readme_file: + readme = readme_file.read() + +requirements = [ + 'binaryornot>=0.4.4', + 'Jinja2>=2.7,<4.0.0', + 'click>=7.0,<8.0.0', + 'pyyaml>=5.3.1', + 'jinja2-time>=0.2.0', + 'python-slugify>=4.0.0', + 'requests>=2.23.0', +] + +setup( + name='cookiecutter', + version=version, + description=( + 'A command-line utility that creates projects from project ' + 'templates, e.g. creating a Python package project from a ' + 'Python package project template.' + ), + long_description=readme, + long_description_content_type='text/markdown', + author='Audrey Feldroy', + author_email='audreyr@gmail.com', + url='https://github.com/cookiecutter/cookiecutter', + packages=['cookiecutter'], + package_dir={'cookiecutter': 'cookiecutter'}, + entry_points={'console_scripts': ['cookiecutter = cookiecutter.__main__:main']}, + include_package_data=True, + python_requires='>=3.6', + install_requires=requirements, + license='BSD', + zip_safe=False, + classifiers=[ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: Developers", + "Natural Language :: English", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Programming Language :: Python", + "Topic :: Software Development", + ], + keywords=[ + "cookiecutter", + "Python", + "projects", + "project templates", + "Jinja2", + "skeleton", + "scaffolding", + "project directory", + "package", + "packaging", + ], +) \ No newline at end of file diff --git a/tox.ini b/tox.ini index b8dd5c5e1..4cde29296 100644 --- a/tox.ini +++ b/tox.ini @@ -42,28 +42,3 @@ commands = safety check --full-report deps = safety - -[testenv:packaging] -description = - Build package, verify metadata, install package and assert behavior when ansible is missing. -deps = - build - twine -skip_install = true -commands = - {envpython} -c 'import os.path, shutil, sys; \ - dist_dir = os.path.join("{toxinidir}", "dist"); \ - os.path.isdir(dist_dir) or sys.exit(0); \ - print("Removing \{!s\} contents...".format(dist_dir), file=sys.stderr); \ - shutil.rmtree(dist_dir)' - # build using moder python build (PEP-517) - {envpython} -m build \ - --sdist \ - --wheel \ - --outdir {toxinidir}/dist/ \ - {toxinidir} - # Validate metadata using twine - twine check {toxinidir}/dist/* - # Install the wheel - sh -c "python3 -m pip install {toxinidir}/dist/*.whl" -whitelist_externals = sh From c578dce794956641b5f3baa86e804b0ccad1276f Mon Sep 17 00:00:00 2001 From: Prathamesh Desai Date: Mon, 27 Dec 2021 23:29:39 +0530 Subject: [PATCH 073/274] Fixed flake8 section in setup.cfg --- setup.cfg | 3 ++- setup.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index 6d65158d3..938b39de6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,8 +1,9 @@ +[flake8] # Excludes due to known issues or incompatibilities with black: # BLK100: Black would make changes. https://pypi.org/project/flake8-black/ # W503: https://github.com/psf/black/search?q=W503&unscoped_q=W503 # E231: https://github.com/psf/black/issues/1202 - +ignore = BLK100,E231,W503 statistics = 1 # black official is 88 max-line-length = 88 diff --git a/setup.py b/setup.py index c2db2f187..654010fa0 100644 --- a/setup.py +++ b/setup.py @@ -38,7 +38,7 @@ install_requires=requirements, license='BSD', zip_safe=False, - classifiers=[ + classifiers=[ "Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: Developers", @@ -67,4 +67,4 @@ "package", "packaging", ], -) \ No newline at end of file +) From 333b6ef9f9d898cb1f2d1363acad4519d8449004 Mon Sep 17 00:00:00 2001 From: Audrey Roy Greenfeld Date: Mon, 27 Dec 2021 10:23:48 -0800 Subject: [PATCH 074/274] Release 2.0.2 --- HISTORY.md | 6 +++++- cookiecutter/__init__.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 0760c1d2b..d9b1a5a28 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -2,6 +2,10 @@ History is important, but our current roadmap can be found [here](https://github.com/cookiecutter/cookiecutter/projects) +## 2.0.2 (2021-12-27) + +* Fix Python version number in cookiecutter --version and test on Python 3.10 (#1621) @ozer550 + ## 2.0.1 (2021-12-11) ### Breaking Changes @@ -91,7 +95,7 @@ History is important, but our current roadmap can be found [here](https://github ### This release was made possible by our wonderful contributors: -@Cadair, @Casyfill, @Cy-dev-tex, @HosamAlmoghraby, @SharpEdgeMarshall, @agateau, @audreyfeldroy, @brettcannon, @chrisbrake, @cjolowicz, @dHannasch, @gliptak, @glumia, @graue70, @grrlic, @insspb, @javiersanp, @jonaswre, @jsoref, @Jthevos, @juhuebner, @logworthy, @lyz-code, @milonimrod, @ndclt, @noirbizarre, @noone234, @oncleben31, @ozer619, @rgreinho, @sebix, @Sahil-101, @simobasso, @smoothml, @ssbarnea, @steltenpower, @wouterdb, @xyb, Christopher Wolfe and Hosam Almoghraby ( RIAG Digital ) +@Cadair, @Casyfill, @Cy-dev-tex, @HosamAlmoghraby, @SharpEdgeMarshall, @agateau, @audreyfeldroy, @brettcannon, @chrisbrake, @cjolowicz, @dHannasch, @gliptak, @glumia, @graue70, @grrlic, @insspb, @javiersanp, @jonaswre, @jsoref, @Jthevos, @juhuebner, @logworthy, @lyz-code, @milonimrod, @ndclt, @noirbizarre, @noone234, @oncleben31, @ozer550, @rgreinho, @sebix, @Sahil-101, @simobasso, @smoothml, @ssbarnea, @steltenpower, @wouterdb, @xyb, Christopher Wolfe and Hosam Almoghraby ( RIAG Digital ) ## 1.7.2 (2020-04-21) diff --git a/cookiecutter/__init__.py b/cookiecutter/__init__.py index fd2954f2e..eab68df8e 100644 --- a/cookiecutter/__init__.py +++ b/cookiecutter/__init__.py @@ -1,2 +1,2 @@ """Main package for Cookiecutter.""" -__version__ = "2.0.1" +__version__ = "2.0.2" From 0b406256d2c0aa728f860692d57f49c0d2beb39d Mon Sep 17 00:00:00 2001 From: Audrey Roy Greenfeld Date: Mon, 27 Dec 2021 10:28:18 -0800 Subject: [PATCH 075/274] Update 2.0.2 release notes --- HISTORY.md | 1 + 1 file changed, 1 insertion(+) diff --git a/HISTORY.md b/HISTORY.md index d9b1a5a28..5fc9fd5a9 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -5,6 +5,7 @@ History is important, but our current roadmap can be found [here](https://github ## 2.0.2 (2021-12-27) * Fix Python version number in cookiecutter --version and test on Python 3.10 (#1621) @ozer550 +* Removed changes related to setuptools_scm (#1629) @audreyfeldroy @ozer550 ## 2.0.1 (2021-12-11) ### Breaking Changes From d2c139dd74bda679883a5d5739fada8ba1b2315c Mon Sep 17 00:00:00 2001 From: Christo Date: Fri, 7 Jan 2022 08:32:48 -0500 Subject: [PATCH 076/274] Unlink unused temp file --- cookiecutter/hooks.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cookiecutter/hooks.py b/cookiecutter/hooks.py index 62c8d485c..ad6d064a0 100644 --- a/cookiecutter/hooks.py +++ b/cookiecutter/hooks.py @@ -114,6 +114,7 @@ def run_script_with_context(script_path, cwd, context): import pathlib temp = tempfile.NamedTemporaryFile(delete=False, mode='wb', suffix=extension, dir='/tmp', prefix=os.path.basename(_)+'+') temp = pathlib.Path(temp.name) + temp.unlink() temp = pathlib.Path(os.path.join(temp.parent, temp.stem.split('+')[0]+temp.suffix)) temp.write_text(output, encoding='utf-8') temp_name = str(temp) From c0e7698097d905938cd9ecd1a08d9a2c99c4f887 Mon Sep 17 00:00:00 2001 From: Marco Westerhof Date: Fri, 18 Mar 2022 17:04:19 +0100 Subject: [PATCH 077/274] Feature/local extensions (#1240) Co-authored-by: Andrey Shpak --- cookiecutter/main.py | 47 ++++++++++----- cookiecutter/utils.py | 13 ++++ docs/advanced/index.rst | 1 + docs/advanced/local_extensions.rst | 60 +++++++++++++++++++ .../local_extension/cookiecutter.json | 10 ++++ .../local_extensions/__init__.py | 1 + .../local_extension/local_extensions/main.py | 21 +++++++ .../{{cookiecutter.project_slug}}/HISTORY.rst | 8 +++ tests/test_cli.py | 31 ++++++++++ 9 files changed, 178 insertions(+), 14 deletions(-) create mode 100644 docs/advanced/local_extensions.rst create mode 100644 tests/test-extensions/local_extension/cookiecutter.json create mode 100644 tests/test-extensions/local_extension/local_extensions/__init__.py create mode 100644 tests/test-extensions/local_extension/local_extensions/main.py create mode 100644 tests/test-extensions/local_extension/{{cookiecutter.project_slug}}/HISTORY.rst diff --git a/cookiecutter/main.py b/cookiecutter/main.py index 7c9eef731..bc2f262df 100644 --- a/cookiecutter/main.py +++ b/cookiecutter/main.py @@ -4,8 +4,10 @@ The code in this module is also a good example of how to use Cookiecutter as a library rather than a script. """ +from copy import copy import logging import os +import sys from cookiecutter.config import get_user_config from cookiecutter.exceptions import InvalidModeException @@ -73,15 +75,17 @@ def cookiecutter( password=password, directory=directory, ) + import_patch = _patch_import_path_for_repo(repo_dir) template_name = os.path.basename(os.path.abspath(repo_dir)) if replay: - if isinstance(replay, bool): - context = load(config_dict['replay_dir'], template_name) - else: - path, template_name = os.path.split(os.path.splitext(replay)[0]) - context = load(path, template_name) + with import_patch: + if isinstance(replay, bool): + context = load(config_dict['replay_dir'], template_name) + else: + path, template_name = os.path.split(os.path.splitext(replay)[0]) + context = load(path, template_name) else: context_file = os.path.join(repo_dir, 'cookiecutter.json') logger.debug('context_file is %s', context_file) @@ -94,7 +98,8 @@ def cookiecutter( # 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) + with import_patch: + context['cookiecutter'] = prompt_for_config(context, no_input) # include template dir or url in the context dict context['cookiecutter']['_template'] = template @@ -105,17 +110,31 @@ def cookiecutter( dump(config_dict['replay_dir'], template_name, context) # Create project from local context and project template. - result = generate_files( - repo_dir=repo_dir, - context=context, - overwrite_if_exists=overwrite_if_exists, - skip_if_file_exists=skip_if_file_exists, - output_dir=output_dir, - accept_hooks=accept_hooks, - ) + with import_patch: + result = generate_files( + repo_dir=repo_dir, + context=context, + overwrite_if_exists=overwrite_if_exists, + skip_if_file_exists=skip_if_file_exists, + output_dir=output_dir, + accept_hooks=accept_hooks, + ) # Cleanup (if required) if cleanup: rmtree(repo_dir) return result + + +class _patch_import_path_for_repo: + def __init__(self, repo_dir): + self._repo_dir = repo_dir + self._path = None + + def __enter__(self): + self._path = copy(sys.path) + sys.path.append(self._repo_dir) + + def __exit__(self, type, value, traceback): + sys.path = self._path diff --git a/cookiecutter/utils.py b/cookiecutter/utils.py index 19b727a52..ef533171a 100644 --- a/cookiecutter/utils.py +++ b/cookiecutter/utils.py @@ -8,6 +8,7 @@ import sys from cookiecutter.prompt import read_user_yes_no +from jinja2.ext import Extension logger = logging.getLogger(__name__) @@ -105,3 +106,15 @@ def prompt_and_delete(path, no_input=False): return False sys.exit() + + +def simple_filter(filter_function): + """Decorate a function to wrap it in a simplified jinja2 extension.""" + + class SimpleFilterExtension(Extension): + def __init__(self, environment): + super(SimpleFilterExtension, self).__init__(environment) + environment.filters[filter_function.__name__] = filter_function + + SimpleFilterExtension.__name__ = filter_function.__name__ + return SimpleFilterExtension diff --git a/docs/advanced/index.rst b/docs/advanced/index.rst index 90e7f2933..66d5faadd 100644 --- a/docs/advanced/index.rst +++ b/docs/advanced/index.rst @@ -23,3 +23,4 @@ Various advanced topics regarding cookiecutter usage. template_extensions directories new_line_characters + local_extensions diff --git a/docs/advanced/local_extensions.rst b/docs/advanced/local_extensions.rst new file mode 100644 index 000000000..bf87caf7b --- /dev/null +++ b/docs/advanced/local_extensions.rst @@ -0,0 +1,60 @@ +.. _`template extensions`: + +Local Extensions +---------------- + +*New in Cookiecutter X.x* + +A template may extend the Cookiecutter environment with local extensions. +These can be part of the template itself, providing it with more sophisticated custom tags and filters. + +To do so, a template author must specify the required extensions in ``cookiecutter.json`` as follows: + +.. code-block:: json + + { + "project_slug": "Foobar", + "year": "{% now 'utc', '%Y' %}", + "_extensions": ["local_extensions.FoobarExtension"] + } + +This example assumes that a ``local_extensions`` folder (python module) exists in the template root. +It will contain a ``main.py`` file, containing the following (for instance): + +.. code-block:: python + + # -*- coding: utf-8 -*- + + from jinja2.ext import Extension + + + class FoobarExtension(Extension): + def __init__(self, environment): + super(FoobarExtension, self).__init__(environment) + environment.filters['foobar'] = lambda v: v * 2 + +This will register the ``foobar`` filter for the template. + +For many cases, this will be unneccessarily complicated. It's likely that we'd only want to register a single function +as a filter. For this, we can use the ``simple_filter`` decorator: + +.. code-block:: json + + { + "project_slug": "Foobar", + "year": "{% now 'utc', '%Y' %}", + "_extensions": ["local_extensions.simplefilterextension"] + } + +.. code-block:: python + + # -*- coding: utf-8 -*- + + from cookiecutter.utils import simple_filter + + + @simple_filter + def simplefilterextension(v): + return v * 2 + +This snippet will achieve the exact same result as the previous one. diff --git a/tests/test-extensions/local_extension/cookiecutter.json b/tests/test-extensions/local_extension/cookiecutter.json new file mode 100644 index 000000000..8141fd508 --- /dev/null +++ b/tests/test-extensions/local_extension/cookiecutter.json @@ -0,0 +1,10 @@ +{ + "project_slug": "Foobar", + "test_value_class_based": "{{cookiecutter.project_slug | foobar}}", + "test_value_function_based": "{{cookiecutter.project_slug | simplefilterextension}}", + "_extensions": [ + "local_extensions.simplefilterextension", + "local_extensions.FoobarExtension" + ] +} + diff --git a/tests/test-extensions/local_extension/local_extensions/__init__.py b/tests/test-extensions/local_extension/local_extensions/__init__.py new file mode 100644 index 000000000..94e854abd --- /dev/null +++ b/tests/test-extensions/local_extension/local_extensions/__init__.py @@ -0,0 +1 @@ +from .main import FoobarExtension, simplefilterextension # noqa diff --git a/tests/test-extensions/local_extension/local_extensions/main.py b/tests/test-extensions/local_extension/local_extensions/main.py new file mode 100644 index 000000000..53f6f8f95 --- /dev/null +++ b/tests/test-extensions/local_extension/local_extensions/main.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + +"""Provides custom extension, exposing a ``foobar`` filter.""" + +from jinja2.ext import Extension +from cookiecutter.utils import simple_filter + + +class FoobarExtension(Extension): + """Simple jinja2 extension for cookiecutter test purposes.""" + + def __init__(self, environment): + """Foobar Extension Constructor.""" + super(FoobarExtension, self).__init__(environment) + environment.filters['foobar'] = lambda v: v * 2 + + +@simple_filter +def simplefilterextension(v): + """Provide a simple function-based filter extension.""" + return v.upper() diff --git a/tests/test-extensions/local_extension/{{cookiecutter.project_slug}}/HISTORY.rst b/tests/test-extensions/local_extension/{{cookiecutter.project_slug}}/HISTORY.rst new file mode 100644 index 000000000..8bb7c6136 --- /dev/null +++ b/tests/test-extensions/local_extension/{{cookiecutter.project_slug}}/HISTORY.rst @@ -0,0 +1,8 @@ +History +------- + +0.1.0 +----- + +First release of {{cookiecutter.test_value_class_based}} on PyPI. +{{cookiecutter.test_value_function_based}} diff --git a/tests/test_cli.py b/tests/test_cli.py index 19740ef26..ad6abd1e0 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -10,6 +10,8 @@ from cookiecutter import utils from cookiecutter.__main__ import main +from cookiecutter.environment import StrictEnvironment +from cookiecutter.exceptions import UnknownExtension from cookiecutter.main import cookiecutter @@ -412,6 +414,35 @@ def test_echo_unknown_extension_error(output_dir, cli_runner): assert 'Unable to load extension: ' in result.output +def test_local_extension(tmpdir, cli_runner): + """Test to verify correct work of extension, included in template.""" + output_dir = str(tmpdir.mkdir('output')) + template_path = 'tests/test-extensions/local_extension/' + + result = cli_runner( + '--no-input', + '--default-config', + '--output-dir', + output_dir, + template_path, + ) + assert result.exit_code == 0 + with open(os.path.join(output_dir, 'Foobar', 'HISTORY.rst')) as f: + data = f.read() + assert 'FoobarFoobar' in data + assert 'FOOBAR' in data + + +def test_local_extension_not_available(tmpdir, cli_runner): + """Test handling of included but unavailable local extension.""" + context = {'cookiecutter': {'_extensions': ['foobar']}} + + with pytest.raises(UnknownExtension) as err: + StrictEnvironment(context=context, keep_trailing_newline=True) + + assert 'Unable to load extension: ' in str(err.value) + + @pytest.mark.usefixtures('remove_fake_project_dir') def test_cli_extra_context(cli_runner): """Cli invocation replace content if called with replacement pairs.""" From 0711b74d03fce772c32090fcfcce74f4efd08f10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20La=C5=84ski?= Date: Fri, 18 Mar 2022 17:05:16 +0100 Subject: [PATCH 078/274] Restore accidentally deleted support for click 8.x (#1643) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 654010fa0..d703c3c29 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ requirements = [ 'binaryornot>=0.4.4', 'Jinja2>=2.7,<4.0.0', - 'click>=7.0,<8.0.0', + 'click>=7.0,<9.0.0', 'pyyaml>=5.3.1', 'jinja2-time>=0.2.0', 'python-slugify>=4.0.0', From 603b0365689d8b5407fed4d6475e1eb8256fdf33 Mon Sep 17 00:00:00 2001 From: Bruno Alla Date: Fri, 18 Mar 2022 16:08:39 +0000 Subject: [PATCH 079/274] Update badge & links from Slack to Discord in README (#1612) Co-authored-by: Michael Joseph Co-authored-by: Sorin Sbarnea --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9b1dfe141..8e7e2b240 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![python](https://img.shields.io/pypi/pyversions/cookiecutter.svg)](https://pypi.org/project/cookiecutter/) [![Build Status](https://github.com/cookiecutter/cookiecutter/actions/workflows/main.yml/badge.svg?branch=master)](https://github.com/cookiecutter/cookiecutter/actions) [![codecov](https://codecov.io/gh/cookiecutter/cookiecutter/branch/master/graphs/badge.svg?branch=master)](https://codecov.io/github/cookiecutter/cookiecutter?branch=master) -[![slack](https://img.shields.io/badge/cookiecutter-Join%20on%20Slack-green?style=flat&logo=slack)](https://join.slack.com/t/cookie-cutter/shared_invite/enQtNzI0Mzg5NjE5Nzk5LTRlYWI2YTZhYmQ4YmU1Y2Q2NmE1ZjkwOGM0NDQyNTIwY2M4ZTgyNDVkNjMxMDdhZGI5ZGE5YmJjM2M3ODJlY2U) +[![discord](https://img.shields.io/badge/Discord-cookiecutter-5865F2?style=flat&logo=discord&logoColor=white)](https://discord.gg/9BrxzPKuEW) [![docs](https://readthedocs.org/projects/cookiecutter/badge/?version=latest)](https://readthedocs.org/projects/cookiecutter/?badge=latest) [![Code Quality](https://img.shields.io/scrutinizer/g/cookiecutter/cookiecutter.svg)](https://scrutinizer-ci.com/g/cookiecutter/cookiecutter/?branch=master) @@ -178,7 +178,7 @@ Stuck? Try one of the following: about the problem, even if it's just "I can't get it to work on this cookiecutter" with a link to your cookiecutter. Don't worry about naming/pinpointing the issue properly. -- Ask for help on [Slack](https://join.slack.com/t/cookie-cutter/shared_invite/enQtNzI0Mzg5NjE5Nzk5LTRlYWI2YTZhYmQ4YmU1Y2Q2NmE1ZjkwOGM0NDQyNTIwY2M4ZTgyNDVkNjMxMDdhZGI5ZGE5YmJjM2M3ODJlY2U) +- Ask for help on [Discord](https://discord.gg/9BrxzPKuEW) if you must (but please try one of the other options first, so that others can benefit from the discussion). @@ -189,8 +189,8 @@ Development on Cookiecutter is community-driven: - Everyone is invited to contribute. Read the [contributing instructions](CONTRIBUTING.md), then get started. - Connect with other Cookiecutter contributors and users on - [Slack](https://join.slack.com/t/cookie-cutter/shared_invite/enQtNzI0Mzg5NjE5Nzk5LTRlYWI2YTZhYmQ4YmU1Y2Q2NmE1ZjkwOGM0NDQyNTIwY2M4ZTgyNDVkNjMxMDdhZGI5ZGE5YmJjM2M3ODJlY2U) - (note: due to work and commitments, a core committer might not always be available) + [Discord](https://discord.gg/9BrxzPKuEW) + (note: due to work and other commitments, a core committer might not always be available) Encouragement is unbelievably motivating. If you want more work done on Cookiecutter, show support: From 682fe854dca3ce735d656a6588e9ea9369c4ea47 Mon Sep 17 00:00:00 2001 From: Prathamesh Desai Date: Fri, 18 Mar 2022 21:39:24 +0530 Subject: [PATCH 080/274] Improve local development step 3 (#1610) --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c17a916c4..f54ae0e06 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -74,7 +74,7 @@ git clone git@github.com:your_name_here/cookiecutter.git ```bash cd cookiecutter/ -python setup.py develop +pip install -e . ``` 4. Create a branch for local development: From 980ff089efaef27b99b97525f6a206bd7c3f2282 Mon Sep 17 00:00:00 2001 From: Maciej Patro Date: Sat, 7 May 2022 09:55:59 +0200 Subject: [PATCH 081/274] test_generate_file_verbose_template_syntax_error fixed Fixes #1655 - relative path that comes directly from Jinja2 TemplateSyntaxError in some cases is in format "./tests/..." in other "tests/... now both cases are accepted as a valid outcome. --- tests/test_generate_file.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/test_generate_file.py b/tests/test_generate_file.py index 2ca30dff2..943399939 100644 --- a/tests/test_generate_file.py +++ b/tests/test_generate_file.py @@ -1,6 +1,7 @@ """Tests for `generate_file` function, part of `generate_files` function workflow.""" import json import os +import re import pytest from jinja2 import FileSystemLoader @@ -114,17 +115,16 @@ def test_generate_file_with_false_condition(env): @pytest.fixture -def expected_msg(): +def expected_msg_regex(): """Fixture. Used to ensure that exception generated text contain full data.""" - msg = ( + return re.compile( 'Missing end of comment tag\n' - ' File "./tests/files/syntax_error.txt", line 1\n' - ' I eat {{ syntax_error }} {# this comment is not closed}' + ' {2}File "(.[/\\\\])*tests[/\\\\]files[/\\\\]syntax_error.txt", line 1\n' + ' {4}I eat {{ syntax_error }} {# this comment is not closed}' ) - return msg.replace("/", os.sep) -def test_generate_file_verbose_template_syntax_error(env, expected_msg): +def test_generate_file_verbose_template_syntax_error(env, expected_msg_regex): """Verify correct exception raised on syntax error in file before generation.""" with pytest.raises(TemplateSyntaxError) as exception: generate.generate_file( @@ -133,7 +133,7 @@ def test_generate_file_verbose_template_syntax_error(env, expected_msg): context={'syntax_error': 'syntax_error'}, env=env, ) - assert str(exception.value) == expected_msg + assert expected_msg_regex.match(str(exception.value)) is not None def test_generate_file_does_not_translate_lf_newlines_to_crlf(env, tmp_path): From 6eb9ea81be1fed3d58197800630e07e14380f7a4 Mon Sep 17 00:00:00 2001 From: Maciej Patro Date: Mon, 16 May 2022 21:31:44 +0200 Subject: [PATCH 082/274] Remove redundant comparison Co-authored-by: jurgenwigg <53076001+jurgenwigg@users.noreply.github.com> --- tests/test_generate_file.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_generate_file.py b/tests/test_generate_file.py index 943399939..a393a2ef8 100644 --- a/tests/test_generate_file.py +++ b/tests/test_generate_file.py @@ -133,7 +133,7 @@ def test_generate_file_verbose_template_syntax_error(env, expected_msg_regex): context={'syntax_error': 'syntax_error'}, env=env, ) - assert expected_msg_regex.match(str(exception.value)) is not None + assert expected_msg_regex.match(str(exception.value)) def test_generate_file_does_not_translate_lf_newlines_to_crlf(env, tmp_path): From 05267c6aac1ee5027c422c086dfbd9fbf867b8bf Mon Sep 17 00:00:00 2001 From: Maciej Patro Date: Mon, 16 May 2022 21:42:20 +0200 Subject: [PATCH 083/274] Fix black incompatibility with click 8.1.0 version --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ce7d43cfc..1ba7fa3e1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ repos: files: \.rst$ require_serial: true - repo: https://github.com/psf/black.git - rev: 21.10b0 + rev: 22.3.0 hooks: - id: black language_version: python3 From 2b6de977046b8d3aeed7f2fd4d1841b59e6396f9 Mon Sep 17 00:00:00 2001 From: Daniel Roy Greenfeld Date: Tue, 24 May 2022 10:37:05 -0700 Subject: [PATCH 084/274] Add Audrey Roy Greenfeld as creator and leader --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 8e7e2b240..6a153c097 100644 --- a/README.md +++ b/README.md @@ -233,3 +233,7 @@ organizations and individuals to support the project. Everyone interacting in the Cookiecutter project's codebases, issue trackers, chat rooms, and mailing lists is expected to follow the [PyPA Code of Conduct](https://www.pypa.io/en/latest/code-of-conduct/). + +## Creator / Leader + +This project was created and is led by [Audrey Roy Greenfeld](https://github.com/audreyfeldroy). From 43ccfcc8e6cfdc41644119678a827b9c1cf524e6 Mon Sep 17 00:00:00 2001 From: Daniel Roy Greenfeld Date: Tue, 24 May 2022 10:37:19 -0700 Subject: [PATCH 085/274] Update README.md --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 6a153c097..a954feca2 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,6 @@ template. ![Cookiecutter](https://raw.githubusercontent.com/cookiecutter/cookiecutter/3ac078356adf5a1a72042dfe72ebfa4a9cd5ef38/logo/cookiecutter_medium.png) -We are proud to be an open source sponsor of -[PyCon 2016](https://us.pycon.org/2016/sponsors/). - ## Features - Cross-platform: Windows, Mac, and Linux are officially supported. From 050246683e2b95f35898c013fff6d14a51c2b5e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20C=2E=20Barrionuevo=20da=20Luz?= Date: Sun, 29 May 2022 16:08:17 -0300 Subject: [PATCH 086/274] Fixed incorrect link on docs. (#1649) Close #1648 --- docs/advanced/choice_variables.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/advanced/choice_variables.rst b/docs/advanced/choice_variables.rst index c86eb11c1..ccb35d4bf 100644 --- a/docs/advanced/choice_variables.rst +++ b/docs/advanced/choice_variables.rst @@ -39,7 +39,7 @@ can be used like this:: {% endif %} -Cookiecutter is using `Jinja2's if conditional expression `_ to determine the correct license. +Cookiecutter is using `Jinja2's if conditional expression `_ to determine the correct license. The created choice variable is still a regular Cookiecutter variable and can be used like this:: From 08fbbee72d3a21431f93bd534432939c2d2999b0 Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Mon, 30 May 2022 01:59:29 +0200 Subject: [PATCH 087/274] Documentation overhaul. --- .gitmodules | 3 - HISTORY.md | 9 +- README.md | 212 ++++++++++++-------------- docs/HelloCookieCutter1 | 1 - docs/advanced/index.rst | 1 - docs/advanced/local_extensions.rst | 2 +- docs/advanced/template_extensions.rst | 2 +- docs/advanced/user_config.rst | 39 +++-- docs/{advanced => }/cli_options.rst | 0 docs/conf.py | 6 +- docs/index.rst | 5 +- docs/installation.rst | 34 +++-- docs/overview.rst | 16 +- docs/tutorials.rst | 33 ---- docs/tutorials/index.rst | 36 +++++ docs/{ => tutorials}/tutorial1.rst | 2 +- docs/{ => tutorials}/tutorial2.rst | 0 17 files changed, 201 insertions(+), 200 deletions(-) delete mode 100644 .gitmodules delete mode 160000 docs/HelloCookieCutter1 rename docs/{advanced => }/cli_options.rst (100%) delete mode 100644 docs/tutorials.rst create mode 100644 docs/tutorials/index.rst rename docs/{ => tutorials}/tutorial1.rst (99%) rename docs/{ => tutorials}/tutorial2.rst (100%) diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index e0372d2b5..000000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "docs/HelloCookieCutter1"] - path = docs/HelloCookieCutter1 - url = https://github.com/BruceEckel/HelloCookieCutter1 diff --git a/HISTORY.md b/HISTORY.md index 5fc9fd5a9..b39699c1b 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -4,10 +4,15 @@ History is important, but our current roadmap can be found [here](https://github ## 2.0.2 (2021-12-27) +*Remark: This release never made it to official PyPI* + * Fix Python version number in cookiecutter --version and test on Python 3.10 (#1621) @ozer550 * Removed changes related to setuptools_scm (#1629) @audreyfeldroy @ozer550 ## 2.0.1 (2021-12-11) + +*Remark: This release never made it to official PyPI* + ### Breaking Changes * Release preparation for 2.0.1rc1 (#1608) @audreyfeldroy @@ -105,7 +110,7 @@ History is important, but our current roadmap can be found [here](https://github ## 1.7.1 (2020-04-21) This release was focused on internal code and CI/CD changes. During this release -all code was verified to match pep8, pep257 and other code-styling guides. +all code was verified to match pep8, pep257 and other code-styling guides. Project CI/CD was significantly changed, Windows platform checks based on Appveyor engine was replaced by GitHub actions tests. Appveyor was removed. Also our CI/CD was extended with Mac builds, to verify project builds on Apple devices. @@ -773,7 +778,7 @@ Other changes: $ cookiecutter cookiecutter-pypackage/ # Create project from the cookiecutter-pypackage.git repo template $ cookiecutter https://github.com/audreyr/cookiecutter-pypackage.git -``` +``` * Can now use Cookiecutter from Python as a package: diff --git a/README.md b/README.md index a954feca2..493f63cc4 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,8 @@ [![docs](https://readthedocs.org/projects/cookiecutter/badge/?version=latest)](https://readthedocs.org/projects/cookiecutter/?badge=latest) [![Code Quality](https://img.shields.io/scrutinizer/g/cookiecutter/cookiecutter.svg)](https://scrutinizer-ci.com/g/cookiecutter/cookiecutter/?branch=master) -A command-line utility that creates projects from **cookiecutters** (project -templates), e.g. creating a Python package project from a Python package project -template. +A command-line utility that creates projects from **cookiecutters** (project templates), +e.g. creating a Python package project from a Python package project template. - Documentation: [https://cookiecutter.readthedocs.io](https://cookiecutter.readthedocs.io) - GitHub: [https://github.com/cookiecutter/cookiecutter](https://github.com/cookiecutter/cookiecutter) @@ -23,64 +22,44 @@ template. - Cross-platform: Windows, Mac, and Linux are officially supported. - You don't have to know/write Python code to use Cookiecutter. -- Works with Python 3.7, 3.8, 3.9. +- Works with Python 3.7, 3.8, 3.9., 3.10 - Project templates can be in any programming language or markup format: Python, JavaScript, Ruby, CoffeeScript, RST, Markdown, CSS, HTML, you name it. You can use multiple languages in the same project template. + +### For users of existing templates + - Simple command line usage: -```bash -# Create project from the cookiecutter-pypackage.git repo template -# You'll be prompted to enter values. -# Then it'll create your Python package in the current working directory, -# based on those values. -$ cookiecutter https://github.com/audreyfeldroy/cookiecutter-pypackage -# For the sake of brevity, repos on GitHub can just use the 'gh' prefix -$ cookiecutter gh:audreyfeldroy/cookiecutter-pypackage -``` + ```bash + # Create project from the cookiecutter-pypackage.git repo template + # You'll be prompted to enter values. + # Then it'll create your Python package in the current working directory, + # based on those values. + $ cookiecutter https://github.com/audreyfeldroy/cookiecutter-pypackage + # For the sake of brevity, repos on GitHub can just use the 'gh' prefix + $ cookiecutter gh:audreyfeldroy/cookiecutter-pypackage + ``` - Use it at the command line with a local template: -```bash -# Create project in the current working directory, from the local -# cookiecutter-pypackage/ template -$ cookiecutter cookiecutter-pypackage/ -``` + ```bash + # Create project in the current working directory, from the local + # cookiecutter-pypackage/ template + $ cookiecutter cookiecutter-pypackage/ + ``` - Or use it from Python: -```py -from cookiecutter.main import cookiecutter - -# Create project from the cookiecutter-pypackage/ template -cookiecutter('cookiecutter-pypackage/') - -# Create project from the cookiecutter-pypackage.git repo template -cookiecutter('https://github.com/audreyfeldroy/cookiecutter-pypackage.git') -``` - -- Directory names and filenames can be templated. For example: + ```py + from cookiecutter.main import cookiecutter -```py -{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}.py -``` + # Create project from the cookiecutter-pypackage/ template + cookiecutter('cookiecutter-pypackage/') -- Supports unlimited levels of directory nesting. -- 100% of templating is done with Jinja2. This includes file and directory names. -- Simply define your template variables in a `cookiecutter.json` file. For example: - -```json -{ - "full_name": "Audrey Roy Greenfeld", - "email": "audreyr@gmail.com", - "project_name": "Complexity", - "repo_name": "complexity", - "project_short_description": "Refreshingly simple static site generator.", - "release_date": "2013-07-10", - "year": "2013", - "version": "0.1.1" -} -``` + # Create project from the cookiecutter-pypackage.git repo template + cookiecutter('https://github.com/audreyfeldroy/cookiecutter-pypackage.git') + ``` - Unless you suppress it with `--no-input`, you are prompted for input: - Prompts are the keys in `cookiecutter.json`. @@ -88,78 +67,93 @@ cookiecutter('https://github.com/audreyfeldroy/cookiecutter-pypackage.git') - Prompts are shown in order. - Cross-platform support for `~/.cookiecutterrc` files: -```yaml -default_context: - full_name: "Audrey Roy Greenfeld" - email: "audreyr@gmail.com" - github_username: "audreyfeldroy" -cookiecutters_dir: "~/.cookiecutters/" -``` - -- Cookiecutters (cloned Cookiecutter project templates) are put into - `~/.cookiecutters/` by default, or cookiecutters_dir if specified. -- If you have already cloned a cookiecutter into `~/.cookiecutters/`, - you can reference it by directory name: - -```bash -# Clone cookiecutter-pypackage -$ cookiecutter gh:audreyfeldroy/cookiecutter-pypackage -# Now you can use the already cloned cookiecutter by name -$ cookiecutter cookiecutter-pypackage -``` - -- You can use local cookiecutters, or remote cookiecutters directly from Git - repos or from Mercurial repos on Bitbucket. -- Default context: specify key/value pairs that you want used as defaults - whenever you generate a project. + ```yaml + default_context: + full_name: "Audrey Roy Greenfeld" + email: "audreyr@gmail.com" + github_username: "audreyfeldroy" + cookiecutters_dir: "~/.cookiecutters/" + ``` + +- Cookiecutters (cloned Cookiecutter project templates) are put into `~/.cookiecutters/` by default, or cookiecutters_dir if specified. +- If you have already cloned a cookiecutter into `~/.cookiecutters/`, you can reference it by directory name: + + ```bash + # Clone cookiecutter-pypackage + $ cookiecutter gh:audreyfeldroy/cookiecutter-pypackage + # Now you can use the already cloned cookiecutter by name + $ cookiecutter cookiecutter-pypackage + ``` + +- You can use local cookiecutters, or remote cookiecutters directly from Git repos or from Mercurial repos on Bitbucket. +- Default context: specify key/value pairs that you want used as defaults whenever you generate a project. - Inject extra context with command-line arguments: -```bash -cookiecutter --no-input gh:msabramo/cookiecutter-supervisor program_name=foobar startsecs=10 -``` + ```bash + cookiecutter --no-input gh:msabramo/cookiecutter-supervisor program_name=foobar startsecs=10 + ``` - Direct access to the Cookiecutter API allows for injection of extra context. -- Pre- and post-generate hooks: Python or shell scripts to run before or after - generating a project. - Paths to local projects can be specified as absolute or relative. -- Projects generated to your current directory or to target directory if - specified with `-o` option. +- Projects generated to your current directory or to target directory if specified with `-o` option. + +### For template creators + +- Supports unlimited levels of directory nesting. +- 100% of templating is done with Jinja2. +- Both, directory names and filenames can be templated. + For example: + + ```py + {{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}.py + ``` +- Simply define your template variables in a `cookiecutter.json` file. + For example: + + ```json + { + "full_name": "Audrey Roy Greenfeld", + "email": "audreyr@gmail.com", + "project_name": "Complexity", + "repo_name": "complexity", + "project_short_description": "Refreshingly simple static site generator.", + "release_date": "2013-07-10", + "year": "2013", + "version": "0.1.1" + } + ``` +- Pre- and post-generate hooks: Python or shell scripts to run before or after generating a project. ## Available Cookiecutters -Making great cookies takes a lot of cookiecutters and contributors. We're so -pleased that there are many Cookiecutter project templates to choose from. We -hope you find a cookiecutter that is just right for your needs. +Making great cookies takes a lot of cookiecutters and contributors. +We're so pleased that there are many Cookiecutter project templates to choose from. +We hope you find a cookiecutter that is just right for your needs. -## A Pantry Full of Cookiecutters +### A Pantry Full of Cookiecutters -The best place to start searching for specific and ready to use cookiecutter -template is [Github search](https://github.com/search?q=cookiecutter&type=Repositories). +The best place to start searching for specific and ready to use cookiecutter template is [Github search](https://github.com/search?q=cookiecutter&type=Repositories). Just type `cookiecutter` and you will discover over 4000 related repositories. -We also recommend you to check related GitHub topics. For general search use -[cookiecutter-template](https://github.com/topics/cookiecutter-template). -For specific topics try to use `cookiecutter-yourtopic`, like -`cookiecutter-python` or `cookiecutter-datascience`. This is a new GitHub feature, -so not all active repositories use it at the moment. +We also recommend you to check related GitHub topics. +For general search use [cookiecutter-template](https://github.com/topics/cookiecutter-template). +For specific topics try to use `cookiecutter-yourtopic`, like `cookiecutter-python` or `cookiecutter-datascience`. +This is a new GitHub feature, so not all active repositories use it at the moment. -If you are template developer please add related -[topics](https://help.github.com/en/github/administering-a-repository/classifying-your-repository-with-topics) -with `cookiecutter` prefix to you repository. We believe it will make it more -discoverable. You are almost not limited in topics amount, use it! +If you are template developer please add related [topics](https://help.github.com/en/github/administering-a-repository/classifying-your-repository-with-topics) with `cookiecutter` prefix to you repository. +We believe it will make it more discoverable. +You are almost not limited in topics amount, use it! -## Cookiecutter Specials +### Cookiecutter Specials These Cookiecutters are maintained by the cookiecutter team: - [cookiecutter-pypackage](https://github.com/audreyfeldroy/cookiecutter-pypackage): [@audreyfeldroy's](https://github.com/audreyfeldroy) ultimate Python package project template. - [cookiecutter-django](https://github.com/pydanny/cookiecutter-django): - A bleeding edge Django project template with Bootstrap 4, customizable users app, - starter templates, working user registration, celery setup, and much more. + A bleeding edge Django project template with Bootstrap 4, customizable users app, starter templates, working user registration, celery setup, and much more. - [cookiecutter-pytest-plugin](https://github.com/pytest-dev/cookiecutter-pytest-plugin): - Minimal Cookiecutter template for authoring [pytest](https://docs.pytest.org/) - plugins that help you to write better programs. + Minimal Cookiecutter template for authoring [pytest](https://docs.pytest.org/) plugins that help you to write better programs. ## Community @@ -171,22 +165,16 @@ Stuck? Try one of the following: - See the [Troubleshooting](https://cookiecutter.readthedocs.io/en/latest/troubleshooting.html) page. - Ask for help on [Stack Overflow](https://stackoverflow.com/questions/tagged/cookiecutter). - You are strongly encouraged to - [file an issue](https://github.com/cookiecutter/cookiecutter/issues?q=is%3Aopen) - about the problem, even if it's just "I can't get it to work on this cookiecutter" - with a link to your cookiecutter. Don't worry about naming/pinpointing the issue - properly. -- Ask for help on [Discord](https://discord.gg/9BrxzPKuEW) - if you must (but please try one of the other options first, so that others - can benefit from the discussion). + [file an issue](https://github.com/cookiecutter/cookiecutter/issues?q=is%3Aopen) about the problem, even if it's just "I can't get it to work on this cookiecutter" with a link to your cookiecutter. + Don't worry about naming/pinpointing the issue properly. +- Ask for help on [Discord](https://discord.gg/9BrxzPKuEW) if you must (but please try one of the other options first, so that others can benefit from the discussion). Development on Cookiecutter is community-driven: -- Huge thanks to all the [contributors](AUTHORS.md) who have pitched in to help - make Cookiecutter an even better tool. -- Everyone is invited to contribute. Read the - [contributing instructions](CONTRIBUTING.md), then get started. -- Connect with other Cookiecutter contributors and users on - [Discord](https://discord.gg/9BrxzPKuEW) +- Huge thanks to all the [contributors](AUTHORS.md) who have pitched in to help make Cookiecutter an even better tool. +- Everyone is invited to contribute. + Read the [contributing instructions](CONTRIBUTING.md), then get started. +- Connect with other Cookiecutter contributors and users on [Discord](https://discord.gg/9BrxzPKuEW) (note: due to work and other commitments, a core committer might not always be available) Encouragement is unbelievably motivating. If you want more work done on diff --git a/docs/HelloCookieCutter1 b/docs/HelloCookieCutter1 deleted file mode 160000 index 239ea6928..000000000 --- a/docs/HelloCookieCutter1 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 239ea692896301eaa280dd407fdd4d5c55cf6998 diff --git a/docs/advanced/index.rst b/docs/advanced/index.rst index 66d5faadd..d2111ad4b 100644 --- a/docs/advanced/index.rst +++ b/docs/advanced/index.rst @@ -17,7 +17,6 @@ Various advanced topics regarding cookiecutter usage. private_variables copy_without_render replay - cli_options choice_variables dict_variables template_extensions diff --git a/docs/advanced/local_extensions.rst b/docs/advanced/local_extensions.rst index bf87caf7b..a90b04d7d 100644 --- a/docs/advanced/local_extensions.rst +++ b/docs/advanced/local_extensions.rst @@ -1,4 +1,4 @@ -.. _`template extensions`: +.. _`local extensions`: Local Extensions ---------------- diff --git a/docs/advanced/template_extensions.rst b/docs/advanced/template_extensions.rst index 940b87b2f..2e2273e8b 100644 --- a/docs/advanced/template_extensions.rst +++ b/docs/advanced/template_extensions.rst @@ -96,7 +96,7 @@ that converts string into its underscored ("slugified") version: Would output: -.. code-block:: json +:: it-s-a-random-version diff --git a/docs/advanced/user_config.rst b/docs/advanced/user_config.rst index 96f500898..a46dc95b6 100644 --- a/docs/advanced/user_config.rst +++ b/docs/advanced/user_config.rst @@ -3,9 +3,8 @@ User Config (0.7.0+) ==================== -If you use Cookiecutter a lot, you'll find it useful to have a user config -file. By default Cookiecutter tries to retrieve settings from a `.cookiecutterrc` -file in your home directory. +If you use Cookiecutter a lot, you'll find it useful to have a user config file. +By default Cookiecutter tries to retrieve settings from a `.cookiecutterrc` file in your home directory. From version 1.3.0 you can also specify a config file on the command line via ``--config-file``:: @@ -15,9 +14,8 @@ Or you can set the ``COOKIECUTTER_CONFIG`` environment variable:: $ export COOKIECUTTER_CONFIG=/home/audreyr/my-custom-config.yaml -If you wish to stick to the built-in config and not load any user config file at all, -use the cli option ``--default-config`` instead. Preventing Cookiecutter from loading -user settings is crucial for writing integration tests in an isolated environment. +If you wish to stick to the built-in config and not load any user config file at all, use the CLI option ``--default-config`` instead. +Preventing Cookiecutter from loading user settings is crucial for writing integration tests in an isolated environment. Example user config: @@ -36,18 +34,17 @@ Example user config: Possible settings are: -* default_context: A list of key/value pairs that you want injected as context - whenever you generate a project with Cookiecutter. These values are treated - like the defaults in `cookiecutter.json`, upon generation of any project. -* cookiecutters_dir: Directory where your cookiecutters are cloned to when you - use Cookiecutter with a repo argument. -* replay_dir: Directory where Cookiecutter dumps context data to, which - you can fetch later on when using the :ref:`replay feature `. -* abbreviations: A list of abbreviations for cookiecutters. Abbreviations can - be simple aliases for a repo name, or can be used as a prefix, in the form - `abbr:suffix`. Any suffix will be inserted into the expansion in place of - the text `{0}`, using standard Python string formatting. With the above - aliases, you could use the `cookiecutter-pypackage` template simply by saying - `cookiecutter pp`, or `cookiecutter gh:audreyr/cookiecutter-pypackage`. - The `gh` (github), `bb` (bitbucket), and `gl` (gitlab) abbreviations shown - above are actually built in, and can be used without defining them yourself. +``default_context``: + A list of key/value pairs that you want injected as context whenever you generate a project with Cookiecutter. + These values are treated like the defaults in `cookiecutter.json`, upon generation of any project. +``cookiecutters_dir`` + Directory where your cookiecutters are cloned to when you use Cookiecutter with a repo argument. +``replay_dir`` + Directory where Cookiecutter dumps context data to, which you can fetch later on when using the + :ref:`replay feature `. +``abbreviations`` + A list of abbreviations for cookiecutters. + Abbreviations can be simple aliases for a repo name, or can be used as a prefix, in the form ``abbr:suffix``. + Any suffix will be inserted into the expansion in place of the text ``{0}``, using standard Python string formatting. + With the above aliases, you could use the `cookiecutter-pypackage` template simply by saying ``cookiecutter pp``, or ``cookiecutter gh:audreyr/cookiecutter-pypackage``. + The ``gh`` (GitHub), ``bb`` (Bitbucket), and ``gl`` (Gitlab) abbreviations shown above are actually **built in**, and can be used without defining them yourself. diff --git a/docs/advanced/cli_options.rst b/docs/cli_options.rst similarity index 100% rename from docs/advanced/cli_options.rst rename to docs/cli_options.rst diff --git a/docs/conf.py b/docs/conf.py index 082a25d03..51648bd10 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -95,7 +95,7 @@ def __getattr__(cls, name): # General information about the project. project = 'cookiecutter' -copyright = '2013-2019, Audrey Roy and Cookiecutter community' +copyright = '2013-2022, Audrey Roy and Cookiecutter community' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -324,7 +324,7 @@ def __getattr__(cls, name): epub_title = 'cookiecutter' epub_author = 'Audrey Roy' epub_publisher = 'Audrey Roy and Cookiecutter community' -epub_copyright = '2013-2019, Audrey Roy and Cookiecutter community' +epub_copyright = '2013-2022, Audrey Roy and Cookiecutter community' # The language of the text. It defaults to the language option # or en if the language is not set. @@ -377,4 +377,4 @@ def __getattr__(cls, name): # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'http://docs.python.org/': None} +intersphinx_mapping = {'https://docs.python.org/3': None} diff --git a/docs/index.rst b/docs/index.rst index 15a953e63..1a391b521 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -18,9 +18,8 @@ Basics overview installation usage - tutorials - tutorial1 - tutorial2 + cli_options + tutorials/index advanced/index troubleshooting diff --git a/docs/installation.rst b/docs/installation.rst index d12709c37..e4084ab81 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -12,9 +12,13 @@ Prerequisites Python interpreter ^^^^^^^^^^^^^^^^^^ -Install Python for your operating system. Consult the official `Python documentation `_ for details. +Install Python for your operating system. +On Windows and macOS this is usually necessary. +Most Linux distributions are coming with Python pre-installed. +Consult the official `Python documentation `_ for details. -You can install the Python binaries from `python.org `_. Alternatively on macOS, you can use the `homebrew `_ package manager. +You can install the Python binaries from `python.org `_. +Alternatively on macOS, you can use the `homebrew `_ package manager. .. code-block:: bash @@ -77,7 +81,7 @@ Or, if you do not have pip: $ easy_install --user cookiecutter -Though, pip is recommended. +Though, pip is recommended, easy_install is deprecated. Or, if you are using conda, first add conda-forge to your channels: @@ -106,32 +110,32 @@ Alternate installations $ pipx install cookiecutter -**Debian/Ubuntu:** -.. code-block:: bash - - $ sudo apt-get install cookiecutter +Upgrading +--------- -Upgrading from 0.6.4 to 0.7.0 or greater ----------------------------------------- +from 0.6.4 to 0.7.0 or greater +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -First, read :doc:`HISTORY` in detail. There are a lot of major -changes. The big ones are: +First, read :doc:`HISTORY` in detail. +There are a lot of major changes. +The big ones are: * Cookiecutter no longer deletes the cloned repo after generating a project. * Cloned repos are saved into `~/.cookiecutters/`. * You can optionally create a `~/.cookiecutterrc` config file. -Upgrade Cookiecutter either with easy_install: + +Or with pip: .. code-block:: bash - $ easy_install --upgrade cookiecutter + $ python3 -m pip install --upgrade cookiecutter -Or with pip: +Upgrade Cookiecutter either with easy_install (deprecated): .. code-block:: bash - $ python3 -m pip install --upgrade cookiecutter + $ easy_install --upgrade cookiecutter Then you should be good to go. diff --git a/docs/overview.rst b/docs/overview.rst index 0a823305b..6b8e029ee 100644 --- a/docs/overview.rst +++ b/docs/overview.rst @@ -2,10 +2,20 @@ Overview ======== +Cookiecutter takes a template provided as a directory structure with template-files. +Templates can be in located in the filesystem, as a ZIP-file or on a VCS-Server (Git/Hg) like GitHub. + +It reads a settings file and prompts the user interactivly wether to change the settings. + +Then it takes both and generates an output directory structure from it. + +Additional the template can provide code (Python or shell-script) to be executed before and after generation (pre-gen- and post-gen-hooks). + + Input ----- -This is the directory structure for a simple cookiecutter:: +This is a directory structure for a simple cookiecutter:: cookiecutter-something/ ├── {{ cookiecutter.project_name }}/ <--------- Project template @@ -17,8 +27,8 @@ This is the directory structure for a simple cookiecutter:: You must have: -* A `cookiecutter.json` file. -* A `{{ cookiecutter.project_name }}/` directory, where +- A `cookiecutter.json` file. +- A `{{ cookiecutter.project_name }}/` directory, where `project_name` is defined in your `cookiecutter.json`. Beyond that, you can have whatever files/directories you want. diff --git a/docs/tutorials.rst b/docs/tutorials.rst deleted file mode 100644 index 16903235e..000000000 --- a/docs/tutorials.rst +++ /dev/null @@ -1,33 +0,0 @@ -==================== -Additional Tutorials -==================== - -Learn How to Use Cookiecutter ------------------------------ - -* :doc:`tutorial1` by `@audreyr`_ - - -Create Your Very Own Cookiecutter Project Template --------------------------------------------------- - -* :doc:`tutorial2` by `@audreyr`_ - -* `Project Templates Made Easy`_ by `@pydanny`_ - -* Cookiedozer Tutorials by `@hackebrot`_ - - * Part 1: `Create your own Cookiecutter template`_ - * Part 2: `Extending our Cookiecutter template`_ - * Part 3: `Wrapping up our Cookiecutter template`_ - - -.. _`Project Templates Made Easy`: http://www.pydanny.com/cookie-project-templates-made-easy.html - -.. _`Create your own Cookiecutter template`: https://raphael.codes/blog/create-your-own-cookiecutter-template/ -.. _`Extending our Cookiecutter template`: https://raphael.codes/blog/extending-our-cookiecutter-template/ -.. _`Wrapping up our Cookiecutter template`: https://raphael.codes/blog/wrapping-up-our-cookiecutter-template/ - -.. _`@audreyr`: https://github.com/audreyr -.. _`@pydanny`: https://github.com/pydanny -.. _`@hackebrot`: https://github.com/hackebrot diff --git a/docs/tutorials/index.rst b/docs/tutorials/index.rst new file mode 100644 index 000000000..81e1b0dff --- /dev/null +++ b/docs/tutorials/index.rst @@ -0,0 +1,36 @@ +==================== +Tutorials +==================== + +Tutorials by `@audreyr`_ + +.. toctree:: + :maxdepth: 2 + + tutorial1 + tutorial2 + + +External Links +-------------- + +- `Learn the Basics of Cookiecutter by Creating a Cookiecutter`_ - first steps tutorial with example template by `@BruceEckel`_ +- `Project Templates Made Easy`_ by `@pydanny`_ +- Cookiedozer Tutorials by `@hackebrot`_ + + - Part 1: `Create your own Cookiecutter template`_ + - Part 2: `Extending our Cookiecutter template`_ + - Part 3: `Wrapping up our Cookiecutter template`_ + + +.. _`Learn the Basics of Cookiecutter by Creating a Cookiecutter`: https://github.com/BruceEckel/HelloCookieCutter1/blob/master/Readme.rst +.. _`Project Templates Made Easy`: http://www.pydanny.com/cookie-project-templates-made-easy.html + +.. _`Create your own Cookiecutter template`: https://raphael.codes/blog/create-your-own-cookiecutter-template/ +.. _`Extending our Cookiecutter template`: https://raphael.codes/blog/extending-our-cookiecutter-template/ +.. _`Wrapping up our Cookiecutter template`: https://raphael.codes/blog/wrapping-up-our-cookiecutter-template/ + +.. _`@audreyr`: https://github.com/audreyr +.. _`@pydanny`: https://github.com/pydanny +.. _`@hackebrot`: https://github.com/hackebrot +.. _`@BruceEckel`: https://github.com/BruceEckel diff --git a/docs/tutorial1.rst b/docs/tutorials/tutorial1.rst similarity index 99% rename from docs/tutorial1.rst rename to docs/tutorials/tutorial1.rst index 2659db8b2..555753609 100644 --- a/docs/tutorial1.rst +++ b/docs/tutorials/tutorial1.rst @@ -3,7 +3,7 @@ Getting to Know Cookiecutter ============================= .. note:: Before you begin, please install Cookiecutter 0.7.0 or higher. - Instructions are in :doc:`installation`. + Instructions are in :doc:`../installation`. Cookiecutter is a tool for creating projects from *cookiecutters* (project templates). diff --git a/docs/tutorial2.rst b/docs/tutorials/tutorial2.rst similarity index 100% rename from docs/tutorial2.rst rename to docs/tutorials/tutorial2.rst From e9dc60e7c11ee9f9eca0db1f01d126ecf4803e76 Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Mon, 30 May 2022 12:26:39 +0200 Subject: [PATCH 088/274] better naming in generated API docs --- Makefile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Makefile b/Makefile index 71ea835bd..2dc6aa3aa 100644 --- a/Makefile +++ b/Makefile @@ -63,6 +63,10 @@ docs: ## Generate Sphinx HTML documentation, including API docs @rm -f docs/cookiecutter.rst @sphinx-apidoc -o docs/ cookiecutter @rm -f docs/modules.rst + @sed -i 's/cookiecutter package/===\nAPI\n===/' docs/cookiecutter.rst + @sed -i 's/====================//' docs/cookiecutter.rst + @sed -i 's/Submodules/This is the Cookiecutter modules API documentation./' docs/cookiecutter.rst + @sed -i 's/^----------$$//' docs/cookiecutter.rst @$(MAKE) -C docs clean @$(MAKE) -C docs html @$(BROWSER) docs/_build/html/index.html From 1f04a3e7bcd17e720aea215abae97a25fbea2c4d Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Mon, 30 May 2022 12:27:24 +0200 Subject: [PATCH 089/274] update troove classifiers, version and required python version --- setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index d703c3c29..9cb220978 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ """cookiecutter distutils configuration.""" from setuptools import setup -version = "2.0.0" +version = "2.0.3.dev0" with open('README.md', encoding='utf-8') as readme_file: readme = readme_file.read() @@ -34,7 +34,7 @@ package_dir={'cookiecutter': 'cookiecutter'}, entry_points={'console_scripts': ['cookiecutter = cookiecutter.__main__:main']}, include_package_data=True, - python_requires='>=3.6', + python_requires='>=3.7', install_requires=requirements, license='BSD', zip_safe=False, @@ -46,10 +46,10 @@ "License :: OSI Approved :: BSD License", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Python", From ed80c90798c575d7956fd6c7ab3e757a606cf606 Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Mon, 30 May 2022 12:28:09 +0200 Subject: [PATCH 090/274] Documentation overhaul (2) --- AUTHORS.md | 1 + CODE_OF_CONDUCT.md | 5 +- CONTRIBUTING.md | 275 +++++++++++++++++++++++------------------- README.md | 50 ++++---- docs/cookiecutter.rst | 7 +- docs/index.rst | 2 +- docs/installation.rst | 18 +-- docs/overview.rst | 5 +- 8 files changed, 190 insertions(+), 173 deletions(-) diff --git a/AUTHORS.md b/AUTHORS.md index fd75a6943..d63785240 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -169,6 +169,7 @@ - Tom Forbes ([@orf](https://github.com/orf)) - Xie Yanbo ([@xyb](https://github.com/xyb)) - Maxim Ivanov ([@ivanovmg](https://github.com/ivanovmg)) +- Jens Klein ([@jensens](https://github.com/jensens)) ## Backers diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 5b484056f..3043a6bab 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,5 +1,4 @@ # Code of Conduct -Everyone interacting in the Cookiecutter project's codebases, issue trackers, -chat rooms, and mailing lists is expected to follow the -[PyPA Code of Conduct](https://www.pypa.io/en/latest/code-of-conduct/). +Everyone interacting in the Cookiecutter project's codebases and documentation is expected to follow the [PyPA Code of Conduct](https://www.pypa.io/en/latest/code-of-conduct/). +This includes, but is not limited to, issue trackers, chat rooms, mailing lists, and other virtual or in real life communication. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f54ae0e06..f0ae0bb42 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,12 +1,13 @@ # Contributing -Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. +Contributions are welcome, and they are greatly appreciated! +Every little bit helps, and credit will always be given. -* [Types of Contributions](#Types-of-Contributions) -* [Contributor Setup](#Setting-Up-the-Code-for-Local-Development) -* [Contributor Guidelines](#Contributor-Guidelines) -* [Contributor Testing](#Testing-with-tox) -* [Core Committer Guide](#Core-Committer-Guide) +- [Types of Contributions](#Types-of-Contributions) +- [Contributor Setup](#Setting-Up-the-Code-for-Local-Development) +- [Contributor Guidelines](#Contributor-Guidelines) +- [Contributor Testing](#Testing-with-tox) +- [Core Committer Guide](#Core-Committer-Guide) ## Types of Contributions @@ -18,22 +19,27 @@ Report bugs at [https://github.com/cookiecutter/cookiecutter/issues](https://git If you are reporting a bug, please include: -* Your operating system name and version. -* Any details about your local setup that might be helpful in troubleshooting. -* If you can, provide detailed steps to reproduce the bug. -* If you don't have steps to reproduce the bug, just note your observations in as much detail as you can. Questions to start a discussion about the issue are welcome. +- Your operating system name and version. +- Any details about your local setup that might be helpful in troubleshooting. +- If you can, provide detailed steps to reproduce the bug. +- If you don't have steps to reproduce the bug, just note your observations in as much detail as you can. + Questions to start a discussion about the issue are welcome. ### Fix Bugs -Look through the GitHub issues for bugs. Anything tagged with "bug" is open to whoever wants to implement it. +Look through the GitHub issues for bugs. +Anything tagged with "bug" is open to whoever wants to implement it. ### Implement Features -Look through the GitHub issues for features. Anything tagged with "enhancement" and "please-help" is open to whoever wants to implement it. +Look through the GitHub issues for features. +Anything tagged with "enhancement" and "please-help" is open to whoever wants to implement it. Please do not combine multiple feature enhancements into a single pull request. -Note: this project is very conservative, so new features that aren't tagged with "please-help" might not get into core. We're trying to keep the code base small, extensible, and streamlined. Whenever possible, it's best to try and implement feature ideas as separate projects outside of the core codebase. +Note: this project is very conservative, so new features that aren't tagged with "please-help" might not get into core. +We're trying to keep the code base small, extensible, and streamlined. +Whenever possible, it's best to try and implement feature ideas as separate projects outside of the core codebase. ### Write Documentation @@ -54,9 +60,9 @@ The best way to send feedback is to file an issue at [https://github.com/cookiec If you are proposing a feature: -* Explain in detail how it would work. -* Keep the scope as narrow as possible, to make it easier to implement. -* Remember that this is a volunteer-driven project, and that contributions are welcome :) +- Explain in detail how it would work. +- Keep the scope as narrow as possible, to make it easier to implement. +- Remember that this is a volunteer-driven project, and that contributions are welcome :) ## Setting Up the Code for Local Development @@ -65,56 +71,58 @@ Here's how to set up `cookiecutter` for local development. 1. Fork the `cookiecutter` repo on GitHub. 2. Clone your fork locally: -```bash -git clone git@github.com:your_name_here/cookiecutter.git -``` + ```bash + git clone git@github.com:your_name_here/cookiecutter.git + ``` -3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development: +3. Install your local copy into a virtualenv. + Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development: -```bash - -cd cookiecutter/ -pip install -e . -``` + ```bash + cd cookiecutter/ + pip install -e . + ``` 4. Create a branch for local development: -```bash -git checkout -b name-of-your-bugfix-or-feature -``` + ```bash + git checkout -b name-of-your-bugfix-or-feature + ``` Now you can make your changes locally. 5. When you're done making changes, check that your changes pass the tests and lint check: -```bash -pip install tox -tox -``` + ```bash + pip install tox + tox + ``` -Please note that tox runs lint check automatically, since we have a test environment for it. + Please note that tox runs lint check automatically, since we have a test environment for it. -If you feel like running only the lint environment, please use the following command: + If you feel like running only the lint environment, please use the following command: -```bash -make lint -``` + ```bash + make lint + ``` -6. Ensure that your feature or commit is fully covered by tests. Check report after regular tox run. You can also run coverage only report and get html report with statement by statement highlighting: +6. Ensure that your feature or commit is fully covered by tests. Check report after regular tox run. + You can also run coverage only report and get html report with statement by statement highlighting: -```bash -make coverage -``` + ```bash + make coverage + ``` -You report will be placed to `htmlcov` directory. Please do not include this directory to your commits. By default this directory in our `.gitignore` file. + You report will be placed to `htmlcov` directory. Please do not include this directory to your commits. + By default this directory in our `.gitignore` file. 7. Commit your changes and push your branch to GitHub: -```bash -git add . -git commit -m "Your detailed description of your changes." -git push origin name-of-your-bugfix-or-feature -``` + ```bash + git add . + git commit -m "Your detailed description of your changes." + git push origin name-of-your-bugfix-or-feature + ``` 8. Submit a pull request through the GitHub website. @@ -125,34 +133,36 @@ git push origin name-of-your-bugfix-or-feature Before you submit a pull request, check that it meets these guidelines: 1. The pull request should include tests. -2. The pull request should be contained: if it's too big consider splitting it into smaller pull requests. -3. If the pull request adds functionality, the docs should be updated. Put your new functionality into a function with a docstring, and add the feature to the list in README.md. +2. The pull request should be contained: + if it's too big consider splitting it into smaller pull requests. +3. If the pull request adds functionality, the docs should be updated. + Put your new functionality into a function with a docstring, and add the feature to the list in README.md. 4. The pull request must pass all CI/CD jobs before being ready for review. 5. If one CI/CD job is failing for unrelated reasons you may want to create another PR to fix that first. ### Coding Standards -* PEP8 -* Functions over classes except in tests -* Quotes via [http://stackoverflow.com/a/56190/5549](http://stackoverflow.com/a/56190/5549) - - * Use double quotes around strings that are used for interpolation or that are natural language messages - * Use single quotes for small symbol-like strings (but break the rules if the strings contain quotes) - * Use triple double quotes for docstrings and raw string literals for regular expressions even if they aren't needed. - * Example: - -```python -LIGHT_MESSAGES = { - 'English': "There are %(number_of_lights)s lights.", - 'Pirate': "Arr! Thar be %(number_of_lights)s lights." -} -def lights_message(language, number_of_lights): - """Return a language-appropriate string reporting the light count.""" - return LIGHT_MESSAGES[language] % locals() -def is_pirate(message): - """Return True if the given message sounds piratical.""" - return re.search(r"(?i)(arr|avast|yohoho)!", message) is not None -``` +- PEP8 +- Functions over classes except in tests +- Quotes via [http://stackoverflow.com/a/56190/5549](http://stackoverflow.com/a/56190/5549) + + - Use double quotes around strings that are used for interpolation or that are natural language messages + - Use single quotes for small symbol-like strings (but break the rules if the strings contain quotes) + - Use triple double quotes for docstrings and raw string literals for regular expressions even if they aren't needed. + - Example: + + ```python + LIGHT_MESSAGES = { + 'English': "There are %(number_of_lights)s lights.", + 'Pirate': "Arr! Thar be %(number_of_lights)s lights." + } + def lights_message(language, number_of_lights): + """Return a language-appropriate string reporting the light count.""" + return LIGHT_MESSAGES[language] % locals() + def is_pirate(message): + """Return True if the given message sounds piratical.""" + return re.search(r"(?i)(arr|avast|yohoho)!", message) is not None + ``` ## Testing with tox @@ -186,7 +196,8 @@ To run all tests using various versions of python in virtualenvs defined in tox. tox ``` -This configuration file setup the pytest-cov plugin and it is an additional dependency. It generate a coverage report after the tests. +This configuration file setup the pytest-cov plugin and it is an additional dependency. +It generate a coverage report after the tests. It is possible to test with specific versions of Python. To do this, the command is: @@ -202,93 +213,97 @@ This will run `py.test` with the `python3.7` and `python3.8` interpreters. Core committers, use this section to: -* Guide your instinct and decisions as a core committer -* Limit the codebase from growing infinitely +- Guide your instinct and decisions as a core committer +- Limit the codebase from growing infinitely #### Command-Line Accessible -* Provides a command-line utility that creates projects from cookiecutters -* Extremely easy to use without having to think too hard -* Flexible for more complex use via optional arguments +- Provides a command-line utility that creates projects from cookiecutters +- Extremely easy to use without having to think too hard +- Flexible for more complex use via optional arguments #### API Accessible -* Entirely function-based and stateless (Class-free by intentional design) -* Usable in pieces for developers of template generation tools +- Entirely function-based and stateless (Class-free by intentional design) +- Usable in pieces for developers of template generation tools #### Being Jinja2-specific -* Sets a standard baseline for project template creators, facilitating reuse -* Minimizes the learning curve for those who already use Flask or Django -* Minimizes scope of Cookiecutter codebase +- Sets a standard baseline for project template creators, facilitating reuse +- Minimizes the learning curve for those who already use Flask or Django +- Minimizes scope of Cookiecutter codebase #### Extensible Being extendable by people with different ideas for Jinja2-based project template tools. -* Entirely function-based -* Aim for statelessness -* Lets anyone write more opinionated tools +- Entirely function-based +- Aim for statelessness +- Lets anyone write more opinionated tools Freedom for Cookiecutter users to build and extend. -* No officially-maintained cookiecutter templates, only ones by individuals -* Commercial project-friendly licensing, allowing for private cookiecutters and private Cookiecutter-based tools +- No officially-maintained cookiecutter templates, only ones by individuals +- Commercial project-friendly licensing, allowing for private cookiecutters and private Cookiecutter-based tools #### Fast and Focused Cookiecutter is designed to do one thing, and do that one thing very well. -* Cover the use cases that the core committers need, and as little as possible beyond that :) -* Generates project templates from the command-line or API, nothing more -* Minimize internal line of code (LOC) count -* Ultra-fast project generation for high performance downstream tools +- Cover the use cases that the core committers need, and as little as possible beyond that :) +- Generates project templates from the command-line or API, nothing more +- Minimize internal line of code (LOC) count +- Ultra-fast project generation for high performance downstream tools #### Inclusive -* Cross-platform and cross-version support are more important than features/functionality -* Fixing Windows bugs even if it's a pain, to allow for use by more beginner coders +- Cross-platform and cross-version support are more important than features/functionality +- Fixing Windows bugs even if it's a pain, to allow for use by more beginner coders #### Stable -* Aim for 100% test coverage and covering corner cases -* No pull requests will be accepted that drop test coverage on any platform, including Windows -* Conservative decisions patterned after CPython's conservative decisions with stability in mind -* Stable APIs that tool builders can rely on -* New features require a +1 from 3 core committers +- Aim for 100% test coverage and covering corner cases +- No pull requests will be accepted that drop test coverage on any platform, including Windows +- Conservative decisions patterned after CPython's conservative decisions with stability in mind +- Stable APIs that tool builders can rely on +- New features require a +1 from 3 core committers #### VCS-Hosted Templates Cookiecutter project templates are intentionally hosted VCS repos as-is. -* They are easily forkable -* It's easy for users to browse forks and files -* They are searchable via standard Github/Bitbucket/other search interface -* Minimizes the need for packaging-related cruft files -* Easy to create a public project template and host it for free -* Easy to collaborate +- They are easily forkable +- It's easy for users to browse forks and files +- They are searchable via standard Github/Bitbucket/other search interface +- Minimizes the need for packaging-related cruft files +- Easy to create a public project template and host it for free +- Easy to collaborate ### Process: Pull Requests If a pull request is untriaged: -* Look at the roadmap -* Set it for the milestone where it makes the most sense -* Add it to the roadmap +- Look at the roadmap +- Set it for the milestone where it makes the most sense +- Add it to the roadmap How to prioritize pull requests, from most to least important: -* Fixes for broken tests. Broken means broken on any supported platform or Python version. -* Extra tests to cover corner cases. -* Minor edits to docs. -* Bug fixes. -* Major edits to docs. -* Features. +- Fixes for broken tests. Broken means broken on any supported platform or Python version. +- Extra tests to cover corner cases. +- Minor edits to docs. +- Bug fixes. +- Major edits to docs. +- Features. #### Pull Requests Review Guidelines -- Think carefully about the long-term implications of the change. How will it affect existing projects that are dependent on this? If this is complicated, do we really want to maintain it forever? -- Take the time to get things right, PRs almost always require additional improvements to meet the bar for quality. **Be very strict about quality.** -- When you merge a pull request take care of closing/updating every related issue explaining how they were affected by those changes. Also, remember to add the author to `AUTHORS.md`. +- Think carefully about the long-term implications of the change. + How will it affect existing projects that are dependent on this? + If this is complicated, do we really want to maintain it forever? +- Take the time to get things right, PRs almost always require additional improvements to meet the bar for quality. + **Be very strict about quality.** +- When you merge a pull request take care of closing/updating every related issue explaining how they were affected by those changes. + Also, remember to add the author to `AUTHORS.md`. ### Process: Issues @@ -307,12 +322,12 @@ Due dates are flexible. Core committers can change them as needed. Note that Git How to number milestones: -* Follow semantic versioning. Look at: [http://semver.org](http://semver.org) +- Follow semantic versioning. Look at: [http://semver.org](http://semver.org) Milestone size: -* If a milestone contains too much, move some to the next milestone. -* Err on the side of more frequent patch releases. +- If a milestone contains too much, move some to the next milestone. +- Err on the side of more frequent patch releases. ### Process: Your own code changes @@ -321,23 +336,29 @@ This rule applies to all the core committers. Exceptions: -* Minor corrections and fixes to pull requests submitted by others. -* While making a formal release, the release manager can make necessary, appropriate changes. -* Small documentation changes that reinforce existing subject matter. Most commonly being, but not limited to spelling and grammar corrections. +- Minor corrections and fixes to pull requests submitted by others. +- While making a formal release, the release manager can make necessary, appropriate changes. +- Small documentation changes that reinforce existing subject matter. + Most commonly being, but not limited to spelling and grammar corrections. ### Responsibilities -* Ensure cross-platform compatibility for every change that's accepted. Windows, macOS and Linux. -* Create issues for any major changes and enhancements that you wish to make. Discuss things transparently and get community feedback. -* Don't add any classes to the codebase unless absolutely needed. Err on the side of using functions. -* Keep feature versions as small as possible, preferably one new feature per version. -* Be welcoming to newcomers and encourage diverse new contributors from all backgrounds. Look at [Code of Conduct](CODE_OF_CONDUCT.md). +- Ensure cross-platform compatibility for every change that's accepted. Windows, macOS and Linux. +- Create issues for any major changes and enhancements that you wish to make. + Discuss things transparently and get community feedback. +- Don't add any classes to the codebase unless absolutely needed. + Err on the side of using functions. +- Keep feature versions as small as possible, preferably one new feature per version. +- Be welcoming to newcomers and encourage diverse new contributors from all backgrounds. + Look at [Code of Conduct](CODE_OF_CONDUCT.md). ### Becoming a Core Committer Contributors may be given core commit privileges. Preference will be given to those with: -1. Past contributions to Cookiecutter and other open-source projects. Contributions to Cookiecutter include both code (both accepted and pending) and friendly participation in the issue tracker. Quantity and quality are considered. +1. Past contributions to Cookiecutter and other open-source projects. + Contributions to Cookiecutter include both code (both accepted and pending) and friendly participation in the issue tracker. + Quantity and quality are considered. 2. A coding style that the other core committers find simple, minimal, and clean. 3. Access to resources for cross-platform development and testing. 4. Time to devote to the project regularly. diff --git a/README.md b/README.md index 493f63cc4..38de8e205 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,7 @@ [![docs](https://readthedocs.org/projects/cookiecutter/badge/?version=latest)](https://readthedocs.org/projects/cookiecutter/?badge=latest) [![Code Quality](https://img.shields.io/scrutinizer/g/cookiecutter/cookiecutter.svg)](https://scrutinizer-ci.com/g/cookiecutter/cookiecutter/?branch=master) -A command-line utility that creates projects from **cookiecutters** (project templates), -e.g. creating a Python package project from a Python package project template. +A command-line utility that creates projects from **cookiecutters** (project templates), e.g. creating a Python package project from a Python package project template. - Documentation: [https://cookiecutter.readthedocs.io](https://cookiecutter.readthedocs.io) - GitHub: [https://github.com/cookiecutter/cookiecutter](https://github.com/cookiecutter/cookiecutter) @@ -149,9 +148,10 @@ You are almost not limited in topics amount, use it! These Cookiecutters are maintained by the cookiecutter team: - [cookiecutter-pypackage](https://github.com/audreyfeldroy/cookiecutter-pypackage): - [@audreyfeldroy's](https://github.com/audreyfeldroy) ultimate Python package project template. + ultimate Python package project template by [@audreyfeldroy's](https://github.com/audreyfeldroy). - [cookiecutter-django](https://github.com/pydanny/cookiecutter-django): - A bleeding edge Django project template with Bootstrap 4, customizable users app, starter templates, working user registration, celery setup, and much more. + a framework for jumpstarting production-ready Django projects quickly. + It is bleeding edge with Bootstrap 5, customizable users app, starter templates, working user registration, celery setup, and much more. - [cookiecutter-pytest-plugin](https://github.com/pytest-dev/cookiecutter-pytest-plugin): Minimal Cookiecutter template for authoring [pytest](https://docs.pytest.org/) plugins that help you to write better programs. @@ -164,8 +164,8 @@ Stuck? Try one of the following: - See the [Troubleshooting](https://cookiecutter.readthedocs.io/en/latest/troubleshooting.html) page. - Ask for help on [Stack Overflow](https://stackoverflow.com/questions/tagged/cookiecutter). -- You are strongly encouraged to - [file an issue](https://github.com/cookiecutter/cookiecutter/issues?q=is%3Aopen) about the problem, even if it's just "I can't get it to work on this cookiecutter" with a link to your cookiecutter. +- You are strongly encouraged to [file an issue](https://github.com/cookiecutter/cookiecutter/issues?q=is%3Aopen) about the problem. + Do it even if it's just "I can't get it to work on this cookiecutter" with a link to your cookiecutter. Don't worry about naming/pinpointing the issue properly. - Ask for help on [Discord](https://discord.gg/9BrxzPKuEW) if you must (but please try one of the other options first, so that others can benefit from the discussion). @@ -177,8 +177,8 @@ Development on Cookiecutter is community-driven: - Connect with other Cookiecutter contributors and users on [Discord](https://discord.gg/9BrxzPKuEW) (note: due to work and other commitments, a core committer might not always be available) -Encouragement is unbelievably motivating. If you want more work done on -Cookiecutter, show support: +Encouragement is unbelievably motivating. +If you want more work done on Cookiecutter, show support: - Thank a core committer for their efforts. - Star [Cookiecutter on GitHub](https://github.com/cookiecutter/cookiecutter). @@ -186,39 +186,35 @@ Cookiecutter, show support: Got criticism or complaints? -- [File an issue](https://github.com/cookiecutter/cookiecutter/issues?q=is%3Aopen) - so that Cookiecutter can be improved. Be friendly and constructive about what - could be better. Make detailed suggestions. -- **Keep us in the loop so that we can help.** For example, if you are - discussing problems with Cookiecutter on a mailing list, - [file an issue](https://github.com/cookiecutter/cookiecutter/issues?q=is%3Aopen) - where you link to the discussion thread and/or cc at least 1 core committer on the email. -- Be encouraging. A comment like "This function ought to be rewritten like this" - is much more likely to result in action than a comment like "Eww, look how bad - this function is." +- [File an issue](https://github.com/cookiecutter/cookiecutter/issues?q=is%3Aopen) so that Cookiecutter can be improved. + Be friendly and constructive about what could be better. + Make detailed suggestions. +- **Keep us in the loop so that we can help.** + For example, if you are discussing problems with Cookiecutter on a mailing list, [file an issue](https://github.com/cookiecutter/cookiecutter/issues?q=is%3Aopen) where you link to the discussion thread and/or cc at least 1 core committer on the email. +- Be encouraging. + A comment like "This function ought to be rewritten like this" is much more likely to result in action than a comment like "Eww, look how bad this function is." Waiting for a response to an issue/question? -- Be patient and persistent. All issues are on the core committer team's radar - and will be considered thoughtfully, but we have a lot of issues to work through. +- Be patient and persistent. All issues are on the core committer team's radar and will be considered thoughtfully, but we have a lot of issues to work through. If urgent, it's fine to ping a core committer in the issue with a reminder. - Ask others to comment, discuss, review, etc. - Search the Cookiecutter repo for issues related to yours. - Need a fix/feature/release/help urgently, and can't wait? - [@audreyfeldroy](https://github.com/audreyfeldroy) is available for hire for consultation - or custom development. + [@audreyfeldroy](https://github.com/audreyfeldroy) is available for hire for consultation or custom development. ## Support This Project -This project is run by volunteers. Shortly we will be providing means for -organizations and individuals to support the project. +This project is run by volunteers. +Shortly we will be providing means for organizations and individuals to support the project. ## Code of Conduct -Everyone interacting in the Cookiecutter project's codebases, issue trackers, -chat rooms, and mailing lists is expected to follow the -[PyPA Code of Conduct](https://www.pypa.io/en/latest/code-of-conduct/). +Everyone interacting in the Cookiecutter project's codebases and documentation is expected to follow the [PyPA Code of Conduct](https://www.pypa.io/en/latest/code-of-conduct/). +This includes, but is not limited to, issue trackers, chat rooms, mailing lists, and other virtual or in real life communication. ## Creator / Leader This project was created and is led by [Audrey Roy Greenfeld](https://github.com/audreyfeldroy). + +She is supported by a team of maintainers. diff --git a/docs/cookiecutter.rst b/docs/cookiecutter.rst index 790a21f7c..16c1c9dc1 100644 --- a/docs/cookiecutter.rst +++ b/docs/cookiecutter.rst @@ -1,8 +1,10 @@ +=== API === -Submodules ----------- + +This is the Cookiecutter modules API documentation. + cookiecutter.cli module ----------------------- @@ -132,7 +134,6 @@ cookiecutter.zipfile module :undoc-members: :show-inheritance: - Module contents --------------- diff --git a/docs/index.rst b/docs/index.rst index 1a391b521..14f5edbcf 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,7 +6,7 @@ Cookiecutter: Better Project Templates ====================================== -Cookiecutter creates projects from project templates, e.g. Python package projects. +Cookiecutter creates projects from **cookiecutters** (project templates), e.g. Python package projects from Python package temnplates. Basics ------ diff --git a/docs/installation.rst b/docs/installation.rst index e4084ab81..068c6d9d9 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -22,7 +22,7 @@ Alternatively on macOS, you can use the `homebrew `_ package ma .. code-block:: bash - $ brew install python3 + brew install python3 Adjust your path @@ -73,13 +73,13 @@ At the command line: .. code-block:: bash - $ python3 -m pip install --user cookiecutter + python3 -m pip install --user cookiecutter Or, if you do not have pip: .. code-block:: bash - $ easy_install --user cookiecutter + easy_install --user cookiecutter Though, pip is recommended, easy_install is deprecated. @@ -87,13 +87,13 @@ Or, if you are using conda, first add conda-forge to your channels: .. code-block:: bash - $ conda config --add channels conda-forge + conda config --add channels conda-forge Once the conda-forge channel has been enabled, cookiecutter can be installed with: .. code-block:: bash - $ conda install cookiecutter + conda install cookiecutter Alternate installations ----------------------- @@ -102,13 +102,13 @@ Alternate installations .. code-block:: bash - $ brew install cookiecutter + brew install cookiecutter **Pipx (Linux, OSX and Windows):** .. code-block:: bash - $ pipx install cookiecutter + pipx install cookiecutter Upgrading @@ -130,12 +130,12 @@ Or with pip: .. code-block:: bash - $ python3 -m pip install --upgrade cookiecutter + python3 -m pip install --upgrade cookiecutter Upgrade Cookiecutter either with easy_install (deprecated): .. code-block:: bash - $ easy_install --upgrade cookiecutter + easy_install --upgrade cookiecutter Then you should be good to go. diff --git a/docs/overview.rst b/docs/overview.rst index 6b8e029ee..6a1895614 100644 --- a/docs/overview.rst +++ b/docs/overview.rst @@ -27,9 +27,8 @@ This is a directory structure for a simple cookiecutter:: You must have: -- A `cookiecutter.json` file. -- A `{{ cookiecutter.project_name }}/` directory, where - `project_name` is defined in your `cookiecutter.json`. +- A ``cookiecutter.json`` file. +- A ``{{ cookiecutter.project_name }}/`` directory, where ``project_name`` is defined in your ``cookiecutter.json``. Beyond that, you can have whatever files/directories you want. From 9ee72e1926de5dbe50608dca26ec5e7c9cf26321 Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Mon, 30 May 2022 18:49:30 +0200 Subject: [PATCH 091/274] housekeeping: run pyupgrade to find remaining py27 artifacts --- cookiecutter/cli.py | 10 ++++----- cookiecutter/config.py | 6 ++--- cookiecutter/environment.py | 8 +++---- cookiecutter/exceptions.py | 8 +++---- cookiecutter/extensions.py | 8 +++---- cookiecutter/generate.py | 14 ++++++------ cookiecutter/hooks.py | 6 ++--- cookiecutter/prompt.py | 10 ++++----- cookiecutter/replay.py | 6 ++--- cookiecutter/utils.py | 2 +- cookiecutter/vcs.py | 4 ++-- cookiecutter/zipfile.py | 4 ++-- tests/replay/test_dump.py | 2 +- tests/replay/test_load.py | 2 +- .../hello_extension/hello_extension.py | 6 ++--- .../local_extension/local_extensions/main.py | 4 +--- tests/test_default_extensions.py | 4 ++-- tests/test_generate_file.py | 12 +++++----- tests/test_generate_files.py | 22 +++++++++---------- tests/test_hooks.py | 8 +++---- tests/test_prompt.py | 14 ++++++------ tests/test_read_user_choice.py | 2 +- tests/vcs/test_clone.py | 16 +++++--------- 23 files changed, 83 insertions(+), 95 deletions(-) diff --git a/cookiecutter/cli.py b/cookiecutter/cli.py index 6b3c583ad..a792fa5f5 100644 --- a/cookiecutter/cli.py +++ b/cookiecutter/cli.py @@ -63,9 +63,9 @@ def list_installed_templates(default_config, passed_config_file): os.path.join(cookiecutter_folder, folder, 'cookiecutter.json') ) ] - click.echo('{} installed templates: '.format(len(template_names))) + click.echo(f'{len(template_names)} installed templates: ') for name in template_names: - click.echo(' * {}'.format(name)) + click.echo(f' * {name}') @click.command(context_settings=dict(help_option_names=['-h', '--help'])) @@ -219,11 +219,11 @@ def main( click.echo(e) sys.exit(1) except UndefinedVariableInTemplate as undefined_err: - click.echo('{}'.format(undefined_err.message)) - click.echo('Error message: {}'.format(undefined_err.error.message)) + click.echo(f'{undefined_err.message}') + click.echo(f'Error message: {undefined_err.error.message}') context_str = json.dumps(undefined_err.context, indent=4, sort_keys=True) - click.echo('Context: {}'.format(context_str)) + click.echo(f'Context: {context_str}') sys.exit(1) diff --git a/cookiecutter/config.py b/cookiecutter/config.py index 90483f532..0d0fa8c7e 100644 --- a/cookiecutter/config.py +++ b/cookiecutter/config.py @@ -55,9 +55,7 @@ def merge_configs(default, overwrite): def get_config(config_path): """Retrieve the config from the specified path, returning a config dict.""" if not os.path.exists(config_path): - raise ConfigDoesNotExistException( - 'Config file {} does not exist.'.format(config_path) - ) + raise ConfigDoesNotExistException(f'Config file {config_path} does not exist.') logger.debug('config_path is %s', config_path) with open(config_path, encoding='utf-8') as file_handle: @@ -65,7 +63,7 @@ def get_config(config_path): yaml_dict = yaml.safe_load(file_handle) except yaml.YAMLError as e: raise InvalidConfiguration( - 'Unable to parse YAML file {}.'.format(config_path) + f'Unable to parse YAML file {config_path}.' ) from e config_dict = merge_configs(DEFAULT_CONFIG, yaml_dict) diff --git a/cookiecutter/environment.py b/cookiecutter/environment.py index 2f91e015a..f2804c595 100644 --- a/cookiecutter/environment.py +++ b/cookiecutter/environment.py @@ -4,7 +4,7 @@ from cookiecutter.exceptions import UnknownExtension -class ExtensionLoaderMixin(object): +class ExtensionLoaderMixin: """Mixin providing sane loading of extensions specified in a given context. The context is being extracted from the keyword arguments before calling @@ -32,9 +32,9 @@ def __init__(self, **kwargs): extensions = default_extensions + self._read_extensions(context) try: - super(ExtensionLoaderMixin, self).__init__(extensions=extensions, **kwargs) + super().__init__(extensions=extensions, **kwargs) except ImportError as err: - raise UnknownExtension('Unable to load extension: {}'.format(err)) + raise UnknownExtension(f'Unable to load extension: {err}') def _read_extensions(self, context): """Return list of extensions as str to be passed on to the Jinja2 env. @@ -62,4 +62,4 @@ def __init__(self, **kwargs): Also loading extensions defined in cookiecutter.json's _extensions key. """ - super(StrictEnvironment, self).__init__(undefined=StrictUndefined, **kwargs) + super().__init__(undefined=StrictUndefined, **kwargs) diff --git a/cookiecutter/exceptions.py b/cookiecutter/exceptions.py index 9461aa985..4acf6dc47 100644 --- a/cookiecutter/exceptions.py +++ b/cookiecutter/exceptions.py @@ -124,10 +124,10 @@ def __init__(self, message, error, context): def __str__(self): """Text representation of UndefinedVariableInTemplate.""" return ( - "{self.message}. " - "Error message: {self.error.message}. " - "Context: {self.context}" - ).format(**locals()) + f"{self.message}. " + f"Error message: {self.error.message}. " + f"Context: {self.context}" + ) class UnknownExtension(CookiecutterException): diff --git a/cookiecutter/extensions.py b/cookiecutter/extensions.py index e3a42b244..6a3161aba 100644 --- a/cookiecutter/extensions.py +++ b/cookiecutter/extensions.py @@ -13,7 +13,7 @@ class JsonifyExtension(Extension): def __init__(self, environment): """Initialize the extension with the given environment.""" - super(JsonifyExtension, self).__init__(environment) + super().__init__(environment) def jsonify(obj): return json.dumps(obj, sort_keys=True, indent=4) @@ -26,7 +26,7 @@ class RandomStringExtension(Extension): def __init__(self, environment): """Jinja2 Extension Constructor.""" - super(RandomStringExtension, self).__init__(environment) + super().__init__(environment) def random_ascii_string(length, punctuation=False): if punctuation: @@ -43,7 +43,7 @@ class SlugifyExtension(Extension): def __init__(self, environment): """Jinja2 Extension constructor.""" - super(SlugifyExtension, self).__init__(environment) + super().__init__(environment) def slugify(value, **kwargs): """Slugifies the value.""" @@ -57,7 +57,7 @@ class UUIDExtension(Extension): def __init__(self, environment): """Jinja2 Extension constructor.""" - super(UUIDExtension, self).__init__(environment) + super().__init__(environment) def uuid4(): """Generate UUID4.""" diff --git a/cookiecutter/generate.py b/cookiecutter/generate.py index 7dbd9867b..7bdce5a8b 100644 --- a/cookiecutter/generate.py +++ b/cookiecutter/generate.py @@ -96,8 +96,8 @@ def generate_context( full_fpath = os.path.abspath(context_file) json_exc_message = str(e) our_exc_message = ( - 'JSON decoding error while loading "{0}". Decoding' - ' error details: "{1}"'.format(full_fpath, json_exc_message) + 'JSON decoding error while loading "{}". Decoding' + ' error details: "{}"'.format(full_fpath, json_exc_message) ) raise ContextDecodingException(our_exc_message) @@ -180,7 +180,7 @@ def generate_file(project_dir, infile, context, env, skip_if_file_exists=False): # Detect original file newline to output the rendered file # note: newline='' ensures newlines are not converted - with open(infile, 'r', encoding='utf-8', newline='') as rd: + with open(infile, encoding='utf-8', newline='') as rd: rd.readline() # Read the first line to load 'newlines' value # Use `_new_lines` overwrite from context, if configured. @@ -219,7 +219,7 @@ def render_and_create_dir( 'Output directory %s already exists, overwriting it', dir_to_create ) else: - msg = 'Error: "{}" directory already exists'.format(dir_to_create) + msg = f'Error: "{dir_to_create}" directory already exists' raise OutputDirExistsException(msg) else: make_sure_path_exists(dir_to_create) @@ -292,7 +292,7 @@ def generate_files( unrendered_dir, context, output_dir, env, overwrite_if_exists ) except UndefinedError as err: - msg = "Unable to create project directory '{}'".format(unrendered_dir) + msg = f"Unable to create project directory '{unrendered_dir}'" raise UndefinedVariableInTemplate(msg, err, context) # We want the Jinja path and the OS paths to match. Consequently, we'll: @@ -354,7 +354,7 @@ def generate_files( if delete_project_on_failure: rmtree(project_dir) _dir = os.path.relpath(unrendered_dir, output_dir) - msg = "Unable to create directory '{}'".format(_dir) + msg = f"Unable to create directory '{_dir}'" raise UndefinedVariableInTemplate(msg, err, context) for f in files: @@ -376,7 +376,7 @@ def generate_files( except UndefinedError as err: if delete_project_on_failure: rmtree(project_dir) - msg = "Unable to create file '{}'".format(infile) + msg = f"Unable to create file '{infile}'" raise UndefinedVariableInTemplate(msg, err, context) if accept_hooks: diff --git a/cookiecutter/hooks.py b/cookiecutter/hooks.py index b6a31a1e0..763287c58 100644 --- a/cookiecutter/hooks.py +++ b/cookiecutter/hooks.py @@ -83,14 +83,14 @@ def run_script(script_path, cwd='.'): exit_status = proc.wait() if exit_status != EXIT_SUCCESS: raise FailedHookException( - 'Hook script failed (exit status: {})'.format(exit_status) + f'Hook script failed (exit status: {exit_status})' ) except OSError as os_error: if os_error.errno == errno.ENOEXEC: raise FailedHookException( 'Hook script failed, might be an empty file or missing a shebang' ) - raise FailedHookException('Hook script failed (error: {})'.format(os_error)) + raise FailedHookException(f'Hook script failed (error: {os_error})') def run_script_with_context(script_path, cwd, context): @@ -102,7 +102,7 @@ def run_script_with_context(script_path, cwd, context): """ _, extension = os.path.splitext(script_path) - with open(script_path, 'r', encoding='utf-8') as file: + with open(script_path, encoding='utf-8') as file: contents = file.read() with tempfile.NamedTemporaryFile(delete=False, mode='wb', suffix=extension) as temp: diff --git a/cookiecutter/prompt.py b/cookiecutter/prompt.py index 4b8b2fbe6..f06cdc3c0 100644 --- a/cookiecutter/prompt.py +++ b/cookiecutter/prompt.py @@ -58,16 +58,14 @@ def read_user_choice(var_name, options): if not options: raise ValueError - choice_map = OrderedDict( - ('{}'.format(i), value) for i, value in enumerate(options, 1) - ) + choice_map = OrderedDict((f'{i}', value) for i, value in enumerate(options, 1)) choices = choice_map.keys() default = '1' choice_lines = ['{} - {}'.format(*c) for c in choice_map.items()] prompt = '\n'.join( ( - 'Select {}:'.format(var_name), + f'Select {var_name}:', '\n'.join(choice_lines), 'Choose from {}'.format(', '.join(choices)), ) @@ -213,7 +211,7 @@ def prompt_for_config(context, no_input=False): cookiecutter_dict[key] = val except UndefinedError as err: - msg = "Unable to render variable '{}'".format(key) + msg = f"Unable to render variable '{key}'" raise UndefinedVariableInTemplate(msg, err, context) # Second pass; handle the dictionaries. @@ -232,7 +230,7 @@ def prompt_for_config(context, no_input=False): cookiecutter_dict[key] = val except UndefinedError as err: - msg = "Unable to render variable '{}'".format(key) + msg = f"Unable to render variable '{key}'" raise UndefinedVariableInTemplate(msg, err, context) return cookiecutter_dict diff --git a/cookiecutter/replay.py b/cookiecutter/replay.py index 504a6a32c..9730e84da 100644 --- a/cookiecutter/replay.py +++ b/cookiecutter/replay.py @@ -12,14 +12,14 @@ def get_file_name(replay_dir, template_name): """Get the name of file.""" suffix = '.json' if not template_name.endswith('.json') else '' - file_name = '{}{}'.format(template_name, suffix) + file_name = f'{template_name}{suffix}' return os.path.join(replay_dir, file_name) def dump(replay_dir, template_name, context): """Write json data to file.""" if not make_sure_path_exists(replay_dir): - raise IOError('Unable to create replay dir at {}'.format(replay_dir)) + raise OSError(f'Unable to create replay dir at {replay_dir}') if not isinstance(template_name, str): raise TypeError('Template name is required to be of type str') @@ -43,7 +43,7 @@ def load(replay_dir, template_name): replay_file = get_file_name(replay_dir, template_name) - with open(replay_file, 'r') as infile: + with open(replay_file) as infile: context = json.load(infile) if 'cookiecutter' not in context: diff --git a/cookiecutter/utils.py b/cookiecutter/utils.py index ef533171a..4750a2663 100644 --- a/cookiecutter/utils.py +++ b/cookiecutter/utils.py @@ -113,7 +113,7 @@ def simple_filter(filter_function): class SimpleFilterExtension(Extension): def __init__(self, environment): - super(SimpleFilterExtension, self).__init__(environment) + super().__init__(environment) environment.filters[filter_function.__name__] = filter_function SimpleFilterExtension.__name__ = filter_function.__name__ diff --git a/cookiecutter/vcs.py b/cookiecutter/vcs.py index d7f7b5ba5..08cb2eb0c 100644 --- a/cookiecutter/vcs.py +++ b/cookiecutter/vcs.py @@ -73,7 +73,7 @@ def clone(repo_url, checkout=None, clone_to_dir='.', no_input=False): # check that the appropriate VCS for the repo_type is installed if not is_vcs_installed(repo_type): - msg = "'{0}' is not installed.".format(repo_type) + msg = f"'{repo_type}' is not installed." raise VCSNotInstalled(msg) repo_url = repo_url.rstrip('/') @@ -83,7 +83,7 @@ def clone(repo_url, checkout=None, clone_to_dir='.', no_input=False): repo_dir = os.path.normpath(os.path.join(clone_to_dir, repo_name)) if repo_type == 'hg': repo_dir = os.path.normpath(os.path.join(clone_to_dir, repo_name)) - logger.debug('repo_dir is {0}'.format(repo_dir)) + logger.debug(f'repo_dir is {repo_dir}') if os.path.isdir(repo_dir): clone = prompt_and_delete(repo_dir, no_input=no_input) diff --git a/cookiecutter/zipfile.py b/cookiecutter/zipfile.py index 24925c7fc..7395ce61b 100644 --- a/cookiecutter/zipfile.py +++ b/cookiecutter/zipfile.py @@ -55,7 +55,7 @@ def unzip(zip_uri, is_url, clone_to_dir='.', no_input=False, password=None): zip_file = ZipFile(zip_path) if len(zip_file.namelist()) == 0: - raise InvalidZipRepository('Zip repository {} is empty'.format(zip_uri)) + raise InvalidZipRepository(f'Zip repository {zip_uri} is empty') # The first record in the zipfile should be the directory entry for # the archive. If it isn't a directory, there's a problem. @@ -106,7 +106,7 @@ def unzip(zip_uri, is_url, clone_to_dir='.', no_input=False, password=None): except BadZipFile: raise InvalidZipRepository( - 'Zip repository {} is not a valid zip archive:'.format(zip_uri) + f'Zip repository {zip_uri} is not a valid zip archive:' ) return unzip_path diff --git a/tests/replay/test_dump.py b/tests/replay/test_dump.py index ec8010b3e..c757321b1 100644 --- a/tests/replay/test_dump.py +++ b/tests/replay/test_dump.py @@ -16,7 +16,7 @@ def template_name(): @pytest.fixture def replay_file(replay_test_dir, template_name): """Fixture to return a actual file name of the dump.""" - file_name = '{}.json'.format(template_name) + file_name = f'{template_name}.json' return os.path.join(replay_test_dir, file_name) diff --git a/tests/replay/test_load.py b/tests/replay/test_load.py index a64a285e1..c8bc453e0 100644 --- a/tests/replay/test_load.py +++ b/tests/replay/test_load.py @@ -16,7 +16,7 @@ def template_name(): @pytest.fixture def replay_file(replay_test_dir, template_name): """Fixture to return a actual file name of the dump.""" - file_name = '{}.json'.format(template_name) + file_name = f'{template_name}.json' return os.path.join(replay_test_dir, file_name) diff --git a/tests/test-extensions/hello_extension/hello_extension.py b/tests/test-extensions/hello_extension/hello_extension.py index f54b6efdd..07f3753b9 100644 --- a/tests/test-extensions/hello_extension/hello_extension.py +++ b/tests/test-extensions/hello_extension/hello_extension.py @@ -6,15 +6,15 @@ class HelloExtension(Extension): """Simple jinja2 extension for cookiecutter test purposes.""" - tags = set(['hello']) + tags = {'hello'} def __init__(self, environment): """Hello Extension Constructor.""" - super(HelloExtension, self).__init__(environment) + super().__init__(environment) def _hello(self, name): """Do actual tag replace when invoked by parser.""" - return 'Hello {name}!'.format(name=name) + return f'Hello {name}!' def parse(self, parser): """Work when something match `tags` variable.""" diff --git a/tests/test-extensions/local_extension/local_extensions/main.py b/tests/test-extensions/local_extension/local_extensions/main.py index 53f6f8f95..b18a25c91 100644 --- a/tests/test-extensions/local_extension/local_extensions/main.py +++ b/tests/test-extensions/local_extension/local_extensions/main.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """Provides custom extension, exposing a ``foobar`` filter.""" from jinja2.ext import Extension @@ -11,7 +9,7 @@ class FoobarExtension(Extension): def __init__(self, environment): """Foobar Extension Constructor.""" - super(FoobarExtension, self).__init__(environment) + super().__init__(environment) environment.filters['foobar'] = lambda v: v * 2 diff --git a/tests/test_default_extensions.py b/tests/test_default_extensions.py index e73ef9c1b..540138d31 100644 --- a/tests/test_default_extensions.py +++ b/tests/test_default_extensions.py @@ -25,7 +25,7 @@ def test_jinja2_time_extension(tmp_path): changelog_file = os.path.join(project_dir, 'HISTORY.rst') assert os.path.isfile(changelog_file) - with open(changelog_file, 'r', encoding='utf-8') as f: + with open(changelog_file, encoding='utf-8') as f: changelog_lines = f.readlines() expected_lines = [ @@ -57,7 +57,7 @@ def test_jinja2_uuid_extension(tmp_path): changelog_file = os.path.join(project_dir, 'id') assert os.path.isfile(changelog_file) - with open(changelog_file, 'r', encoding='utf-8') as f: + with open(changelog_file, encoding='utf-8') as f: changelog_lines = f.readlines() uuid.UUID(changelog_lines[0], version=4) diff --git a/tests/test_generate_file.py b/tests/test_generate_file.py index a393a2ef8..18c811eea 100644 --- a/tests/test_generate_file.py +++ b/tests/test_generate_file.py @@ -45,7 +45,7 @@ def test_generate_file(env): env=env, ) assert os.path.isfile('tests/files/cheese.txt') - with open('tests/files/cheese.txt', 'rt') as f: + with open('tests/files/cheese.txt') as f: generated_text = f.read() assert generated_text == 'Testing cheese' @@ -58,7 +58,7 @@ def test_generate_file_jsonify_filter(env): project_dir=".", infile=infile, context={'cookiecutter': data}, env=env ) assert os.path.isfile('tests/files/cheese.txt') - with open('tests/files/cheese.txt', 'rt') as f: + with open('tests/files/cheese.txt') as f: generated_text = f.read() assert json.loads(generated_text) == data @@ -72,7 +72,7 @@ def test_generate_file_random_ascii_string(env, length, punctuation): context = {"cookiecutter": data, "length": length, "punctuation": punctuation} generate.generate_file(project_dir=".", infile=infile, context=context, env=env) assert os.path.isfile('tests/files/cheese.txt') - with open('tests/files/cheese.txt', 'rt') as f: + with open('tests/files/cheese.txt') as f: generated_text = f.read() assert len(generated_text) == length @@ -92,7 +92,7 @@ def test_generate_file_with_true_condition(env): env=env, ) assert os.path.isfile('tests/files/cheese.txt') - with open('tests/files/cheese.txt', 'rt') as f: + with open('tests/files/cheese.txt') as f: generated_text = f.read() assert generated_text == 'Testing that generate_file was y' @@ -148,7 +148,7 @@ def test_generate_file_does_not_translate_lf_newlines_to_crlf(env, tmp_path): # this generated file should have a LF line ending gf = 'tests/files/cheese_lf_newlines.txt' - with open(gf, 'r', encoding='utf-8', newline='') as f: + with open(gf, encoding='utf-8', newline='') as f: simple_text = f.readline() assert simple_text == 'newline is LF\n' assert f.newlines == '\n' @@ -166,7 +166,7 @@ def test_generate_file_does_not_translate_crlf_newlines_to_lf(env): # this generated file should have a CRLF line ending gf = 'tests/files/cheese_crlf_newlines.txt' - with open(gf, 'r', encoding='utf-8', newline='') as f: + with open(gf, encoding='utf-8', newline='') as f: simple_text = f.readline() assert simple_text == 'newline is CRLF\r\n' assert f.newlines == '\r\n' diff --git a/tests/test_generate_files.py b/tests/test_generate_files.py index 4e21c7053..4d6ef1113 100644 --- a/tests/test_generate_files.py +++ b/tests/test_generate_files.py @@ -44,7 +44,7 @@ def test_generate_files(tmp_path): assert simple_file.exists() assert simple_file.is_file() - simple_text = open(simple_file, 'rt', encoding='utf-8').read() + simple_text = open(simple_file, encoding='utf-8').read() assert simple_text == 'I eat pizzä' @@ -60,7 +60,7 @@ def test_generate_files_with_linux_newline(tmp_path): assert newline_file.is_file() assert newline_file.exists() - with open(newline_file, 'r', encoding='utf-8', newline='') as f: + with open(newline_file, encoding='utf-8', newline='') as f: simple_text = f.readline() assert simple_text == 'newline is LF\n' assert f.newlines == '\n' @@ -84,7 +84,7 @@ def test_generate_files_with_jinja2_environment(tmp_path): assert conditions_file.exists() simple_text = conditions_file.open('rt', encoding='utf-8').read() - assert simple_text == u'I eat pizzä\n' + assert simple_text == 'I eat pizzä\n' def test_generate_files_with_trailing_newline_forced_to_linux_by_context(tmp_path): @@ -100,7 +100,7 @@ def test_generate_files_with_trailing_newline_forced_to_linux_by_context(tmp_pat assert newline_file.is_file() assert newline_file.exists() - with open(newline_file, 'r', encoding='utf-8', newline='') as f: + with open(newline_file, encoding='utf-8', newline='') as f: simple_text = f.readline() assert simple_text == 'newline is LF\r\n' assert f.newlines == '\r\n' @@ -118,7 +118,7 @@ def test_generate_files_with_windows_newline(tmp_path): assert newline_file.is_file() assert newline_file.exists() - with open(newline_file, 'r', encoding='utf-8', newline='') as f: + with open(newline_file, encoding='utf-8', newline='') as f: simple_text = f.readline() assert simple_text == 'newline is CRLF\r\n' assert f.newlines == '\r\n' @@ -136,7 +136,7 @@ def test_generate_files_with_windows_newline_forced_to_linux_by_context(tmp_path assert newline_file.is_file() assert newline_file.exists() - with open(newline_file, 'r', encoding='utf-8', newline='') as f: + with open(newline_file, encoding='utf-8', newline='') as f: simple_text = f.readline() assert simple_text == 'newline is CRLF\n' @@ -257,7 +257,7 @@ def test_generate_files_with_overwrite_if_exists_with_skip_if_file_exists(tmp_pa assert Path(simple_with_new_line_file).is_file() assert Path(simple_with_new_line_file).exists() - simple_text = open(simple_file, 'rt', encoding='utf-8').read() + simple_text = open(simple_file, encoding='utf-8').read() assert simple_text == 'temp' @@ -283,7 +283,7 @@ def test_generate_files_with_skip_if_file_exists(tmp_path): assert not Path(simple_with_new_line_file).is_file() assert not Path(simple_with_new_line_file).exists() - simple_text = open(simple_file, 'rt', encoding='utf-8').read() + simple_text = open(simple_file, encoding='utf-8').read() assert simple_text == 'temp' @@ -308,7 +308,7 @@ def test_generate_files_with_overwrite_if_exists(tmp_path): assert Path(simple_with_new_line_file).is_file() assert Path(simple_with_new_line_file).exists() - simple_text = open(simple_file, 'rt', encoding='utf-8').read() + simple_text = open(simple_file, encoding='utf-8').read() assert simple_text == 'I eat pizzä' @@ -382,7 +382,7 @@ def test_raise_undefined_variable_dir_name(output_dir, undefined_context): error = err.value directory = Path('testproject', '{{cookiecutter.foobar}}') - msg = "Unable to create directory '{}'".format(directory) + msg = f"Unable to create directory '{directory}'" assert msg == error.message assert error.context == undefined_context @@ -407,7 +407,7 @@ def test_raise_undefined_variable_dir_name_existing_project( error = err.value directory = Path('testproject', '{{cookiecutter.foobar}}') - msg = "Unable to create directory '{}'".format(directory) + msg = f"Unable to create directory '{directory}'" assert msg == error.message assert error.context == undefined_context diff --git a/tests/test_hooks.py b/tests/test_hooks.py index d8b55dff2..d214e714b 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -71,7 +71,7 @@ def make_test_repo(name, multiple_hooks=False): return post -class TestFindHooks(object): +class TestFindHooks: """Class to unite find hooks related tests in one place.""" repo_path = 'tests/test-hooks' @@ -91,7 +91,7 @@ def test_find_hook(self): actual_hook_path = hooks.find_hook('pre_gen_project') assert expected_pre == actual_hook_path[0] - expected_post = os.path.abspath('hooks/{}'.format(self.post_hook)) + expected_post = os.path.abspath(f'hooks/{self.post_hook}') actual_hook_path = hooks.find_hook('post_gen_project') assert expected_post == actual_hook_path[0] @@ -111,7 +111,7 @@ def test_hook_not_found(self): assert hooks.find_hook('unknown_hook') is None -class TestExternalHooks(object): +class TestExternalHooks: """Class to unite tests for hooks with different project paths.""" repo_path = os.path.abspath('tests/test-hooks/') @@ -154,7 +154,7 @@ def test_run_failing_script(self, mocker): with pytest.raises(exceptions.FailedHookException) as excinfo: hooks.run_script(os.path.join(self.hooks_path, self.post_hook)) - assert 'Hook script failed (error: {})'.format(err) in str(excinfo.value) + assert f'Hook script failed (error: {err})' in str(excinfo.value) def test_run_failing_script_enoexec(self, mocker): """Test correct exception raise if run_script fails.""" diff --git a/tests/test_prompt.py b/tests/test_prompt.py index 8187cb283..037591dd4 100644 --- a/tests/test_prompt.py +++ b/tests/test_prompt.py @@ -66,7 +66,7 @@ def test_convert_to_str_complex_variables(self, raw_var, rendered_var): assert result == rendered_var -class TestPrompt(object): +class TestPrompt: """Class to unite user prompt related tests.""" @pytest.mark.parametrize( @@ -210,11 +210,11 @@ def test_should_render_private_variables_with_two_underscores(self): [ ('foo', 'Hello world'), ('bar', 123), - ('rendered_foo', u'{{ cookiecutter.foo|lower }}'), + ('rendered_foo', '{{ cookiecutter.foo|lower }}'), ('rendered_bar', 123), - ('_hidden_foo', u'{{ cookiecutter.foo|lower }}'), + ('_hidden_foo', '{{ cookiecutter.foo|lower }}'), ('_hidden_bar', 123), - ('__rendered_hidden_foo', u'{{ cookiecutter.foo|lower }}'), + ('__rendered_hidden_foo', '{{ cookiecutter.foo|lower }}'), ('__rendered_hidden_bar', 123), ] ) @@ -226,7 +226,7 @@ def test_should_render_private_variables_with_two_underscores(self): ('bar', '123'), ('rendered_foo', 'hello world'), ('rendered_bar', '123'), - ('_hidden_foo', u'{{ cookiecutter.foo|lower }}'), + ('_hidden_foo', '{{ cookiecutter.foo|lower }}'), ('_hidden_bar', 123), ('__rendered_hidden_foo', 'hello world'), ('__rendered_hidden_bar', '123'), @@ -252,7 +252,7 @@ def test_should_not_render_private_variables(self): assert cookiecutter_dict == context['cookiecutter'] -class TestReadUserChoice(object): +class TestReadUserChoice: """Class to unite choices prompt related tests.""" def test_should_invoke_read_user_choice(self, mocker): @@ -332,7 +332,7 @@ def test_should_render_choices(self, mocker): assert cookiecutter_dict == expected -class TestPromptChoiceForConfig(object): +class TestPromptChoiceForConfig: """Class to unite choices prompt related tests with config test.""" @pytest.fixture diff --git a/tests/test_read_user_choice.py b/tests/test_read_user_choice.py index ef9ae62f4..f3573593c 100644 --- a/tests/test_read_user_choice.py +++ b/tests/test_read_user_choice.py @@ -24,7 +24,7 @@ def test_click_invocation(mocker, user_choice, expected_value): choice.return_value = click.Choice(OPTIONS) prompt = mocker.patch('click.prompt') - prompt.return_value = '{}'.format(user_choice) + prompt.return_value = f'{user_choice}' assert read_user_choice('varname', OPTIONS) == expected_value diff --git a/tests/vcs/test_clone.py b/tests/vcs/test_clone.py index cd4ac13d7..9fc3b24fa 100644 --- a/tests/vcs/test_clone.py +++ b/tests/vcs/test_clone.py @@ -130,10 +130,8 @@ def test_clone_should_invoke_vcs_command( @pytest.mark.parametrize( 'error_message', [ - ( - "fatal: repository 'https://github.com/hackebro/cookiedozer' not found" - ).encode('utf-8'), - 'hg: abort: HTTP Error 404: Not Found'.encode('utf-8'), + (b"fatal: repository 'https://github.com/hackebro/cookiedozer' not found"), + b'hg: abort: HTTP Error 404: Not Found', ], ) def test_clone_handles_repo_typo(mocker, clone_dir, error_message): @@ -160,10 +158,8 @@ def test_clone_handles_repo_typo(mocker, clone_dir, error_message): @pytest.mark.parametrize( 'error_message', [ - ( - "error: pathspec 'unknown_branch' did not match any file(s) known to git" - ).encode('utf-8'), - "hg: abort: unknown revision 'unknown_branch'!".encode('utf-8'), + b"error: pathspec 'unknown_branch' did not match any file(s) known to git", + b"hg: abort: unknown revision 'unknown_branch'!", ], ) def test_clone_handles_branch_typo(mocker, clone_dir, error_message): @@ -196,9 +192,7 @@ def test_clone_unknown_subprocess_error(mocker, clone_dir): 'cookiecutter.vcs.subprocess.check_output', autospec=True, side_effect=[ - subprocess.CalledProcessError( - -1, 'cmd', output='Something went wrong'.encode('utf-8') - ) + subprocess.CalledProcessError(-1, 'cmd', output=b'Something went wrong') ], ) From f01124001b21e8f19a0b06d5518b70515c3ef332 Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Mon, 30 May 2022 20:24:02 +0200 Subject: [PATCH 092/274] Documentation overhaul (3) --- docs/advanced/calling_from_python.rst | 10 ++-- docs/advanced/choice_variables.rst | 28 ++++++++---- docs/advanced/copy_without_render.rst | 14 ++++-- docs/advanced/dict_variables.rst | 24 +++++----- docs/advanced/directories.rst | 15 +++--- docs/advanced/hooks.rst | 51 +++++++++------------ docs/advanced/injecting_context.rst | 33 ++++++++++---- docs/advanced/local_extensions.rst | 4 +- docs/advanced/new_line_characters.rst | 23 ++++++---- docs/advanced/private_variables.rst | 17 +++++-- docs/advanced/replay.rst | 20 +++++--- docs/advanced/suppressing_prompts.rst | 17 ++++--- docs/advanced/template_extensions.rst | 35 ++++++-------- docs/advanced/templates_in_context.rst | 28 ++++++------ docs/advanced/user_config.rst | 26 +++++++---- docs/tutorials/tutorial1.rst | 63 ++++++++++++-------------- docs/tutorials/tutorial2.rst | 13 +++--- 17 files changed, 235 insertions(+), 186 deletions(-) diff --git a/docs/advanced/calling_from_python.rst b/docs/advanced/calling_from_python.rst index 4c7f2e9ad..b497b6bf0 100644 --- a/docs/advanced/calling_from_python.rst +++ b/docs/advanced/calling_from_python.rst @@ -3,7 +3,9 @@ Calling Cookiecutter Functions From Python ------------------------------------------ -You can use Cookiecutter from Python:: +You can use Cookiecutter from Python: + +.. code-block:: python from cookiecutter.main import cookiecutter @@ -13,6 +15,6 @@ You can use Cookiecutter from Python:: # Create project from the cookiecutter-pypackage.git repo template cookiecutter('https://github.com/audreyr/cookiecutter-pypackage.git') -This is useful if, for example, you're writing a web framework and need to -provide developers with a tool similar to `django-admin.py startproject` or -`npm init`. +This is useful if, for example, you're writing a web framework and need to provide developers with a tool similar to `django-admin.py startproject` or `npm init`. + +See the :ref:`API Reference ` for more details. diff --git a/docs/advanced/choice_variables.rst b/docs/advanced/choice_variables.rst index ccb35d4bf..706d53947 100644 --- a/docs/advanced/choice_variables.rst +++ b/docs/advanced/choice_variables.rst @@ -1,17 +1,21 @@ .. _choice-variables: -Choice Variables (1.1+) ------------------------ +Choice Variables +---------------- -Choice variables provide different choices when creating a project. Depending on a user's choice -the template renders things differently. +*New in Cookiecutter 1.1* + +Choice variables provide different choices when creating a project. +Depending on a user's choice the template renders things differently. Basic Usage ~~~~~~~~~~~ Choice variables are regular key / value pairs, but with the value being a list of strings. -For example, if you provide the following choice variable in your ``cookiecutter.json``:: +For example, if you provide the following choice variable in your ``cookiecutter.json``: + +.. code-block:: JSON { "license": ["MIT", "BSD-3", "GNU GPL v3.0", "Apache Software License 2.0"] @@ -28,8 +32,9 @@ you'd get the following choices when running Cookiecutter:: Depending on an user's choice, a different license is rendered by Cookiecutter. -The above ``license`` choice variable creates ``cookiecutter.license``, which -can be used like this:: +The above ``license`` choice variable creates ``cookiecutter.license``, which can be used like this: + +.. code-block:: html+jinja {%- if cookiecutter.license == "MIT" -%} # Possible license content here @@ -41,7 +46,10 @@ can be used like this:: Cookiecutter is using `Jinja2's if conditional expression `_ to determine the correct license. -The created choice variable is still a regular Cookiecutter variable and can be used like this:: +The created choice variable is still a regular Cookiecutter variable and can be used like this: + +.. code-block:: html+jinja + License ------- @@ -53,7 +61,9 @@ Overwriting Default Choice Values Choice Variables are overwritable using a :ref:`user-config` file. -For example, a choice variable can be created in ``cookiecutter.json`` by using a list as value:: +For example, a choice variable can be created in ``cookiecutter.json`` by using a list as value: + +.. code-block:: JSON { "license": ["MIT", "BSD-3", "GNU GPL v3.0", "Apache Software License 2.0"] diff --git a/docs/advanced/copy_without_render.rst b/docs/advanced/copy_without_render.rst index 2cdb68084..5e4bc008d 100644 --- a/docs/advanced/copy_without_render.rst +++ b/docs/advanced/copy_without_render.rst @@ -5,7 +5,10 @@ Copy without Render *New in Cookiecutter 1.1* -To avoid rendering directories and files of a cookiecutter, the `_copy_without_render` key can be used in the `cookiecutter.json`. The value of this key accepts a list of Unix shell-style wildcards:: +To avoid rendering directories and files of a cookiecutter, the ``_copy_without_render`` key can be used in the ``cookiecutter.json``. +The value of this key accepts a list of Unix shell-style wildcards: + +.. code-block:: JSON { "project_slug": "sample", @@ -16,7 +19,12 @@ To avoid rendering directories and files of a cookiecutter, the `_copy_without_r ] } -**Note**: Only the content of the files will be copied without being rendered. The paths are subject to rendering. This allows you to write:: +**Note**: +Only the content of the files will be copied without being rendered. +The paths are subject to rendering. +This allows you to write: + +.. code-block:: JSON { "project_slug": "sample", @@ -25,4 +33,4 @@ To avoid rendering directories and files of a cookiecutter, the `_copy_without_r ] } -In this example, `{{cookiecutter.repo_name}}` will be rendered as expected but the html file content will be copied without rendering. +In this example, ``{{cookiecutter.repo_name}}`` will be rendered as expected but the html file content will be copied without rendering. diff --git a/docs/advanced/dict_variables.rst b/docs/advanced/dict_variables.rst index 1db841fa1..6142f4bd8 100644 --- a/docs/advanced/dict_variables.rst +++ b/docs/advanced/dict_variables.rst @@ -1,20 +1,21 @@ .. _dict-variables: -Dictionary Variables (1.5+) ---------------------------- +Dictionary Variables +-------------------- -Dictionary variables provide a way to define deep structured information when -rendering a template. +*New in Cookiecutter 1.5* + +Dictionary variables provide a way to define deep structured information when rendering a template. Basic Usage ~~~~~~~~~~~ -Dictionary variables are, as the name suggests, dictionaries of key-value -pairs. The dictionary values can, themselves, be other dictionaries and lists -- the data structure can be as deep as you need. +Dictionary variables are, as the name suggests, dictionaries of key-value pairs. +The dictionary values can, themselves, be other dictionaries and lists - the data structure can be as deep as you need. + +For example, you could provide the following dictionary variable in your ``cookiecutter.json``: -For example, you could provide the following dictionary variable in your -``cookiecutter.json``:: +.. code-block:: json { "project_slug": "new_project", @@ -38,8 +39,9 @@ For example, you could provide the following dictionary variable in your } -The above ``file_type`` dictionary variable creates -``cookiecutter.file_types``, which can be used like this:: +The above ``file_type`` dictionary variable creates ``cookiecutter.file_types``, which can be used like this: + +.. code-block:: html+jinja {% for extension, details in cookiecutter.file_types|dictsort %}
diff --git a/docs/advanced/directories.rst b/docs/advanced/directories.rst index cbec8e4ae..ba0596ded 100644 --- a/docs/advanced/directories.rst +++ b/docs/advanced/directories.rst @@ -1,14 +1,13 @@ .. _directories: -Organizing cookiecutters in directories (1.7+) ---------------------------------------------------- +Organizing cookiecutters in directories +--------------------------------------- *New in Cookiecutter 1.7* -Cookiecutter introduces the ability to organize several templates in one -repository or zip file, separating them by directories. This allows using -symlinks for general files. Here's an example repository demonstrating -this feature:: +Cookiecutter introduces the ability to organize several templates in one repository or zip file, separating them by directories. +This allows using symlinks for general files. +Here's an example repository demonstrating this feature:: https://github.com/user/repo-name.git ├── directory1-name/ @@ -18,6 +17,8 @@ this feature:: ├── {{cookiecutter.project_slug}}/ └── cookiecutter.json -To activate one of templates within a subdirectory, use the ``--directory`` option:: +To activate one of templates within a subdirectory, use the ``--directory`` option: + +.. code-block:: bash cookiecutter https://github.com/user/repo-name.git --directory="directory1-name" diff --git a/docs/advanced/hooks.rst b/docs/advanced/hooks.rst index c659fd407..d08cf9e75 100644 --- a/docs/advanced/hooks.rst +++ b/docs/advanced/hooks.rst @@ -1,12 +1,13 @@ .. _user-hooks: -Using Pre/Post-Generate Hooks (0.7.0+) -====================================== +Using Pre/Post-Generate Hooks +============================= -You can have Python or Shell scripts that run before and/or after your project -is generated. +*New in cookiecutter 0.7* -Put them in `hooks/` like this:: +You can have Python or Shell scripts that run before and/or after your project is generated. + +Put them in ``hooks/`` like this:: cookiecutter-something/ ├── {{cookiecutter.project_slug}}/ @@ -24,13 +25,11 @@ Shell scripts work similarly:: │ └── post_gen_project.sh └── cookiecutter.json -It shouldn't be too hard to extend Cookiecutter to work with other types of -scripts too. Pull requests are welcome. +It shouldn't be too hard to extend Cookiecutter to work with other types of scripts too. +Pull requests are welcome. -For portability, you should use Python scripts (with extension `.py`) for your -hooks, as these can be run on any platform. However, if you intend for your -template to only be run on a single platform, a shell script (or `.bat` file -on Windows) can be a quicker alternative. +For portability, you should use Python scripts (with extension `.py`) for your hooks, as these can be run on any platform. +However, if you intend for your template to only be run on a single platform, a shell script (or `.bat` file on Windows) can be a quicker alternative. Writing hooks ------------- @@ -40,25 +39,21 @@ Here are some details on how to write pre/post-generate hook scripts. Exit with an appropriate status ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Make sure your hook scripts work in a robust manner. If a hook script fails -(that is, `if it finishes with a nonzero exit status -`_), the project -generation will stop and the generated directory will be cleaned up. +Make sure your hook scripts work in a robust manner. +If a hook script fails (that is, `if it finishes with a nonzero exit status `_), the project generation will stop and the generated directory will be cleaned up. Current working directory ^^^^^^^^^^^^^^^^^^^^^^^^^ -When the hook scripts script are run, their current working directory is the -root of the generated project. This makes it easy for a post-generate hook to -find generated files using relative paths. +When the hook scripts script are run, their current working directory is the root of the generated project. +This makes it easy for a post-generate hook to find generated files using relative paths. Template variables are rendered in the script ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Just like your project template, Cookiecutter also renders Jinja template -syntax in your scripts. This lets you incorporate Jinja template variables in -your scripts. For example, this line of Python sets ``module_name`` to the -value of the ``cookiecutter.module_name`` template variable: +Just like your project template, Cookiecutter also renders Jinja template syntax in your scripts. +This lets you incorporate Jinja template variables in your scripts. +For example, this line of Python sets ``module_name`` to the value of the ``cookiecutter.module_name`` template variable: .. code-block:: python @@ -67,9 +62,7 @@ value of the ``cookiecutter.module_name`` template variable: Example: Validating template variables -------------------------------------- -Here is an example of a pre-generate hook script, defined at -``hooks/pre_gen_project.py``, that validates a template variable before generating the -project: +Here is an example of a pre-generate hook script, defined at ``hooks/pre_gen_project.py``, that validates a template variable before generating the project: .. code-block:: python @@ -90,12 +83,10 @@ project: Example: Conditional files / directories ---------------------------------------- -Here is an example of a post-generate hook script, defined at -``hooks/post_gen_project.py``, on how to achieve conditional control of files and -directories after generating the project. +Here is an example of a post-generate hook script. +The file ``hooks/post_gen_project.py`` shows how to achieve conditional control of files and directories after generating the project. -The script ensures that the directory structure is as expected by -removing unwanted files and directories: +The script ensures that the directory structure is as expected by removing unwanted files and directories: .. code-block:: python diff --git a/docs/advanced/injecting_context.rst b/docs/advanced/injecting_context.rst index 32927c07f..5561b3d83 100644 --- a/docs/advanced/injecting_context.rst +++ b/docs/advanced/injecting_context.rst @@ -3,17 +3,30 @@ Injecting Extra Context ----------------------- -You can specify an `extra_context` dictionary that will override values from `cookiecutter.json` or `.cookiecutterrc`:: +You can specify an ``extra_context`` dictionary that will override values from ``cookiecutter.json`` or ``.cookiecutterrc``: - cookiecutter('cookiecutter-pypackage/', - extra_context={'project_name': 'TheGreatest'}) +.. code-block:: python + + cookiecutter( + 'cookiecutter-pypackage/', + extra_context={'project_name': 'TheGreatest'}, + ) + +This works as command-line parameters as well: + +.. code-block:: bash + + cookiecutter --no-input cookiecutter-pypackage/ project_name=TheGreatest + +You will also need to add these keys to the ``cookiecutter.json`` or ``.cookiecutterrc``. -You will also need to add these keys to the `cookiecutter.json` or `.cookiecutterrc`. Example: Injecting a Timestamp ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -If you have ``cookiecutter.json`` that has the following keys:: +If you have ``cookiecutter.json`` that has the following keys: + +.. code-block:: JSON { "timestamp": "{{ cookiecutter.timestamp }}" @@ -21,7 +34,9 @@ If you have ``cookiecutter.json`` that has the following keys:: This Python script will dynamically inject a timestamp value as the project is -generated:: +generated: + +.. code-block:: python from cookiecutter.main import cookiecutter @@ -34,6 +49,6 @@ generated:: How this works: -1. The script uses `datetime` to get the current UTC time in ISO format. -2. To generate the project, `cookiecutter()` is called, passing the timestamp - in as context via the `extra_context` dict. +1. The script uses ``datetime`` to get the current UTC time in ISO format. +2. To generate the project, ``cookiecutter()`` is called, passing the timestamp + in as context via the ``extra_context``` dict. diff --git a/docs/advanced/local_extensions.rst b/docs/advanced/local_extensions.rst index a90b04d7d..9a3d4b940 100644 --- a/docs/advanced/local_extensions.rst +++ b/docs/advanced/local_extensions.rst @@ -35,8 +35,8 @@ It will contain a ``main.py`` file, containing the following (for instance): This will register the ``foobar`` filter for the template. -For many cases, this will be unneccessarily complicated. It's likely that we'd only want to register a single function -as a filter. For this, we can use the ``simple_filter`` decorator: +For many cases, this will be unneccessarily complicated. +It's likely that we'd only want to register a single function as a filter. For this, we can use the ``simple_filter`` decorator: .. code-block:: json diff --git a/docs/advanced/new_line_characters.rst b/docs/advanced/new_line_characters.rst index 64f009898..e75ca9fcc 100644 --- a/docs/advanced/new_line_characters.rst +++ b/docs/advanced/new_line_characters.rst @@ -5,19 +5,22 @@ Working with line-ends special symbols LF/CRLF *New in Cookiecutter 2.0* -Before version 2.0 Cookiecutter silently used system line end character. -LF for POSIX and CRLF for Windows. Since version 2.0 this behaviour changed -and now can be forced at template level. +.. note:: -By default Cookiecutter now check every file at render stage and use same line -end as in source. This allow template developers to have both types of files in -the same template. Developers should correctly configure their `.gitattributes` -file to avoid line-end character overwrite by git. + Before version 2.0 Cookiecutter silently used system line end character. + LF for POSIX and CRLF for Windows. + Since version 2.0 this behaviour changed and now can be forced at template level. -Special template variable `_new_lines` was added in Cookiecutter 2.0. -Acceptable variables: `'\n\r'` for CRLF and `'\n'` for POSIX. +By default Cookiecutter checks every file at render stage and uses the same line end as in source. +This allow template developers to have both types of files in the same template. +Developers should correctly configure their ``.gitattributes`` file to avoid line-end character overwrite by git. -Here is example how to force line endings to CRLF on any deployment:: +The special template variable ``_new_lines`` enforces a specific line ending. +Acceptable variables: ``'\n\r'`` for CRLF and ``'\n'`` for POSIX. + +Here is example how to force line endings to CRLF on any deployment: + +.. code-block:: JSON { "project_slug": "sample", diff --git a/docs/advanced/private_variables.rst b/docs/advanced/private_variables.rst index 08ba9d943..6d17268b3 100644 --- a/docs/advanced/private_variables.rst +++ b/docs/advanced/private_variables.rst @@ -3,7 +3,12 @@ Private Variables ----------------- -Cookiecutter allows the definition private variables - those the user will not be required to fill in - by prepending an underscore to the variable name. These can either be not rendered, by using a prepending underscore, or rendered, prepending a double underscore. For example, the ``cookiecutter.json``:: +Cookiecutter allows the definition private variables by prepending an underscore to the variable name. +The user will not be required to fill those variables in. +These can either be not rendered, by using a prepending underscore, or rendered, prepending a double underscore. +For example, the ``cookiecutter.json``: + +.. code-block:: JSON { "project_name": "Really cool project", @@ -11,7 +16,9 @@ Cookiecutter allows the definition private variables - those the user will not b "__rendered": "{{ cookiecutter.project_name|lower }}" } -Will be rendered as:: +Will be rendered as: + +.. code-block:: JSON { "project_name": "Really cool project", @@ -21,7 +28,11 @@ Will be rendered as:: The user will only be asked for ``project_name``. -Non-rendered private variables can be used for defining constants. An example of where you may wish to use private **rendered** variables is creating a Python package repository and want to enforce naming consistency. To ensure the repository and package name are based on the project name, you could create a ``cookiecutter.json`` such as:: +Non-rendered private variables can be used for defining constants. +An example of where you may wish to use private **rendered** variables is creating a Python package repository and want to enforce naming consistency. +To ensure the repository and package name are based on the project name, you could create a ``cookiecutter.json`` such as: + +.. code-block:: JSON { "project_name": "Project Name", diff --git a/docs/advanced/replay.rst b/docs/advanced/replay.rst index 14afbfd24..426f0274e 100644 --- a/docs/advanced/replay.rst +++ b/docs/advanced/replay.rst @@ -9,7 +9,9 @@ On invocation **Cookiecutter** dumps a json file to ``~/.cookiecutter_replay/`` In other words, it persists your **input** for a template and fetches it when you run the same template again. -Example for a replay file (which was created via ``cookiecutter gh:hackebrot/cookiedozer``):: +Example for a replay file (which was created via ``cookiecutter gh:hackebrot/cookiedozer``): + +.. code-block:: JSON { "cookiecutter": { @@ -28,27 +30,31 @@ Example for a replay file (which was created via ``cookiecutter gh:hackebrot/coo To fetch this context data without being prompted on the command line you can use either of the following methods. -Pass the according option on the CLI:: +Pass the according option on the CLI: + +.. code-block:: bash cookiecutter --replay gh:hackebrot/cookiedozer Or use the Python API:: +.. code-block:: python + from cookiecutter.main import cookiecutter cookiecutter('gh:hackebrot/cookiedozer', replay=True) - -This feature is comes in handy if, for instance, you want to create a new project from an updated template. +This feature comes in handy if, for instance, you want to create a new project from an updated template. Custom replay file ~~~~~~~~~~~~~~~~~~ *New in Cookiecutter 2.0* -To specify a custom filename, you can use the ``--replay-file`` option:: +To specify a custom filename, you can use the ``--replay-file`` option: + +.. code-block:: bash cookiecutter --replay-file ./cookiedozer.json gh:hackebrot/cookiedozer -This may be useful to run the same replay file over several machines, in tests -or when a user of the template reports a problem +This may be useful to run the same replay file over several machines, in tests or when a user of the template reports a problem. diff --git a/docs/advanced/suppressing_prompts.rst b/docs/advanced/suppressing_prompts.rst index 1f73ff9fd..1064ad965 100644 --- a/docs/advanced/suppressing_prompts.rst +++ b/docs/advanced/suppressing_prompts.rst @@ -3,12 +3,14 @@ Suppressing Command-Line Prompts -------------------------------- -To suppress the prompts asking for input, use `no_input`. +To suppress the prompts asking for input, use ``no_input``. Basic Example: Using the Defaults ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Cookiecutter will pick a default value if used with `no_input`:: +Cookiecutter will pick a default value if used with ``no_input``: + +.. code-block:: python from cookiecutter.main import cookiecutter cookiecutter( @@ -16,18 +18,21 @@ Cookiecutter will pick a default value if used with `no_input`:: no_input=True, ) -In this case it will be using the default defined in `cookiecutter.json` or `.cookiecutterrc`. +In this case it will be using the default defined in ``cookiecutter.json`` or ``.cookiecutterrc``. .. note:: - values from `cookiecutter.json` will be overridden by values from `.cookiecutterrc` + values from ``cookiecutter.json`` will be overridden by values from ``.cookiecutterrc`` Advanced Example: Defaults + Extra Context ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -If you combine an `extra_context` dict with the `no_input` argument, you can programmatically create the project with a set list of context parameters and without any command line prompts:: +If you combine an ``extra_context`` dict with the ``no_input`` argument, you can programmatically create the project with a set list of context parameters and without any command line prompts: + +.. code-block:: JSON cookiecutter('cookiecutter-pypackage/', no_input=True, extra_context={'project_name': 'TheGreatest'}) -See the :ref:`API Reference ` for more details. + +See also :ref:`injecting-extra-content` and the :ref:`API Reference ` for more details. diff --git a/docs/advanced/template_extensions.rst b/docs/advanced/template_extensions.rst index 2e2273e8b..397460fa8 100644 --- a/docs/advanced/template_extensions.rst +++ b/docs/advanced/template_extensions.rst @@ -5,8 +5,8 @@ Template Extensions *New in Cookiecutter 1.4* -A template may extend the Cookiecutter environment with custom `Jinja2 extensions`_, -that can add extra filters, tests, globals or even extend the parser. +A template may extend the Cookiecutter environment with custom `Jinja2 extensions`_. +It can add extra filters, tests, globals or even extend the parser. To do so, a template author must specify the required extensions in ``cookiecutter.json`` as follows: @@ -20,12 +20,10 @@ To do so, a template author must specify the required extensions in ``cookiecutt On invocation Cookiecutter tries to import the extensions and add them to its environment respectively. -In the above example, Cookiecutter provides the additional tag `now`_, after -installing the `jinja2_time.TimeExtension`_ and enabling it in ``cookiecutter.json``. +In the above example, Cookiecutter provides the additional tag `now`_, after installing the `jinja2_time.TimeExtension`_ and enabling it in ``cookiecutter.json``. Please note that Cookiecutter will **not** install any dependencies on its own! -As a user you need to make sure you have all the extensions installed, before -running Cookiecutter on a template that requires custom Jinja2 extensions. +As a user you need to make sure you have all the extensions installed, before running Cookiecutter on a template that requires custom Jinja2 extensions. By default Cookiecutter includes the following extensions: @@ -38,8 +36,7 @@ By default Cookiecutter includes the following extensions: Jsonify extension ~~~~~~~~~~~~~~~~~ -The ``cookiecutter.extensions.JsonifyExtension`` extension provides a ``jsonify`` filter in templates -that converts a Python object to JSON: +The ``cookiecutter.extensions.JsonifyExtension`` extension provides a ``jsonify`` filter in templates that converts a Python object to JSON: .. code-block:: jinja @@ -56,10 +53,10 @@ Random string extension *New in Cookiecutter 1.7* -The ``cookiecutter.extensions.RandomStringExtension`` extension provides a ``random_ascii_string`` -method in templates that generates a random fixed-length string, optionally with punctuation. +The ``cookiecutter.extensions.RandomStringExtension`` extension provides a ``random_ascii_string`` method in templates that generates a random fixed-length string, optionally with punctuation. -Generate a random n-size character string. Example for n=12: +Generate a random n-size character string. +Example for n=12: .. code-block:: jinja @@ -71,8 +68,7 @@ Outputs: bIIUczoNvswh -The second argument controls if punctuation and special characters -``!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~`` should be present in the result: +The second argument controls if punctuation and special characters ``!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~`` should be present in the result: .. code-block:: jinja @@ -87,8 +83,7 @@ Outputs: Slugify extension ~~~~~~~~~~~~~~~~~ -The ``cookiecutter.extensions.SlugifyExtension`` extension provides a ``slugify`` filter in templates -that converts string into its underscored ("slugified") version: +The ``cookiecutter.extensions.SlugifyExtension`` extension provides a ``slugify`` filter in templates that converts string into its dashed ("slugified") version: .. code-block:: jinja @@ -100,16 +95,14 @@ Would output: it-s-a-random-version -It is diferent from a mere replace of spaces since it also trates some special characters -such as `'` in the example above. The function accepts all arguments that can be passed to -the `slugify` function of python-slugify. For example to change the output from -`it-s-a-random-version` to `it_s_a_random_version`, the parameter `separator='_'` would -be passed to `slugify()`. +It is different from a mere replace of spaces since it also trates some special characters such as ``'`` in the example above. +The function accepts all arguments that can be passed to the ``slugify`` function of `python-slugify`_. +For example to change the output from ``it-s-a-random-version``` to ``it_s_a_random_version``, the ``separator`` parameter would be passed: ``slugify(separator='_')``. .. _`Jinja2 extensions`: http://jinja.pocoo.org/docs/latest/extensions/ .. _`now`: https://github.com/hackebrot/jinja2-time#now-tag .. _`jinja2_time.TimeExtension`: https://github.com/hackebrot/jinja2-time -.. _`python-slugify`: https://github.com/un33k/python-slugify +.. _`python-slugify`: https://pypi.org/project/python-slugify UUID4 extension ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/advanced/templates_in_context.rst b/docs/advanced/templates_in_context.rst index faccb3c72..f324c60fe 100644 --- a/docs/advanced/templates_in_context.rst +++ b/docs/advanced/templates_in_context.rst @@ -4,20 +4,21 @@ Templates in Context Values -------------------------------- The values (but not the keys!) of `cookiecutter.json` are also Jinja2 templates. -Values from user prompts are added to the context immediately, such that one -context value can be derived from previous values. This approach can potentially -save your user a lot of keystrokes by providing more sensible defaults. +Values from user prompts are added to the context immediately, such that one context value can be derived from previous values. +This approach can potentially save your user a lot of keystrokes by providing more sensible defaults. Basic Example: Templates in Context -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Python packages show some patterns for their naming conventions: -* a human-readable project name -* a lowercase, dashed repository name -* an importable, dash-less package name +- a human-readable project name +- a lowercase, dashed repository name +- an importable, dash-less package name -Here is a `cookiecutter.json` with templated values for this pattern:: +Here is a `cookiecutter.json` with templated values for this pattern: + +.. code-block:: JSON { "project_name": "My New Project", @@ -25,13 +26,12 @@ Here is a `cookiecutter.json` with templated values for this pattern:: "pkg_name": "{{ cookiecutter.project_slug|replace('-', '') }}" } -If the user takes the defaults, or uses `no_input`, the templated values will -be: +If the user takes the defaults, or uses `no_input`, the templated values will be: -* `my-new-project` -* `mynewproject` +- `my-new-project` +- `mynewproject` Or, if the user gives `Yet Another New Project`, the values will be: -* `yet-another-new-project` -* `yetanothernewproject` +- ``yet-another-new-project`` +- ``yetanothernewproject`` diff --git a/docs/advanced/user_config.rst b/docs/advanced/user_config.rst index a46dc95b6..c7a8e9826 100644 --- a/docs/advanced/user_config.rst +++ b/docs/advanced/user_config.rst @@ -1,18 +1,26 @@ .. _user-config: -User Config (0.7.0+) -==================== +User Config +=========== + +*New in Cookiecutter 0.7* If you use Cookiecutter a lot, you'll find it useful to have a user config file. By default Cookiecutter tries to retrieve settings from a `.cookiecutterrc` file in your home directory. -From version 1.3.0 you can also specify a config file on the command line via ``--config-file``:: +*New in Cookiecutter 1.3* + +You can also specify a config file on the command line via ``--config-file``. + +.. code-block:: bash - $ cookiecutter --config-file /home/audreyr/my-custom-config.yaml cookiecutter-pypackage + cookiecutter --config-file /home/audreyr/my-custom-config.yaml cookiecutter-pypackage -Or you can set the ``COOKIECUTTER_CONFIG`` environment variable:: +Or you can set the ``COOKIECUTTER_CONFIG`` environment variable: - $ export COOKIECUTTER_CONFIG=/home/audreyr/my-custom-config.yaml +.. code-block:: bash + + export COOKIECUTTER_CONFIG=/home/audreyr/my-custom-config.yaml If you wish to stick to the built-in config and not load any user config file at all, use the CLI option ``--default-config`` instead. Preventing Cookiecutter from loading user settings is crucial for writing integration tests in an isolated environment. @@ -36,7 +44,7 @@ Possible settings are: ``default_context``: A list of key/value pairs that you want injected as context whenever you generate a project with Cookiecutter. - These values are treated like the defaults in `cookiecutter.json`, upon generation of any project. + These values are treated like the defaults in ``cookiecutter.json``, upon generation of any project. ``cookiecutters_dir`` Directory where your cookiecutters are cloned to when you use Cookiecutter with a repo argument. ``replay_dir`` @@ -46,5 +54,7 @@ Possible settings are: A list of abbreviations for cookiecutters. Abbreviations can be simple aliases for a repo name, or can be used as a prefix, in the form ``abbr:suffix``. Any suffix will be inserted into the expansion in place of the text ``{0}``, using standard Python string formatting. - With the above aliases, you could use the `cookiecutter-pypackage` template simply by saying ``cookiecutter pp``, or ``cookiecutter gh:audreyr/cookiecutter-pypackage``. + With the above aliases, you could use the ``cookiecutter-pypackage`` template simply by saying ``cookiecutter pp``, or ``cookiecutter gh:audreyr/cookiecutter-pypackage``. The ``gh`` (GitHub), ``bb`` (Bitbucket), and ``gl`` (Gitlab) abbreviations shown above are actually **built in**, and can be used without defining them yourself. + +Read also: :ref:`injecting-extra-content` diff --git a/docs/tutorials/tutorial1.rst b/docs/tutorials/tutorial1.rst index 555753609..0871ca696 100644 --- a/docs/tutorials/tutorial1.rst +++ b/docs/tutorials/tutorial1.rst @@ -5,28 +5,25 @@ Getting to Know Cookiecutter .. note:: Before you begin, please install Cookiecutter 0.7.0 or higher. Instructions are in :doc:`../installation`. -Cookiecutter is a tool for creating projects from *cookiecutters* (project -templates). +Cookiecutter is a tool for creating projects from *cookiecutters* (project templates). What exactly does this mean? Read on! Case Study: cookiecutter-pypackage ----------------------------------- -*cookiecutter-pypackage* is a cookiecutter template that creates the starter -boilerplate for a Python package. +*cookiecutter-pypackage* is a cookiecutter template that creates the starter boilerplate for a Python package. -.. note:: There are several variations of it, but for this tutorial we'll use - the original version at https://github.com/audreyr/cookiecutter-pypackage/. +.. note:: + There are several variations of it. + For this tutorial we'll use the original version at https://github.com/audreyr/cookiecutter-pypackage/. Step 1: Generate a Python Package Project ------------------------------------------ -Open your shell and cd into the directory where you'd like to create a starter -Python package project. +Open your shell and cd into the directory where you'd like to create a starter Python package project. -At the command line, run the cookiecutter command, passing in the link to -cookiecutter-pypackage's HTTPS clone URL like this: +At the command line, run the cookiecutter command, passing in the link to cookiecutter-pypackage's HTTPS clone URL like this: .. code-block:: bash @@ -35,18 +32,17 @@ cookiecutter-pypackage's HTTPS clone URL like this: Local Cloning of Project Template ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -First, cookiecutter-pypackage gets cloned to `~/.cookiecutters/` (or equivalent -on Windows). Cookiecutter does this for you, so sit back and wait. +First, cookiecutter-pypackage gets cloned to `~/.cookiecutters/` (or equivalent on Windows). +Cookiecutter does this for you, so sit back and wait. Local Generation of Project ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -When cloning is complete, you will be prompted to enter a bunch of values, such -as `full_name`, `email`, and `project_name`. Either enter your info, or simply -press return/enter to accept the default values. +When cloning is complete, you will be prompted to enter a bunch of values, such as `full_name`, `email`, and `project_name`. +Either enter your info, or simply press return/enter to accept the default values. -This info will be used to fill in the blanks for your project. For example, -your name and the year will be placed into the LICENSE file. +This info will be used to fill in the blanks for your project. +For example, your name and the year will be placed into the LICENSE file. Step 2: Explore What Got Generated ---------------------------------- @@ -58,8 +54,7 @@ In your current directory, you should see that a project got generated: $ ls boilerplate -Looking inside the `boilerplate/` (or directory corresponding to your `project_slug`) -directory, you should see something like this: +Looking inside the `boilerplate/` (or directory corresponding to your `project_slug`) directory, you should see something like this: .. code-block:: bash @@ -94,38 +89,38 @@ Notice how it was auto-populated with your (or my) name and email. Also take note of the fact that you are looking at a ReStructuredText file. Cookiecutter can generate a project with text files of any type. -Great, you just generated a skeleton Python package. How did that work? +Great, you just generated a skeleton Python package. +How did that work? Step 3: Observe How It Was Generated ------------------------------------ -Let's take a look at cookiecutter-pypackage together. Open https://github.com/audreyr/cookiecutter-pypackage in a new browser window. +Let's take a look at cookiecutter-pypackage together. +Open https://github.com/audreyr/cookiecutter-pypackage in a new browser window. {{ cookiecutter.project_slug }} ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Find the directory called `{{ cookiecutter.project_slug }}`. Click on it. Observe -the files inside of it. You should see that this directory and its contents -corresponds to the project that you just generated. +Find the directory called `{{ cookiecutter.project_slug }}`. +Click on it. +Observe the files inside of it. +You should see that this directory and its contents corresponds to the project that you just generated. This happens in `find.py`, where the `find_template()` method looks for the first jinja-like directory name that starts with `cookiecutter`. AUTHORS.rst ~~~~~~~~~~~ -Look at the raw version of `{{ cookiecutter.project_slug }}/AUTHORS.rst`, at -https://raw.github.com/audreyr/cookiecutter-pypackage/master/%7B%7Bcookiecutter.project_slug%7D%7D/AUTHORS.rst. +Look at the raw version of `{{ cookiecutter.project_slug }}/AUTHORS.rst`, at https://raw.github.com/audreyr/cookiecutter-pypackage/master/%7B%7Bcookiecutter.project_slug%7D%7D/AUTHORS.rst. Observe how it corresponds to the `AUTHORS.rst` file that you generated. cookiecutter.json ~~~~~~~~~~~~~~~~~ -Now navigate back up to `cookiecutter-pypackage/` and look at the -`cookiecutter.json` file. +Now navigate back up to `cookiecutter-pypackage/` and look at the `cookiecutter.json` file. -You should see JSON that corresponds to the prompts and default values shown -earlier during project generation: +You should see JSON that corresponds to the prompts and default values shown earlier during project generation: .. code-block:: json @@ -147,13 +142,11 @@ earlier during project generation: Questions? ---------- -If anything needs better explanation, please take a moment to file an issue at https://github.com/audreyr/cookiecutter/issues with what could be improved -about this tutorial. +If anything needs better explanation, please take a moment to file an issue at https://github.com/audreyr/cookiecutter/issues with what could be improved about this tutorial. Summary ------- -You have learned how to use Cookiecutter to generate your first project from a -cookiecutter project template. +You have learned how to use Cookiecutter to generate your first project from a cookiecutter project template. -In Tutorial 2, you'll see how to create cookiecutters of your own, from scratch. +In tutorial 2 (:ref:`tutorial2`), you'll see how to create cookiecutters of your own, from scratch. diff --git a/docs/tutorials/tutorial2.rst b/docs/tutorials/tutorial2.rst index 830206788..53fcc1d21 100644 --- a/docs/tutorials/tutorial2.rst +++ b/docs/tutorials/tutorial2.rst @@ -1,3 +1,5 @@ +.. _tutorial2: + ================================== Create a Cookiecutter From Scratch ================================== @@ -5,8 +7,7 @@ Create a Cookiecutter From Scratch Step 1: Name Your Cookiecutter ------------------------------ -In this tutorial, we are creating *cookiecutter-website-simple*, a cookiecutter -for generating simple, bare-bones websites. +In this tutorial, we are creating *cookiecutter-website-simple*, a cookiecutter for generating simple, bare-bones websites. Create the directory for your cookiecutter and cd into it: @@ -18,15 +19,13 @@ Create the directory for your cookiecutter and cd into it: Step 2: Create `project_slug` Directory --------------------------------------- -Create a directory called `{{ cookiecutter.project_slug }}`. +Create a directory called ``{{ cookiecutter.project_slug }}``. -This value will be replaced with the repo name of projects that you generate -from this cookiecutter. +This value will be replaced with the repo name of projects that you generate from this cookiecutter. Step 3: Create Files -------------------- -Inside of `{{ cookiecutter.project_slug }}`, create `index.html`, `site.css`, and -`site.js`. +Inside of ``{{ cookiecutter.project_slug }}``, create ``index.html``, ``site.css``, and ``site.js``. To be continued... From cd845ab8bc49d1e4544e8f1099caba03bde3b436 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89rico=20Andrei?= Date: Mon, 30 May 2022 15:34:37 -0300 Subject: [PATCH 093/274] Fix typo in dict_variables.rst --- docs/advanced/dict_variables.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/advanced/dict_variables.rst b/docs/advanced/dict_variables.rst index 6142f4bd8..ccf1df499 100644 --- a/docs/advanced/dict_variables.rst +++ b/docs/advanced/dict_variables.rst @@ -39,7 +39,7 @@ For example, you could provide the following dictionary variable in your ``cooki } -The above ``file_type`` dictionary variable creates ``cookiecutter.file_types``, which can be used like this: +The above ``file_types`` dictionary variable creates ``cookiecutter.file_types``, which can be used like this: .. code-block:: html+jinja @@ -64,4 +64,3 @@ The above ``file_type`` dictionary variable creates ``cookiecutter.file_types``, Cookiecutter is using `Jinja2's for expression `_ to iterate over the items in the dictionary. - From 55047a61ba504417fc0ed32f246d883e1e7f76df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89rico=20Andrei?= Date: Mon, 30 May 2022 15:35:05 -0300 Subject: [PATCH 094/274] =?UTF-8?q?Add=20Jens=20and=20=C3=89rico=20to=20th?= =?UTF-8?q?e=20list=20of=20core=20contributors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AUTHORS.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AUTHORS.md b/AUTHORS.md index d63785240..9e5a01494 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -14,6 +14,8 @@ - Sorin Sbarnea ([@ssbarnea](https://github.com/ssbarnea)) - Fábio C. Barrionuevo da Luz ([@luzfcb](https://github.com/luzfcb)) - Simone Basso ([@simobasso](https://github.com/simobasso)) +- Jens Klein ([@jensens](https://github.com/jensens)) +- Érico Andrei ([@ericof](https://github.com/ericof)) ## Contributors @@ -169,7 +171,6 @@ - Tom Forbes ([@orf](https://github.com/orf)) - Xie Yanbo ([@xyb](https://github.com/xyb)) - Maxim Ivanov ([@ivanovmg](https://github.com/ivanovmg)) -- Jens Klein ([@jensens](https://github.com/jensens)) ## Backers From e6b828a9a9ed53a4884ef30d957635be4b27c695 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89rico=20Andrei?= Date: Mon, 30 May 2022 16:18:33 -0300 Subject: [PATCH 095/274] Follow PyPA guide to release package using GitHub Actions. --- .github/workflows/pip-publish.yml | 37 +++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/.github/workflows/pip-publish.yml b/.github/workflows/pip-publish.yml index a4803d54c..f9fae6871 100644 --- a/.github/workflows/pip-publish.yml +++ b/.github/workflows/pip-publish.yml @@ -8,19 +8,32 @@ jobs: upload: runs-on: ubuntu-latest steps: + - uses: actions/checkout@v2 + - name: Set up Python uses: actions/setup-python@v1 with: - python-version: "3.7" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install setuptools wheel twine - - name: Package project - run: python setup.py sdist bdist_wheel - - name: Upload distributions - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} - run: twine upload dist/* + python-version: 3.9 + + - name: Install pypa/build + run: >- + python -m + pip install + build + --user + + - name: Build a binary wheel and a source tarball + run: >- + python -m + build + --sdist + --wheel + --outdir dist/ + . + + - name: Publish a Python distribution to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} From a7b1bc2ff92d1254614f49bf07aac62e57060a54 Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Mon, 30 May 2022 21:23:44 +0200 Subject: [PATCH 096/274] exists under docs, is enough --- case_studies.md | 25 ------------------------- docs/case_studies.md | 26 +++++++++++++++++++++++++- 2 files changed, 25 insertions(+), 26 deletions(-) delete mode 100644 case_studies.md mode change 120000 => 100644 docs/case_studies.md diff --git a/case_studies.md b/case_studies.md deleted file mode 100644 index db101b979..000000000 --- a/case_studies.md +++ /dev/null @@ -1,25 +0,0 @@ -# Case Studies - -This showcase is where organizations can describe how they are using Cookiecutter. - -## [BeeWare](https://beeware.org/) - -Building Python tools for platforms like mobile phones and set top boxes requires a lot of boilerplate code just to get the project running. Cookiecutter has enabled us to very quickly stub out a starter project in which running Python code can be placed, and makes maintaining those templates very easy. With Cookiecutter we've been able to deliver support [Android devices](https://github.com/beeware/Python-Android-template), [iOS devices](https://github.com/beeware/Python-iOS-template), tvOS boxes, and we're planning to add native support for iOS and Windows devices in the future. - -[BeeWare](https://beeware.org/) is an organization building open source libraries for Python support on all platforms. - -## [ChrisDev](https://chrisdev.com/) - -Anytime we start a new project we begin with a [Cookiecutter template that generates a Django/Wagtail project](https://github.com/chrisdev/wagtail-cookiecutter-foundation) Our developers like it for maintainability and our designers enjoy being able to spin up new sites using our tool chain very quickly. Cookiecutter is very useful for because it supports both Mac OSX and Windows users. - -[ChrisDev](https://chrisdev.com/) is a Trinidad-based consulting agency. - -## [OpenStack](https://www.openstack.org/) - -OpenStack uses several Cookiecutter templates to generate: - -* [Openstack compliant puppet-modules](https://github.com/openstack/puppet-openstack-cookiecutter) -* [Install guides](https://github.com/openstack/installguide-cookiecutter) -* [New tempest plugins](https://github.com/openstack/tempest-plugin-cookiecutter) - -[OpenStack](https://www.openstack.org/) is open source software for creating private and public clouds. diff --git a/docs/case_studies.md b/docs/case_studies.md deleted file mode 120000 index 214ddc2b6..000000000 --- a/docs/case_studies.md +++ /dev/null @@ -1 +0,0 @@ -../case_studies.md \ No newline at end of file diff --git a/docs/case_studies.md b/docs/case_studies.md new file mode 100644 index 000000000..db101b979 --- /dev/null +++ b/docs/case_studies.md @@ -0,0 +1,25 @@ +# Case Studies + +This showcase is where organizations can describe how they are using Cookiecutter. + +## [BeeWare](https://beeware.org/) + +Building Python tools for platforms like mobile phones and set top boxes requires a lot of boilerplate code just to get the project running. Cookiecutter has enabled us to very quickly stub out a starter project in which running Python code can be placed, and makes maintaining those templates very easy. With Cookiecutter we've been able to deliver support [Android devices](https://github.com/beeware/Python-Android-template), [iOS devices](https://github.com/beeware/Python-iOS-template), tvOS boxes, and we're planning to add native support for iOS and Windows devices in the future. + +[BeeWare](https://beeware.org/) is an organization building open source libraries for Python support on all platforms. + +## [ChrisDev](https://chrisdev.com/) + +Anytime we start a new project we begin with a [Cookiecutter template that generates a Django/Wagtail project](https://github.com/chrisdev/wagtail-cookiecutter-foundation) Our developers like it for maintainability and our designers enjoy being able to spin up new sites using our tool chain very quickly. Cookiecutter is very useful for because it supports both Mac OSX and Windows users. + +[ChrisDev](https://chrisdev.com/) is a Trinidad-based consulting agency. + +## [OpenStack](https://www.openstack.org/) + +OpenStack uses several Cookiecutter templates to generate: + +* [Openstack compliant puppet-modules](https://github.com/openstack/puppet-openstack-cookiecutter) +* [Install guides](https://github.com/openstack/installguide-cookiecutter) +* [New tempest plugins](https://github.com/openstack/tempest-plugin-cookiecutter) + +[OpenStack](https://www.openstack.org/) is open source software for creating private and public clouds. From 2a819d754c596ebb20d48ed4b73028adf153b769 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89rico=20Andrei?= Date: Mon, 30 May 2022 16:28:37 -0300 Subject: [PATCH 097/274] Explicitly use GitHub Actions environment --- .github/workflows/pip-publish.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/pip-publish.yml b/.github/workflows/pip-publish.yml index f9fae6871..c5e01d9e1 100644 --- a/.github/workflows/pip-publish.yml +++ b/.github/workflows/pip-publish.yml @@ -7,6 +7,11 @@ on: jobs: upload: runs-on: ubuntu-latest + + environment: + name: pypi.org + url: https://pypi.org/project/cookiecutter/ + steps: - uses: actions/checkout@v2 From 563472e88b7e83bcac45143ca0074ba2c7296211 Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Mon, 30 May 2022 21:34:40 +0200 Subject: [PATCH 098/274] add check-manifest to pre-commit and fix Manifest.in --- .pre-commit-config.yaml | 4 ++++ BACKERS.md | 9 --------- MANIFEST.in | 12 ++++++++++-- 3 files changed, 14 insertions(+), 11 deletions(-) delete mode 100644 BACKERS.md diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1ba7fa3e1..7be7e3bb8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,3 +39,7 @@ repos: hooks: - id: bandit args: [--ini, .bandit] + - repo: https://github.com/mgedmin/check-manifest + rev: "0.48" + hooks: + - id: check-manifest \ No newline at end of file diff --git a/BACKERS.md b/BACKERS.md deleted file mode 100644 index 0c120e061..000000000 --- a/BACKERS.md +++ /dev/null @@ -1,9 +0,0 @@ -# Backers - -We would like to thank the following people for supporting us in our efforts to maintain and improve Cookiecutter: - -* Alex DeBrie -* Alexandre Y. Harano -* Bruno Alla -* Carol Willing -* Russell Keith-Magee diff --git a/MANIFEST.in b/MANIFEST.in index 2a2838a1d..0e1034643 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,11 +1,19 @@ include AUTHORS.md +include CODE_OF_CONDUCT.md include CONTRIBUTING.md include HISTORY.md include LICENSE include README.md +exclude Makefile +exclude __main__.py +exclude .* +exclude codecov.yml +exclude test_requirements.txt +exclude tox.ini + recursive-include tests * recursive-exclude * __pycache__ recursive-exclude * *.py[co] - -recursive-include docs *.rst conf.py Makefile make.bat +recursive-exclude docs * +recursive-exclude logo * From 96c68260eac572505f33381e627ad42b61aef357 Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Mon, 30 May 2022 21:49:48 +0200 Subject: [PATCH 099/274] bump version and edit historie --- HISTORY.md | 28 ++++++++++++++++++++++++++++ cookiecutter/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index b39699c1b..edef710f0 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -2,6 +2,34 @@ History is important, but our current roadmap can be found [here](https://github.com/cookiecutter/cookiecutter/projects) +## 2.1.0 (2022-05-30) + +### Changes + +* Move contributors and backers to credits section (#1599) @doobrie +* test_generate_file_verbose_template_syntax_error fixed (#1671) @MaciejPatro +* Removed changes related to setuptools_scm (#1629) @ozer550 +* Feature/local extensions (#1240) @mwesterhof + +### CI/CD and QA changes + +* Check manifest: pre-commit, fixes, cleaning (#1683) @jensens +* Follow PyPA guide to release package using GitHub Actions. (#1682) @ericof + +### Documentation updates + +* Fix typo in dict_variables.rst (#1680) @ericof +* Documentation overhaul (#1677) @jensens +* Fixed incorrect link on docs. (#1649) @luzfcb + +### Bugfixes + +* Restore accidentally deleted support for click 8.x (#1643) @jaklan + +### This release was made possible by our wonderful contributors: + +@doobrie, @jensens, @ericof, @luzfcb + ## 2.0.2 (2021-12-27) *Remark: This release never made it to official PyPI* diff --git a/cookiecutter/__init__.py b/cookiecutter/__init__.py index eab68df8e..1075a8418 100644 --- a/cookiecutter/__init__.py +++ b/cookiecutter/__init__.py @@ -1,2 +1,2 @@ """Main package for Cookiecutter.""" -__version__ = "2.0.2" +__version__ = "2.1.0" diff --git a/setup.py b/setup.py index 9cb220978..5cd1729bc 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ """cookiecutter distutils configuration.""" from setuptools import setup -version = "2.0.3.dev0" +version = "2.1.0" with open('README.md', encoding='utf-8') as readme_file: readme = readme_file.read() From 58d716f51fda78ec793975eea5876691aa576b2c Mon Sep 17 00:00:00 2001 From: alkatar21 <61387986+alkatar21@users.noreply.github.com> Date: Tue, 31 May 2022 10:22:17 +0200 Subject: [PATCH 100/274] [Docs] Fix local extensions documentation --- docs/advanced/local_extensions.rst | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/advanced/local_extensions.rst b/docs/advanced/local_extensions.rst index 9a3d4b940..d7eba4603 100644 --- a/docs/advanced/local_extensions.rst +++ b/docs/advanced/local_extensions.rst @@ -3,7 +3,7 @@ Local Extensions ---------------- -*New in Cookiecutter X.x* +*New in Cookiecutter 2.1* A template may extend the Cookiecutter environment with local extensions. These can be part of the template itself, providing it with more sophisticated custom tags and filters. @@ -18,13 +18,10 @@ To do so, a template author must specify the required extensions in ``cookiecutt "_extensions": ["local_extensions.FoobarExtension"] } -This example assumes that a ``local_extensions`` folder (python module) exists in the template root. -It will contain a ``main.py`` file, containing the following (for instance): +This example uses a simple module ``local_extensions.py`` which exists in the template root, containing the following (for instance): .. code-block:: python - # -*- coding: utf-8 -*- - from jinja2.ext import Extension @@ -48,8 +45,6 @@ It's likely that we'd only want to register a single function as a filter. For t .. code-block:: python - # -*- coding: utf-8 -*- - from cookiecutter.utils import simple_filter @@ -58,3 +53,7 @@ It's likely that we'd only want to register a single function as a filter. For t return v * 2 This snippet will achieve the exact same result as the previous one. + +For complex use cases, a python module ``local_extensions`` (a folder with an ``__init__.py``) can also be created in the template root. +Here, for example, a module ``main.py`` would have to export all extensions with ``from .main import FoobarExtension, simplefilterextension`` or ``from .main import *`` in the ``__init__.py``. + From 8b33e96c94ac75277e8f67cc1a71d90f488b5edb Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Tue, 31 May 2022 10:35:35 +0200 Subject: [PATCH 101/274] Bump version to 2.1.1.dev0 --- cookiecutter/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cookiecutter/__init__.py b/cookiecutter/__init__.py index 1075a8418..88fb2f6e7 100644 --- a/cookiecutter/__init__.py +++ b/cookiecutter/__init__.py @@ -1,2 +1,2 @@ """Main package for Cookiecutter.""" -__version__ = "2.1.0" +__version__ = "2.1.1.dev0" diff --git a/setup.py b/setup.py index 5cd1729bc..907b0307f 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ """cookiecutter distutils configuration.""" from setuptools import setup -version = "2.1.0" +version = "2.1.1.dev0" with open('README.md', encoding='utf-8') as readme_file: readme = readme_file.read() From e26c46582cd9033dcea318f1c29a1f06fb74f456 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89rico=20Andrei?= Date: Tue, 31 May 2022 20:32:38 -0300 Subject: [PATCH 102/274] Sanitize Mercurial branch information before checkout. --- HISTORY.md | 2 ++ cookiecutter/vcs.py | 14 +++++++++----- tests/vcs/test_clone.py | 16 +++++++++++----- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index edef710f0..50f855ba8 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -2,6 +2,8 @@ History is important, but our current roadmap can be found [here](https://github.com/cookiecutter/cookiecutter/projects) +## 2.1.1 (unreleased) + ## 2.1.0 (2022-05-30) ### Changes diff --git a/cookiecutter/vcs.py b/cookiecutter/vcs.py index 08cb2eb0c..bb4356b31 100644 --- a/cookiecutter/vcs.py +++ b/cookiecutter/vcs.py @@ -98,8 +98,12 @@ def clone(repo_url, checkout=None, clone_to_dir='.', no_input=False): stderr=subprocess.STDOUT, ) if checkout is not None: + checkout_params = [checkout] + # Avoid Mercurial "--config" and "--debugger" injection vulnerability + if repo_type == "hg": + checkout_params.insert(0, "--") subprocess.check_output( # nosec - [repo_type, 'checkout', checkout], + [repo_type, 'checkout', *checkout_params], cwd=repo_dir, stderr=subprocess.STDOUT, ) @@ -107,13 +111,13 @@ def clone(repo_url, checkout=None, clone_to_dir='.', no_input=False): output = clone_error.output.decode('utf-8') if 'not found' in output.lower(): raise RepositoryNotFound( - 'The repository {} could not be found, ' - 'have you made a typo?'.format(repo_url) + f'The repository {repo_url} could not be found, ' + 'have you made a typo?' ) if any(error in output for error in BRANCH_ERRORS): raise RepositoryCloneFailed( - 'The {} branch of repository {} could not found, ' - 'have you made a typo?'.format(checkout, repo_url) + f'The {checkout} branch of repository ' + f'{repo_url} could not found, have you made a typo?' ) logger.error('git clone failed with error: %s', output) raise diff --git a/tests/vcs/test_clone.py b/tests/vcs/test_clone.py index 9fc3b24fa..ef62ef587 100644 --- a/tests/vcs/test_clone.py +++ b/tests/vcs/test_clone.py @@ -122,8 +122,14 @@ def test_clone_should_invoke_vcs_command( mock_subprocess.assert_any_call( [repo_type, 'clone', repo_url], cwd=str(clone_dir), stderr=subprocess.STDOUT ) + + branch_info = [branch] + # We sanitize branch information for Mercurial + if repo_type == "hg": + branch_info.insert(0, "--") + mock_subprocess.assert_any_call( - [repo_type, 'checkout', branch], cwd=expected_repo_dir, stderr=subprocess.STDOUT + [repo_type, 'checkout', *branch_info], cwd=expected_repo_dir, stderr=subprocess.STDOUT ) @@ -151,8 +157,8 @@ def test_clone_handles_repo_typo(mocker, clone_dir, error_message): vcs.clone(repository_url, clone_to_dir=str(clone_dir), no_input=True) assert str(err.value) == ( - 'The repository {} could not be found, have you made a typo?' - ).format(repository_url) + f'The repository {repository_url} could not be found, have you made a typo?' + ) @pytest.mark.parametrize( @@ -182,8 +188,8 @@ def test_clone_handles_branch_typo(mocker, clone_dir, error_message): assert str(err.value) == ( 'The unknown_branch branch of repository ' - '{} could not found, have you made a typo?' - ).format(repository_url) + f'{repository_url} could not found, have you made a typo?' + ) def test_clone_unknown_subprocess_error(mocker, clone_dir): From 85a7884f11a5200535706a6c5d31a9acbdadae1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89rico=20Andrei?= Date: Tue, 31 May 2022 20:37:22 -0300 Subject: [PATCH 103/274] Lint fixes --- tests/vcs/test_clone.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/vcs/test_clone.py b/tests/vcs/test_clone.py index ef62ef587..bd19ef1ab 100644 --- a/tests/vcs/test_clone.py +++ b/tests/vcs/test_clone.py @@ -129,7 +129,9 @@ def test_clone_should_invoke_vcs_command( branch_info.insert(0, "--") mock_subprocess.assert_any_call( - [repo_type, 'checkout', *branch_info], cwd=expected_repo_dir, stderr=subprocess.STDOUT + [repo_type, 'checkout', *branch_info], + cwd=expected_repo_dir, + stderr=subprocess.STDOUT, ) From 23182a564d19f5de753a58e70ab47f7c008ce0a5 Mon Sep 17 00:00:00 2001 From: Maciej Patro Date: Sat, 7 May 2022 01:35:35 +0200 Subject: [PATCH 104/274] Add CLI option to keep project files on failure. Fixes #1631 --- cookiecutter/cli.py | 7 +++++++ cookiecutter/generate.py | 5 ++++- cookiecutter/main.py | 4 ++++ tests/test_cli.py | 9 +++++++++ tests/test_generate_files.py | 12 ++++++++++++ tests/test_specify_output_dir.py | 2 ++ 6 files changed, 38 insertions(+), 1 deletion(-) diff --git a/cookiecutter/cli.py b/cookiecutter/cli.py index a792fa5f5..208901a92 100644 --- a/cookiecutter/cli.py +++ b/cookiecutter/cli.py @@ -144,6 +144,11 @@ def list_installed_templates(default_config, passed_config_file): @click.option( '-l', '--list-installed', is_flag=True, help='List currently installed templates.' ) +@click.option( + '--keep-project-on-failure', + is_flag=True, + help='Do not delete project folder on failure', +) def main( template, extra_context, @@ -161,6 +166,7 @@ def main( accept_hooks, replay_file, list_installed, + keep_project_on_failure, ): """Create a project from a Cookiecutter project template (TEMPLATE). @@ -205,6 +211,7 @@ def main( directory=directory, skip_if_file_exists=skip_if_file_exists, accept_hooks=_accept_hooks, + keep_project_on_failure=keep_project_on_failure, ) except ( ContextDecodingException, diff --git a/cookiecutter/generate.py b/cookiecutter/generate.py index 7bdce5a8b..cd7d34df2 100644 --- a/cookiecutter/generate.py +++ b/cookiecutter/generate.py @@ -268,6 +268,7 @@ def generate_files( overwrite_if_exists=False, skip_if_file_exists=False, accept_hooks=True, + keep_project_on_failure=False, ): """Render the templates and saves them to files. @@ -277,6 +278,8 @@ def generate_files( :param overwrite_if_exists: Overwrite the contents of the output directory if it exists. :param accept_hooks: Accept pre and post hooks if set to `True`. + :param keep_project_on_failure: If `True` keep generated project directory even when + generation fails """ template_dir = find_template(repo_dir) logger.debug('Generating project from %s...', template_dir) @@ -307,7 +310,7 @@ def generate_files( # if we created the output directory, then it's ok to remove it # if rendering fails - delete_project_on_failure = output_directory_created + delete_project_on_failure = output_directory_created and not keep_project_on_failure if accept_hooks: _run_hook_from_repo_dir( diff --git a/cookiecutter/main.py b/cookiecutter/main.py index bc2f262df..64a686ad1 100644 --- a/cookiecutter/main.py +++ b/cookiecutter/main.py @@ -34,6 +34,7 @@ def cookiecutter( directory=None, skip_if_file_exists=False, accept_hooks=True, + keep_project_on_failure=False, ): """ Run Cookiecutter just as if using it from the command line. @@ -53,6 +54,8 @@ def cookiecutter( :param password: The password to use when extracting the repository. :param directory: Relative path to a cookiecutter template in a repository. :param accept_hooks: Accept pre and post hooks if set to `True`. + :param keep_project_on_failure: If `True` keep generated project directory even when + generation fails """ if replay and ((no_input is not False) or (extra_context is not None)): err_msg = ( @@ -118,6 +121,7 @@ def cookiecutter( skip_if_file_exists=skip_if_file_exists, output_dir=output_dir, accept_hooks=accept_hooks, + keep_project_on_failure=keep_project_on_failure, ) # Cleanup (if required) diff --git a/tests/test_cli.py b/tests/test_cli.py index ad6abd1e0..1bc2fdd55 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -109,6 +109,7 @@ def test_cli_replay(mocker, cli_runner): password=None, directory=None, accept_hooks=True, + keep_project_on_failure=False, ) @@ -135,6 +136,7 @@ def test_cli_replay_file(mocker, cli_runner): password=None, directory=None, accept_hooks=True, + keep_project_on_failure=False, ) @@ -170,6 +172,7 @@ def test_cli_exit_on_noinput_and_replay(mocker, cli_runner): password=None, directory=None, accept_hooks=True, + keep_project_on_failure=False, ) @@ -205,6 +208,7 @@ def test_run_cookiecutter_on_overwrite_if_exists_and_replay( password=None, directory=None, accept_hooks=True, + keep_project_on_failure=False, ) @@ -261,6 +265,7 @@ def test_cli_output_dir(mocker, cli_runner, output_dir_flag, output_dir): password=None, directory=None, accept_hooks=True, + keep_project_on_failure=False, ) @@ -305,6 +310,7 @@ def test_user_config(mocker, cli_runner, user_config_path): password=None, directory=None, accept_hooks=True, + keep_project_on_failure=False, ) @@ -335,6 +341,7 @@ def test_default_user_config_overwrite(mocker, cli_runner, user_config_path): password=None, directory=None, accept_hooks=True, + keep_project_on_failure=False, ) @@ -360,6 +367,7 @@ def test_default_user_config(mocker, cli_runner): password=None, directory=None, accept_hooks=True, + keep_project_on_failure=False, ) @@ -629,6 +637,7 @@ def test_cli_accept_hooks( directory=None, skip_if_file_exists=False, accept_hooks=expected, + keep_project_on_failure=False, ) diff --git a/tests/test_generate_files.py b/tests/test_generate_files.py index 4d6ef1113..9cf4929aa 100644 --- a/tests/test_generate_files.py +++ b/tests/test_generate_files.py @@ -390,6 +390,18 @@ def test_raise_undefined_variable_dir_name(output_dir, undefined_context): assert not Path(output_dir).joinpath('testproject').exists() +def test_keep_project_dir_on_failure(output_dir, undefined_context): + """Verify correct error raised when directory name cannot be rendered.""" + with pytest.raises(exceptions.UndefinedVariableInTemplate): + generate.generate_files( + repo_dir='tests/undefined-variable/dir-name/', + output_dir=output_dir, + context=undefined_context, + keep_project_on_failure=True, + ) + assert Path(output_dir).joinpath('testproject').exists() + + def test_raise_undefined_variable_dir_name_existing_project( output_dir, undefined_context ): diff --git a/tests/test_specify_output_dir.py b/tests/test_specify_output_dir.py index 56c9eda6a..c907f2855 100644 --- a/tests/test_specify_output_dir.py +++ b/tests/test_specify_output_dir.py @@ -57,6 +57,7 @@ def test_api_invocation(mocker, template, output_dir, context): skip_if_file_exists=False, output_dir=output_dir, accept_hooks=True, + keep_project_on_failure=False, ) @@ -73,4 +74,5 @@ def test_default_output_dir(mocker, template, context): skip_if_file_exists=False, output_dir='.', accept_hooks=True, + keep_project_on_failure=False, ) From f9376a96097086476ce9eb0b93297a471ae520e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89rico=20Andrei?= Date: Wed, 1 Jun 2022 13:43:52 -0300 Subject: [PATCH 105/274] Prepare release 2.1.1 --- HISTORY.md | 15 ++++++++++++++- cookiecutter/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 50f855ba8..fe58416ce 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -2,7 +2,20 @@ History is important, but our current roadmap can be found [here](https://github.com/cookiecutter/cookiecutter/projects) -## 2.1.1 (unreleased) +## 2.1.1 (2022-06-01) + +### Documentation updates + +* Fix local extensions documentation (#1686) @alkatar21 + +### Bugfixes + +* Sanitize Mercurial branch information before checkout. (#1689) @ericof + +### This release is made by wonderfull contributors: + +@alkatar21, @ericof and @jensens + ## 2.1.0 (2022-05-30) diff --git a/cookiecutter/__init__.py b/cookiecutter/__init__.py index 88fb2f6e7..f0e3a2c38 100644 --- a/cookiecutter/__init__.py +++ b/cookiecutter/__init__.py @@ -1,2 +1,2 @@ """Main package for Cookiecutter.""" -__version__ = "2.1.1.dev0" +__version__ = "2.1.1" diff --git a/setup.py b/setup.py index 907b0307f..6b057835e 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ """cookiecutter distutils configuration.""" from setuptools import setup -version = "2.1.1.dev0" +version = "2.1.1" with open('README.md', encoding='utf-8') as readme_file: readme = readme_file.read() From 8e3cf3671b1964d3f7d7987cb8ee704a1c809fb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89rico=20Andrei?= Date: Wed, 1 Jun 2022 13:46:10 -0300 Subject: [PATCH 106/274] Back to development --- HISTORY.md | 3 +++ cookiecutter/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index fe58416ce..5f647aa3a 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -2,6 +2,9 @@ History is important, but our current roadmap can be found [here](https://github.com/cookiecutter/cookiecutter/projects) + +## 2.1.2 (unreleased) + ## 2.1.1 (2022-06-01) ### Documentation updates diff --git a/cookiecutter/__init__.py b/cookiecutter/__init__.py index f0e3a2c38..a54486529 100644 --- a/cookiecutter/__init__.py +++ b/cookiecutter/__init__.py @@ -1,2 +1,2 @@ """Main package for Cookiecutter.""" -__version__ = "2.1.1" +__version__ = "2.1.2.dev0" diff --git a/setup.py b/setup.py index 6b057835e..7c6a677b2 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ """cookiecutter distutils configuration.""" from setuptools import setup -version = "2.1.1" +version = "2.1.2.dev0" with open('README.md', encoding='utf-8') as readme_file: readme = readme_file.read() From b888fd4c17afc9b6993d9edec146d25d54930c8f Mon Sep 17 00:00:00 2001 From: Ryan Russell Date: Wed, 1 Jun 2022 16:14:58 -0500 Subject: [PATCH 107/274] Improve Docs Readability Signed-off-by: Ryan Russell --- docs/advanced/local_extensions.rst | 2 +- docs/advanced/template_extensions.rst | 2 +- docs/index.rst | 2 +- docs/overview.rst | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/advanced/local_extensions.rst b/docs/advanced/local_extensions.rst index d7eba4603..a9eb0a3fa 100644 --- a/docs/advanced/local_extensions.rst +++ b/docs/advanced/local_extensions.rst @@ -32,7 +32,7 @@ This example uses a simple module ``local_extensions.py`` which exists in the te This will register the ``foobar`` filter for the template. -For many cases, this will be unneccessarily complicated. +For many cases, this will be unnecessarily complicated. It's likely that we'd only want to register a single function as a filter. For this, we can use the ``simple_filter`` decorator: .. code-block:: json diff --git a/docs/advanced/template_extensions.rst b/docs/advanced/template_extensions.rst index 397460fa8..3e10bf838 100644 --- a/docs/advanced/template_extensions.rst +++ b/docs/advanced/template_extensions.rst @@ -95,7 +95,7 @@ Would output: it-s-a-random-version -It is different from a mere replace of spaces since it also trates some special characters such as ``'`` in the example above. +It is different from a mere replace of spaces since it also treats some special characters differently such as ``'`` in the example above. The function accepts all arguments that can be passed to the ``slugify`` function of `python-slugify`_. For example to change the output from ``it-s-a-random-version``` to ``it_s_a_random_version``, the ``separator`` parameter would be passed: ``slugify(separator='_')``. diff --git a/docs/index.rst b/docs/index.rst index 14f5edbcf..7e82fc2db 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,7 +6,7 @@ Cookiecutter: Better Project Templates ====================================== -Cookiecutter creates projects from **cookiecutters** (project templates), e.g. Python package projects from Python package temnplates. +Cookiecutter creates projects from **cookiecutters** (project templates), e.g. Python package projects from Python package templates. Basics ------ diff --git a/docs/overview.rst b/docs/overview.rst index 6a1895614..dd06999ef 100644 --- a/docs/overview.rst +++ b/docs/overview.rst @@ -5,7 +5,7 @@ Overview Cookiecutter takes a template provided as a directory structure with template-files. Templates can be in located in the filesystem, as a ZIP-file or on a VCS-Server (Git/Hg) like GitHub. -It reads a settings file and prompts the user interactivly wether to change the settings. +It reads a settings file and prompts the user interactively whether or not to change the settings. Then it takes both and generates an output directory structure from it. From cb7fb883511954eea7068d665d61bcee08d08e27 Mon Sep 17 00:00:00 2001 From: ri0t Date: Tue, 19 Oct 2021 18:44:44 +0200 Subject: [PATCH 108/274] fixes wrong (renamed) repository owner for cookiecutter-pypackage --- HISTORY.md | 10 +++++----- docs/advanced/calling_from_python.rst | 2 +- docs/advanced/user_config.rst | 2 +- docs/overview.rst | 2 +- docs/tutorials/tutorial1.rst | 16 ++++++++-------- docs/usage.rst | 10 +++++----- tests/repository/test_is_repo_url.py | 6 +++--- tests/vcs/test_identify_repo.py | 8 ++++---- 8 files changed, 28 insertions(+), 28 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 5f647aa3a..5f8626cb7 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -226,7 +226,7 @@ Other Changes: * Tests update: use sys.executable when invoking python in python 3 only environment thanks to [@vincentbernat](https://github.com/vincentbernat) (#1221) * Prevent `click` API v7.0 from showing choices when already shown, thanks to [@rly](https://github.com/rly) and [@luzfcb](https://github.com/luzfcb) (#1168) * Test the codebase with python3.8 beta on tox and travis-ci (#1206), thanks to [@mihrab34](https://github.com/mihrab34) -* Add a [CODE\_OF\_CONDUCT.md](https://github.com/audreyr/cookiecutter/blob/master/CODE_OF_CONDUCT.md) file to the project, thanks to [@andreagrandi](https://github.com/andreagrandi) (#1009) +* Add a [CODE\_OF\_CONDUCT.md](https://github.com/audreyfeldroy/cookiecutter/blob/master/CODE_OF_CONDUCT.md) file to the project, thanks to [@andreagrandi](https://github.com/andreagrandi) (#1009) * Update docstrings in `cookiecutter/main.py`, `cookiecutter/__init__.py`, and `cookiecutter/log.py` to follow the PEP 257 style guide, thanks to [@meahow](https://github.com/meahow) (#998, #999, #1000) * Update docstrings in `cookiecutter/utils.py` to follow the PEP 257 style guide, thanks to [@dornheimer](https://github.com/dornheimer)(#1026) * Fix grammar in *Choice Variables* documentation, thanks to [@jubrilissa](https://github.com/jubrilissa) (#1011) @@ -548,7 +548,7 @@ Other Changes: * Enable py35 support on Travis by using Python 3.5 as base Python ([@maiksensi](https://github.com/maiksensi) / #540) * If a filename is empty, do not generate. Log instead ([@iljabauer](https://github.com/iljabauer) / #444) -* Fix tests as per last changes in [cookiecutter-pypackage](https://github.com/audreyr/cookiecutter-pypackage), thanks to [@eliasdorneles](https://github.com/eliasdorneles)(#555). +* Fix tests as per last changes in [cookiecutter-pypackage](https://github.com/audreyfeldroy/cookiecutter-pypackage), thanks to [@eliasdorneles](https://github.com/eliasdorneles)(#555). * Removed deprecated cookiecutter-pylibrary-minimal from the list, thanks to [@ionelmc](https://github.com/ionelmc) (#556) * Moved to using rualmel.yaml instead of PyYAML, except for Windows users on Python 2.7, thanks to [@pydanny](https://github.com/pydanny) (#557) @@ -823,8 +823,8 @@ Other changes: # Create project from the cookiecutter-pypackage/ template $ cookiecutter cookiecutter-pypackage/ # Create project from the cookiecutter-pypackage.git repo template - $ cookiecutter https://github.com/audreyr/cookiecutter-pypackage.git -``` + $ cookiecutter https://github.com/audreyfeldroy/cookiecutter-pypackage.git +``` * Can now use Cookiecutter from Python as a package: @@ -835,7 +835,7 @@ Other changes: cookiecutter('cookiecutter-pypackage/') # Create project from the cookiecutter-pypackage.git repo template - cookiecutter('https://github.com/audreyr/cookiecutter-pypackage.git') + cookiecutter('https://github.com/audreyfeldroy/cookiecutter-pypackage.git') ``` * Internal refactor to remove any code that changes the working diff --git a/docs/advanced/calling_from_python.rst b/docs/advanced/calling_from_python.rst index b497b6bf0..c51b09f0b 100644 --- a/docs/advanced/calling_from_python.rst +++ b/docs/advanced/calling_from_python.rst @@ -13,7 +13,7 @@ You can use Cookiecutter from Python: cookiecutter('cookiecutter-pypackage/') # Create project from the cookiecutter-pypackage.git repo template - cookiecutter('https://github.com/audreyr/cookiecutter-pypackage.git') + cookiecutter('https://github.com/audreyfeldroy/cookiecutter-pypackage.git') This is useful if, for example, you're writing a web framework and need to provide developers with a tool similar to `django-admin.py startproject` or `npm init`. diff --git a/docs/advanced/user_config.rst b/docs/advanced/user_config.rst index c7a8e9826..59063b291 100644 --- a/docs/advanced/user_config.rst +++ b/docs/advanced/user_config.rst @@ -36,7 +36,7 @@ Example user config: cookiecutters_dir: "/home/audreyr/my-custom-cookiecutters-dir/" replay_dir: "/home/audreyr/my-custom-replay-dir/" abbreviations: - pp: https://github.com/audreyr/cookiecutter-pypackage.git + pp: https://github.com/audreyfeldroy/cookiecutter-pypackage.git gh: https://github.com/{0}.git bb: https://bitbucket.org/{0} diff --git a/docs/overview.rst b/docs/overview.rst index dd06999ef..6c75f77fe 100644 --- a/docs/overview.rst +++ b/docs/overview.rst @@ -32,7 +32,7 @@ You must have: Beyond that, you can have whatever files/directories you want. -See https://github.com/audreyr/cookiecutter-pypackage for a real-world example +See https://github.com/audreyfeldroy/cookiecutter-pypackage for a real-world example of this. Output diff --git a/docs/tutorials/tutorial1.rst b/docs/tutorials/tutorial1.rst index 0871ca696..e7133fe91 100644 --- a/docs/tutorials/tutorial1.rst +++ b/docs/tutorials/tutorial1.rst @@ -14,9 +14,8 @@ Case Study: cookiecutter-pypackage *cookiecutter-pypackage* is a cookiecutter template that creates the starter boilerplate for a Python package. -.. note:: - There are several variations of it. - For this tutorial we'll use the original version at https://github.com/audreyr/cookiecutter-pypackage/. +.. note:: There are several variations of it, but for this tutorial we'll use + the original version at https://github.com/audreyfeldroy/cookiecutter-pypackage/. Step 1: Generate a Python Package Project ------------------------------------------ @@ -27,7 +26,7 @@ At the command line, run the cookiecutter command, passing in the link to cookie .. code-block:: bash - $ cookiecutter https://github.com/audreyr/cookiecutter-pypackage.git + $ cookiecutter https://github.com/audreyfeldroy/cookiecutter-pypackage.git Local Cloning of Project Template ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -95,8 +94,7 @@ How did that work? Step 3: Observe How It Was Generated ------------------------------------ -Let's take a look at cookiecutter-pypackage together. -Open https://github.com/audreyr/cookiecutter-pypackage in a new browser window. +Let's take a look at cookiecutter-pypackage together. Open https://github.com/audreyfeldroy/cookiecutter-pypackage in a new browser window. {{ cookiecutter.project_slug }} ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -111,7 +109,8 @@ This happens in `find.py`, where the `find_template()` method looks for the firs AUTHORS.rst ~~~~~~~~~~~ -Look at the raw version of `{{ cookiecutter.project_slug }}/AUTHORS.rst`, at https://raw.github.com/audreyr/cookiecutter-pypackage/master/%7B%7Bcookiecutter.project_slug%7D%7D/AUTHORS.rst. +Look at the raw version of `{{ cookiecutter.project_slug }}/AUTHORS.rst`, at +https://raw.github.com/audreyfeldroy/cookiecutter-pypackage/master/%7B%7Bcookiecutter.project_slug%7D%7D/AUTHORS.rst. Observe how it corresponds to the `AUTHORS.rst` file that you generated. @@ -142,7 +141,8 @@ You should see JSON that corresponds to the prompts and default values shown ear Questions? ---------- -If anything needs better explanation, please take a moment to file an issue at https://github.com/audreyr/cookiecutter/issues with what could be improved about this tutorial. +If anything needs better explanation, please take a moment to file an issue at https://github.com/audreyfeldroy/cookiecutter/issues with what could be improved +about this tutorial. Summary ------- diff --git a/docs/usage.rst b/docs/usage.rst index 9b7b95fe5..872cbe2b7 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -7,7 +7,7 @@ Grab a Cookiecutter template First, clone a Cookiecutter project template:: - $ git clone git@github.com:audreyr/cookiecutter-pypackage.git + $ git clone https://github.com/audreyfeldroy/cookiecutter-pypackage.git Make your changes ----------------- @@ -40,14 +40,14 @@ Works directly with git and hg (mercurial) repos too To create a project from the cookiecutter-pypackage.git repo template:: - $ cookiecutter gh:audreyr/cookiecutter-pypackage + $ cookiecutter gh:audreyfeldroy/cookiecutter-pypackage Cookiecutter knows abbreviations for Github (``gh``), Bitbucket (``bb``), and GitLab (``gl``) projects, but you can also give it the full URL to any repository:: - $ cookiecutter https://github.com/audreyr/cookiecutter-pypackage.git - $ cookiecutter git+ssh://git@github.com/audreyr/cookiecutter-pypackage.git + $ cookiecutter https://github.com/audreyfeldroy/cookiecutter-pypackage.git + $ cookiecutter git+ssh://git@github.com/audreyfeldroy/cookiecutter-pypackage.git $ cookiecutter hg+ssh://hg@bitbucket.org/audreyr/cookiecutter-pypackage You will be prompted to enter a bunch of project config values. (These are @@ -58,7 +58,7 @@ that you entered. It will be placed in your current directory. And if you want to specify a branch you can do that with:: - $ cookiecutter https://github.com/audreyr/cookiecutter-pypackage.git --checkout develop + $ cookiecutter https://github.com/audreyfeldroy/cookiecutter-pypackage.git --checkout develop Works with private repos ------------------------ diff --git a/tests/repository/test_is_repo_url.py b/tests/repository/test_is_repo_url.py index 64238e02e..5591be0fc 100644 --- a/tests/repository/test_is_repo_url.py +++ b/tests/repository/test_is_repo_url.py @@ -25,8 +25,8 @@ def test_is_zip_file(zipfile): @pytest.fixture( params=[ 'gitolite@server:team/repo', - 'git@github.com:audreyr/cookiecutter.git', - 'https://github.com/audreyr/cookiecutter.git', + 'git@github.com:audreyfeldroy/cookiecutter.git', + 'https://github.com/cookiecutter/cookiecutter.git', 'git+https://private.com/gitrepo', 'hg+https://private.com/mercurialrepo', 'https://bitbucket.org/pokoli/cookiecutter.hg', @@ -65,7 +65,7 @@ def test_is_repo_url_for_local_urls(local_repo_url): def test_expand_abbreviations(): """Validate `repository.expand_abbreviations` correctly translate url.""" - template = 'gh:audreyr/cookiecutter-pypackage' + template = 'gh:audreyfeldroy/cookiecutter-pypackage' # This is not a valid repo url just yet! # First `repository.expand_abbreviations` needs to translate it diff --git a/tests/vcs/test_identify_repo.py b/tests/vcs/test_identify_repo.py index fe264edcd..bfb3d56a2 100644 --- a/tests/vcs/test_identify_repo.py +++ b/tests/vcs/test_identify_repo.py @@ -24,14 +24,14 @@ ), ('https://bitbucket.org/foo/bar.hg', 'hg', 'https://bitbucket.org/foo/bar.hg'), ( - 'https://github.com/audreyr/cookiecutter-pypackage.git', + 'https://github.com/audreyfeldroy/cookiecutter-pypackage.git', 'git', - 'https://github.com/audreyr/cookiecutter-pypackage.git', + 'https://github.com/audreyfeldroy/cookiecutter-pypackage.git', ), ( - 'https://github.com/audreyr/cookiecutter-pypackage', + 'https://github.com/audreyfeldroy/cookiecutter-pypackage', 'git', - 'https://github.com/audreyr/cookiecutter-pypackage', + 'https://github.com/audreyfeldroy/cookiecutter-pypackage', ), ( 'git@gitorious.org:cookiecutter-gitorious/cookiecutter-gitorious.git', From d3d163803f322cc69c6a99534e7ff80c3c7f959e Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Sun, 5 Jun 2022 23:46:17 +0300 Subject: [PATCH 109/274] More renames --- AUTHORS.md | 2 +- HISTORY.md | 16 ++++++++-------- docs/tutorials/index.rst | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/AUTHORS.md b/AUTHORS.md index 9e5a01494..6b734589d 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -191,7 +191,7 @@ Contributions include user testing, debugging, improving documentation, reviewin - Adam Chainz ([@adamchainz](https://github.com/adamchainz)) - Andrew Ittner ([@tephyr](https://github.com/tephyr)) -- Audrey Roy Greenfeld ([@audreyr](https://github.com/audreyr)) +- Audrey Roy Greenfeld ([@audreyr](https://github.com/audreyfeldroy)) - Carol Willing ([@willingc](https://github.com/willingc)) - Christopher Clarke ([@chrisdev](https://github.com/chrisdev)) - Citlalli Murillo ([@citmusa](https://github.com/citmusa)) diff --git a/HISTORY.md b/HISTORY.md index 5f8626cb7..097509753 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -403,7 +403,7 @@ Other Changes: * Refactor cookiecutter template identification, thanks to [@michaeljoseph](https://github.com/michaeljoseph) (#777) * Add a `cli_runner` test fixture to simplify CLI tests, thanks to [@hackebrot](https://github.com/hackebrot) (#790) * Add a check to ensure cookiecutter repositories have JSON context, thanks to [@michaeljoseph](https://github.com/michaeljoseph)(#782) -* Rename the internal function that determines whether a file should be rendered, thanks to [@audreyr](https://github.com/audreyr) for raising the issue and [@hackebrot](https://github.com/hackebrot)for the PR (#741, #802) +* Rename the internal function that determines whether a file should be rendered, thanks to [@audreyr](https://github.com/audreyfeldroy) for raising the issue and [@hackebrot](https://github.com/hackebrot)for the PR (#741, #802) * Fix typo in docs, thanks to [@mwarkentin](https://github.com/mwarkentin) (#828) * Fix broken link to *Invoke* docs, thanks to [@B3QL](https://github.com/B3QL) (#820) * Add documentation to `render_variable` function in `prompt.py`, thanks to [@pydanny](https://github.com/pydanny) (#678) @@ -481,11 +481,11 @@ Other Changes: * Removed xfail in test\_cookiecutters, thanks to [@hackebrot](https://github.com/hackebrot) (#618) * Removed django-cms-plugin on account of 404 error, thanks to [@mativs](https://github.com/mativs) and [@pydanny](https://github.com/pydanny) (#593) * Fixed docs/usage.rst, thanks to [@macrotim](https://github.com/macrotim) (#604) -* Update .gitignore to latest Python.gitignore and ignore PyCharm files, thanks to [@audreyr](https://github.com/audreyr) +* Update .gitignore to latest Python.gitignore and ignore PyCharm files, thanks to [@audreyr](https://github.com/audreyfeldroy) * Use open context manager to read context\_file in generate() function, thanks to [@hackebrot](https://github.com/hackebrot) (#607, #608) * Added documentation for choice variables, thanks to [@maiksensi](https://github.com/maiksensi) (#611) -* Set up Scrutinizer to check code quality, thanks to [@audreyr](https://github.com/audreyr) +* Set up Scrutinizer to check code quality, thanks to [@audreyr](https://github.com/audreyfeldroy) * Drop distutils support in setup.py, thanks to [@hackebrot](https://github.com/hackebrot) (#606, #609) * Change cookiecutter-pypackage-minimal link, thanks to [@kragniz](https://github.com/kragniz) (#614) * Fix typo in one of the template\'s description, thanks to [@ryanfreckleton](https://github.com/ryanfreckleton) (#643) @@ -671,7 +671,7 @@ The goal of this release was to allow for injection of extra context via the Coo Features: -* cookiecutter() now takes an optional extra\_context parameter, thanks to [@michaeljoseph](https://github.com/michaeljoseph), [@fcurella](https://github.com/fcurella), [@aventurella](https://github.com/aventurella), [@emonty](https://github.com/emonty), [@schacki](https://github.com/schacki), [@ryanolson](https://github.com/ryanolson), [@pfmoore](https://github.com/pfmoore), [@pydanny](https://github.com/pydanny), [@audreyr](https://github.com/audreyr) (#260). +* cookiecutter() now takes an optional extra\_context parameter, thanks to [@michaeljoseph](https://github.com/michaeljoseph), [@fcurella](https://github.com/fcurella), [@aventurella](https://github.com/aventurella), [@emonty](https://github.com/emonty), [@schacki](https://github.com/schacki), [@ryanolson](https://github.com/ryanolson), [@pfmoore](https://github.com/pfmoore), [@pydanny](https://github.com/pydanny), [@audreyr](https://github.com/audreyfeldroy) (#260). * Context is now injected into hooks, thanks to [@michaeljoseph](https://github.com/michaeljoseph) and [@dinopetrone](https://github.com/dinopetrone). * Moved all Python 2/3 compatibility code into cookiecutter.compat, making the eventual move to six easier, thanks to [@michaeljoseph](https://github.com/michaeljoseph) (#60, #102). * Added cookiecutterrc defined aliases for cookiecutters, thanks to [@pfmoore](https://github.com/pfmoore) (#246) @@ -711,12 +711,12 @@ Bug Fixes: Other Changes: -* [@audreyr](https://github.com/audreyr) formally accepted position as **BDFL of cookiecutter**. +* [@audreyr](https://github.com/audreyfeldroy) formally accepted position as **BDFL of cookiecutter**. * Elevated [@pydanny](https://github.com/pydanny), [@michaeljoseph](https://github.com/michaeljoseph), and [@pfmoore](https://github.com/pfmoore) to core committer status. -* Added Core Committer guide, by [@audreyr](https://github.com/audreyr). -* Generated apidocs from make docs, by [@audreyr](https://github.com/audreyr). +* Added Core Committer guide, by [@audreyr](https://github.com/audreyfeldroy). +* Generated apidocs from make docs, by [@audreyr](https://github.com/audreyfeldroy). * Added contributing command to the makedocs function, by [@pydanny](https://github.com/pydanny). -* Refactored contributing documentation, included adding core committer instructions, by [@pydanny](https://github.com/pydanny) and [@audreyr](https://github.com/audreyr). +* Refactored contributing documentation, included adding core committer instructions, by [@pydanny](https://github.com/pydanny) and [@audreyr](https://github.com/audreyfeldroy). * Do not convert input prompt to bytes, thanks to [@uranusjr](https://github.com/uranusjr) (#192). * Added troubleshooting info about Python 3.3 tests and tox. * Added documentation about command line arguments, thanks to [@saxix](https://github.com/saxix). diff --git a/docs/tutorials/index.rst b/docs/tutorials/index.rst index 81e1b0dff..88848a848 100644 --- a/docs/tutorials/index.rst +++ b/docs/tutorials/index.rst @@ -30,7 +30,7 @@ External Links .. _`Extending our Cookiecutter template`: https://raphael.codes/blog/extending-our-cookiecutter-template/ .. _`Wrapping up our Cookiecutter template`: https://raphael.codes/blog/wrapping-up-our-cookiecutter-template/ -.. _`@audreyr`: https://github.com/audreyr +.. _`@audreyr`: https://github.com/audreyfeldroy .. _`@pydanny`: https://github.com/pydanny .. _`@hackebrot`: https://github.com/hackebrot .. _`@BruceEckel`: https://github.com/BruceEckel From 7907a69f16c370bea23b6647cdf4b90136f1b8b9 Mon Sep 17 00:00:00 2001 From: Miro Jelaska <2855316+miro-jelaska@users.noreply.github.com> Date: Tue, 14 Sep 2021 02:08:22 +0200 Subject: [PATCH 110/274] update Docs for Create a Cookiecutter From Scratch --- docs/tutorials/tutorial2.rst | 92 ++++++++++++++++++++++++++++++++---- 1 file changed, 84 insertions(+), 8 deletions(-) diff --git a/docs/tutorials/tutorial2.rst b/docs/tutorials/tutorial2.rst index 53fcc1d21..e866b9b35 100644 --- a/docs/tutorials/tutorial2.rst +++ b/docs/tutorials/tutorial2.rst @@ -4,28 +4,104 @@ Create a Cookiecutter From Scratch ================================== +In this tutorial, we are creating `cookiecutter-website-simple`, a cookiecutter for generating simple, bare-bones websites. + Step 1: Name Your Cookiecutter ------------------------------ -In this tutorial, we are creating *cookiecutter-website-simple*, a cookiecutter for generating simple, bare-bones websites. - Create the directory for your cookiecutter and cd into it: .. code-block:: bash $ mkdir cookiecutter-website-simple $ cd cookiecutter-website-simple/ + +Step 2: Create cookiecutter.json +---------------------------------- + +`cookiecutter.json` is a JSON file that contains fields which can be referenced in the cookiecutter template. For each, default value is defined and user will be prompted for input during cookiecutter execution. Only mandatory field is `project_slug` and it should comply with package naming conventions defined in `PEP8 Naming Conventions `_ . + +.. code-block:: json + + { + "project_name": "Cookiecutter Website Simple", + "project_slug": "{{ cookiecutter.project_name.lower().replace(' ', '_') }}", + "author": "Anonymous" + } -Step 2: Create `project_slug` Directory + +Step 3: Create project_slug Directory --------------------------------------- -Create a directory called ``{{ cookiecutter.project_slug }}``. +Create a directory called `{{ cookiecutter.project_slug }}`. This value will be replaced with the repo name of projects that you generate from this cookiecutter. -Step 3: Create Files --------------------- +Step 4: Create index.html +-------------------------- + +Inside of `{{ cookiecutter.project_slug }}`, create `index.html` with following content: + +.. code-block:: html + + + + + + {{ cookiecutter.project_name }} + + + +

{{ cookiecutter.project_name }}

+

by {{ cookiecutter.author }}

+ + + +Step 5: Pack cookiecutter into ZIP +---------------------------------- +There are many ways to run Cookiecutter templates, and they are described in details in `Usage chapter `_. In this tutorial we are going to ZIP cookiecutter and then run it for testing. + +By running following command `cookiecutter.zip` will get generated which can be used to run cookiecutter. Script will generate `cookiecutter.zip` ZIP file and echo full path to the file. + +.. code-block:: bash + + $ (SOURCE_DIR=$(basename $PWD) ZIP=cookiecutter.zip && # Set variables + pushd && # Set parent directory as working directory + zip -r $ZIP $SOURCE_DIR --exclude $SOURCE_DIR/$ZIP --quiet && # ZIP cookiecutter + mv $ZIP $SOURCE_DIR/$ZIP && # Move ZIP to original directory + popd && # Restore original work directory + echo "Cookiecutter full path: $PWD/$ZIP") + +Step 6: Run cookiecutter +------------------------ +Set your work directory to whatever directory you would like to run cookiecutter at. Use cookiecutter full path and run the following command: + +.. code-block:: bash + + $ cookiecutter + +You can expect similar output: + +.. code-block:: bash + + $ cookiecutter /Users/admin/cookiecutter-website-simple/cookiecutter.zip + project_name [Cookiecutter Website Simple]: Test web + project_slug [test_web]: + author [Anonymous]: Cookiecutter Developer + +Resulting directory should be inside your work directory with a name that matches `project_slug` you defined. Inside that direcory there should be `index.html` with generated source: + +.. code-block:: html -Inside of ``{{ cookiecutter.project_slug }}``, create ``index.html``, ``site.css``, and ``site.js``. + + + + + Test web + -To be continued... + +

Cookiecutter Developer

+

by Test web

+ + From a5197d05cf890c1e18931c83186c6d91c6c70e2f Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Mon, 6 Jun 2022 04:27:25 +0300 Subject: [PATCH 111/274] Fix spaces for linting --- docs/tutorials/tutorial2.rst | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/tutorials/tutorial2.rst b/docs/tutorials/tutorial2.rst index e866b9b35..202412537 100644 --- a/docs/tutorials/tutorial2.rst +++ b/docs/tutorials/tutorial2.rst @@ -15,7 +15,7 @@ Create the directory for your cookiecutter and cd into it: $ mkdir cookiecutter-website-simple $ cd cookiecutter-website-simple/ - + Step 2: Create cookiecutter.json ---------------------------------- @@ -56,39 +56,39 @@ Inside of `{{ cookiecutter.project_slug }}`, create `index.html` with following

by {{ cookiecutter.author }}

- + Step 5: Pack cookiecutter into ZIP ---------------------------------- -There are many ways to run Cookiecutter templates, and they are described in details in `Usage chapter `_. In this tutorial we are going to ZIP cookiecutter and then run it for testing. +There are many ways to run Cookiecutter templates, and they are described in details in `Usage chapter `_. In this tutorial we are going to ZIP cookiecutter and then run it for testing. By running following command `cookiecutter.zip` will get generated which can be used to run cookiecutter. Script will generate `cookiecutter.zip` ZIP file and echo full path to the file. .. code-block:: bash - + $ (SOURCE_DIR=$(basename $PWD) ZIP=cookiecutter.zip && # Set variables pushd && # Set parent directory as working directory zip -r $ZIP $SOURCE_DIR --exclude $SOURCE_DIR/$ZIP --quiet && # ZIP cookiecutter mv $ZIP $SOURCE_DIR/$ZIP && # Move ZIP to original directory popd && # Restore original work directory echo "Cookiecutter full path: $PWD/$ZIP") - + Step 6: Run cookiecutter ------------------------ Set your work directory to whatever directory you would like to run cookiecutter at. Use cookiecutter full path and run the following command: .. code-block:: bash - + $ cookiecutter - + You can expect similar output: .. code-block:: bash - + $ cookiecutter /Users/admin/cookiecutter-website-simple/cookiecutter.zip project_name [Cookiecutter Website Simple]: Test web project_slug [test_web]: author [Anonymous]: Cookiecutter Developer - + Resulting directory should be inside your work directory with a name that matches `project_slug` you defined. Inside that direcory there should be `index.html` with generated source: .. code-block:: html From fcd417de9c1917c59535e7adcc7ae36cb9717c3d Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Mon, 6 Jun 2022 05:29:37 +0300 Subject: [PATCH 112/274] Replace jinja.pocoo.org to https://jinja.palletsprojects.com/en --- docs/advanced/dict_variables.rst | 2 +- docs/advanced/template_extensions.rst | 2 +- docs/troubleshooting.rst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/advanced/dict_variables.rst b/docs/advanced/dict_variables.rst index ccf1df499..63241c0a0 100644 --- a/docs/advanced/dict_variables.rst +++ b/docs/advanced/dict_variables.rst @@ -63,4 +63,4 @@ The above ``file_types`` dictionary variable creates ``cookiecutter.file_types`` {% endfor %} -Cookiecutter is using `Jinja2's for expression `_ to iterate over the items in the dictionary. +Cookiecutter is using `Jinja2's for expression `_ to iterate over the items in the dictionary. diff --git a/docs/advanced/template_extensions.rst b/docs/advanced/template_extensions.rst index 3e10bf838..272c42b0c 100644 --- a/docs/advanced/template_extensions.rst +++ b/docs/advanced/template_extensions.rst @@ -99,7 +99,7 @@ It is different from a mere replace of spaces since it also treats some special The function accepts all arguments that can be passed to the ``slugify`` function of `python-slugify`_. For example to change the output from ``it-s-a-random-version``` to ``it_s_a_random_version``, the ``separator`` parameter would be passed: ``slugify(separator='_')``. -.. _`Jinja2 extensions`: http://jinja.pocoo.org/docs/latest/extensions/ +.. _`Jinja2 extensions`: https://jinja.palletsprojects.com/en/latest/extensions/ .. _`now`: https://github.com/hackebrot/jinja2-time#now-tag .. _`jinja2_time.TimeExtension`: https://github.com/hackebrot/jinja2-time .. _`python-slugify`: https://pypi.org/project/python-slugify diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst index 2e45a91d5..cca4b82fd 100644 --- a/docs/troubleshooting.rst +++ b/docs/troubleshooting.rst @@ -25,7 +25,7 @@ Or this:: {{ {{ url_for('home') }} }} -See http://jinja.pocoo.org/docs/templates/#escaping for more info. +See https://jinja.palletsprojects.com/en/latest/templates/#escaping for more info. You can also use the `_copy_without_render`_ key in your `cookiecutter.json` file to escape entire files and directories. From 3b5b5bfba33aa93430b6cac219be62031f67f770 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Mon, 6 Jun 2022 05:41:04 +0300 Subject: [PATCH 113/274] Fix @audreyr to @audreyfeldroy --- AUTHORS.md | 2 +- HISTORY.md | 16 ++++++++-------- docs/tutorials/index.rst | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/AUTHORS.md b/AUTHORS.md index 6b734589d..76ebc2622 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -191,7 +191,7 @@ Contributions include user testing, debugging, improving documentation, reviewin - Adam Chainz ([@adamchainz](https://github.com/adamchainz)) - Andrew Ittner ([@tephyr](https://github.com/tephyr)) -- Audrey Roy Greenfeld ([@audreyr](https://github.com/audreyfeldroy)) +- Audrey Roy Greenfeld ([@audreyfeldroy](https://github.com/audreyfeldroy)) - Carol Willing ([@willingc](https://github.com/willingc)) - Christopher Clarke ([@chrisdev](https://github.com/chrisdev)) - Citlalli Murillo ([@citmusa](https://github.com/citmusa)) diff --git a/HISTORY.md b/HISTORY.md index 097509753..cc11b368a 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -403,7 +403,7 @@ Other Changes: * Refactor cookiecutter template identification, thanks to [@michaeljoseph](https://github.com/michaeljoseph) (#777) * Add a `cli_runner` test fixture to simplify CLI tests, thanks to [@hackebrot](https://github.com/hackebrot) (#790) * Add a check to ensure cookiecutter repositories have JSON context, thanks to [@michaeljoseph](https://github.com/michaeljoseph)(#782) -* Rename the internal function that determines whether a file should be rendered, thanks to [@audreyr](https://github.com/audreyfeldroy) for raising the issue and [@hackebrot](https://github.com/hackebrot)for the PR (#741, #802) +* Rename the internal function that determines whether a file should be rendered, thanks to [@audreyfeldroy](https://github.com/audreyfeldroy) for raising the issue and [@hackebrot](https://github.com/hackebrot)for the PR (#741, #802) * Fix typo in docs, thanks to [@mwarkentin](https://github.com/mwarkentin) (#828) * Fix broken link to *Invoke* docs, thanks to [@B3QL](https://github.com/B3QL) (#820) * Add documentation to `render_variable` function in `prompt.py`, thanks to [@pydanny](https://github.com/pydanny) (#678) @@ -481,11 +481,11 @@ Other Changes: * Removed xfail in test\_cookiecutters, thanks to [@hackebrot](https://github.com/hackebrot) (#618) * Removed django-cms-plugin on account of 404 error, thanks to [@mativs](https://github.com/mativs) and [@pydanny](https://github.com/pydanny) (#593) * Fixed docs/usage.rst, thanks to [@macrotim](https://github.com/macrotim) (#604) -* Update .gitignore to latest Python.gitignore and ignore PyCharm files, thanks to [@audreyr](https://github.com/audreyfeldroy) +* Update .gitignore to latest Python.gitignore and ignore PyCharm files, thanks to [@audreyfeldroy](https://github.com/audreyfeldroy) * Use open context manager to read context\_file in generate() function, thanks to [@hackebrot](https://github.com/hackebrot) (#607, #608) * Added documentation for choice variables, thanks to [@maiksensi](https://github.com/maiksensi) (#611) -* Set up Scrutinizer to check code quality, thanks to [@audreyr](https://github.com/audreyfeldroy) +* Set up Scrutinizer to check code quality, thanks to [@audreyfeldroy](https://github.com/audreyfeldroy) * Drop distutils support in setup.py, thanks to [@hackebrot](https://github.com/hackebrot) (#606, #609) * Change cookiecutter-pypackage-minimal link, thanks to [@kragniz](https://github.com/kragniz) (#614) * Fix typo in one of the template\'s description, thanks to [@ryanfreckleton](https://github.com/ryanfreckleton) (#643) @@ -671,7 +671,7 @@ The goal of this release was to allow for injection of extra context via the Coo Features: -* cookiecutter() now takes an optional extra\_context parameter, thanks to [@michaeljoseph](https://github.com/michaeljoseph), [@fcurella](https://github.com/fcurella), [@aventurella](https://github.com/aventurella), [@emonty](https://github.com/emonty), [@schacki](https://github.com/schacki), [@ryanolson](https://github.com/ryanolson), [@pfmoore](https://github.com/pfmoore), [@pydanny](https://github.com/pydanny), [@audreyr](https://github.com/audreyfeldroy) (#260). +* cookiecutter() now takes an optional extra\_context parameter, thanks to [@michaeljoseph](https://github.com/michaeljoseph), [@fcurella](https://github.com/fcurella), [@aventurella](https://github.com/aventurella), [@emonty](https://github.com/emonty), [@schacki](https://github.com/schacki), [@ryanolson](https://github.com/ryanolson), [@pfmoore](https://github.com/pfmoore), [@pydanny](https://github.com/pydanny), [@audreyfeldroy](https://github.com/audreyfeldroy) (#260). * Context is now injected into hooks, thanks to [@michaeljoseph](https://github.com/michaeljoseph) and [@dinopetrone](https://github.com/dinopetrone). * Moved all Python 2/3 compatibility code into cookiecutter.compat, making the eventual move to six easier, thanks to [@michaeljoseph](https://github.com/michaeljoseph) (#60, #102). * Added cookiecutterrc defined aliases for cookiecutters, thanks to [@pfmoore](https://github.com/pfmoore) (#246) @@ -711,12 +711,12 @@ Bug Fixes: Other Changes: -* [@audreyr](https://github.com/audreyfeldroy) formally accepted position as **BDFL of cookiecutter**. +* [@audreyfeldroy](https://github.com/audreyfeldroy) formally accepted position as **BDFL of cookiecutter**. * Elevated [@pydanny](https://github.com/pydanny), [@michaeljoseph](https://github.com/michaeljoseph), and [@pfmoore](https://github.com/pfmoore) to core committer status. -* Added Core Committer guide, by [@audreyr](https://github.com/audreyfeldroy). -* Generated apidocs from make docs, by [@audreyr](https://github.com/audreyfeldroy). +* Added Core Committer guide, by [@audreyfeldroy](https://github.com/audreyfeldroy). +* Generated apidocs from make docs, by [@audreyfeldroy](https://github.com/audreyfeldroy). * Added contributing command to the makedocs function, by [@pydanny](https://github.com/pydanny). -* Refactored contributing documentation, included adding core committer instructions, by [@pydanny](https://github.com/pydanny) and [@audreyr](https://github.com/audreyfeldroy). +* Refactored contributing documentation, included adding core committer instructions, by [@pydanny](https://github.com/pydanny) and [@audreyfeldroy](https://github.com/audreyfeldroy). * Do not convert input prompt to bytes, thanks to [@uranusjr](https://github.com/uranusjr) (#192). * Added troubleshooting info about Python 3.3 tests and tox. * Added documentation about command line arguments, thanks to [@saxix](https://github.com/saxix). diff --git a/docs/tutorials/index.rst b/docs/tutorials/index.rst index 88848a848..0fa1c98b0 100644 --- a/docs/tutorials/index.rst +++ b/docs/tutorials/index.rst @@ -2,7 +2,7 @@ Tutorials ==================== -Tutorials by `@audreyr`_ +Tutorials by `@audreyfeldroy`_ .. toctree:: :maxdepth: 2 @@ -30,7 +30,7 @@ External Links .. _`Extending our Cookiecutter template`: https://raphael.codes/blog/extending-our-cookiecutter-template/ .. _`Wrapping up our Cookiecutter template`: https://raphael.codes/blog/wrapping-up-our-cookiecutter-template/ -.. _`@audreyr`: https://github.com/audreyfeldroy +.. _`@audreyfeldroy`: https://github.com/audreyfeldroy .. _`@pydanny`: https://github.com/pydanny .. _`@hackebrot`: https://github.com/hackebrot .. _`@BruceEckel`: https://github.com/BruceEckel From ed4748f99d6f9d1f6a640f759fea72e1e3f2e034 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Mon, 6 Jun 2022 05:41:53 +0300 Subject: [PATCH 114/274] Fix LICENSE years --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 06486a8f3..9661fad7a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2013-2021, Audrey Roy Greenfeld +Copyright (c) 2013-2022, Audrey Roy Greenfeld All rights reserved. Redistribution and use in source and binary forms, with or From f4939ff5403316184a37c7d36a34ed261b198ab8 Mon Sep 17 00:00:00 2001 From: cksac Date: Mon, 6 Jun 2022 15:36:36 +0800 Subject: [PATCH 115/274] support partially overwrite keys in nested dict --- cookiecutter/generate.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cookiecutter/generate.py b/cookiecutter/generate.py index cd7d34df2..fcaf9f319 100644 --- a/cookiecutter/generate.py +++ b/cookiecutter/generate.py @@ -68,6 +68,10 @@ def apply_overwrites_to_context(context, overwrite_context): "{} provided for choice variable {}, but the " "choices are {}.".format(overwrite, variable, context_value) ) + if isinstance(context_value, dict) and isinstance(overwrite, dict): + # Partially overwrite some keys in original dict + apply_overwrites_to_context(context_value, overwrite) + context[variable] = context_value else: # Simply overwrite the value for this variable context[variable] = overwrite From ef839722d4449021ab1642b07b813a774a702692 Mon Sep 17 00:00:00 2001 From: Zhong Dai <4401438+zhongdai@users.noreply.github.com> Date: Wed, 2 Mar 2022 15:52:24 +1100 Subject: [PATCH 116/274] fixed the override not working with copy only dir --- cookiecutter/generate.py | 5 + .../{{cookiecutter.repo_name}}/README.rst | 5 + .../{{cookiecutter.repo_name}}/README.txt | 5 + .../rendered/not_rendered.yml | 2 + .../README.rst | 5 + .../README.md | 3 + .../README.rst | 5 + .../README.txt | 5 + ...t_generate_copy_without_render_override.py | 95 +++++++++++++++++++ 9 files changed, 130 insertions(+) create mode 100644 tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/README.rst create mode 100644 tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/README.txt create mode 100644 tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/rendered/not_rendered.yml create mode 100644 tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-not-rendered/README.rst create mode 100644 tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-rendered/README.md create mode 100644 tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-rendered/README.rst create mode 100644 tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-rendered/README.txt create mode 100644 tests/test_generate_copy_without_render_override.py diff --git a/cookiecutter/generate.py b/cookiecutter/generate.py index cd7d34df2..65d1f4076 100644 --- a/cookiecutter/generate.py +++ b/cookiecutter/generate.py @@ -333,6 +333,7 @@ def generate_files( # specified in the ``_copy_without_render`` setting, but # we store just the dir name if is_copy_only_path(d_, context): + logger.debug('Found copy only path %s', d) copy_dirs.append(d) else: render_dirs.append(d) @@ -342,6 +343,10 @@ def generate_files( outdir = os.path.normpath(os.path.join(project_dir, indir)) outdir = env.from_string(outdir).render(**context) logger.debug('Copying dir %s to %s without rendering', indir, outdir) + + # if the outdir is there, it must be a overwrite execution + if os.path.isdir(outdir): + shutil.rmtree(outdir) shutil.copytree(indir, outdir) # We mutate ``dirs``, because we only want to go through these dirs diff --git a/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/README.rst b/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/README.rst new file mode 100644 index 000000000..2413548bf --- /dev/null +++ b/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/README.rst @@ -0,0 +1,5 @@ +============ +Fake Project +============ + +{{cookiecutter.render_test}} diff --git a/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/README.txt b/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/README.txt new file mode 100644 index 000000000..2413548bf --- /dev/null +++ b/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/README.txt @@ -0,0 +1,5 @@ +============ +Fake Project +============ + +{{cookiecutter.render_test}} diff --git a/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/rendered/not_rendered.yml b/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/rendered/not_rendered.yml new file mode 100644 index 000000000..a31cf752c --- /dev/null +++ b/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/rendered/not_rendered.yml @@ -0,0 +1,2 @@ +--- +- name: {{cookiecutter.render_test}} diff --git a/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-not-rendered/README.rst b/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-not-rendered/README.rst new file mode 100644 index 000000000..2413548bf --- /dev/null +++ b/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-not-rendered/README.rst @@ -0,0 +1,5 @@ +============ +Fake Project +============ + +{{cookiecutter.render_test}} diff --git a/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-rendered/README.md b/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-rendered/README.md new file mode 100644 index 000000000..0e74081d8 --- /dev/null +++ b/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-rendered/README.md @@ -0,0 +1,3 @@ +# Fake Project + +{{cookiecutter.render_test}} diff --git a/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-rendered/README.rst b/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-rendered/README.rst new file mode 100644 index 000000000..2413548bf --- /dev/null +++ b/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-rendered/README.rst @@ -0,0 +1,5 @@ +============ +Fake Project +============ + +{{cookiecutter.render_test}} diff --git a/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-rendered/README.txt b/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-rendered/README.txt new file mode 100644 index 000000000..2413548bf --- /dev/null +++ b/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-rendered/README.txt @@ -0,0 +1,5 @@ +============ +Fake Project +============ + +{{cookiecutter.render_test}} diff --git a/tests/test_generate_copy_without_render_override.py b/tests/test_generate_copy_without_render_override.py new file mode 100644 index 000000000..d0a798196 --- /dev/null +++ b/tests/test_generate_copy_without_render_override.py @@ -0,0 +1,95 @@ +"""Verify correct work of `_copy_without_render` context option.""" +import os + +import pytest + +from cookiecutter import generate +from cookiecutter import utils + + +@pytest.fixture +def remove_test_dir(): + """Fixture. Remove the folder that is created by the test.""" + yield + if os.path.exists('test_copy_without_render'): + utils.rmtree('test_copy_without_render') + + +@pytest.mark.usefixtures('clean_system', 'remove_test_dir') +def test_generate_copy_without_render_extensions(): + """Verify correct work of `_copy_without_render` context option. + + Some files/directories should be rendered during invocation, + some just copied, without any modification. + """ + + # first run + generate.generate_files( + context={ + 'cookiecutter': { + 'repo_name': 'test_copy_without_render', + 'render_test': 'I have been rendered!', + '_copy_without_render': [ + '*not-rendered', + 'rendered/not_rendered.yml', + '*.txt', + '{{cookiecutter.repo_name}}-rendered/README.md', + ], + } + }, + repo_dir='tests/test-generate-copy-without-render-override', + ) + + # second run with override flag to True + generate.generate_files( + context={ + 'cookiecutter': { + 'repo_name': 'test_copy_without_render', + 'render_test': 'I have been rendered!', + '_copy_without_render': [ + '*not-rendered', + 'rendered/not_rendered.yml', + '*.txt', + '{{cookiecutter.repo_name}}-rendered/README.md', + ], + } + }, + overwrite_if_exists=True, + repo_dir='tests/test-generate-copy-without-render', + ) + + dir_contents = os.listdir('test_copy_without_render') + + assert 'test_copy_without_render-not-rendered' in dir_contents + assert 'test_copy_without_render-rendered' in dir_contents + + with open('test_copy_without_render/README.txt') as f: + assert '{{cookiecutter.render_test}}' in f.read() + + with open('test_copy_without_render/README.rst') as f: + assert 'I have been rendered!' in f.read() + + with open( + 'test_copy_without_render/test_copy_without_render-rendered/README.txt' + ) as f: + assert '{{cookiecutter.render_test}}' in f.read() + + with open( + 'test_copy_without_render/test_copy_without_render-rendered/README.rst' + ) as f: + assert 'I have been rendered' in f.read() + + with open( + 'test_copy_without_render/' + 'test_copy_without_render-not-rendered/' + 'README.rst' + ) as f: + assert '{{cookiecutter.render_test}}' in f.read() + + with open('test_copy_without_render/rendered/not_rendered.yml') as f: + assert '{{cookiecutter.render_test}}' in f.read() + + with open( + 'test_copy_without_render/' 'test_copy_without_render-rendered/' 'README.md' + ) as f: + assert '{{cookiecutter.render_test}}' in f.read() From cf3c85b266f89437498d7cf4e339d874bff0648e Mon Sep 17 00:00:00 2001 From: Zhong Dai <4401438+zhongdai@users.noreply.github.com> Date: Mon, 14 Mar 2022 19:58:37 +1100 Subject: [PATCH 117/274] remove the blank line after the docstring --- tests/test_generate_copy_without_render_override.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_generate_copy_without_render_override.py b/tests/test_generate_copy_without_render_override.py index d0a798196..af85c4b04 100644 --- a/tests/test_generate_copy_without_render_override.py +++ b/tests/test_generate_copy_without_render_override.py @@ -22,7 +22,6 @@ def test_generate_copy_without_render_extensions(): Some files/directories should be rendered during invocation, some just copied, without any modification. """ - # first run generate.generate_files( context={ From 855a1c3fc2c603a38298bfe5c5c5723f7a0031b2 Mon Sep 17 00:00:00 2001 From: Zhong Dai <4401438+zhongdai@users.noreply.github.com> Date: Mon, 11 Apr 2022 15:35:51 +1000 Subject: [PATCH 118/274] add extra check for the overwrite_if_exists flag Co-authored-by: Andrew Tribone <780132+att14@users.noreply.github.com> --- cookiecutter/generate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cookiecutter/generate.py b/cookiecutter/generate.py index 65d1f4076..8350df0f9 100644 --- a/cookiecutter/generate.py +++ b/cookiecutter/generate.py @@ -345,7 +345,7 @@ def generate_files( logger.debug('Copying dir %s to %s without rendering', indir, outdir) # if the outdir is there, it must be a overwrite execution - if os.path.isdir(outdir): + if os.path.isdir(outdir) and overwrite_if_exists: shutil.rmtree(outdir) shutil.copytree(indir, outdir) From c7729fa88b9c7b929d4de76c0d88a3675119d210 Mon Sep 17 00:00:00 2001 From: Zhong Dai <4401438+zhongdai@users.noreply.github.com> Date: Wed, 13 Apr 2022 09:06:58 +1000 Subject: [PATCH 119/274] added the function doc for skip_if_file_exists --- cookiecutter/generate.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cookiecutter/generate.py b/cookiecutter/generate.py index 8350df0f9..ce2bc0ab9 100644 --- a/cookiecutter/generate.py +++ b/cookiecutter/generate.py @@ -277,6 +277,8 @@ def generate_files( :param output_dir: Where to output the generated project dir into. :param overwrite_if_exists: Overwrite the contents of the output directory if it exists. + :param skip_if_file_exists: Skip the files in the corresponding directories + if they already exist :param accept_hooks: Accept pre and post hooks if set to `True`. :param keep_project_on_failure: If `True` keep generated project directory even when generation fails From 418e316d36e440697e3d14c76ef5490175c3a094 Mon Sep 17 00:00:00 2001 From: Zhong Dai <4401438+zhongdai@users.noreply.github.com> Date: Wed, 13 Apr 2022 09:25:53 +1000 Subject: [PATCH 120/274] fixed the exception test case --- cookiecutter/generate.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cookiecutter/generate.py b/cookiecutter/generate.py index ce2bc0ab9..605b8952a 100644 --- a/cookiecutter/generate.py +++ b/cookiecutter/generate.py @@ -346,8 +346,10 @@ def generate_files( outdir = env.from_string(outdir).render(**context) logger.debug('Copying dir %s to %s without rendering', indir, outdir) - # if the outdir is there, it must be a overwrite execution - if os.path.isdir(outdir) and overwrite_if_exists: + # The outdir is not the root dir, it is the dir which marked as copy + # only in the config file. If the program hits this line, which means + # the overwrite_if_exists = True, and root dir exists + if os.path.isdir(outdir): shutil.rmtree(outdir) shutil.copytree(indir, outdir) From c041d91782b1a7adb2ecc7a7c92ad907dc11e7f2 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Mon, 6 Jun 2022 11:37:01 +0300 Subject: [PATCH 121/274] Update cookiecutter/prompt.py --- cookiecutter/prompt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cookiecutter/prompt.py b/cookiecutter/prompt.py index c3aa6b957..140340012 100644 --- a/cookiecutter/prompt.py +++ b/cookiecutter/prompt.py @@ -168,7 +168,7 @@ def render_variable(env, raw, cookiecutter_dict): def prompt_choice_for_config(cookiecutter_dict, env, key, options, no_input): """Prompt user with a set of options to choose from. - :no_input: Do not prompt for user input and return the first available option. + :param no_input: Do not prompt for user input and return the first available option. """ rendered_options = [render_variable(env, raw, cookiecutter_dict) for raw in options] if no_input: From cfad8f96a30aae0e30dfdf877a876f05c25d741b Mon Sep 17 00:00:00 2001 From: cksac Date: Mon, 6 Jun 2022 16:48:12 +0800 Subject: [PATCH 122/274] add test --- cookiecutter/generate.py | 2 +- tests/test-generate-context/nested_dict.json | 10 +++++ tests/test_generate_context.py | 42 ++++++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 tests/test-generate-context/nested_dict.json diff --git a/cookiecutter/generate.py b/cookiecutter/generate.py index fcaf9f319..a20510c8d 100644 --- a/cookiecutter/generate.py +++ b/cookiecutter/generate.py @@ -68,7 +68,7 @@ def apply_overwrites_to_context(context, overwrite_context): "{} provided for choice variable {}, but the " "choices are {}.".format(overwrite, variable, context_value) ) - if isinstance(context_value, dict) and isinstance(overwrite, dict): + elif isinstance(context_value, dict) and isinstance(overwrite, dict): # Partially overwrite some keys in original dict apply_overwrites_to_context(context_value, overwrite) context[variable] = context_value diff --git a/tests/test-generate-context/nested_dict.json b/tests/test-generate-context/nested_dict.json new file mode 100644 index 000000000..c13ea10f3 --- /dev/null +++ b/tests/test-generate-context/nested_dict.json @@ -0,0 +1,10 @@ +{ + "full_name": "Raphael Pierzina", + "github_username": "hackebrot", + "project": { + "name": "Kivy Project", + "description": "Kivy Project", + "repo_name": "{{cookiecutter.project_name|lower}}", + "orientation": ["all", "landscape", "portrait"] + } +} \ No newline at end of file diff --git a/tests/test_generate_context.py b/tests/test_generate_context.py index 44ab99194..892a7588b 100644 --- a/tests/test_generate_context.py +++ b/tests/test_generate_context.py @@ -202,3 +202,45 @@ def test_apply_overwrites_sets_default_for_choice_variable(template_context): ) assert template_context['orientation'] == ['landscape', 'all', 'portrait'] + + +def test_apply_overwrites_in_nested_dict(): + """Verify nested dict in default content settings are correctly replaced.""" + expected_context = { + 'nested_dict': OrderedDict( + [ + ('full_name', 'Raphael Pierzina'), + ('github_username', 'hackebrot'), + ( + 'project', + OrderedDict( + [ + ('name', 'My Kivy Project'), + ('description', 'My Kivy Project'), + ('repo_name', '{{cookiecutter.project_name|lower}}'), + ('orientation', ["all", "landscape", "portrait"]), + ] + ), + ), + ] + ) + } + + generated_context = generate.generate_context( + context_file='tests/test-generate-context/nested_dict.json', + default_context={ + 'not_in_template': 'foobar', + 'project': { + 'description': 'My Kivy Project', + }, + }, + extra_context={ + 'also_not_in_template': 'foobar2', + 'github_username': 'hackebrot', + 'project': { + 'name': 'My Kivy Project', + }, + }, + ) + + assert generated_context == expected_context From 347ac18ccfb8a38cc76d4bab9056e3a4bb8b2e4d Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Sat, 2 Jan 2021 18:59:17 +0100 Subject: [PATCH 123/274] generate: introduces templates inheritance implement a templates directory so a maintainer can use `extends`, `includes`, `blocks`, `import` and `super` inside the cookiecutter project template using jinja directly. docs: https://jinja.palletsprojects.com/en/2.11.x/templates/#template-inheritance closes: https://github.com/cookiecutter/cookiecutter/issues/1484 ref: https://github.com/cookiecutter/cookiecutter/issues/706 ref: https://github.com/cookiecutter/cookiecutter/issues/59 ref: https://github.com/cookiecutter/cookiecutter/issues/1004 --- cookiecutter/generate.py | 2 +- docs/advanced/index.rst | 1 + docs/advanced/templates.rst | 34 +++++++++++++++ .../test-templates/extends/cookiecutter.json | 5 +++ .../extends/templates/base-requirements.jinja | 6 +++ .../templates/click-requirements.jinja | 1 + .../templates/pytest-requirements.jinja | 1 + .../requirements.txt | 1 + .../test-templates/include/cookiecutter.json | 5 +++ .../templates/click-requirements.jinja | 1 + .../templates/pytest-requirements.jinja | 1 + .../requirements.txt | 5 +++ .../no-templates/cookiecutter.json | 5 +++ .../requirements.txt | 5 +++ tests/test-templates/super/cookiecutter.json | 5 +++ .../super/templates/base-requirements.jinja | 7 ++++ .../super/templates/click-requirements.jinja | 1 + .../super/templates/pytest-requirements.jinja | 1 + .../requirements.txt | 2 + tests/test_templates.py | 41 +++++++++++++++++++ 20 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 docs/advanced/templates.rst create mode 100644 tests/test-templates/extends/cookiecutter.json create mode 100644 tests/test-templates/extends/templates/base-requirements.jinja create mode 100644 tests/test-templates/extends/templates/click-requirements.jinja create mode 100644 tests/test-templates/extends/templates/pytest-requirements.jinja create mode 100644 tests/test-templates/extends/{{cookiecutter.project_slug}}/requirements.txt create mode 100644 tests/test-templates/include/cookiecutter.json create mode 100644 tests/test-templates/include/templates/click-requirements.jinja create mode 100644 tests/test-templates/include/templates/pytest-requirements.jinja create mode 100644 tests/test-templates/include/{{cookiecutter.project_slug}}/requirements.txt create mode 100644 tests/test-templates/no-templates/cookiecutter.json create mode 100644 tests/test-templates/no-templates/{{cookiecutter.project_slug}}/requirements.txt create mode 100644 tests/test-templates/super/cookiecutter.json create mode 100644 tests/test-templates/super/templates/base-requirements.jinja create mode 100644 tests/test-templates/super/templates/click-requirements.jinja create mode 100644 tests/test-templates/super/templates/pytest-requirements.jinja create mode 100644 tests/test-templates/super/{{cookiecutter.project_slug}}/requirements.txt create mode 100644 tests/test_templates.py diff --git a/cookiecutter/generate.py b/cookiecutter/generate.py index 4b4250b7c..01cbcf8a0 100644 --- a/cookiecutter/generate.py +++ b/cookiecutter/generate.py @@ -324,7 +324,7 @@ def generate_files( ) with work_in(template_dir): - env.loader = FileSystemLoader('.') + env.loader = FileSystemLoader(['.', '../templates']) for root, dirs, files in os.walk('.'): # We must separate the two types of dirs into different lists. diff --git a/docs/advanced/index.rst b/docs/advanced/index.rst index d2111ad4b..216731d91 100644 --- a/docs/advanced/index.rst +++ b/docs/advanced/index.rst @@ -19,6 +19,7 @@ Various advanced topics regarding cookiecutter usage. replay choice_variables dict_variables + templates template_extensions directories new_line_characters diff --git a/docs/advanced/templates.rst b/docs/advanced/templates.rst new file mode 100644 index 000000000..cf670ad0e --- /dev/null +++ b/docs/advanced/templates.rst @@ -0,0 +1,34 @@ +.. _templates: + +Templates inheritance (1.X+) +--------------------------------------------------- + +*New in Cookiecutter 1.X* + +Sometimes you need to extend a base template with a different +configuration to avoid nested blocks. + +Cookiecutter introduces the ability to use common templates +using the power of jinja: `extends`, `include` and `super`. + +Here's an example repository:: + + https://github.com/user/repo-name.git + ├── {{cookiecutter.project_slug}}/ + | └── file.txt + ├── templates/ + | └── base.txt + └── cookiecutter.json + +every file in the `templates` directory will become referable inside the project itself, +and the path should be relative from the `templates` folder like :: + + # file.txt + {% extends "base.txt" %} + + ... or ... + + # file.txt + {% include "base.txt" %} + +see more on https://jinja.palletsprojects.com/en/2.11.x/templates/ diff --git a/tests/test-templates/extends/cookiecutter.json b/tests/test-templates/extends/cookiecutter.json new file mode 100644 index 000000000..e8798e956 --- /dev/null +++ b/tests/test-templates/extends/cookiecutter.json @@ -0,0 +1,5 @@ +{ + "project_slug": "foobar", + "command_line_interface": "click", + "use_pytest": "y" +} diff --git a/tests/test-templates/extends/templates/base-requirements.jinja b/tests/test-templates/extends/templates/base-requirements.jinja new file mode 100644 index 000000000..fbf1a17e7 --- /dev/null +++ b/tests/test-templates/extends/templates/base-requirements.jinja @@ -0,0 +1,6 @@ +pip==19.2.3 +{% if cookiecutter.command_line_interface|lower == 'click' -%} +{% include 'click-requirements.jinja' %}{% endif %} +{% if cookiecutter.use_pytest == 'y' -%} +{% include 'pytest-requirements.jinja' %}{% endif %} +{% block dependencies %}{% endblock %} \ No newline at end of file diff --git a/tests/test-templates/extends/templates/click-requirements.jinja b/tests/test-templates/extends/templates/click-requirements.jinja new file mode 100644 index 000000000..8e1cde652 --- /dev/null +++ b/tests/test-templates/extends/templates/click-requirements.jinja @@ -0,0 +1 @@ +Click==7.0 \ No newline at end of file diff --git a/tests/test-templates/extends/templates/pytest-requirements.jinja b/tests/test-templates/extends/templates/pytest-requirements.jinja new file mode 100644 index 000000000..18a0e4267 --- /dev/null +++ b/tests/test-templates/extends/templates/pytest-requirements.jinja @@ -0,0 +1 @@ +pytest==4.6.5 \ No newline at end of file diff --git a/tests/test-templates/extends/{{cookiecutter.project_slug}}/requirements.txt b/tests/test-templates/extends/{{cookiecutter.project_slug}}/requirements.txt new file mode 100644 index 000000000..910e3721e --- /dev/null +++ b/tests/test-templates/extends/{{cookiecutter.project_slug}}/requirements.txt @@ -0,0 +1 @@ +{% extends "base-requirements.jinja" %} \ No newline at end of file diff --git a/tests/test-templates/include/cookiecutter.json b/tests/test-templates/include/cookiecutter.json new file mode 100644 index 000000000..e8798e956 --- /dev/null +++ b/tests/test-templates/include/cookiecutter.json @@ -0,0 +1,5 @@ +{ + "project_slug": "foobar", + "command_line_interface": "click", + "use_pytest": "y" +} diff --git a/tests/test-templates/include/templates/click-requirements.jinja b/tests/test-templates/include/templates/click-requirements.jinja new file mode 100644 index 000000000..8e1cde652 --- /dev/null +++ b/tests/test-templates/include/templates/click-requirements.jinja @@ -0,0 +1 @@ +Click==7.0 \ No newline at end of file diff --git a/tests/test-templates/include/templates/pytest-requirements.jinja b/tests/test-templates/include/templates/pytest-requirements.jinja new file mode 100644 index 000000000..18a0e4267 --- /dev/null +++ b/tests/test-templates/include/templates/pytest-requirements.jinja @@ -0,0 +1 @@ +pytest==4.6.5 \ No newline at end of file diff --git a/tests/test-templates/include/{{cookiecutter.project_slug}}/requirements.txt b/tests/test-templates/include/{{cookiecutter.project_slug}}/requirements.txt new file mode 100644 index 000000000..4ea7b13f6 --- /dev/null +++ b/tests/test-templates/include/{{cookiecutter.project_slug}}/requirements.txt @@ -0,0 +1,5 @@ +pip==19.2.3 +{% if cookiecutter.command_line_interface|lower == 'click' -%} +{% include 'click-requirements.jinja' %}{% endif %} +{% if cookiecutter.use_pytest == 'y' -%} +{% include 'pytest-requirements.jinja' %}{% endif %} \ No newline at end of file diff --git a/tests/test-templates/no-templates/cookiecutter.json b/tests/test-templates/no-templates/cookiecutter.json new file mode 100644 index 000000000..e8798e956 --- /dev/null +++ b/tests/test-templates/no-templates/cookiecutter.json @@ -0,0 +1,5 @@ +{ + "project_slug": "foobar", + "command_line_interface": "click", + "use_pytest": "y" +} diff --git a/tests/test-templates/no-templates/{{cookiecutter.project_slug}}/requirements.txt b/tests/test-templates/no-templates/{{cookiecutter.project_slug}}/requirements.txt new file mode 100644 index 000000000..6554e75ff --- /dev/null +++ b/tests/test-templates/no-templates/{{cookiecutter.project_slug}}/requirements.txt @@ -0,0 +1,5 @@ +pip==19.2.3 +{% if cookiecutter.command_line_interface|lower == 'click' -%} +Click==7.0{% endif %} +{% if cookiecutter.use_pytest == 'y' -%} +pytest==4.6.5{% endif %} \ No newline at end of file diff --git a/tests/test-templates/super/cookiecutter.json b/tests/test-templates/super/cookiecutter.json new file mode 100644 index 000000000..e8798e956 --- /dev/null +++ b/tests/test-templates/super/cookiecutter.json @@ -0,0 +1,5 @@ +{ + "project_slug": "foobar", + "command_line_interface": "click", + "use_pytest": "y" +} diff --git a/tests/test-templates/super/templates/base-requirements.jinja b/tests/test-templates/super/templates/base-requirements.jinja new file mode 100644 index 000000000..c4fd8141e --- /dev/null +++ b/tests/test-templates/super/templates/base-requirements.jinja @@ -0,0 +1,7 @@ +pip==19.2.3 +{% if cookiecutter.command_line_interface|lower == 'click' -%} +{% include 'click-requirements.jinja' %}{% endif %} +{%- block dev_dependencies %} +{% if cookiecutter.use_pytest == 'y' -%}{% include 'pytest-requirements.jinja' %}{% endif %} +{%- endblock %} +{% block dependencies %}{% endblock %} \ No newline at end of file diff --git a/tests/test-templates/super/templates/click-requirements.jinja b/tests/test-templates/super/templates/click-requirements.jinja new file mode 100644 index 000000000..8e1cde652 --- /dev/null +++ b/tests/test-templates/super/templates/click-requirements.jinja @@ -0,0 +1 @@ +Click==7.0 \ No newline at end of file diff --git a/tests/test-templates/super/templates/pytest-requirements.jinja b/tests/test-templates/super/templates/pytest-requirements.jinja new file mode 100644 index 000000000..18a0e4267 --- /dev/null +++ b/tests/test-templates/super/templates/pytest-requirements.jinja @@ -0,0 +1 @@ +pytest==4.6.5 \ No newline at end of file diff --git a/tests/test-templates/super/{{cookiecutter.project_slug}}/requirements.txt b/tests/test-templates/super/{{cookiecutter.project_slug}}/requirements.txt new file mode 100644 index 000000000..602b5772f --- /dev/null +++ b/tests/test-templates/super/{{cookiecutter.project_slug}}/requirements.txt @@ -0,0 +1,2 @@ +{% extends "base-requirements.jinja" %} +{% block dev_dependencies %}{{ super() }}{% endblock %} \ No newline at end of file diff --git a/tests/test_templates.py b/tests/test_templates.py new file mode 100644 index 000000000..a981c81ab --- /dev/null +++ b/tests/test_templates.py @@ -0,0 +1,41 @@ +""" +test_custom_extension_in_hooks. + +Tests to ensure custom cookiecutter extensions are properly made available to +pre- and post-gen hooks. +""" +import codecs +import os + +import pytest + +from cookiecutter import main + + +@pytest.fixture +def output_dir(tmpdir): + """Fixture. Create and return custom temp directory for test.""" + return str(tmpdir.mkdir('templates')) + + +@pytest.mark.parametrize("template", ["include", "no-templates", "extends", "super"]) +def test_build_templates(template, output_dir): + """ + Verify Templates Design keywords. + + no-templates is a compatibility tests for repo without `templates` directory + """ + project_dir = main.cookiecutter( + f'tests/test-templates/{template}', no_input=True, output_dir=output_dir, + ) + + readme_file = os.path.join(project_dir, 'requirements.txt') + + with codecs.open(readme_file, encoding='utf8') as f: + readme = f.read().splitlines() + + assert readme == [ + "pip==19.2.3", + "Click==7.0", + "pytest==4.6.5", + ] From 13526907ba9aa8c97604d1c0db055128c7a1d58f Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Mon, 6 Jun 2022 12:51:46 +0300 Subject: [PATCH 124/274] Fix linting in #1485 --- tests/test_templates.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_templates.py b/tests/test_templates.py index a981c81ab..70cf76b5a 100644 --- a/tests/test_templates.py +++ b/tests/test_templates.py @@ -26,7 +26,9 @@ def test_build_templates(template, output_dir): no-templates is a compatibility tests for repo without `templates` directory """ project_dir = main.cookiecutter( - f'tests/test-templates/{template}', no_input=True, output_dir=output_dir, + f'tests/test-templates/{template}', + no_input=True, + output_dir=output_dir, ) readme_file = os.path.join(project_dir, 'requirements.txt') From 012d04a51922e5670b01ccf9d9dcd0c13a8e5178 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Mon, 6 Jun 2022 13:12:47 +0300 Subject: [PATCH 125/274] Update docs/advanced/templates.rst --- docs/advanced/templates.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/advanced/templates.rst b/docs/advanced/templates.rst index cf670ad0e..f297357c1 100644 --- a/docs/advanced/templates.rst +++ b/docs/advanced/templates.rst @@ -1,6 +1,6 @@ .. _templates: -Templates inheritance (1.X+) +Templates inheritance (2.2+) --------------------------------------------------- *New in Cookiecutter 1.X* From d269707f8f96e450cff37a1eb6bfe6284029cb8a Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Mon, 6 Jun 2022 13:12:53 +0300 Subject: [PATCH 126/274] Update docs/advanced/templates.rst --- docs/advanced/templates.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/advanced/templates.rst b/docs/advanced/templates.rst index f297357c1..5113bf940 100644 --- a/docs/advanced/templates.rst +++ b/docs/advanced/templates.rst @@ -3,7 +3,7 @@ Templates inheritance (2.2+) --------------------------------------------------- -*New in Cookiecutter 1.X* +*New in Cookiecutter 2.2+* Sometimes you need to extend a base template with a different configuration to avoid nested blocks. From 185feb10974796aaebd3d2eb40c0c5e7a9f494d8 Mon Sep 17 00:00:00 2001 From: liortct Date: Mon, 20 Dec 2021 00:17:22 +0200 Subject: [PATCH 127/274] Add boolean variable support --- cookiecutter/prompt.py | 8 +++++ docs/advanced/boolean_variables.rst | 46 +++++++++++++++++++++++++++++ docs/advanced/index.rst | 1 + tests/test_prompt.py | 44 ++++++++++++++++++++++++--- 4 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 docs/advanced/boolean_variables.rst diff --git a/cookiecutter/prompt.py b/cookiecutter/prompt.py index a067d0932..4e32d30f9 100644 --- a/cookiecutter/prompt.py +++ b/cookiecutter/prompt.py @@ -145,6 +145,8 @@ def render_variable(env, raw, cookiecutter_dict): """ if raw is None: return None + elif isinstance(raw, bool): + return raw elif isinstance(raw, dict): return { render_variable(env, k, cookiecutter_dict): render_variable( @@ -201,6 +203,12 @@ def prompt_for_config(context, no_input=False): cookiecutter_dict, env, key, raw, no_input ) cookiecutter_dict[key] = val + elif isinstance(raw, bool): + # We are dealing with a boolean variable + val = render_variable(env, raw, cookiecutter_dict) + if not no_input: + val = read_user_yes_no(key, raw) + cookiecutter_dict[key] = val elif not isinstance(raw, dict): # We are dealing with a regular variable val = render_variable(env, raw, cookiecutter_dict) diff --git a/docs/advanced/boolean_variables.rst b/docs/advanced/boolean_variables.rst new file mode 100644 index 000000000..3016c8ae7 --- /dev/null +++ b/docs/advanced/boolean_variables.rst @@ -0,0 +1,46 @@ +.. _boolean-variables: + +Boolean Variables (2.0+) +------------------------ + +Boolean variables are used for answering True/False questions. + +Basic Usage +~~~~~~~~~~~ + +Boolean variables are regular key / value pairs, but with the value being True/False. + +For example, if you provide the following boolean variable in your ``cookiecutter.json``:: + + { + "run_as_docker": true + } + +you'd get the following user input when running Cookiecutter:: + + run_as_docker [True]: + +Depending on the user's input, a different condition will be selected. + +The above ``run_as_docker`` boolean variable creates ``cookiecutter.run_as_docker``, which +can be used like this:: + + {%- if cookiecutter.run_as_docker -%} + # In case of True add your content here + + {%- else -%} + # In case of False add your content here + + {% endif %} + +Cookiecutter is using `Jinja2's if conditional expression `_ to determine the correct ``run_as_docker``. + +Input Validation +~~~~~~~~~~~~~~~~ +If a non valid value is inserted to a boolean field, the following error will be printed: + +.. code-block:: bash + + run_as_docker [True]: docker + Error: docker is not a valid boolean + diff --git a/docs/advanced/index.rst b/docs/advanced/index.rst index 216731d91..89150af5d 100644 --- a/docs/advanced/index.rst +++ b/docs/advanced/index.rst @@ -18,6 +18,7 @@ Various advanced topics regarding cookiecutter usage. copy_without_render replay choice_variables + boolean_variables dict_variables templates template_extensions diff --git a/tests/test_prompt.py b/tests/test_prompt.py index 037591dd4..9e85bcd9e 100644 --- a/tests/test_prompt.py +++ b/tests/test_prompt.py @@ -21,7 +21,7 @@ class TestRenderVariable: 'raw_var, rendered_var', [ (1, '1'), - (True, 'True'), + (True, True), ('foo', 'foo'), ('{{cookiecutter.project}}', 'foobar'), (None, None), @@ -39,7 +39,7 @@ def test_convert_to_str(self, mocker, raw_var, rendered_var): assert result == rendered_var # Make sure that non None non str variables are converted beforehand - if raw_var is not None: + if raw_var is not None and not isinstance(raw_var, bool): if not isinstance(raw_var, str): raw_var = str(raw_var) from_string.assert_called_once_with(raw_var) @@ -49,10 +49,10 @@ def test_convert_to_str(self, mocker, raw_var, rendered_var): @pytest.mark.parametrize( 'raw_var, rendered_var', [ - ({1: True, 'foo': False}, {'1': 'True', 'foo': 'False'}), + ({1: True, 'foo': False}, {'1': True, 'foo': False}), ( {'{{cookiecutter.project}}': ['foo', 1], 'bar': False}, - {'foobar': ['foo', '1'], 'bar': 'False'}, + {'foobar': ['foo', '1'], 'bar': False}, ), (['foo', '{{cookiecutter.project}}', None], ['foo', 'foobar', None]), ], @@ -380,6 +380,42 @@ def test_should_read_user_choice(self, mocker, choices, context): assert expected_choice == actual_choice +class TestReadUserYesNo(object): + """Class to unite boolean prompt related tests.""" + + @pytest.mark.parametrize( + 'run_as_docker', + ( + True, + False, + ), + ) + def test_should_invoke_read_user_yes_no(self, mocker, run_as_docker): + """Verify correct function called for boolean variables.""" + read_user_yes_no = mocker.patch('cookiecutter.prompt.read_user_yes_no') + read_user_yes_no.return_value = run_as_docker + + read_user_variable = mocker.patch('cookiecutter.prompt.read_user_variable') + + context = {'cookiecutter': {'run_as_docker': run_as_docker}} + + cookiecutter_dict = prompt.prompt_for_config(context) + + assert not read_user_variable.called + read_user_yes_no.assert_called_once_with('run_as_docker', run_as_docker) + assert cookiecutter_dict == {'run_as_docker': run_as_docker} + + def test_boolean_parameter_no_input(self): + """Verify boolean parameter sent to prompt for config with no input.""" + context = { + 'cookiecutter': { + 'run_as_docker': True, + } + } + cookiecutter_dict = prompt.prompt_for_config(context, no_input=True) + assert cookiecutter_dict == context['cookiecutter'] + + @pytest.mark.parametrize( 'context', ( From 789d6d045a393bc770287ae193edb25aaf84a4ad Mon Sep 17 00:00:00 2001 From: liortct Date: Mon, 20 Dec 2021 00:56:27 +0200 Subject: [PATCH 128/274] Fix code to follow Code Climate suggestions --- cookiecutter/prompt.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/cookiecutter/prompt.py b/cookiecutter/prompt.py index 4e32d30f9..003ace67d 100644 --- a/cookiecutter/prompt.py +++ b/cookiecutter/prompt.py @@ -143,9 +143,7 @@ def render_variable(env, raw, cookiecutter_dict): being populated with variables. :return: The rendered value for the default variable. """ - if raw is None: - return None - elif isinstance(raw, bool): + if raw is None or isinstance(raw, bool): return raw elif isinstance(raw, dict): return { @@ -205,10 +203,12 @@ def prompt_for_config(context, no_input=False): cookiecutter_dict[key] = val elif isinstance(raw, bool): # We are dealing with a boolean variable - val = render_variable(env, raw, cookiecutter_dict) - if not no_input: - val = read_user_yes_no(key, raw) - cookiecutter_dict[key] = val + if no_input: + cookiecutter_dict[key] = render_variable( + env, raw, cookiecutter_dict + ) + else: + cookiecutter_dict[key] = read_user_yes_no(key, raw) elif not isinstance(raw, dict): # We are dealing with a regular variable val = render_variable(env, raw, cookiecutter_dict) From 768bfcf6eeeb29d896718066c7d47c85f2c6a393 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Mon, 6 Jun 2022 13:34:56 +0300 Subject: [PATCH 129/274] Update docs/advanced/boolean_variables.rst --- docs/advanced/boolean_variables.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/advanced/boolean_variables.rst b/docs/advanced/boolean_variables.rst index 3016c8ae7..58c5e48a7 100644 --- a/docs/advanced/boolean_variables.rst +++ b/docs/advanced/boolean_variables.rst @@ -1,6 +1,6 @@ .. _boolean-variables: -Boolean Variables (2.0+) +Boolean Variables (2.2+) ------------------------ Boolean variables are used for answering True/False questions. From ec58a7176a95f77b676fb6a5df0f2aeebd257df6 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Mon, 6 Jun 2022 22:12:47 +0300 Subject: [PATCH 130/274] Fix release-drafter template typo. Closes #1699 --- .github/release-drafter.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 6eb8aeb13..c25ca352b 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -32,6 +32,6 @@ template: | $CHANGES - ## This release is made by wonderfull contributors: + ## This release is made by wonderful contributors: $CONTRIBUTORS From d5d212070f53047cea9d1d29e5976ecfe813d429 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Mon, 6 Jun 2022 22:20:55 +0300 Subject: [PATCH 131/274] Add autolabeler support for workflow --- .github/workflows/release-drafter.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 17fdb961d..c7cd0bbcc 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -4,9 +4,18 @@ on: push: branches: - master + # autolabeler + pull_request: + types: + - opened + - reopened + - synchronize jobs: update_release_draft: + permissions: + contents: write + pull-requests: write runs-on: ubuntu-latest steps: - uses: release-drafter/release-drafter@v5 From 902b3cb63806629c62e44a4be178af61a82b71f8 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Mon, 6 Jun 2022 22:28:01 +0300 Subject: [PATCH 132/274] Add version resolver to release-drafter --- .github/release-drafter.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index c25ca352b..ced9f1fa8 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -1,5 +1,25 @@ filter-by-commitish: true commitish: master +name-template: '$RESOLVED_VERSION' +tag-template: '$RESOLVED_VERSION' +version-resolver: + major: + labels: + - 'breaking-change' + - 'major' + minor: + labels: + - 'enhancement' + - 'feature' + patch: + labels: + - 'bug' + - 'CI/CD' + - 'code style' + - 'documentation' + - 'tests' + - 'patch' + default: patch categories: - title: 'Breaking Changes' labels: From 084a03bbd660f8b407d0c619e694488c31c8515b Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Mon, 6 Jun 2022 22:58:14 +0300 Subject: [PATCH 133/274] Add autolabeler to release-drafter --- .github/release-drafter.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index ced9f1fa8..1cda0440c 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -20,6 +20,23 @@ version-resolver: - 'tests' - 'patch' default: patch +autolabeler: + - label: 'CI/CD' + files: + - '.github/*' + - '.pre-commit-config.yaml' + - '*.cfg' + - '*.ini' + - 'noxfile.py' + - 'setup.*' + - 'docs/conf.py' + - 'Makefile' + - 'make.bat' + - '*requirements*.txt' + - label: 'documentation' + files: + - '*.md' + - '*.rst' categories: - title: 'Breaking Changes' labels: From a1a61e3285030b106e46163231520a0e77269dd3 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Mon, 6 Jun 2022 23:00:25 +0300 Subject: [PATCH 134/274] Configure notes sorting in release-drafter --- .github/release-drafter.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 1cda0440c..13ec354d8 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -62,6 +62,8 @@ categories: - title: 'Deprecations' labels: - 'deprecated' +sort-by: title +sort-direction: ascending exclude-labels: - 'skip-changelog' template: | From 5f6aaee3af0b0c192a314962f76ab8902fd405cb Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Tue, 7 Jun 2022 01:43:17 +0300 Subject: [PATCH 135/274] Extend tests requirements --- test_requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test_requirements.txt b/test_requirements.txt index 4607cefe1..febcb2ae3 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -2,3 +2,5 @@ pytest pytest-cov pytest-mock freezegun +safety +pre-commit From 018705635d3746317923c09b296b3293db23df82 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Tue, 7 Jun 2022 01:48:41 +0300 Subject: [PATCH 136/274] Add nox configuration --- MANIFEST.in | 1 + noxfile.py | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 noxfile.py diff --git a/MANIFEST.in b/MANIFEST.in index 0e1034643..4e36e27df 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -11,6 +11,7 @@ exclude .* exclude codecov.yml exclude test_requirements.txt exclude tox.ini +exclude noxfile.py recursive-include tests * recursive-exclude * __pycache__ diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 000000000..d2789664b --- /dev/null +++ b/noxfile.py @@ -0,0 +1,73 @@ +"""Nox tool configuration file. + +Nox is Tox tool replacement. +""" +import nox + +nox.options.keywords = "not docs" + + +def base_install(session): + """Create basic environment setup for tests and linting.""" + session.install("-r", "test_requirements.txt") + session.install("-e", ".") + return session + + +@nox.session(python="3.10") +def lint(session): + """Run linting check locally.""" + session = base_install(session) + session.run("pre-commit", "run", "-a") + + +@nox.session(python=["3.7", "3.8", "3.9", "3.10"]) +def tests(session): + """Run test suite with pytest.""" + session = base_install(session) + session.run( + "pytest", + "--cov-report=html", + "--cov-report=xml", + "--cov-branch", + "--cov-fail-under=100", + ) + + +@nox.session(python=["3.7", "3.8", "3.9", "3.10"]) +def safety_tests(session): + """Run safety tests.""" + session = base_install(session) + session.run("safety", "check", "--full-report") + + +@nox.session(python="3.10") +def documentation_tests(session): + """Run documentation tests.""" + return docs(session, batch_run=True) + + +@nox.session(python="3.10") +def docs(session, batch_run: bool = False): + """Build the documentation or serve documentation interactively.""" + session.run("rm", "-rf", "docs/_build", external=True) + session.install("-r", "docs/requirements.txt") + session.install(".") + session.cd("docs") + sphinx_args = ["-b", "html", "-W", ".", "_build/html"] + + if not session.interactive or batch_run: + sphinx_cmd = "sphinx-build" + else: + sphinx_cmd = "sphinx-autobuild" + sphinx_args.extend( + [ + "--open-browser", + "--port", + "9812", + "--watch", + "../cookiecutter", + ] + ) + + session.run(sphinx_cmd, *sphinx_args) From aa951c1716e8da41c71de78b914df9217dd01df5 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Tue, 7 Jun 2022 01:49:10 +0300 Subject: [PATCH 137/274] Fix codeblock style (failed docs build) --- docs/advanced/suppressing_prompts.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/advanced/suppressing_prompts.rst b/docs/advanced/suppressing_prompts.rst index be8a549fd..68f4a292e 100644 --- a/docs/advanced/suppressing_prompts.rst +++ b/docs/advanced/suppressing_prompts.rst @@ -30,7 +30,7 @@ Advanced Example: Defaults + Extra Context If you combine an ``extra_context`` dict with the ``no_input`` argument, you can programmatically create the project with a set list of context parameters and without any command line prompts: -.. code-block:: JSON +.. code-block:: python cookiecutter('cookiecutter-pypackage/', no_input=True, From 59bfbb3dfd0c1dc6929ad46613e90b2b12a62c55 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Tue, 7 Jun 2022 01:49:52 +0300 Subject: [PATCH 138/274] Add sphinx-autobuild for nox correct run #1698 --- docs/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/requirements.txt b/docs/requirements.txt index 40157c3ae..5b5407b85 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -2,3 +2,4 @@ watchdog>=0.10.2 sphinx-rtd-theme>=0.4.3 sphinx-click>=2.3.2 recommonmark>=0.6.0 +sphinx-autobuild>=2021.3.14 \ No newline at end of file From 47d2341a5d4baa5f99c9f8a0d2a171453fca8224 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Tue, 7 Jun 2022 02:20:17 +0300 Subject: [PATCH 139/274] Added readthedocs build config --- .readthedocs.yaml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .readthedocs.yaml diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 000000000..6c06c6854 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,19 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details +version: 2 +build: + os: ubuntu-20.04 + tools: + python: "3.10" +sphinx: + configuration: docs/conf.py +formats: + - htmlzip + - pdf + - epub +python: + install: + - requirements: docs/requirements.txt + - method: pip + path: . \ No newline at end of file From d177d33fa2414f834c88978f7fc5b6446ffdd7a0 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Tue, 7 Jun 2022 02:34:16 +0300 Subject: [PATCH 140/274] Use latest ubuntu for builds --- .readthedocs.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 6c06c6854..988f6ffbf 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -3,7 +3,7 @@ # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details version: 2 build: - os: ubuntu-20.04 + os: ubuntu-22.04 tools: python: "3.10" sphinx: @@ -16,4 +16,4 @@ python: install: - requirements: docs/requirements.txt - method: pip - path: . \ No newline at end of file + path: . From 7c7d39a8444972853c26a32a51c8fb3f3632dc1b Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Tue, 7 Jun 2022 03:44:20 +0300 Subject: [PATCH 141/274] Update sphinx requirements --- docs/requirements.txt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 5b5407b85..14510c6a8 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,6 @@ watchdog>=0.10.2 -sphinx-rtd-theme>=0.4.3 -sphinx-click>=2.3.2 -recommonmark>=0.6.0 -sphinx-autobuild>=2021.3.14 \ No newline at end of file +sphinx-rtd-theme>=1.0.0 +sphinx-click>=4.1.0 +myst-parser>=0.17.2 +sphinx-autobuild>=2021.3.14 +Sphinx>=4.0.1 From c8fbb3192b76a092b463b4842cfa75b4aea0c4a5 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Tue, 7 Jun 2022 03:44:35 +0300 Subject: [PATCH 142/274] Fix myst broken links --- CONTRIBUTING.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f0ae0bb42..283506e9b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,11 +3,11 @@ Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. -- [Types of Contributions](#Types-of-Contributions) -- [Contributor Setup](#Setting-Up-the-Code-for-Local-Development) -- [Contributor Guidelines](#Contributor-Guidelines) -- [Contributor Testing](#Testing-with-tox) -- [Core Committer Guide](#Core-Committer-Guide) +- [Types of Contributions](#types-of-contributions) +- [Contributor Setup](#setting-up-the-code-for-local-development) +- [Contributor Guidelines](#contributor-guidelines) +- [Contributor Testing](#testing-with-tox) +- [Core Committer Guide](#core-committer-guide) ## Types of Contributions From 30c691e4afe68e28e7134d3b1571297aa82759b1 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Tue, 7 Jun 2022 03:53:58 +0300 Subject: [PATCH 143/274] Update sphinx configuration --- docs/conf.py | 33 +++++++-------------------------- 1 file changed, 7 insertions(+), 26 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 51648bd10..cb4be1464 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -28,31 +28,6 @@ # flake8: noqa D107,D105 - -class Mock(object): - def __init__(self, *args, **kwargs): - pass - - def __call__(self, *args, **kwargs): - return Mock() - - @classmethod - def __getattr__(cls, name): - if name in ('__file__', '__path__'): - return '/dev/null' - elif name[0] == name[0].upper(): - mockType = type(name, (), {}) - mockType.__module__ = __name__ - return mockType - else: - return Mock() - - -MOCK_MODULES = ['yaml'] -for mod_name in MOCK_MODULES: - sys.modules[mod_name] = Mock() - - # Add parent dir to path cwd = os.getcwd() parent = os.path.dirname(cwd) @@ -78,7 +53,7 @@ def __getattr__(cls, name): 'sphinx.ext.ifconfig', 'sphinx.ext.viewcode', 'sphinx_click.ext', - 'recommonmark', + 'myst_parser', ] # Add any paths that contain templates here, relative to this directory. @@ -378,3 +353,9 @@ def __getattr__(cls, name): # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = {'https://docs.python.org/3': None} +myst_enable_extensions = [ + "tasklist", + "strikethrough", + "fieldlist", +] +myst_heading_anchors = 3 From 9d445f4ca46996de47f774bed737888c326eccfc Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Tue, 7 Jun 2022 05:24:27 +0300 Subject: [PATCH 144/274] remove versions from template, to exclude dependabot warnings --- .../extends/templates/base-requirements.jinja | 2 +- .../extends/templates/click-requirements.jinja | 2 +- .../extends/templates/pytest-requirements.jinja | 2 +- .../include/templates/click-requirements.jinja | 2 +- .../include/templates/pytest-requirements.jinja | 2 +- .../include/{{cookiecutter.project_slug}}/requirements.txt | 2 +- .../{{cookiecutter.project_slug}}/requirements.txt | 6 +++--- .../test-templates/super/templates/base-requirements.jinja | 2 +- .../test-templates/super/templates/click-requirements.jinja | 2 +- .../super/templates/pytest-requirements.jinja | 2 +- 10 files changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/test-templates/extends/templates/base-requirements.jinja b/tests/test-templates/extends/templates/base-requirements.jinja index fbf1a17e7..964fa3379 100644 --- a/tests/test-templates/extends/templates/base-requirements.jinja +++ b/tests/test-templates/extends/templates/base-requirements.jinja @@ -1,4 +1,4 @@ -pip==19.2.3 +pip {% if cookiecutter.command_line_interface|lower == 'click' -%} {% include 'click-requirements.jinja' %}{% endif %} {% if cookiecutter.use_pytest == 'y' -%} diff --git a/tests/test-templates/extends/templates/click-requirements.jinja b/tests/test-templates/extends/templates/click-requirements.jinja index 8e1cde652..4d441badc 100644 --- a/tests/test-templates/extends/templates/click-requirements.jinja +++ b/tests/test-templates/extends/templates/click-requirements.jinja @@ -1 +1 @@ -Click==7.0 \ No newline at end of file +Click \ No newline at end of file diff --git a/tests/test-templates/extends/templates/pytest-requirements.jinja b/tests/test-templates/extends/templates/pytest-requirements.jinja index 18a0e4267..55b033e90 100644 --- a/tests/test-templates/extends/templates/pytest-requirements.jinja +++ b/tests/test-templates/extends/templates/pytest-requirements.jinja @@ -1 +1 @@ -pytest==4.6.5 \ No newline at end of file +pytest \ No newline at end of file diff --git a/tests/test-templates/include/templates/click-requirements.jinja b/tests/test-templates/include/templates/click-requirements.jinja index 8e1cde652..4d441badc 100644 --- a/tests/test-templates/include/templates/click-requirements.jinja +++ b/tests/test-templates/include/templates/click-requirements.jinja @@ -1 +1 @@ -Click==7.0 \ No newline at end of file +Click \ No newline at end of file diff --git a/tests/test-templates/include/templates/pytest-requirements.jinja b/tests/test-templates/include/templates/pytest-requirements.jinja index 18a0e4267..55b033e90 100644 --- a/tests/test-templates/include/templates/pytest-requirements.jinja +++ b/tests/test-templates/include/templates/pytest-requirements.jinja @@ -1 +1 @@ -pytest==4.6.5 \ No newline at end of file +pytest \ No newline at end of file diff --git a/tests/test-templates/include/{{cookiecutter.project_slug}}/requirements.txt b/tests/test-templates/include/{{cookiecutter.project_slug}}/requirements.txt index 4ea7b13f6..62645fd16 100644 --- a/tests/test-templates/include/{{cookiecutter.project_slug}}/requirements.txt +++ b/tests/test-templates/include/{{cookiecutter.project_slug}}/requirements.txt @@ -1,4 +1,4 @@ -pip==19.2.3 +pip {% if cookiecutter.command_line_interface|lower == 'click' -%} {% include 'click-requirements.jinja' %}{% endif %} {% if cookiecutter.use_pytest == 'y' -%} diff --git a/tests/test-templates/no-templates/{{cookiecutter.project_slug}}/requirements.txt b/tests/test-templates/no-templates/{{cookiecutter.project_slug}}/requirements.txt index 6554e75ff..b8b92ac2b 100644 --- a/tests/test-templates/no-templates/{{cookiecutter.project_slug}}/requirements.txt +++ b/tests/test-templates/no-templates/{{cookiecutter.project_slug}}/requirements.txt @@ -1,5 +1,5 @@ -pip==19.2.3 +pip {% if cookiecutter.command_line_interface|lower == 'click' -%} -Click==7.0{% endif %} +Click{% endif %} {% if cookiecutter.use_pytest == 'y' -%} -pytest==4.6.5{% endif %} \ No newline at end of file +pytest{% endif %} \ No newline at end of file diff --git a/tests/test-templates/super/templates/base-requirements.jinja b/tests/test-templates/super/templates/base-requirements.jinja index c4fd8141e..23c1ca511 100644 --- a/tests/test-templates/super/templates/base-requirements.jinja +++ b/tests/test-templates/super/templates/base-requirements.jinja @@ -1,4 +1,4 @@ -pip==19.2.3 +pip {% if cookiecutter.command_line_interface|lower == 'click' -%} {% include 'click-requirements.jinja' %}{% endif %} {%- block dev_dependencies %} diff --git a/tests/test-templates/super/templates/click-requirements.jinja b/tests/test-templates/super/templates/click-requirements.jinja index 8e1cde652..4d441badc 100644 --- a/tests/test-templates/super/templates/click-requirements.jinja +++ b/tests/test-templates/super/templates/click-requirements.jinja @@ -1 +1 @@ -Click==7.0 \ No newline at end of file +Click \ No newline at end of file diff --git a/tests/test-templates/super/templates/pytest-requirements.jinja b/tests/test-templates/super/templates/pytest-requirements.jinja index 18a0e4267..55b033e90 100644 --- a/tests/test-templates/super/templates/pytest-requirements.jinja +++ b/tests/test-templates/super/templates/pytest-requirements.jinja @@ -1 +1 @@ -pytest==4.6.5 \ No newline at end of file +pytest \ No newline at end of file From 0610b651bfd0bb10a4f1f3f2be254315a65c792e Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Tue, 7 Jun 2022 05:24:43 +0300 Subject: [PATCH 145/274] Exclude versions from tests --- tests/test_templates.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_templates.py b/tests/test_templates.py index 70cf76b5a..27f40428b 100644 --- a/tests/test_templates.py +++ b/tests/test_templates.py @@ -37,7 +37,7 @@ def test_build_templates(template, output_dir): readme = f.read().splitlines() assert readme == [ - "pip==19.2.3", - "Click==7.0", - "pytest==4.6.5", + "pip", + "Click", + "pytest", ] From a11e8153bfbdc98d6ceb47423ebb2ac469474280 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Tue, 7 Jun 2022 05:59:07 +0300 Subject: [PATCH 146/274] Update pre-commit config --- .pre-commit-config.yaml | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7be7e3bb8..96d89ad9a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ --- repos: - repo: https://github.com/PyCQA/doc8 - rev: 0.8.1 + rev: 0.11.2 hooks: - id: doc8 name: doc8 @@ -17,17 +17,26 @@ repos: language_version: python3 exclude: ^(tests\/hooks-abort-render\/hooks|docs\/HelloCookieCutter1) - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.4.0 + rev: v4.2.0 hooks: - id: trailing-whitespace args: [--markdown-linebreak-ext=md] - id: mixed-line-ending - - id: check-byte-order-marker + - id: fix-byte-order-marker - id: check-executables-have-shebangs + - id: check-shebang-scripts-are-executable - id: check-merge-conflict - id: check-symlinks + - id: check-case-conflict + - id: check-docstring-first + - id: check-json + exclude: "invalid-syntax.json|tests/fake-repo-bad-json/cookiecutter.json|tests/fake-repo/cookiecutter.json" + - id: check-toml + - id: check-xml + - id: check-yaml + exclude: "not_rendered.yml|invalid-config.yaml" - repo: https://gitlab.com/pycqa/flake8 - rev: 3.7.9 + rev: 4.0.1 hooks: - id: flake8 additional_dependencies: @@ -35,7 +44,7 @@ repos: - flake8-black - flake8-docstrings - repo: https://github.com/PyCQA/bandit - rev: 1.6.0 + rev: 1.7.4 hooks: - id: bandit args: [--ini, .bandit] From 6fdbb92e0793015e35e43a8cf53de4991283cf8b Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Tue, 7 Jun 2022 05:59:21 +0300 Subject: [PATCH 147/274] Remove unneeded shebangs to fix pre-commit issues --- setup.py | 1 - tests/hooks-abort-render/hooks/post_gen_project.py | 1 - tests/hooks-abort-render/hooks/pre_gen_project.py | 1 - tests/test-pyhooks/hooks/post_gen_project.py | 1 - tests/test-pyhooks/hooks/pre_gen_project.py | 1 - tests/test-pyshellhooks/hooks/post_gen_project.py | 1 - tests/test-pyshellhooks/hooks/pre_gen_project.py | 1 - 7 files changed, 7 deletions(-) diff --git a/setup.py b/setup.py index 7c6a677b2..d61fbd6ba 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python """cookiecutter distutils configuration.""" from setuptools import setup diff --git a/tests/hooks-abort-render/hooks/post_gen_project.py b/tests/hooks-abort-render/hooks/post_gen_project.py index 706cc440d..d95ca59fa 100644 --- a/tests/hooks-abort-render/hooks/post_gen_project.py +++ b/tests/hooks-abort-render/hooks/post_gen_project.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # flake8: noqa """Simple post-gen hook for testing the handling of different exit codes.""" diff --git a/tests/hooks-abort-render/hooks/pre_gen_project.py b/tests/hooks-abort-render/hooks/pre_gen_project.py index a132af807..3bd59868c 100644 --- a/tests/hooks-abort-render/hooks/pre_gen_project.py +++ b/tests/hooks-abort-render/hooks/pre_gen_project.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # flake8: noqa """Simple pre-gen hook for testing the handling of different exit codes.""" diff --git a/tests/test-pyhooks/hooks/post_gen_project.py b/tests/test-pyhooks/hooks/post_gen_project.py index c8b7c194f..98a5a353b 100644 --- a/tests/test-pyhooks/hooks/post_gen_project.py +++ b/tests/test-pyhooks/hooks/post_gen_project.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python """Simple post-gen hook for testing project folder and custom file creation.""" print('pre generation hook') diff --git a/tests/test-pyhooks/hooks/pre_gen_project.py b/tests/test-pyhooks/hooks/pre_gen_project.py index 4d84bd3ec..6f1887bd4 100644 --- a/tests/test-pyhooks/hooks/pre_gen_project.py +++ b/tests/test-pyhooks/hooks/pre_gen_project.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python """Simple pre-gen hook for testing project folder and custom file creation.""" print('pre generation hook') diff --git a/tests/test-pyshellhooks/hooks/post_gen_project.py b/tests/test-pyshellhooks/hooks/post_gen_project.py index c8b7c194f..98a5a353b 100644 --- a/tests/test-pyshellhooks/hooks/post_gen_project.py +++ b/tests/test-pyshellhooks/hooks/post_gen_project.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python """Simple post-gen hook for testing project folder and custom file creation.""" print('pre generation hook') diff --git a/tests/test-pyshellhooks/hooks/pre_gen_project.py b/tests/test-pyshellhooks/hooks/pre_gen_project.py index db8bfc6a7..daeb59acb 100644 --- a/tests/test-pyshellhooks/hooks/pre_gen_project.py +++ b/tests/test-pyshellhooks/hooks/pre_gen_project.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python """Simple pre-gen hook for testing project folder and custom file creation.""" From 7efd07b49a77715e30e06dee1795690c0b04408f Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Tue, 7 Jun 2022 07:13:55 +0300 Subject: [PATCH 148/274] Remove unused sphinx files --- docs/Makefile | 177 ------------------------------------ docs/make.bat | 242 -------------------------------------------------- 2 files changed, 419 deletions(-) delete mode 100644 docs/Makefile delete mode 100644 docs/make.bat diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index 01325d08b..000000000 --- a/docs/Makefile +++ /dev/null @@ -1,177 +0,0 @@ -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = -BUILDDIR = _build - -# User-friendly check for sphinx-build -ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) -$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) -endif - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . - -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext - -help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " xml to make Docutils-native XML files" - @echo " pseudoxml to make pseudoxml-XML files for display purposes" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - -clean: - rm -rf $(BUILDDIR)/* - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/cookiecutter.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/cookiecutter.qhc" - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/cookiecutter" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/cookiecutter" - @echo "# devhelp" - -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -latexpdfja: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through platex and dvipdfmx..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -texinfo: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -info: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -gettext: - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." - -xml: - $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml - @echo - @echo "Build finished. The XML files are in $(BUILDDIR)/xml." - -pseudoxml: - $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml - @echo - @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index 908517ffa..000000000 --- a/docs/make.bat +++ /dev/null @@ -1,242 +0,0 @@ -@ECHO OFF - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set BUILDDIR=_build -set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . -set I18NSPHINXOPTS=%SPHINXOPTS% . -if NOT "%PAPER%" == "" ( - set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% - set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% -) - -if "%1" == "" goto help - -if "%1" == "help" ( - :help - echo.Please use `make ^` where ^ is one of - echo. html to make standalone HTML files - echo. dirhtml to make HTML files named index.html in directories - echo. singlehtml to make a single large HTML file - echo. pickle to make pickle files - echo. json to make JSON files - echo. htmlhelp to make HTML files and a HTML help project - echo. qthelp to make HTML files and a qthelp project - echo. devhelp to make HTML files and a Devhelp project - echo. epub to make an epub - echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter - echo. text to make text files - echo. man to make manual pages - echo. texinfo to make Texinfo files - echo. gettext to make PO message catalogs - echo. changes to make an overview over all changed/added/deprecated items - echo. xml to make Docutils-native XML files - echo. pseudoxml to make pseudoxml-XML files for display purposes - echo. linkcheck to check all external links for integrity - echo. doctest to run all doctests embedded in the documentation if enabled - goto end -) - -if "%1" == "clean" ( - for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i - del /q /s %BUILDDIR%\* - goto end -) - - -%SPHINXBUILD% 2> nul -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -if "%1" == "html" ( - %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/html. - goto end -) - -if "%1" == "dirhtml" ( - %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. - goto end -) - -if "%1" == "singlehtml" ( - %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. - goto end -) - -if "%1" == "pickle" ( - %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the pickle files. - goto end -) - -if "%1" == "json" ( - %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the JSON files. - goto end -) - -if "%1" == "htmlhelp" ( - %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run HTML Help Workshop with the ^ -.hhp project file in %BUILDDIR%/htmlhelp. - goto end -) - -if "%1" == "qthelp" ( - %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run "qcollectiongenerator" with the ^ -.qhcp project file in %BUILDDIR%/qthelp, like this: - echo.^> qcollectiongenerator %BUILDDIR%\qthelp\cookiecutter.qhcp - echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\cookiecutter.ghc - goto end -) - -if "%1" == "devhelp" ( - %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. - goto end -) - -if "%1" == "epub" ( - %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The epub file is in %BUILDDIR%/epub. - goto end -) - -if "%1" == "latex" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdf" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf - cd %BUILDDIR%/.. - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdfja" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf-ja - cd %BUILDDIR%/.. - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "text" ( - %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The text files are in %BUILDDIR%/text. - goto end -) - -if "%1" == "man" ( - %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The manual pages are in %BUILDDIR%/man. - goto end -) - -if "%1" == "texinfo" ( - %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. - goto end -) - -if "%1" == "gettext" ( - %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The message catalogs are in %BUILDDIR%/locale. - goto end -) - -if "%1" == "changes" ( - %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes - if errorlevel 1 exit /b 1 - echo. - echo.The overview file is in %BUILDDIR%/changes. - goto end -) - -if "%1" == "linkcheck" ( - %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck - if errorlevel 1 exit /b 1 - echo. - echo.Link check complete; look for any errors in the above output ^ -or in %BUILDDIR%/linkcheck/output.txt. - goto end -) - -if "%1" == "doctest" ( - %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest - if errorlevel 1 exit /b 1 - echo. - echo.Testing of doctests in the sources finished, look at the ^ -results in %BUILDDIR%/doctest/output.txt. - goto end -) - -if "%1" == "xml" ( - %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The XML files are in %BUILDDIR%/xml. - goto end -) - -if "%1" == "pseudoxml" ( - %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. - goto end -) - -:end From 94631a846035a24ba20d233a5c9347f4cf0b6b1e Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Tue, 7 Jun 2022 09:02:31 +0300 Subject: [PATCH 149/274] Add nox files to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 8c96562ec..24aec0102 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,7 @@ pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ +.nox/ .coverage .coverage.* .cache From ac047dc48075eed678ec08f59526a6759f6cc305 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Tue, 7 Jun 2022 09:03:53 +0300 Subject: [PATCH 150/274] Replace not windows supported sed with nox workflow and template --- Makefile | 14 +++------- docs/_templates/package.rst_t | 51 +++++++++++++++++++++++++++++++++++ docs/conf.py | 6 +++++ docs/cookiecutter.rst | 1 - docs/requirements.txt | 2 +- noxfile.py | 5 +++- 6 files changed, 65 insertions(+), 14 deletions(-) create mode 100644 docs/_templates/package.rst_t diff --git a/Makefile b/Makefile index 2dc6aa3aa..8c993be1e 100644 --- a/Makefile +++ b/Makefile @@ -60,21 +60,13 @@ coverage: ## Check code coverage quickly with the default Python .PHONY: docs docs: ## Generate Sphinx HTML documentation, including API docs @echo "+ $@" - @rm -f docs/cookiecutter.rst - @sphinx-apidoc -o docs/ cookiecutter - @rm -f docs/modules.rst - @sed -i 's/cookiecutter package/===\nAPI\n===/' docs/cookiecutter.rst - @sed -i 's/====================//' docs/cookiecutter.rst - @sed -i 's/Submodules/This is the Cookiecutter modules API documentation./' docs/cookiecutter.rst - @sed -i 's/^----------$$//' docs/cookiecutter.rst - @$(MAKE) -C docs clean - @$(MAKE) -C docs html + @nox --non-interactive -s docs @$(BROWSER) docs/_build/html/index.html .PHONY: servedocs -servedocs: docs ## Rebuild docs automatically +servedocs: ## Rebuild docs automatically @echo "+ $@" - @watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D . + @nox -s docs .PHONY: submodules submodules: ## Pull and update git submodules recursively diff --git a/docs/_templates/package.rst_t b/docs/_templates/package.rst_t new file mode 100644 index 000000000..b18365dec --- /dev/null +++ b/docs/_templates/package.rst_t @@ -0,0 +1,51 @@ +{%- macro automodule(modname, options) -%} +.. automodule:: {{ modname }} +{%- for option in options %} + :{{ option }}: +{%- endfor %} +{%- endmacro %} +{%- macro toctree(docnames) -%} +.. toctree:: + :maxdepth: {{ maxdepth }} +{% for docname in docnames %} + {{ docname }} +{%- endfor %} +{%- endmacro -%} +=== +API +=== + + +{%- if modulefirst and not is_namespace %} +{{ automodule(pkgname, automodule_options) }} +{% endif %} + +{%- if subpackages %} +Subpackages +----------- + +{{ toctree(subpackages) }} +{% endif %} + +{%- if submodules %} + +This is the Cookiecutter modules API documentation. + +{% if separatemodules %} +{{ toctree(submodules) }} +{% else %} +{%- for submodule in submodules %} +{% if show_headings %} +{{- [submodule, "module"] | join(" ") | e | heading(2) }} +{% endif %} +{{ automodule(submodule, automodule_options) }} +{% endfor %} +{%- endif %} +{%- endif %} + +{%- if not modulefirst and not is_namespace %} +Module contents +--------------- + +{{ automodule(pkgname, automodule_options) }} +{% endif %} diff --git a/docs/conf.py b/docs/conf.py index cb4be1464..d608b0308 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -54,6 +54,7 @@ 'sphinx.ext.viewcode', 'sphinx_click.ext', 'myst_parser', + 'sphinxcontrib.apidoc', ] # Add any paths that contain templates here, relative to this directory. @@ -359,3 +360,8 @@ "fieldlist", ] myst_heading_anchors = 3 +# Apidoc extension config +apidoc_module_dir = "../cookiecutter" +apidoc_output_dir = "." +apidoc_toc_file = False +apidoc_extra_args = ["-t", "_templates"] diff --git a/docs/cookiecutter.rst b/docs/cookiecutter.rst index 16c1c9dc1..89c857dc4 100644 --- a/docs/cookiecutter.rst +++ b/docs/cookiecutter.rst @@ -2,7 +2,6 @@ API === - This is the Cookiecutter modules API documentation. diff --git a/docs/requirements.txt b/docs/requirements.txt index 14510c6a8..89204a752 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,6 +1,6 @@ -watchdog>=0.10.2 sphinx-rtd-theme>=1.0.0 sphinx-click>=4.1.0 myst-parser>=0.17.2 sphinx-autobuild>=2021.3.14 Sphinx>=4.0.1 +sphinxcontrib-apidoc>=0.3.0 diff --git a/noxfile.py b/noxfile.py index d2789664b..e1b0e021f 100644 --- a/noxfile.py +++ b/noxfile.py @@ -2,6 +2,9 @@ Nox is Tox tool replacement. """ +import shutil +from pathlib import Path + import nox nox.options.keywords = "not docs" @@ -50,7 +53,7 @@ def documentation_tests(session): @nox.session(python="3.10") def docs(session, batch_run: bool = False): """Build the documentation or serve documentation interactively.""" - session.run("rm", "-rf", "docs/_build", external=True) + shutil.rmtree(Path("docs").joinpath("_build"), ignore_errors=True) session.install("-r", "docs/requirements.txt") session.install(".") session.cd("docs") From 567ab632839576885ec80c4e741bed2e742c60fa Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Tue, 7 Jun 2022 09:04:11 +0300 Subject: [PATCH 151/274] Whatch all codebase for changes in docs --- noxfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index e1b0e021f..ffe49f360 100644 --- a/noxfile.py +++ b/noxfile.py @@ -69,7 +69,7 @@ def docs(session, batch_run: bool = False): "--port", "9812", "--watch", - "../cookiecutter", + "../", ] ) From 8c9e1e532b1fd5bd551c7de0bf3eebbbd25e07e2 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Tue, 7 Jun 2022 09:11:41 +0300 Subject: [PATCH 152/274] Extend makefile with new clean options --- Makefile | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 8c993be1e..a39716fde 100644 --- a/Makefile +++ b/Makefile @@ -19,6 +19,28 @@ clean-tox: ## Remove tox testing artifacts @echo "+ $@" @rm -rf .tox/ +.PHONY: clean-nox +clean-nox: ## Remove nox testing artifacts + @echo "+ $@" + @rm -rf .nox/ + +.PHONY: clean-coverage +clean-coverage: ## Remove coverage reports + @echo "+ $@" + @rm -rf htmlcov/ + @rm -rf .coverage + @rm -rf .coverage.xml + +.PHONY: clean-pytest +clean-pytest: ## Remove pytest cache + @echo "+ $@" + @rm -rf .pytest_cache/ + +.PHONY: clean-docs-build +clean-docs-build: ## Remove local docs + @echo "+ $@" + @rm -rf docs/_build + .PHONY: clean-build clean-build: ## Remove build artifacts @echo "+ $@" @@ -33,8 +55,8 @@ clean-pyc: ## Remove Python file artifacts @find . -type f -name '*.py[co]' -exec rm -f {} + @find . -name '*~' -exec rm -f {} + -.PHONY: clean -clean: clean-tox clean-build clean-pyc ## Remove all file artifacts +.PHONY: clean ## Remove all file artifacts +clean: clean-tox clean-build clean-pyc clean-nox clean-coverage clean-pytest clean-docs-build .PHONY: lint lint: ## Check code style From 60c36ffd7bb106d8a25e75c1afd9106082fb2dac Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Tue, 7 Jun 2022 09:14:47 +0300 Subject: [PATCH 153/274] Replace lint in makefile --- Makefile | 2 +- noxfile.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index a39716fde..a2701eac6 100644 --- a/Makefile +++ b/Makefile @@ -61,7 +61,7 @@ clean: clean-tox clean-build clean-pyc clean-nox clean-coverage clean-pytest cle .PHONY: lint lint: ## Check code style @echo "+ $@" - @tox -e lint + @nox -s lint .PHONY: test test: ## Run tests quickly with the default Python diff --git a/noxfile.py b/noxfile.py index ffe49f360..aa5de2e2b 100644 --- a/noxfile.py +++ b/noxfile.py @@ -20,7 +20,7 @@ def base_install(session): @nox.session(python="3.10") def lint(session): """Run linting check locally.""" - session = base_install(session) + session.install("pre-commit") session.run("pre-commit", "run", "-a") From 5f71bc6235038607befe1bdd6b714a1e63def6a9 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Tue, 7 Jun 2022 09:15:33 +0300 Subject: [PATCH 154/274] Fix typo in filename --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index a2701eac6..6a287a97c 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ clean-coverage: ## Remove coverage reports @echo "+ $@" @rm -rf htmlcov/ @rm -rf .coverage - @rm -rf .coverage.xml + @rm -rf coverage.xml .PHONY: clean-pytest clean-pytest: ## Remove pytest cache From b64ea6386a30d5e8571dc13f7417cead008797e8 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Tue, 7 Jun 2022 09:30:46 +0300 Subject: [PATCH 155/274] Replace tox make steps with nox --- Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 6a287a97c..0392aeb33 100644 --- a/Makefile +++ b/Makefile @@ -66,17 +66,17 @@ lint: ## Check code style .PHONY: test test: ## Run tests quickly with the default Python @echo "+ $@" - @tox -e py + @nox -p 3.10 .PHONY: test-all test-all: ## Run tests on every Python version with tox @echo "+ $@" - @tox + @nox .PHONY: coverage coverage: ## Check code coverage quickly with the default Python @echo "+ $@" - @tox -e cov-report + @nox -s tests -p 3.10 @$(BROWSER) htmlcov/index.html .PHONY: docs From f073dd5a6b122fc757d475ae529c15f9e294aff7 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Tue, 7 Jun 2022 10:05:49 +0300 Subject: [PATCH 156/274] Update documentation workflow to use nox --- .github/workflows/docs.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index d3744b683..4a0299117 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -8,19 +8,19 @@ jobs: docs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: submodules: 'recursive' - - uses: actions/setup-python@v1 + - uses: actions/setup-python@v3 with: - python-version: "3.7" + python-version: "3.10" - name: Install dependencies run: | python -m pip install --upgrade pip - pip install tox virtualenv + pip install nox virtualenv - name: Build docs - run: tox -e docs - - uses: actions/upload-artifact@v1 + run: nox -s docs + - uses: actions/upload-artifact@v3 with: name: DocumentationHTML path: docs/_build/html/ From 292ccdaf72f1d688eac012df0d1db82b7446aaad Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Tue, 7 Jun 2022 10:12:06 +0300 Subject: [PATCH 157/274] Update github actions major versions --- .github/workflows/main.yml | 14 +++++++------- .github/workflows/pip-publish.yml | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8fb12c2b4..61d05c901 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,10 +14,10 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v1 + - uses: actions/checkout@v3 + - uses: actions/setup-python@v3 with: - python-version: "3.9" + python-version: "3.10" - name: Install dependencies run: | python -m pip install --upgrade pip @@ -100,9 +100,9 @@ jobs: tox_env: "py310" steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v3 with: python-version: ${{ matrix.python }} - name: Install dependencies @@ -112,14 +112,14 @@ jobs: - name: Test build run: "tox -e ${{ matrix.tox_env }}-cov-report" - name: Send coverage report to codeclimate - uses: paambaati/codeclimate-action@v2.6.0 + uses: paambaati/codeclimate-action@v3 with: coverageCommand: echo "Ignore rerun" coverageLocations: ${{github.workspace}}/coverage.xml:coverage.py env: CC_TEST_REPORTER_ID: ${{secrets.CC_TEST_REPORTER_ID}} - name: Send coverage report to codecov - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v3 with: env_vars: OS=${{ matrix.os }},PYTHON=${{ matrix.python }} file: ./coverage.xml diff --git a/.github/workflows/pip-publish.yml b/.github/workflows/pip-publish.yml index c5e01d9e1..1c6ad40bf 100644 --- a/.github/workflows/pip-publish.yml +++ b/.github/workflows/pip-publish.yml @@ -14,10 +14,10 @@ jobs: steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v1 + uses: actions/setup-python@v3 with: python-version: 3.9 From 15be977e322b2622c2cd28a8e2c2083b36f0f930 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Tue, 7 Jun 2022 10:26:46 +0300 Subject: [PATCH 158/274] Github Actions: Tox with Nox replace + Safety check for all python versions --- .github/workflows/main.yml | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 61d05c901..50cbe9375 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -21,11 +21,9 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install tox virtualenv + pip install nox virtualenv - name: Lint - run: "tox -e lint" - - name: Safety - run: "tox -e safety" + run: "nox -s lint" build: runs-on: ${{ matrix.os }} strategy: @@ -51,53 +49,41 @@ jobs: - name: "ubuntu-py37" python: "3.7" os: ubuntu-latest - tox_env: "py37" - name: "ubuntu-py38" python: "3.8" os: ubuntu-latest - tox_env: "py38" - name: "ubuntu-py39" python: "3.9" os: ubuntu-latest - tox_env: "py39" - name: "ubuntu-py310" python: "3.10" os: ubuntu-latest - tox_env: "py310" - name: "macos-py37" python: "3.7" os: macos-latest - tox_env: "py37" - name: "macos-py38" python: "3.8" os: macos-latest - tox_env: "py38" - name: "macos-py39" python: "3.9" os: macos-latest - tox_env: "py39" - name: "macos-py310" python: "3.10" os: macos-latest - tox_env: "py310" - name: "windows-py37" python: "3.7" os: windows-latest - tox_env: "py37" - name: "windows-py38" python: "3.8" os: windows-latest - tox_env: "py38" - name: "windows-py39" python: "3.9" os: windows-latest - tox_env: "py39" - name: "windows-py310" python: "3.10" os: windows-latest - tox_env: "py310" steps: - uses: actions/checkout@v3 @@ -108,9 +94,11 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install tox virtualenv - - name: Test build - run: "tox -e ${{ matrix.tox_env }}-cov-report" + pip install nox virtualenv + - name: Project internals test build + run: "nox -p ${{ matrix.python }} -s tests" + - name: Project security test + run: "nox -p ${{ matrix.python }} -s safety_tests" - name: Send coverage report to codeclimate uses: paambaati/codeclimate-action@v3 with: From 1179fe020b79dbe958d0d0522e3e6af8164ded2f Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Tue, 7 Jun 2022 10:29:10 +0300 Subject: [PATCH 159/274] Use python 3.10 for deploy to pip --- .github/workflows/pip-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pip-publish.yml b/.github/workflows/pip-publish.yml index 1c6ad40bf..ae4cb3c7a 100644 --- a/.github/workflows/pip-publish.yml +++ b/.github/workflows/pip-publish.yml @@ -19,7 +19,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v3 with: - python-version: 3.9 + python-version: 3.10 - name: Install pypa/build run: >- From d8625b1b98ffa48522cf86a3d3f97d8ff2c6e6d2 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Tue, 7 Jun 2022 10:53:53 +0300 Subject: [PATCH 160/274] Fix codeclimate version --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 50cbe9375..5706edb44 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -100,7 +100,7 @@ jobs: - name: Project security test run: "nox -p ${{ matrix.python }} -s safety_tests" - name: Send coverage report to codeclimate - uses: paambaati/codeclimate-action@v3 + uses: paambaati/codeclimate-action@v3.0.0 with: coverageCommand: echo "Ignore rerun" coverageLocations: ${{github.workspace}}/coverage.xml:coverage.py From 86b47cd3d2bdf06d7bcef26cbdabe63580c34e58 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Tue, 7 Jun 2022 11:06:42 +0300 Subject: [PATCH 161/274] Simplify github actions matrix definition --- .github/workflows/main.yml | 69 ++++++-------------------------------- 1 file changed, 11 insertions(+), 58 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5706edb44..822e11e35 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -11,7 +11,7 @@ on: - "*" jobs: - lint: + linting: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 @@ -24,67 +24,20 @@ jobs: pip install nox virtualenv - name: Lint run: "nox -s lint" - build: + tests_run: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: - name: - - "ubuntu-py37" - - "ubuntu-py38" - - "ubuntu-py39" - - "ubuntu-py310" - - - "macos-py37" - - "macos-py38" - - "macos-py39" - - "macos-py310" - - - "windows-py37" - - "windows-py38" - - "windows-py39" - - "windows-py310" - - include: - - name: "ubuntu-py37" - python: "3.7" - os: ubuntu-latest - - name: "ubuntu-py38" - python: "3.8" - os: ubuntu-latest - - name: "ubuntu-py39" - python: "3.9" - os: ubuntu-latest - - name: "ubuntu-py310" - python: "3.10" - os: ubuntu-latest - - - name: "macos-py37" - python: "3.7" - os: macos-latest - - name: "macos-py38" - python: "3.8" - os: macos-latest - - name: "macos-py39" - python: "3.9" - os: macos-latest - - name: "macos-py310" - python: "3.10" - os: macos-latest - - - name: "windows-py37" - python: "3.7" - os: windows-latest - - name: "windows-py38" - python: "3.8" - os: windows-latest - - name: "windows-py39" - python: "3.9" - os: windows-latest - - name: "windows-py310" - python: "3.10" - os: windows-latest - + os: + - ubuntu-latest + - macos-latest + - windows-latest + python: + - "3.7" + - "3.8" + - "3.9" + - "3.10" steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python }} From a94e2ae10831b851f766c7ebe35ce3eba9f9ccfe Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Mon, 6 Jun 2022 23:21:37 +0300 Subject: [PATCH 162/274] Rename release-drafter yml to exclude IDE known formats warning --- .github/workflows/{release-drafter.yml => drafter.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{release-drafter.yml => drafter.yml} (100%) diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/drafter.yml similarity index 100% rename from .github/workflows/release-drafter.yml rename to .github/workflows/drafter.yml From 09c19a39ad050a7e1973c4c5da55e60cc3a08b13 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Tue, 7 Jun 2022 11:16:37 +0300 Subject: [PATCH 163/274] Rename main workflow to tests workflow --- .github/workflows/{main.yml => tests.yml} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename .github/workflows/{main.yml => tests.yml} (98%) diff --git a/.github/workflows/main.yml b/.github/workflows/tests.yml similarity index 98% rename from .github/workflows/main.yml rename to .github/workflows/tests.yml index 822e11e35..bd686f993 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/tests.yml @@ -1,4 +1,4 @@ -name: CI Tests +name: CI/CD Tests on: push: From b1f8ca648e0516f9589b2e2d6404e57cdcb74f55 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Tue, 7 Jun 2022 11:18:29 +0300 Subject: [PATCH 164/274] Move documentation workflow to main tests workflow to see all on one screen --- .github/workflows/docs.yml | 26 -------------------------- .github/workflows/tests.yml | 19 +++++++++++++++++++ 2 files changed, 19 insertions(+), 26 deletions(-) delete mode 100644 .github/workflows/docs.yml diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml deleted file mode 100644 index 4a0299117..000000000 --- a/.github/workflows/docs.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: "Documentation build check" -on: - pull_request: - branches: - - "*" - -jobs: - docs: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - submodules: 'recursive' - - uses: actions/setup-python@v3 - with: - python-version: "3.10" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install nox virtualenv - - name: Build docs - run: nox -s docs - - uses: actions/upload-artifact@v3 - with: - name: DocumentationHTML - path: docs/_build/html/ diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bd686f993..f4963bda3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,6 +11,25 @@ on: - "*" jobs: + documentation_build_test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: 'recursive' + - uses: actions/setup-python@v3 + with: + python-version: "3.10" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install nox virtualenv + - name: Build docs + run: nox -s docs + - uses: actions/upload-artifact@v3 + with: + name: DocumentationHTML + path: docs/_build/html/ linting: runs-on: ubuntu-latest steps: From 8fd1194e3d8a7541035e8e4771354c958faab2c7 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Tue, 7 Jun 2022 11:30:20 +0300 Subject: [PATCH 165/274] Attempt to fix design problems on read the docs --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 89204a752..cd980601c 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -2,5 +2,5 @@ sphinx-rtd-theme>=1.0.0 sphinx-click>=4.1.0 myst-parser>=0.17.2 sphinx-autobuild>=2021.3.14 -Sphinx>=4.0.1 +Sphinx>=4.5.0 sphinxcontrib-apidoc>=0.3.0 From a263ad0e01a4eccf9addf3eb40c1a52e24101250 Mon Sep 17 00:00:00 2001 From: Italo Maia Date: Mon, 6 Jun 2022 23:17:35 +0200 Subject: [PATCH 166/274] Update boolean_variables.rst Adding explanation on what is considered valid input for boolean configuration. --- docs/advanced/boolean_variables.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/advanced/boolean_variables.rst b/docs/advanced/boolean_variables.rst index 58c5e48a7..964049152 100644 --- a/docs/advanced/boolean_variables.rst +++ b/docs/advanced/boolean_variables.rst @@ -20,7 +20,10 @@ you'd get the following user input when running Cookiecutter:: run_as_docker [True]: -Depending on the user's input, a different condition will be selected. +The following values are considered valid input: + + true, 1, yes, y **or** + false, 0, no, n The above ``run_as_docker`` boolean variable creates ``cookiecutter.run_as_docker``, which can be used like this:: From c6dff1b9bd0f4b29eeb4cd65bd559a3202d622fd Mon Sep 17 00:00:00 2001 From: Italo Maia Date: Tue, 7 Jun 2022 00:15:20 +0200 Subject: [PATCH 167/274] Update boolean_variables.rst Removing trailing whitespace --- docs/advanced/boolean_variables.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/advanced/boolean_variables.rst b/docs/advanced/boolean_variables.rst index 964049152..85d4bc461 100644 --- a/docs/advanced/boolean_variables.rst +++ b/docs/advanced/boolean_variables.rst @@ -23,7 +23,7 @@ you'd get the following user input when running Cookiecutter:: The following values are considered valid input: true, 1, yes, y **or** - false, 0, no, n + false, 0, no, n The above ``run_as_docker`` boolean variable creates ``cookiecutter.run_as_docker``, which can be used like this:: From 1ad2375fc4582572f25c5e668fb541d72e33bab6 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Tue, 7 Jun 2022 13:47:04 +0300 Subject: [PATCH 168/274] Fix CI/CD for first time contributors --- .github/workflows/tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f4963bda3..a84a59992 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -72,6 +72,7 @@ jobs: - name: Project security test run: "nox -p ${{ matrix.python }} -s safety_tests" - name: Send coverage report to codeclimate + continue-on-error: true uses: paambaati/codeclimate-action@v3.0.0 with: coverageCommand: echo "Ignore rerun" From 6008c7123b0a7dbaf8495b4ceaae840bb478d6fc Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Tue, 7 Jun 2022 15:10:27 +0300 Subject: [PATCH 169/274] Update documentation intersphinx_mapping to external sources --- docs/conf.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index d608b0308..332153029 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -353,7 +353,11 @@ # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'https://docs.python.org/3': None} +intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), + "requests": ("https://requests.readthedocs.io/en/latest/", None), + "click": ("https://click.palletsprojects.com/en/latest", None), +} myst_enable_extensions = [ "tasklist", "strikethrough", From 4d40ed5ea3e0ff79264861d1ffbedbc5c01e3de5 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Tue, 7 Jun 2022 15:11:08 +0300 Subject: [PATCH 170/274] Update list of directories for looking for files changes in interactive documentation build --- noxfile.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/noxfile.py b/noxfile.py index aa5de2e2b..25189967e 100644 --- a/noxfile.py +++ b/noxfile.py @@ -55,7 +55,7 @@ def docs(session, batch_run: bool = False): """Build the documentation or serve documentation interactively.""" shutil.rmtree(Path("docs").joinpath("_build"), ignore_errors=True) session.install("-r", "docs/requirements.txt") - session.install(".") + session.install("-e", ".") session.cd("docs") sphinx_args = ["-b", "html", "-W", ".", "_build/html"] @@ -69,7 +69,13 @@ def docs(session, batch_run: bool = False): "--port", "9812", "--watch", - "../", + "../*.md", + "--watch", + "../*.rst", + "--watch", + "../*.py", + "--watch", + "../cookiecutter", ] ) From fbc68f7456a4e152dc8c6443c1d688fd0d56c955 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Tue, 7 Jun 2022 15:12:06 +0300 Subject: [PATCH 171/274] Extend read_user_yes_no docstrings --- cookiecutter/prompt.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/cookiecutter/prompt.py b/cookiecutter/prompt.py index 003ace67d..d27481f7b 100644 --- a/cookiecutter/prompt.py +++ b/cookiecutter/prompt.py @@ -16,20 +16,23 @@ def read_user_variable(var_name, default_value): :param str var_name: Variable of the context to query the user :param default_value: Value that will be returned if no input happens """ - # Please see https://click.palletsprojects.com/en/7.x/api/#click.prompt return click.prompt(var_name, default=default_value) def read_user_yes_no(question, default_value): """Prompt the user to reply with 'yes' or 'no' (or equivalent values). - Note: - Possible choices are 'true', '1', 'yes', 'y' or 'false', '0', 'no', 'n' + - These input values will be converted to ``True``: + "1", "true", "t", "yes", "y", "on" + - These input values will be converted to ``False``: + "0", "false", "f", "no", "n", "off" + + Actual parsing done by :func:`click.prompt`; Check this function codebase change in + case of unexpected behaviour. :param str question: Question to the user :param default_value: Value that will be returned if no input happens """ - # Please see https://click.palletsprojects.com/en/7.x/api/#click.prompt return click.prompt(question, default=default_value, type=click.BOOL) @@ -38,7 +41,6 @@ def read_repo_password(question): :param str question: Question to the user """ - # Please see https://click.palletsprojects.com/en/7.x/api/#click.prompt return click.prompt(question, hide_input=True) @@ -51,7 +53,6 @@ def read_user_choice(var_name, options): :param list options: Sequence of options that are available to select from :return: Exactly one item of ``options`` that has been chosen by the user """ - # Please see https://click.palletsprojects.com/en/7.x/api/#click.prompt if not isinstance(options, list): raise TypeError @@ -109,7 +110,6 @@ def read_user_dict(var_name, default_value): :param default_value: Value that will be returned if no input is provided :return: A Python dictionary to use in the context. """ - # Please see https://click.palletsprojects.com/en/7.x/api/#click.prompt if not isinstance(default_value, dict): raise TypeError From 621739292da406b549f44a09aca76b5b4c46671f Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Tue, 7 Jun 2022 16:14:53 +0300 Subject: [PATCH 172/274] Finalize Boolean Variables documentation update (rst) --- docs/advanced/boolean_variables.rst | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/docs/advanced/boolean_variables.rst b/docs/advanced/boolean_variables.rst index 85d4bc461..8d9cf6997 100644 --- a/docs/advanced/boolean_variables.rst +++ b/docs/advanced/boolean_variables.rst @@ -1,32 +1,35 @@ -.. _boolean-variables: +Boolean Variables +----------------- -Boolean Variables (2.2+) ------------------------- +.. versionadded:: 2.2.0 Boolean variables are used for answering True/False questions. Basic Usage ~~~~~~~~~~~ -Boolean variables are regular key / value pairs, but with the value being True/False. +Boolean variables are regular key / value pairs, but with the value being +``True``/``False``. -For example, if you provide the following boolean variable in your ``cookiecutter.json``:: +For example, if you provide the following boolean variable in your +``cookiecutter.json``:: { "run_as_docker": true } -you'd get the following user input when running Cookiecutter:: +you will get the following user input when running Cookiecutter:: run_as_docker [True]: -The following values are considered valid input: +User input will be parsed by :func:`~cookiecutter.prompt.read_user_yes_no`. The +following values are considered as valid user input: - true, 1, yes, y **or** - false, 0, no, n + - ``True`` values: "1", "true", "t", "yes", "y", "on" + - ``False`` values: "0", "false", "f", "no", "n", "off" -The above ``run_as_docker`` boolean variable creates ``cookiecutter.run_as_docker``, which -can be used like this:: +The above ``run_as_docker`` boolean variable creates ``cookiecutter.run_as_docker``, +which can be used like this:: {%- if cookiecutter.run_as_docker -%} # In case of True add your content here @@ -36,7 +39,8 @@ can be used like this:: {% endif %} -Cookiecutter is using `Jinja2's if conditional expression `_ to determine the correct ``run_as_docker``. +Cookiecutter is using `Jinja2's if conditional expression `_ to determine the correct ``run_as_docker``. Input Validation ~~~~~~~~~~~~~~~~ From 252478aa03330aa7408f85b6053ab7572ca364a9 Mon Sep 17 00:00:00 2001 From: Johann Christensen Date: Tue, 7 Jun 2022 13:42:32 +0200 Subject: [PATCH 173/274] Fix Python code block Due to the `::` after the `Or use the Python API`, the following code block was rendered incorrectly. --- docs/advanced/replay.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/advanced/replay.rst b/docs/advanced/replay.rst index 426f0274e..79672e284 100644 --- a/docs/advanced/replay.rst +++ b/docs/advanced/replay.rst @@ -37,7 +37,7 @@ Pass the according option on the CLI: cookiecutter --replay gh:hackebrot/cookiedozer -Or use the Python API:: +Or use the Python API: .. code-block:: python From 17c07bf9fb7369c4030a42feeba70c72ce38a9bb Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Wed, 8 Jun 2022 13:36:49 +0300 Subject: [PATCH 174/274] Replace strings methods with f-string (tests) --- tests/test_custom_extensions_in_hooks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_custom_extensions_in_hooks.py b/tests/test_custom_extensions_in_hooks.py index cd5f8aa3a..4300bbf4d 100644 --- a/tests/test_custom_extensions_in_hooks.py +++ b/tests/test_custom_extensions_in_hooks.py @@ -18,7 +18,7 @@ ) def template(request): """Fixture. Allows to split pre and post hooks test directories.""" - return 'tests/test-extensions/' + request.param + return f"tests/test-extensions/{request.param}" @pytest.fixture(autouse=True) From 6d92aae82537b85c04eac22eb3fc601fcd258f65 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Wed, 8 Jun 2022 13:37:47 +0300 Subject: [PATCH 175/274] Simplify 'if' block in utils tests --- tests/test_utils.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 988fc3887..537f1017f 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -175,10 +175,7 @@ def test_prompt_should_ask_and_keep_repo_on_reuse(mocker, tmp_path): cloned template repo, it should not be deleted.""" def answer(question, default): - if 'okay to delete' in question: - return False - else: - return True + return 'okay to delete' not in question mock_read_user = mocker.patch( 'cookiecutter.utils.read_user_yes_no', side_effect=answer, autospec=True From efd5dce699dd5b48ba681d8724c67fe9ef3c2301 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Wed, 8 Jun 2022 13:38:23 +0300 Subject: [PATCH 176/274] Remove meaningless test statement (test_jinja2_uuid_extension) --- tests/test_default_extensions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_default_extensions.py b/tests/test_default_extensions.py index 540138d31..85924a68e 100644 --- a/tests/test_default_extensions.py +++ b/tests/test_default_extensions.py @@ -61,4 +61,3 @@ def test_jinja2_uuid_extension(tmp_path): changelog_lines = f.readlines() uuid.UUID(changelog_lines[0], version=4) - assert True From 1d1869636f97654800581e07e0b28dbf1e4c3ec2 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Wed, 8 Jun 2022 14:45:48 +0300 Subject: [PATCH 177/274] Tests: Use pathlib for files read/write (test_cli.py) --- tests/test_cli.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 1bc2fdd55..45a4a0019 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -3,7 +3,7 @@ import json import os import re - +from pathlib import Path import pytest from click.testing import CliRunner @@ -72,7 +72,7 @@ def test_cli(cli_runner): result = cli_runner('tests/fake-repo-pre/', '--no-input') assert result.exit_code == 0 assert os.path.isdir('fake-project') - with open(os.path.join('fake-project', 'README.rst')) as f: + with Path("fake-project", "README.rst").open() as f: assert 'Project name: **Fake Project**' in f.read() @@ -82,7 +82,7 @@ def test_cli_verbose(cli_runner): result = cli_runner('tests/fake-repo-pre/', '--no-input', '-v') assert result.exit_code == 0 assert os.path.isdir('fake-project') - with open(os.path.join('fake-project', 'README.rst')) as f: + with Path("fake-project", "README.rst").open() as f: assert 'Project name: **Fake Project**' in f.read() @@ -435,7 +435,7 @@ def test_local_extension(tmpdir, cli_runner): template_path, ) assert result.exit_code == 0 - with open(os.path.join(output_dir, 'Foobar', 'HISTORY.rst')) as f: + with Path(output_dir, 'Foobar', 'HISTORY.rst').open() as f: data = f.read() assert 'FoobarFoobar' in data assert 'FOOBAR' in data @@ -462,7 +462,7 @@ def test_cli_extra_context(cli_runner): ) assert result.exit_code == 0 assert os.path.isdir('fake-project') - with open(os.path.join('fake-project', 'README.rst')) as f: + with Path('fake-project', 'README.rst').open() as f: assert 'Project name: **Awesomez**' in f.read() @@ -544,13 +544,14 @@ def test_debug_list_installed_templates(cli_runner, debug_file, user_config_path """Verify --list-installed command correct invocation.""" fake_template_dir = os.path.dirname(os.path.abspath('fake-project')) os.makedirs(os.path.dirname(user_config_path)) - with open(user_config_path, 'w') as config_file: + with Path(user_config_path).open('w') as config_file: # In YAML, double quotes mean to use escape sequences. # Single quotes mean we will have unescaped backslahes. # http://blogs.perl.org/users/tinita/2018/03/ # strings-in-yaml---to-quote-or-not-to-quote.html config_file.write("cookiecutters_dir: '%s'" % fake_template_dir) - open(os.path.join('fake-project', 'cookiecutter.json'), 'w').write('{}') + with Path("fake-project", "cookiecutter.json").open("w") as f: + f.write('{}') result = cli_runner( '--list-installed', @@ -568,7 +569,7 @@ def test_debug_list_installed_templates_failure( ): """Verify --list-installed command error on invocation.""" os.makedirs(os.path.dirname(user_config_path)) - with open(user_config_path, 'w') as config_file: + with Path(user_config_path).open('w') as config_file: config_file.write('cookiecutters_dir: "/notarealplace/"') result = cli_runner( @@ -590,7 +591,7 @@ def test_directory_repo(cli_runner): ) assert result.exit_code == 0 assert os.path.isdir("fake-project") - with open(os.path.join("fake-project", "README.rst")) as f: + with Path("fake-project", "README.rst").open() as f: assert "Project name: **Fake Project**" in f.read() From d233e33e288e3f573c0f1f1206137c1344c562ef Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Wed, 8 Jun 2022 14:46:03 +0300 Subject: [PATCH 178/274] Tests: Use pathlib for files read/write (test_hooks.py) --- tests/test_hooks.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/tests/test_hooks.py b/tests/test_hooks.py index d214e714b..ce4a2f426 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -1,9 +1,10 @@ """Tests for `cookiecutter.hooks` module.""" -import os import errno +import os import stat import sys import textwrap +from pathlib import Path import pytest @@ -18,10 +19,10 @@ def make_test_repo(name, multiple_hooks=False): os.mkdir(hook_dir) os.mkdir(template) - with open(os.path.join(template, 'README.rst'), 'w') as f: + with Path(template, 'README.rst').open('w') as f: f.write("foo\n===\n\nbar\n") - with open(os.path.join(hook_dir, 'pre_gen_project.py'), 'w') as f: + with Path(hook_dir, 'pre_gen_project.py').open('w') as f: f.write("#!/usr/bin/env python\n") f.write("# -*- coding: utf-8 -*-\n") f.write("from __future__ import print_function\n") @@ -32,7 +33,7 @@ def make_test_repo(name, multiple_hooks=False): if sys.platform.startswith('win'): post = 'post_gen_project.bat' - with open(os.path.join(hook_dir, post), 'w') as f: + with Path(hook_dir, post).open('w') as f: f.write("@echo off\n") f.write("\n") f.write("echo post generation hook\n") @@ -40,7 +41,7 @@ def make_test_repo(name, multiple_hooks=False): else: post = 'post_gen_project.sh' filename = os.path.join(hook_dir, post) - with open(filename, 'w') as f: + with Path(filename).open('w') as f: f.write("#!/bin/bash\n") f.write("\n") f.write("echo 'post generation hook';\n") @@ -52,7 +53,7 @@ def make_test_repo(name, multiple_hooks=False): if multiple_hooks: if sys.platform.startswith('win'): pre = 'pre_gen_project.bat' - with open(os.path.join(hook_dir, pre), 'w') as f: + with Path(hook_dir, pre).open('w') as f: f.write("@echo off\n") f.write("\n") f.write("echo post generation hook\n") @@ -60,7 +61,7 @@ def make_test_repo(name, multiple_hooks=False): else: pre = 'pre_gen_project.sh' filename = os.path.join(hook_dir, pre) - with open(filename, 'w') as f: + with Path(filename).open('w') as f: f.write("#!/bin/bash\n") f.write("\n") f.write("echo 'post generation hook';\n") @@ -182,13 +183,13 @@ def test_run_script_with_context(self): if sys.platform.startswith('win'): post = 'post_gen_project.bat' - with open(os.path.join(self.hooks_path, post), 'w') as f: + with Path(self.hooks_path, post).open('w') as f: f.write("@echo off\n") f.write("\n") f.write("echo post generation hook\n") f.write("echo. >{{cookiecutter.file}}\n") else: - with open(hook_path, 'w') as fh: + with Path(hook_path).open('w') as fh: fh.write("#!/bin/bash\n") fh.write("\n") fh.write("echo 'post generation hook';\n") @@ -221,7 +222,7 @@ def test_run_failing_hook(self): hook_path = os.path.join(self.hooks_path, 'pre_gen_project.py') tests_dir = os.path.join(self.repo_path, 'input{{hooks}}') - with open(hook_path, 'w') as f: + with Path(hook_path).open('w') as f: f.write("#!/usr/bin/env python\n") f.write("import sys; sys.exit(1)\n") From 6840956054bedf9dad5f219ea4c69717d811ee3c Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Wed, 8 Jun 2022 14:46:19 +0300 Subject: [PATCH 179/274] Tests: Use pathlib for files read/write (test_utils.py) --- tests/test_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 988fc3887..d144d7eea 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -18,7 +18,7 @@ def test_force_delete(mocker, tmp_path): """Verify `utils.force_delete` makes files writable.""" ro_file = Path(tmp_path, 'bar') - with open(ro_file, "w") as f: + with Path(ro_file).open("w") as f: f.write("Test data") make_readonly(ro_file) @@ -33,7 +33,7 @@ def test_force_delete(mocker, tmp_path): def test_rmtree(tmp_path): """Verify `utils.rmtree` remove files marked as read-only.""" - with open(Path(tmp_path, 'bar'), "w") as f: + with Path(tmp_path, "bar").open("w") as f: f.write("Test data") make_readonly(Path(tmp_path, 'bar')) From 71c435fab8ef661811fac2f6fbca0589c357ee0e Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Wed, 8 Jun 2022 14:46:34 +0300 Subject: [PATCH 180/274] Tests: Use pathlib for files read/write (test_unzip.py) --- tests/zipfile/test_unzip.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/zipfile/test_unzip.py b/tests/zipfile/test_unzip.py index 2bf58b00f..9d4448e59 100644 --- a/tests/zipfile/test_unzip.py +++ b/tests/zipfile/test_unzip.py @@ -1,8 +1,9 @@ """Tests for function unzip() from zipfile module.""" +import shutil import tempfile +from pathlib import Path import pytest -import shutil from cookiecutter import zipfile from cookiecutter.exceptions import InvalidZipRepository @@ -10,7 +11,7 @@ def mock_download(): """Fake download function.""" - with open('tests/files/fake-repo-tmpl.zip', 'rb') as zf: + with Path('tests/files/fake-repo-tmpl.zip').open('rb') as zf: chunk = zf.read(1024) while chunk: yield chunk @@ -20,7 +21,7 @@ def mock_download(): def mock_download_with_empty_chunks(): """Fake download function.""" yield - with open('tests/files/fake-repo-tmpl.zip', 'rb') as zf: + with Path('tests/files/fake-repo-tmpl.zip').open('rb') as zf: chunk = zf.read(1024) while chunk: yield chunk From a06d852fed6bac454231c65fbbf79811aca01a06 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Wed, 8 Jun 2022 14:46:50 +0300 Subject: [PATCH 181/274] Tests: Use pathlib for files read/write (test_templates.py) --- tests/test_templates.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/test_templates.py b/tests/test_templates.py index 27f40428b..ac0e0e85d 100644 --- a/tests/test_templates.py +++ b/tests/test_templates.py @@ -4,8 +4,7 @@ Tests to ensure custom cookiecutter extensions are properly made available to pre- and post-gen hooks. """ -import codecs -import os +from pathlib import Path import pytest @@ -31,9 +30,7 @@ def test_build_templates(template, output_dir): output_dir=output_dir, ) - readme_file = os.path.join(project_dir, 'requirements.txt') - - with codecs.open(readme_file, encoding='utf8') as f: + with Path(project_dir, 'requirements.txt').open() as f: readme = f.read().splitlines() assert readme == [ From f98f0676f1c1ace76f185c8f7890b4c30a0e6cdb Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Wed, 8 Jun 2022 14:47:14 +0300 Subject: [PATCH 182/274] Tests: Use pathlib for files read/write (test_generate_file.py) --- tests/test_generate_file.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/test_generate_file.py b/tests/test_generate_file.py index 18c811eea..6e6604bea 100644 --- a/tests/test_generate_file.py +++ b/tests/test_generate_file.py @@ -2,6 +2,7 @@ import json import os import re +from pathlib import Path import pytest from jinja2 import FileSystemLoader @@ -45,7 +46,7 @@ def test_generate_file(env): env=env, ) assert os.path.isfile('tests/files/cheese.txt') - with open('tests/files/cheese.txt') as f: + with Path('tests/files/cheese.txt').open() as f: generated_text = f.read() assert generated_text == 'Testing cheese' @@ -58,7 +59,7 @@ def test_generate_file_jsonify_filter(env): project_dir=".", infile=infile, context={'cookiecutter': data}, env=env ) assert os.path.isfile('tests/files/cheese.txt') - with open('tests/files/cheese.txt') as f: + with Path('tests/files/cheese.txt').open() as f: generated_text = f.read() assert json.loads(generated_text) == data @@ -72,7 +73,7 @@ def test_generate_file_random_ascii_string(env, length, punctuation): context = {"cookiecutter": data, "length": length, "punctuation": punctuation} generate.generate_file(project_dir=".", infile=infile, context=context, env=env) assert os.path.isfile('tests/files/cheese.txt') - with open('tests/files/cheese.txt') as f: + with Path('tests/files/cheese.txt').open() as f: generated_text = f.read() assert len(generated_text) == length @@ -92,7 +93,7 @@ def test_generate_file_with_true_condition(env): env=env, ) assert os.path.isfile('tests/files/cheese.txt') - with open('tests/files/cheese.txt') as f: + with Path('tests/files/cheese.txt').open() as f: generated_text = f.read() assert generated_text == 'Testing that generate_file was y' @@ -148,7 +149,7 @@ def test_generate_file_does_not_translate_lf_newlines_to_crlf(env, tmp_path): # this generated file should have a LF line ending gf = 'tests/files/cheese_lf_newlines.txt' - with open(gf, encoding='utf-8', newline='') as f: + with Path(gf).open(newline='') as f: simple_text = f.readline() assert simple_text == 'newline is LF\n' assert f.newlines == '\n' @@ -166,7 +167,7 @@ def test_generate_file_does_not_translate_crlf_newlines_to_lf(env): # this generated file should have a CRLF line ending gf = 'tests/files/cheese_crlf_newlines.txt' - with open(gf, encoding='utf-8', newline='') as f: + with Path(gf).open(newline='') as f: simple_text = f.readline() assert simple_text == 'newline is CRLF\r\n' assert f.newlines == '\r\n' From 8876b10b2673a2ed61b2941066d012e624d85286 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Wed, 8 Jun 2022 14:47:25 +0300 Subject: [PATCH 183/274] Tests: Use pathlib for files read/write (test_generate_files.py) --- tests/test_generate_files.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/test_generate_files.py b/tests/test_generate_files.py index 9cf4929aa..436d83ac6 100644 --- a/tests/test_generate_files.py +++ b/tests/test_generate_files.py @@ -44,7 +44,7 @@ def test_generate_files(tmp_path): assert simple_file.exists() assert simple_file.is_file() - simple_text = open(simple_file, encoding='utf-8').read() + simple_text = Path(simple_file).open().read() assert simple_text == 'I eat pizzä' @@ -60,7 +60,7 @@ def test_generate_files_with_linux_newline(tmp_path): assert newline_file.is_file() assert newline_file.exists() - with open(newline_file, encoding='utf-8', newline='') as f: + with Path(newline_file).open(newline='') as f: simple_text = f.readline() assert simple_text == 'newline is LF\n' assert f.newlines == '\n' @@ -83,7 +83,7 @@ def test_generate_files_with_jinja2_environment(tmp_path): assert conditions_file.is_file() assert conditions_file.exists() - simple_text = conditions_file.open('rt', encoding='utf-8').read() + simple_text = conditions_file.open('rt').read() assert simple_text == 'I eat pizzä\n' @@ -100,7 +100,7 @@ def test_generate_files_with_trailing_newline_forced_to_linux_by_context(tmp_pat assert newline_file.is_file() assert newline_file.exists() - with open(newline_file, encoding='utf-8', newline='') as f: + with Path(newline_file).open(newline='') as f: simple_text = f.readline() assert simple_text == 'newline is LF\r\n' assert f.newlines == '\r\n' @@ -118,7 +118,7 @@ def test_generate_files_with_windows_newline(tmp_path): assert newline_file.is_file() assert newline_file.exists() - with open(newline_file, encoding='utf-8', newline='') as f: + with Path(newline_file).open(newline='') as f: simple_text = f.readline() assert simple_text == 'newline is CRLF\r\n' assert f.newlines == '\r\n' @@ -136,7 +136,7 @@ def test_generate_files_with_windows_newline_forced_to_linux_by_context(tmp_path assert newline_file.is_file() assert newline_file.exists() - with open(newline_file, encoding='utf-8', newline='') as f: + with Path(newline_file).open(newline='') as f: simple_text = f.readline() assert simple_text == 'newline is CRLF\n' @@ -241,7 +241,7 @@ def test_generate_files_with_overwrite_if_exists_with_skip_if_file_exists(tmp_pa simple_with_new_line_file = Path(tmp_path, 'inputpizzä/simple-with-newline.txt') Path(tmp_path, 'inputpizzä').mkdir(parents=True) - with open(simple_file, 'w') as f: + with Path(simple_file).open('w') as f: f.write('temp') generate.generate_files( @@ -257,7 +257,7 @@ def test_generate_files_with_overwrite_if_exists_with_skip_if_file_exists(tmp_pa assert Path(simple_with_new_line_file).is_file() assert Path(simple_with_new_line_file).exists() - simple_text = open(simple_file, encoding='utf-8').read() + simple_text = Path(simple_file).open().read() assert simple_text == 'temp' @@ -267,7 +267,7 @@ def test_generate_files_with_skip_if_file_exists(tmp_path): simple_with_new_line_file = Path(tmp_path, 'inputpizzä/simple-with-newline.txt') Path(tmp_path, 'inputpizzä').mkdir(parents=True) - with open(simple_file, 'w') as f: + with Path(simple_file).open('w') as f: f.write('temp') with pytest.raises(exceptions.OutputDirExistsException): @@ -283,7 +283,7 @@ def test_generate_files_with_skip_if_file_exists(tmp_path): assert not Path(simple_with_new_line_file).is_file() assert not Path(simple_with_new_line_file).exists() - simple_text = open(simple_file, encoding='utf-8').read() + simple_text = Path(simple_file).open().read() assert simple_text == 'temp' @@ -293,7 +293,7 @@ def test_generate_files_with_overwrite_if_exists(tmp_path): simple_with_new_line_file = Path(tmp_path, 'inputpizzä/simple-with-newline.txt') Path(tmp_path, 'inputpizzä').mkdir(parents=True) - with open(simple_file, 'w') as f: + with Path(simple_file).open('w') as f: f.write('temp') generate.generate_files( @@ -308,7 +308,7 @@ def test_generate_files_with_overwrite_if_exists(tmp_path): assert Path(simple_with_new_line_file).is_file() assert Path(simple_with_new_line_file).exists() - simple_text = open(simple_file, encoding='utf-8').read() + simple_text = Path(simple_file).open().read() assert simple_text == 'I eat pizzä' From eb7930ee59d4488d2b871039a6a20aae938e832c Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Wed, 8 Jun 2022 14:47:44 +0300 Subject: [PATCH 184/274] Tests: Use pathlib for files read/write (test_generate_hooks.py) --- tests/test_generate_hooks.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_generate_hooks.py b/tests/test_generate_hooks.py index 9624bb8ae..a57e0dbde 100644 --- a/tests/test_generate_hooks.py +++ b/tests/test_generate_hooks.py @@ -2,6 +2,7 @@ import errno import os import sys +from pathlib import Path import pytest @@ -123,7 +124,7 @@ def test_run_failing_hook_removes_output_directory(): hook_path = os.path.join(hooks_path, 'pre_gen_project.py') - with open(hook_path, 'w') as f: + with Path(hook_path).open('w') as f: f.write("#!/usr/bin/env python\n") f.write("import sys; sys.exit(1)\n") @@ -152,7 +153,7 @@ def test_run_failing_hook_preserves_existing_output_directory(): hook_path = os.path.join(hooks_path, 'pre_gen_project.py') - with open(hook_path, 'w') as f: + with Path(hook_path).open('w') as f: f.write("#!/usr/bin/env python\n") f.write("import sys; sys.exit(1)\n") From 8e6a475cd15554a4ee54125ef854f34dfc27cdd6 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Wed, 8 Jun 2022 14:48:18 +0300 Subject: [PATCH 185/274] Tests: Use pathlib for files read/write (test_output_folder.py) --- tests/test_output_folder.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_output_folder.py b/tests/test_output_folder.py index 166a45005..daea7360c 100644 --- a/tests/test_output_folder.py +++ b/tests/test_output_folder.py @@ -5,6 +5,7 @@ TestOutputFolder.test_output_folder """ import os +from pathlib import Path import pytest @@ -32,11 +33,11 @@ def test_output_folder(): something = """Hi! My name is Audrey Greenfeld. It is 2014.""" - something2 = open('output_folder/something.txt').read() + something2 = Path('output_folder/something.txt').open().read() assert something == something2 in_folder = "The color is green and the letter is D." - in_folder2 = open('output_folder/folder/in_folder.txt').read() + in_folder2 = Path('output_folder/folder/in_folder.txt').open().read() assert in_folder == in_folder2 assert os.path.isdir('output_folder/im_a.dir') From eef9a82c56d7248f74c0892912184a670cb3c839 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Wed, 8 Jun 2022 14:48:44 +0300 Subject: [PATCH 186/274] Tests: Use pathlib for files read/write (test_default_extensions.py) --- tests/test_default_extensions.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_default_extensions.py b/tests/test_default_extensions.py index 540138d31..13690097f 100644 --- a/tests/test_default_extensions.py +++ b/tests/test_default_extensions.py @@ -1,9 +1,10 @@ """Verify Jinja2 filters/extensions are available from pre-gen/post-gen hooks.""" import os +import uuid +from pathlib import Path import freezegun import pytest -import uuid from cookiecutter.main import cookiecutter @@ -25,7 +26,7 @@ def test_jinja2_time_extension(tmp_path): changelog_file = os.path.join(project_dir, 'HISTORY.rst') assert os.path.isfile(changelog_file) - with open(changelog_file, encoding='utf-8') as f: + with Path(changelog_file).open() as f: changelog_lines = f.readlines() expected_lines = [ @@ -57,7 +58,7 @@ def test_jinja2_uuid_extension(tmp_path): changelog_file = os.path.join(project_dir, 'id') assert os.path.isfile(changelog_file) - with open(changelog_file, encoding='utf-8') as f: + with Path(changelog_file).open() as f: changelog_lines = f.readlines() uuid.UUID(changelog_lines[0], version=4) From 15e3bd05c8d74adcef8db95ae58d80aaff8720cc Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Wed, 8 Jun 2022 14:51:12 +0300 Subject: [PATCH 187/274] Tests: Use pathlib for files read/write (test_cookiecutter_local_no_input.py) --- tests/test_cookiecutter_local_no_input.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_cookiecutter_local_no_input.py b/tests/test_cookiecutter_local_no_input.py index 0031cbcbf..eb1e52ed3 100644 --- a/tests/test_cookiecutter_local_no_input.py +++ b/tests/test_cookiecutter_local_no_input.py @@ -5,6 +5,7 @@ """ import os import textwrap +from pathlib import Path import pytest @@ -65,7 +66,7 @@ def test_cookiecutter_no_input_return_rendered_file(): """Verify Jinja2 templating correctly works in `cookiecutter.json` file.""" project_dir = main.cookiecutter('tests/fake-repo-pre', no_input=True) assert project_dir == os.path.abspath('fake-project') - with open(os.path.join(project_dir, 'README.rst')) as fh: + with Path(project_dir, 'README.rst').open() as fh: contents = fh.read() assert "Project name: **Fake Project**" in contents @@ -76,7 +77,7 @@ def test_cookiecutter_dict_values_in_context(): project_dir = main.cookiecutter('tests/fake-repo-dict', no_input=True) assert project_dir == os.path.abspath('fake-project-dict') - with open(os.path.join(project_dir, 'README.md')) as fh: + with Path(project_dir, 'README.md').open() as fh: contents = fh.read() assert ( From 2477c7956813be73c883e4bbd520c5d5323c7cf5 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Wed, 8 Jun 2022 14:52:22 +0300 Subject: [PATCH 188/274] Tests: Use pathlib for files read/write (test_custom_extensions_in_hooks.py) --- tests/test_custom_extensions_in_hooks.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/test_custom_extensions_in_hooks.py b/tests/test_custom_extensions_in_hooks.py index cd5f8aa3a..ef9103021 100644 --- a/tests/test_custom_extensions_in_hooks.py +++ b/tests/test_custom_extensions_in_hooks.py @@ -4,8 +4,7 @@ Tests to ensure custom cookiecutter extensions are properly made available to pre- and post-gen hooks. """ -import codecs -import os +from pathlib import Path import pytest @@ -40,9 +39,7 @@ def test_hook_with_extension(template, output_dir): extra_context={'project_slug': 'foobar', 'name': 'Cookiemonster'}, ) - readme_file = os.path.join(project_dir, 'README.rst') - - with codecs.open(readme_file, encoding='utf8') as f: + with Path(project_dir, 'README.rst').open() as f: readme = f.read().strip() assert readme == 'Hello Cookiemonster!' From b40fb40b578fdd5a25bd166f2eb60631de2cdf10 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Wed, 8 Jun 2022 14:53:20 +0300 Subject: [PATCH 189/274] Tests: Use pathlib for files read/write (test_generate_copy_without_render.py) --- tests/test_generate_copy_without_render.py | 23 +++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/tests/test_generate_copy_without_render.py b/tests/test_generate_copy_without_render.py index 7d614824c..5e4df7b3b 100644 --- a/tests/test_generate_copy_without_render.py +++ b/tests/test_generate_copy_without_render.py @@ -1,5 +1,6 @@ """Verify correct work of `_copy_without_render` context option.""" import os +from pathlib import Path import pytest @@ -43,33 +44,33 @@ def test_generate_copy_without_render_extensions(): assert 'test_copy_without_render-not-rendered' in dir_contents assert 'test_copy_without_render-rendered' in dir_contents - with open('test_copy_without_render/README.txt') as f: + with Path('test_copy_without_render/README.txt').open() as f: assert '{{cookiecutter.render_test}}' in f.read() - with open('test_copy_without_render/README.rst') as f: + with Path('test_copy_without_render/README.rst').open() as f: assert 'I have been rendered!' in f.read() - with open( + with Path( 'test_copy_without_render/test_copy_without_render-rendered/README.txt' - ) as f: + ).open() as f: assert '{{cookiecutter.render_test}}' in f.read() - with open( + with Path( 'test_copy_without_render/test_copy_without_render-rendered/README.rst' - ) as f: + ).open() as f: assert 'I have been rendered' in f.read() - with open( + with Path( 'test_copy_without_render/' 'test_copy_without_render-not-rendered/' 'README.rst' - ) as f: + ).open() as f: assert '{{cookiecutter.render_test}}' in f.read() - with open('test_copy_without_render/rendered/not_rendered.yml') as f: + with Path('test_copy_without_render/rendered/not_rendered.yml').open() as f: assert '{{cookiecutter.render_test}}' in f.read() - with open( + with Path( 'test_copy_without_render/' 'test_copy_without_render-rendered/' 'README.md' - ) as f: + ).open() as f: assert '{{cookiecutter.render_test}}' in f.read() From 14433e1ec2b08ddcd3bcd9044904e66551787808 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Wed, 8 Jun 2022 14:53:33 +0300 Subject: [PATCH 190/274] Tests: Use pathlib for files read/write (test_generate_copy_without_render_override.py) --- ...t_generate_copy_without_render_override.py | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/tests/test_generate_copy_without_render_override.py b/tests/test_generate_copy_without_render_override.py index af85c4b04..1bda073d9 100644 --- a/tests/test_generate_copy_without_render_override.py +++ b/tests/test_generate_copy_without_render_override.py @@ -1,5 +1,6 @@ """Verify correct work of `_copy_without_render` context option.""" import os +from pathlib import Path import pytest @@ -62,33 +63,33 @@ def test_generate_copy_without_render_extensions(): assert 'test_copy_without_render-not-rendered' in dir_contents assert 'test_copy_without_render-rendered' in dir_contents - with open('test_copy_without_render/README.txt') as f: + with Path('test_copy_without_render/README.txt').open() as f: assert '{{cookiecutter.render_test}}' in f.read() - with open('test_copy_without_render/README.rst') as f: + with Path('test_copy_without_render/README.rst').open() as f: assert 'I have been rendered!' in f.read() - with open( + with Path( 'test_copy_without_render/test_copy_without_render-rendered/README.txt' - ) as f: + ).open() as f: assert '{{cookiecutter.render_test}}' in f.read() - with open( + with Path( 'test_copy_without_render/test_copy_without_render-rendered/README.rst' - ) as f: + ).open() as f: assert 'I have been rendered' in f.read() - with open( + with Path( 'test_copy_without_render/' 'test_copy_without_render-not-rendered/' 'README.rst' - ) as f: + ).open() as f: assert '{{cookiecutter.render_test}}' in f.read() - with open('test_copy_without_render/rendered/not_rendered.yml') as f: + with Path('test_copy_without_render/rendered/not_rendered.yml').open() as f: assert '{{cookiecutter.render_test}}' in f.read() - with open( + with Path( 'test_copy_without_render/' 'test_copy_without_render-rendered/' 'README.md' - ) as f: + ).open() as f: assert '{{cookiecutter.render_test}}' in f.read() From 4aac87e1578bdb39875f38f0f2083077db40d331 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Wed, 8 Jun 2022 14:55:52 +0300 Subject: [PATCH 191/274] Tests: Use pathlib for files read/write (test_determine_repo_dir_finds_existing_cookiecutter.py) --- .../test_determine_repo_dir_finds_existing_cookiecutter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/repository/test_determine_repo_dir_finds_existing_cookiecutter.py b/tests/repository/test_determine_repo_dir_finds_existing_cookiecutter.py index 107e64aa3..ebd7df320 100644 --- a/tests/repository/test_determine_repo_dir_finds_existing_cookiecutter.py +++ b/tests/repository/test_determine_repo_dir_finds_existing_cookiecutter.py @@ -1,5 +1,6 @@ """Tests around detection whether cookiecutter templates are cached locally.""" import os +from pathlib import Path import pytest @@ -20,7 +21,7 @@ def cloned_cookiecutter_path(user_config_data, template): cloned_template_path = os.path.join(cookiecutters_dir, template) os.mkdir(cloned_template_path) - open(os.path.join(cloned_template_path, 'cookiecutter.json'), 'w') + Path(cloned_template_path, "cookiecutter.json").open('w') # creates file return cloned_template_path From b7309ac4b9d1b840195dad3e67627528dbc7a11f Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Wed, 8 Jun 2022 14:56:32 +0300 Subject: [PATCH 192/274] Tests: Use pathlib for files read/write (test_determine_repo_dir_finds_existing_cookiecutter.py) --- .../repository/test_determine_repo_dir_finds_subdirectories.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/repository/test_determine_repo_dir_finds_subdirectories.py b/tests/repository/test_determine_repo_dir_finds_subdirectories.py index f40e6063d..a1f7a034c 100644 --- a/tests/repository/test_determine_repo_dir_finds_subdirectories.py +++ b/tests/repository/test_determine_repo_dir_finds_subdirectories.py @@ -1,5 +1,6 @@ """Tests around locally cached cookiecutter template repositories.""" import os +from pathlib import Path import pytest @@ -24,7 +25,7 @@ def cloned_cookiecutter_path(user_config_data, template): subdir_template_path = os.path.join(cloned_template_path, 'my-dir') if not os.path.exists(subdir_template_path): os.mkdir(subdir_template_path) - open(os.path.join(subdir_template_path, 'cookiecutter.json'), 'w') + Path(subdir_template_path, 'cookiecutter.json').open('w') # creates file return subdir_template_path From fe0ce13c5512b8c5ab7f816d17e1666e1d5487f4 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Wed, 8 Jun 2022 16:23:31 +0300 Subject: [PATCH 193/274] Tests: Use pathlib read_text/write_text (test_cli.py) --- tests/test_cli.py | 40 ++++++++++++++++++---------------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 45a4a0019..39e924093 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -72,8 +72,8 @@ def test_cli(cli_runner): result = cli_runner('tests/fake-repo-pre/', '--no-input') assert result.exit_code == 0 assert os.path.isdir('fake-project') - with Path("fake-project", "README.rst").open() as f: - assert 'Project name: **Fake Project**' in f.read() + content = Path("fake-project", "README.rst").read_text() + assert 'Project name: **Fake Project**' in content @pytest.mark.usefixtures('remove_fake_project_dir') @@ -82,8 +82,8 @@ def test_cli_verbose(cli_runner): result = cli_runner('tests/fake-repo-pre/', '--no-input', '-v') assert result.exit_code == 0 assert os.path.isdir('fake-project') - with Path("fake-project", "README.rst").open() as f: - assert 'Project name: **Fake Project**' in f.read() + content = Path("fake-project", "README.rst").read_text() + assert 'Project name: **Fake Project**' in content @pytest.mark.usefixtures('remove_fake_project_dir') @@ -435,10 +435,9 @@ def test_local_extension(tmpdir, cli_runner): template_path, ) assert result.exit_code == 0 - with Path(output_dir, 'Foobar', 'HISTORY.rst').open() as f: - data = f.read() - assert 'FoobarFoobar' in data - assert 'FOOBAR' in data + content = Path(output_dir, 'Foobar', 'HISTORY.rst').read_text() + assert 'FoobarFoobar' in content + assert 'FOOBAR' in content def test_local_extension_not_available(tmpdir, cli_runner): @@ -462,8 +461,8 @@ def test_cli_extra_context(cli_runner): ) assert result.exit_code == 0 assert os.path.isdir('fake-project') - with Path('fake-project', 'README.rst').open() as f: - assert 'Project name: **Awesomez**' in f.read() + content = Path('fake-project', 'README.rst').read_text() + assert 'Project name: **Awesomez**' in content @pytest.mark.usefixtures('remove_fake_project_dir') @@ -544,14 +543,12 @@ def test_debug_list_installed_templates(cli_runner, debug_file, user_config_path """Verify --list-installed command correct invocation.""" fake_template_dir = os.path.dirname(os.path.abspath('fake-project')) os.makedirs(os.path.dirname(user_config_path)) - with Path(user_config_path).open('w') as config_file: - # In YAML, double quotes mean to use escape sequences. - # Single quotes mean we will have unescaped backslahes. - # http://blogs.perl.org/users/tinita/2018/03/ - # strings-in-yaml---to-quote-or-not-to-quote.html - config_file.write("cookiecutters_dir: '%s'" % fake_template_dir) - with Path("fake-project", "cookiecutter.json").open("w") as f: - f.write('{}') + # In YAML, double quotes mean to use escape sequences. + # Single quotes mean we will have unescaped backslahes. + # http://blogs.perl.org/users/tinita/2018/03/ + # strings-in-yaml---to-quote-or-not-to-quote.html + Path(user_config_path).write_text(f"cookiecutters_dir: '{fake_template_dir}'") + Path("fake-project", "cookiecutter.json").write_text('{}') result = cli_runner( '--list-installed', @@ -569,8 +566,7 @@ def test_debug_list_installed_templates_failure( ): """Verify --list-installed command error on invocation.""" os.makedirs(os.path.dirname(user_config_path)) - with Path(user_config_path).open('w') as config_file: - config_file.write('cookiecutters_dir: "/notarealplace/"') + Path(user_config_path).write_text('cookiecutters_dir: "/notarealplace/"') result = cli_runner( '--list-installed', '--config-file', user_config_path, str(debug_file) @@ -591,8 +587,8 @@ def test_directory_repo(cli_runner): ) assert result.exit_code == 0 assert os.path.isdir("fake-project") - with Path("fake-project", "README.rst").open() as f: - assert "Project name: **Fake Project**" in f.read() + content = Path("fake-project", "README.rst").read_text() + assert "Project name: **Fake Project**" in content cli_accept_hook_arg_testdata = [ From a3d60399cd4b63dece93af5605f8720c4d0f4692 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Wed, 8 Jun 2022 16:24:01 +0300 Subject: [PATCH 194/274] Tests: Use pathlib read_text/write_text (test_utils.py) --- tests/test_utils.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index d144d7eea..c9c37939d 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -17,9 +17,7 @@ def make_readonly(path): def test_force_delete(mocker, tmp_path): """Verify `utils.force_delete` makes files writable.""" ro_file = Path(tmp_path, 'bar') - - with Path(ro_file).open("w") as f: - f.write("Test data") + ro_file.write_text("Test data") make_readonly(ro_file) rmtree = mocker.Mock() @@ -33,9 +31,9 @@ def test_force_delete(mocker, tmp_path): def test_rmtree(tmp_path): """Verify `utils.rmtree` remove files marked as read-only.""" - with Path(tmp_path, "bar").open("w") as f: - f.write("Test data") - make_readonly(Path(tmp_path, 'bar')) + file_path = Path(tmp_path, "bar") + file_path.write_text("Test data") + make_readonly(file_path) utils.rmtree(tmp_path) From 9185db1aca3cb8cc0fe6c34ef6f6f633ffce9d23 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Wed, 8 Jun 2022 16:25:36 +0300 Subject: [PATCH 195/274] Tests: Use pathlib read_text/write_text with encoding (test_templates.py) --- tests/test_templates.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_templates.py b/tests/test_templates.py index ac0e0e85d..e3aed3598 100644 --- a/tests/test_templates.py +++ b/tests/test_templates.py @@ -30,10 +30,9 @@ def test_build_templates(template, output_dir): output_dir=output_dir, ) - with Path(project_dir, 'requirements.txt').open() as f: - readme = f.read().splitlines() + readme = Path(project_dir, 'requirements.txt').read_text(encoding='utf-8') - assert readme == [ + assert readme.splitlines() == [ "pip", "Click", "pytest", From 9b80ba6786869c4be985f569507170be2a277c16 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Wed, 8 Jun 2022 16:26:16 +0300 Subject: [PATCH 196/274] Tests: Use pathlib read_text/write_text (test_hooks.py) --- tests/test_hooks.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_hooks.py b/tests/test_hooks.py index ce4a2f426..9abd66af2 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -19,8 +19,7 @@ def make_test_repo(name, multiple_hooks=False): os.mkdir(hook_dir) os.mkdir(template) - with Path(template, 'README.rst').open('w') as f: - f.write("foo\n===\n\nbar\n") + Path(template, 'README.rst').write_text("foo\n===\n\nbar\n") with Path(hook_dir, 'pre_gen_project.py').open('w') as f: f.write("#!/usr/bin/env python\n") From f8ad02ae951009c0a167c42388d1f42709fe8cfd Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Wed, 8 Jun 2022 16:26:43 +0300 Subject: [PATCH 197/274] Tests: Use pathlib read_text/write_text (test_output_folder.py) --- tests/test_output_folder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_output_folder.py b/tests/test_output_folder.py index daea7360c..00c4f7846 100644 --- a/tests/test_output_folder.py +++ b/tests/test_output_folder.py @@ -33,11 +33,11 @@ def test_output_folder(): something = """Hi! My name is Audrey Greenfeld. It is 2014.""" - something2 = Path('output_folder/something.txt').open().read() + something2 = Path('output_folder/something.txt').read_text() assert something == something2 in_folder = "The color is green and the letter is D." - in_folder2 = Path('output_folder/folder/in_folder.txt').open().read() + in_folder2 = Path('output_folder/folder/in_folder.txt').read_text() assert in_folder == in_folder2 assert os.path.isdir('output_folder/im_a.dir') From 258de5a419fa9ff719e15d23213443ddc9bfd5f9 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Wed, 8 Jun 2022 16:27:22 +0300 Subject: [PATCH 198/274] Tests: Use pathlib read_text/write_text with encoding (test_default_extensions.py) --- tests/test_default_extensions.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/test_default_extensions.py b/tests/test_default_extensions.py index 13690097f..fc3bc08cb 100644 --- a/tests/test_default_extensions.py +++ b/tests/test_default_extensions.py @@ -26,8 +26,7 @@ def test_jinja2_time_extension(tmp_path): changelog_file = os.path.join(project_dir, 'HISTORY.rst') assert os.path.isfile(changelog_file) - with Path(changelog_file).open() as f: - changelog_lines = f.readlines() + changelog_lines = Path(changelog_file).open(encoding='utf-8').readlines() expected_lines = [ 'History\n', @@ -58,8 +57,7 @@ def test_jinja2_uuid_extension(tmp_path): changelog_file = os.path.join(project_dir, 'id') assert os.path.isfile(changelog_file) - with Path(changelog_file).open() as f: - changelog_lines = f.readlines() + changelog_lines = Path(changelog_file).open(encoding='utf-8').readlines() uuid.UUID(changelog_lines[0], version=4) assert True From b2be238d7414c7be05f6be7fdf2efedd12e80d55 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Wed, 8 Jun 2022 16:28:01 +0300 Subject: [PATCH 199/274] Tests: Use pathlib read_text/write_text (test_generate_copy_without_render.py) --- tests/test_generate_copy_without_render.py | 36 +++++++++++----------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/tests/test_generate_copy_without_render.py b/tests/test_generate_copy_without_render.py index 5e4df7b3b..9e6039787 100644 --- a/tests/test_generate_copy_without_render.py +++ b/tests/test_generate_copy_without_render.py @@ -44,33 +44,33 @@ def test_generate_copy_without_render_extensions(): assert 'test_copy_without_render-not-rendered' in dir_contents assert 'test_copy_without_render-rendered' in dir_contents - with Path('test_copy_without_render/README.txt').open() as f: - assert '{{cookiecutter.render_test}}' in f.read() + file_1 = Path('test_copy_without_render/README.txt').read_text() + assert '{{cookiecutter.render_test}}' in file_1 - with Path('test_copy_without_render/README.rst').open() as f: - assert 'I have been rendered!' in f.read() + file_2 = Path('test_copy_without_render/README.rst').read_text() + assert 'I have been rendered!' in file_2 - with Path( + file_3 = Path( 'test_copy_without_render/test_copy_without_render-rendered/README.txt' - ).open() as f: - assert '{{cookiecutter.render_test}}' in f.read() + ).read_text() + assert '{{cookiecutter.render_test}}' in file_3 - with Path( + file_4 = Path( 'test_copy_without_render/test_copy_without_render-rendered/README.rst' - ).open() as f: - assert 'I have been rendered' in f.read() + ).read_text() + assert 'I have been rendered' in file_4 - with Path( + file_5 = Path( 'test_copy_without_render/' 'test_copy_without_render-not-rendered/' 'README.rst' - ).open() as f: - assert '{{cookiecutter.render_test}}' in f.read() + ).read_text() + assert '{{cookiecutter.render_test}}' in file_5 - with Path('test_copy_without_render/rendered/not_rendered.yml').open() as f: - assert '{{cookiecutter.render_test}}' in f.read() + file_6 = Path('test_copy_without_render/rendered/not_rendered.yml').read_text() + assert '{{cookiecutter.render_test}}' in file_6 - with Path( + file_7 = Path( 'test_copy_without_render/' 'test_copy_without_render-rendered/' 'README.md' - ).open() as f: - assert '{{cookiecutter.render_test}}' in f.read() + ).read_text() + assert '{{cookiecutter.render_test}}' in file_7 From 8e9f138d474d34dbf84d7b4595f2a1452b7f4fd0 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Wed, 8 Jun 2022 16:28:21 +0300 Subject: [PATCH 200/274] Tests: Use pathlib read_text/write_text (test_generate_copy_without_render_override.py) --- ...t_generate_copy_without_render_override.py | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/tests/test_generate_copy_without_render_override.py b/tests/test_generate_copy_without_render_override.py index 1bda073d9..c2d836e3d 100644 --- a/tests/test_generate_copy_without_render_override.py +++ b/tests/test_generate_copy_without_render_override.py @@ -63,33 +63,33 @@ def test_generate_copy_without_render_extensions(): assert 'test_copy_without_render-not-rendered' in dir_contents assert 'test_copy_without_render-rendered' in dir_contents - with Path('test_copy_without_render/README.txt').open() as f: - assert '{{cookiecutter.render_test}}' in f.read() + file_1 = Path('test_copy_without_render/README.txt').read_text() + assert '{{cookiecutter.render_test}}' in file_1 - with Path('test_copy_without_render/README.rst').open() as f: - assert 'I have been rendered!' in f.read() + file_2 = Path('test_copy_without_render/README.rst').read_text() + assert 'I have been rendered!' in file_2 - with Path( + file_3 = Path( 'test_copy_without_render/test_copy_without_render-rendered/README.txt' - ).open() as f: - assert '{{cookiecutter.render_test}}' in f.read() + ).read_text() + assert '{{cookiecutter.render_test}}' in file_3 - with Path( + file_4 = Path( 'test_copy_without_render/test_copy_without_render-rendered/README.rst' - ).open() as f: - assert 'I have been rendered' in f.read() + ).read_text() + assert 'I have been rendered' in file_4 - with Path( + file_5 = Path( 'test_copy_without_render/' 'test_copy_without_render-not-rendered/' 'README.rst' - ).open() as f: - assert '{{cookiecutter.render_test}}' in f.read() + ).read_text() + assert '{{cookiecutter.render_test}}' in file_5 - with Path('test_copy_without_render/rendered/not_rendered.yml').open() as f: - assert '{{cookiecutter.render_test}}' in f.read() + file_6 = Path('test_copy_without_render/rendered/not_rendered.yml').read_text() + assert '{{cookiecutter.render_test}}' in file_6 - with Path( + file_7 = Path( 'test_copy_without_render/' 'test_copy_without_render-rendered/' 'README.md' - ).open() as f: - assert '{{cookiecutter.render_test}}' in f.read() + ).read_text() + assert '{{cookiecutter.render_test}}' in file_7 From 8413ed9a71940d7f3399da18b23c77c1cc3fb5dc Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Wed, 8 Jun 2022 16:28:49 +0300 Subject: [PATCH 201/274] Tests: Use pathlib read_text/write_text with encoding (test_generate_file.py) --- tests/test_generate_file.py | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/tests/test_generate_file.py b/tests/test_generate_file.py index 6e6604bea..9ff622168 100644 --- a/tests/test_generate_file.py +++ b/tests/test_generate_file.py @@ -46,9 +46,8 @@ def test_generate_file(env): env=env, ) assert os.path.isfile('tests/files/cheese.txt') - with Path('tests/files/cheese.txt').open() as f: - generated_text = f.read() - assert generated_text == 'Testing cheese' + generated_text = Path('tests/files/cheese.txt').read_text() + assert generated_text == 'Testing cheese' def test_generate_file_jsonify_filter(env): @@ -59,9 +58,8 @@ def test_generate_file_jsonify_filter(env): project_dir=".", infile=infile, context={'cookiecutter': data}, env=env ) assert os.path.isfile('tests/files/cheese.txt') - with Path('tests/files/cheese.txt').open() as f: - generated_text = f.read() - assert json.loads(generated_text) == data + generated_text = Path('tests/files/cheese.txt').read_text() + assert json.loads(generated_text) == data @pytest.mark.parametrize("length", (10, 40)) @@ -73,9 +71,8 @@ def test_generate_file_random_ascii_string(env, length, punctuation): context = {"cookiecutter": data, "length": length, "punctuation": punctuation} generate.generate_file(project_dir=".", infile=infile, context=context, env=env) assert os.path.isfile('tests/files/cheese.txt') - with Path('tests/files/cheese.txt').open() as f: - generated_text = f.read() - assert len(generated_text) == length + generated_text = Path('tests/files/cheese.txt').read_text() + assert len(generated_text) == length def test_generate_file_with_true_condition(env): @@ -93,9 +90,8 @@ def test_generate_file_with_true_condition(env): env=env, ) assert os.path.isfile('tests/files/cheese.txt') - with Path('tests/files/cheese.txt').open() as f: - generated_text = f.read() - assert generated_text == 'Testing that generate_file was y' + generated_text = Path('tests/files/cheese.txt').read_text() + assert generated_text == 'Testing that generate_file was y' def test_generate_file_with_false_condition(env): @@ -149,7 +145,7 @@ def test_generate_file_does_not_translate_lf_newlines_to_crlf(env, tmp_path): # this generated file should have a LF line ending gf = 'tests/files/cheese_lf_newlines.txt' - with Path(gf).open(newline='') as f: + with Path(gf).open(encoding='utf-8', newline='') as f: simple_text = f.readline() assert simple_text == 'newline is LF\n' assert f.newlines == '\n' @@ -167,7 +163,7 @@ def test_generate_file_does_not_translate_crlf_newlines_to_lf(env): # this generated file should have a CRLF line ending gf = 'tests/files/cheese_crlf_newlines.txt' - with Path(gf).open(newline='') as f: + with Path(gf).open(encoding='utf-8', newline='') as f: simple_text = f.readline() assert simple_text == 'newline is CRLF\r\n' assert f.newlines == '\r\n' From 9cdcdc0ae5622ac899daa5df772911cd0c80e5e2 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Wed, 8 Jun 2022 16:29:10 +0300 Subject: [PATCH 202/274] Tests: Use pathlib read_text/write_text with encoding (test_generate_files.py) --- tests/test_generate_files.py | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/tests/test_generate_files.py b/tests/test_generate_files.py index 436d83ac6..bb4075cae 100644 --- a/tests/test_generate_files.py +++ b/tests/test_generate_files.py @@ -44,7 +44,7 @@ def test_generate_files(tmp_path): assert simple_file.exists() assert simple_file.is_file() - simple_text = Path(simple_file).open().read() + simple_text = Path(simple_file).read_text(encoding='utf-8') assert simple_text == 'I eat pizzä' @@ -60,7 +60,7 @@ def test_generate_files_with_linux_newline(tmp_path): assert newline_file.is_file() assert newline_file.exists() - with Path(newline_file).open(newline='') as f: + with Path(newline_file).open(encoding='utf-8', newline='') as f: simple_text = f.readline() assert simple_text == 'newline is LF\n' assert f.newlines == '\n' @@ -83,7 +83,7 @@ def test_generate_files_with_jinja2_environment(tmp_path): assert conditions_file.is_file() assert conditions_file.exists() - simple_text = conditions_file.open('rt').read() + simple_text = conditions_file.read_text(encoding='utf-8') assert simple_text == 'I eat pizzä\n' @@ -100,7 +100,7 @@ def test_generate_files_with_trailing_newline_forced_to_linux_by_context(tmp_pat assert newline_file.is_file() assert newline_file.exists() - with Path(newline_file).open(newline='') as f: + with Path(newline_file).open(encoding='utf-8', newline='') as f: simple_text = f.readline() assert simple_text == 'newline is LF\r\n' assert f.newlines == '\r\n' @@ -118,7 +118,7 @@ def test_generate_files_with_windows_newline(tmp_path): assert newline_file.is_file() assert newline_file.exists() - with Path(newline_file).open(newline='') as f: + with Path(newline_file).open(encoding='utf-8', newline='') as f: simple_text = f.readline() assert simple_text == 'newline is CRLF\r\n' assert f.newlines == '\r\n' @@ -136,7 +136,7 @@ def test_generate_files_with_windows_newline_forced_to_linux_by_context(tmp_path assert newline_file.is_file() assert newline_file.exists() - with Path(newline_file).open(newline='') as f: + with Path(newline_file).open(encoding='utf-8', newline='') as f: simple_text = f.readline() assert simple_text == 'newline is CRLF\n' @@ -202,7 +202,6 @@ def test_generate_files_permissions(tmp_path): output_dir=tmp_path, ) - assert Path(tmp_path, 'inputpermissions/simple.txt').exists() assert Path(tmp_path, 'inputpermissions/simple.txt').is_file() # Verify source simple.txt should still be 0o644 @@ -257,7 +256,7 @@ def test_generate_files_with_overwrite_if_exists_with_skip_if_file_exists(tmp_pa assert Path(simple_with_new_line_file).is_file() assert Path(simple_with_new_line_file).exists() - simple_text = Path(simple_file).open().read() + simple_text = Path(simple_file).read_text(encoding='utf-8') assert simple_text == 'temp' @@ -267,8 +266,7 @@ def test_generate_files_with_skip_if_file_exists(tmp_path): simple_with_new_line_file = Path(tmp_path, 'inputpizzä/simple-with-newline.txt') Path(tmp_path, 'inputpizzä').mkdir(parents=True) - with Path(simple_file).open('w') as f: - f.write('temp') + Path(simple_file).write_text('temp') with pytest.raises(exceptions.OutputDirExistsException): generate.generate_files( @@ -279,11 +277,10 @@ def test_generate_files_with_skip_if_file_exists(tmp_path): ) assert Path(simple_file).is_file() - assert Path(simple_file).exists() assert not Path(simple_with_new_line_file).is_file() assert not Path(simple_with_new_line_file).exists() - simple_text = Path(simple_file).open().read() + simple_text = Path(simple_file).read_text(encoding='utf-8') assert simple_text == 'temp' @@ -293,8 +290,7 @@ def test_generate_files_with_overwrite_if_exists(tmp_path): simple_with_new_line_file = Path(tmp_path, 'inputpizzä/simple-with-newline.txt') Path(tmp_path, 'inputpizzä').mkdir(parents=True) - with Path(simple_file).open('w') as f: - f.write('temp') + Path(simple_file).write_text('temp') generate.generate_files( context={'cookiecutter': {'food': 'pizzä'}}, @@ -308,7 +304,7 @@ def test_generate_files_with_overwrite_if_exists(tmp_path): assert Path(simple_with_new_line_file).is_file() assert Path(simple_with_new_line_file).exists() - simple_text = Path(simple_file).open().read() + simple_text = Path(simple_file).read_text(encoding='utf-8') assert simple_text == 'I eat pizzä' From b403dbec5165aa7153391e7012b103ac7cc25572 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Wed, 8 Jun 2022 16:31:45 +0300 Subject: [PATCH 203/274] Tests: Use pathlib read_text/write_text with encoding(test_custom_extensions_in_hooks.py) --- tests/test_custom_extensions_in_hooks.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/test_custom_extensions_in_hooks.py b/tests/test_custom_extensions_in_hooks.py index ef9103021..b08225e44 100644 --- a/tests/test_custom_extensions_in_hooks.py +++ b/tests/test_custom_extensions_in_hooks.py @@ -39,7 +39,5 @@ def test_hook_with_extension(template, output_dir): extra_context={'project_slug': 'foobar', 'name': 'Cookiemonster'}, ) - with Path(project_dir, 'README.rst').open() as f: - readme = f.read().strip() - - assert readme == 'Hello Cookiemonster!' + readme = Path(project_dir, 'README.rst').read_text(encoding="utf8") + assert readme.strip() == 'Hello Cookiemonster!' From 2d4b630d618d238e9694b214f4338c30f8d16c5f Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Wed, 8 Jun 2022 16:32:05 +0300 Subject: [PATCH 204/274] Tests: Use pathlib read_text/write_text (test_cookiecutter_local_no_input.py) --- tests/test_cookiecutter_local_no_input.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/tests/test_cookiecutter_local_no_input.py b/tests/test_cookiecutter_local_no_input.py index eb1e52ed3..9e89c9539 100644 --- a/tests/test_cookiecutter_local_no_input.py +++ b/tests/test_cookiecutter_local_no_input.py @@ -66,9 +66,8 @@ def test_cookiecutter_no_input_return_rendered_file(): """Verify Jinja2 templating correctly works in `cookiecutter.json` file.""" project_dir = main.cookiecutter('tests/fake-repo-pre', no_input=True) assert project_dir == os.path.abspath('fake-project') - with Path(project_dir, 'README.rst').open() as fh: - contents = fh.read() - assert "Project name: **Fake Project**" in contents + content = Path(project_dir, 'README.rst').read_text() + assert "Project name: **Fake Project**" in content @pytest.mark.usefixtures('clean_system', 'remove_additional_dirs') @@ -77,11 +76,9 @@ def test_cookiecutter_dict_values_in_context(): project_dir = main.cookiecutter('tests/fake-repo-dict', no_input=True) assert project_dir == os.path.abspath('fake-project-dict') - with Path(project_dir, 'README.md').open() as fh: - contents = fh.read() - + content = Path(project_dir, 'README.md').read_text() assert ( - contents + content == textwrap.dedent( """ # README From 37da8d8bacf2b02fa21971848629852bc22c4d94 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Wed, 8 Jun 2022 12:52:19 +0300 Subject: [PATCH 205/274] Convert 'format' usage to f-strings (cli.py) --- cookiecutter/cli.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/cookiecutter/cli.py b/cookiecutter/cli.py index 4d92de749..ebc8dd0db 100644 --- a/cookiecutter/cli.py +++ b/cookiecutter/cli.py @@ -27,17 +27,16 @@ def version_msg(): """Return the Cookiecutter version, location and Python powering it.""" python_version = sys.version location = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - message = 'Cookiecutter %(version)s from {} (Python {})' - return message.format(location, python_version) + return f"Cookiecutter {__version__} from {location} (Python {python_version})" def validate_extra_context(ctx, param, value): """Validate extra context.""" - for s in value: - if '=' not in s: + for string in value: + if '=' not in string: raise click.BadParameter( - 'EXTRA_CONTEXT should contain items of the form key=value; ' - "'{}' doesn't match that form".format(s) + f"EXTRA_CONTEXT should contain items of the form key=value; " + f"'{string}' doesn't match that form" ) # Convert tuple -- e.g.: ('program_name=foobar', 'startsecs=66') @@ -51,8 +50,8 @@ def list_installed_templates(default_config, passed_config_file): cookiecutter_folder = config.get('cookiecutters_dir') if not os.path.exists(cookiecutter_folder): click.echo( - 'Error: Cannot list installed templates. Folder does not exist: ' - '{}'.format(cookiecutter_folder) + f"Error: Cannot list installed templates. " + f"Folder does not exist: {cookiecutter_folder}" ) sys.exit(-1) From 164caa4b03d1887676f6cf1a78faac8302d6c932 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Wed, 8 Jun 2022 12:53:03 +0300 Subject: [PATCH 206/274] Convert 'format' usage to f-strings (generate.py) --- cookiecutter/generate.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cookiecutter/generate.py b/cookiecutter/generate.py index 01cbcf8a0..f71aba261 100644 --- a/cookiecutter/generate.py +++ b/cookiecutter/generate.py @@ -65,8 +65,8 @@ def apply_overwrites_to_context(context, overwrite_context): context_value.insert(0, overwrite) else: raise ValueError( - "{} provided for choice variable {}, but the " - "choices are {}.".format(overwrite, variable, context_value) + f"{overwrite} provided for choice variable {variable}, " + f"but the choices are {context_value}." ) elif isinstance(context_value, dict) and isinstance(overwrite, dict): # Partially overwrite some keys in original dict @@ -100,8 +100,8 @@ def generate_context( full_fpath = os.path.abspath(context_file) json_exc_message = str(e) our_exc_message = ( - 'JSON decoding error while loading "{}". Decoding' - ' error details: "{}"'.format(full_fpath, json_exc_message) + f"JSON decoding error while loading '{full_fpath}'. " + f"Decoding error details: '{json_exc_message}'" ) raise ContextDecodingException(our_exc_message) @@ -115,8 +115,8 @@ def generate_context( if default_context: try: apply_overwrites_to_context(obj, default_context) - except ValueError as ex: - warnings.warn("Invalid default received: " + str(ex)) + except ValueError as error: + warnings.warn(f"Invalid default received: {error}") if extra_context: apply_overwrites_to_context(obj, extra_context) From a1d48684c0a797a2b8ade2cadf99a67121917278 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Wed, 8 Jun 2022 12:53:42 +0300 Subject: [PATCH 207/274] Convert 'format' usage to f-strings (prompt.py) --- cookiecutter/prompt.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cookiecutter/prompt.py b/cookiecutter/prompt.py index d27481f7b..d556528e1 100644 --- a/cookiecutter/prompt.py +++ b/cookiecutter/prompt.py @@ -66,9 +66,9 @@ def read_user_choice(var_name, options): choice_lines = ['{} - {}'.format(*c) for c in choice_map.items()] prompt = '\n'.join( ( - f'Select {var_name}:', - '\n'.join(choice_lines), - 'Choose from {}'.format(', '.join(choices)), + f"Select {var_name}:", + "\n".join(choice_lines), + f"Choose from {', '.join(choices)}", ) ) From f4996f24fcf5215932859e24fcc23789c7c9b64e Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Wed, 8 Jun 2022 12:53:56 +0300 Subject: [PATCH 208/274] Convert 'format' usage to f-strings (utils.py) --- cookiecutter/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cookiecutter/utils.py b/cookiecutter/utils.py index 4750a2663..6796849fb 100644 --- a/cookiecutter/utils.py +++ b/cookiecutter/utils.py @@ -86,8 +86,8 @@ def prompt_and_delete(path, no_input=False): ok_to_delete = True else: question = ( - "You've downloaded {} before. Is it okay to delete and re-download it?" - ).format(path) + f"You've downloaded {path} before. Is it okay to delete and re-download it?" + ) ok_to_delete = read_user_yes_no(question, 'yes') From 282cf585d708fdc5f980b45edf70d644fcda936b Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Wed, 8 Jun 2022 12:54:09 +0300 Subject: [PATCH 209/274] Convert 'format' usage to f-strings (zipfile.py) --- cookiecutter/zipfile.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cookiecutter/zipfile.py b/cookiecutter/zipfile.py index 0342f1d71..241944e62 100644 --- a/cookiecutter/zipfile.py +++ b/cookiecutter/zipfile.py @@ -63,8 +63,7 @@ def unzip(zip_uri, is_url, clone_to_dir='.', no_input=False, password=None): first_filename = zip_file.namelist()[0] if not first_filename.endswith('/'): raise InvalidZipRepository( - 'Zip repository {} does not include ' - 'a top-level directory'.format(zip_uri) + f"Zip repository {zip_uri} does not include a top-level directory" ) # Construct the final target directory From b789202854d101d873c2c4373a0983326617ddae Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Wed, 8 Jun 2022 16:46:30 +0300 Subject: [PATCH 210/274] Remove encoding (test_templates.py) --- tests/test_templates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_templates.py b/tests/test_templates.py index e3aed3598..44b9475d2 100644 --- a/tests/test_templates.py +++ b/tests/test_templates.py @@ -30,7 +30,7 @@ def test_build_templates(template, output_dir): output_dir=output_dir, ) - readme = Path(project_dir, 'requirements.txt').read_text(encoding='utf-8') + readme = Path(project_dir, 'requirements.txt').read_text() assert readme.splitlines() == [ "pip", From c0c2d5cfcf8287665bfb15f93a0e8dcdbd59001e Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Wed, 8 Jun 2022 16:46:54 +0300 Subject: [PATCH 211/274] Fix encoding typo (test_custom_extensions_in_hooks.py) --- tests/test_custom_extensions_in_hooks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_custom_extensions_in_hooks.py b/tests/test_custom_extensions_in_hooks.py index b08225e44..9f13733ba 100644 --- a/tests/test_custom_extensions_in_hooks.py +++ b/tests/test_custom_extensions_in_hooks.py @@ -39,5 +39,5 @@ def test_hook_with_extension(template, output_dir): extra_context={'project_slug': 'foobar', 'name': 'Cookiemonster'}, ) - readme = Path(project_dir, 'README.rst').read_text(encoding="utf8") + readme = Path(project_dir, 'README.rst').read_text(encoding="utf-8") assert readme.strip() == 'Hello Cookiemonster!' From 1e821d2945d29e575a6a65e1850b910b34885924 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Wed, 8 Jun 2022 17:16:50 +0300 Subject: [PATCH 212/274] Use touch instead of open('w') (test_determine_repo_dir_finds_existing_cookiecutter.py) Co-authored-by: alkatar21 <61387986+alkatar21@users.noreply.github.com> --- .../test_determine_repo_dir_finds_existing_cookiecutter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/repository/test_determine_repo_dir_finds_existing_cookiecutter.py b/tests/repository/test_determine_repo_dir_finds_existing_cookiecutter.py index ebd7df320..3810668f6 100644 --- a/tests/repository/test_determine_repo_dir_finds_existing_cookiecutter.py +++ b/tests/repository/test_determine_repo_dir_finds_existing_cookiecutter.py @@ -21,7 +21,7 @@ def cloned_cookiecutter_path(user_config_data, template): cloned_template_path = os.path.join(cookiecutters_dir, template) os.mkdir(cloned_template_path) - Path(cloned_template_path, "cookiecutter.json").open('w') # creates file + Path(cloned_template_path, "cookiecutter.json").touch() # creates file return cloned_template_path From 0fe509c30c83ace5215211204275c73f928a2236 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Wed, 8 Jun 2022 17:17:16 +0300 Subject: [PATCH 213/274] Use touch instead of open('w') (test_determine_repo_dir_finds_subdirectories.py) Co-authored-by: alkatar21 <61387986+alkatar21@users.noreply.github.com> --- .../repository/test_determine_repo_dir_finds_subdirectories.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/repository/test_determine_repo_dir_finds_subdirectories.py b/tests/repository/test_determine_repo_dir_finds_subdirectories.py index a1f7a034c..bcea1b387 100644 --- a/tests/repository/test_determine_repo_dir_finds_subdirectories.py +++ b/tests/repository/test_determine_repo_dir_finds_subdirectories.py @@ -25,7 +25,7 @@ def cloned_cookiecutter_path(user_config_data, template): subdir_template_path = os.path.join(cloned_template_path, 'my-dir') if not os.path.exists(subdir_template_path): os.mkdir(subdir_template_path) - Path(subdir_template_path, 'cookiecutter.json').open('w') # creates file + Path(subdir_template_path, 'cookiecutter.json').touch() # creates file return subdir_template_path From 8a731d1e21db8150c741d5e6e393fc2c0827b6bf Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Wed, 8 Jun 2022 18:02:44 +0300 Subject: [PATCH 214/274] Include caused exception details on reraise (PEP 3134) (environment.py) --- cookiecutter/environment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cookiecutter/environment.py b/cookiecutter/environment.py index f2804c595..02e2abf40 100644 --- a/cookiecutter/environment.py +++ b/cookiecutter/environment.py @@ -34,7 +34,7 @@ def __init__(self, **kwargs): try: super().__init__(extensions=extensions, **kwargs) except ImportError as err: - raise UnknownExtension(f'Unable to load extension: {err}') + raise UnknownExtension(f'Unable to load extension: {err}') from err def _read_extensions(self, context): """Return list of extensions as str to be passed on to the Jinja2 env. From 0c94173df7e7ca876fe88fb23372452fecc9fff4 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Wed, 8 Jun 2022 18:02:55 +0300 Subject: [PATCH 215/274] Include caused exception details on reraise (PEP 3134) (generate.py) --- cookiecutter/generate.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cookiecutter/generate.py b/cookiecutter/generate.py index f71aba261..16d218c93 100644 --- a/cookiecutter/generate.py +++ b/cookiecutter/generate.py @@ -103,7 +103,7 @@ def generate_context( f"JSON decoding error while loading '{full_fpath}'. " f"Decoding error details: '{json_exc_message}'" ) - raise ContextDecodingException(our_exc_message) + raise ContextDecodingException(our_exc_message) from e # Add the Python object to the context dictionary file_name = os.path.split(context_file)[1] @@ -302,7 +302,7 @@ def generate_files( ) except UndefinedError as err: msg = f"Unable to create project directory '{unrendered_dir}'" - raise UndefinedVariableInTemplate(msg, err, context) + raise UndefinedVariableInTemplate(msg, err, context) from err # We want the Jinja path and the OS paths to match. Consequently, we'll: # + CD to the template folder @@ -371,7 +371,7 @@ def generate_files( rmtree(project_dir) _dir = os.path.relpath(unrendered_dir, output_dir) msg = f"Unable to create directory '{_dir}'" - raise UndefinedVariableInTemplate(msg, err, context) + raise UndefinedVariableInTemplate(msg, err, context) from err for f in files: infile = os.path.normpath(os.path.join(root, f)) @@ -393,7 +393,7 @@ def generate_files( if delete_project_on_failure: rmtree(project_dir) msg = f"Unable to create file '{infile}'" - raise UndefinedVariableInTemplate(msg, err, context) + raise UndefinedVariableInTemplate(msg, err, context) from err if accept_hooks: _run_hook_from_repo_dir( From 1c09d347e9ef1911a589edb78a2ec01eaadc3180 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Wed, 8 Jun 2022 18:03:06 +0300 Subject: [PATCH 216/274] Include caused exception details on reraise (PEP 3134) (hooks.py) --- cookiecutter/hooks.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cookiecutter/hooks.py b/cookiecutter/hooks.py index 763287c58..5b08e7d70 100644 --- a/cookiecutter/hooks.py +++ b/cookiecutter/hooks.py @@ -85,12 +85,12 @@ def run_script(script_path, cwd='.'): raise FailedHookException( f'Hook script failed (exit status: {exit_status})' ) - except OSError as os_error: - if os_error.errno == errno.ENOEXEC: + except OSError as err: + if err.errno == errno.ENOEXEC: raise FailedHookException( 'Hook script failed, might be an empty file or missing a shebang' - ) - raise FailedHookException(f'Hook script failed (error: {os_error})') + ) from err + raise FailedHookException(f'Hook script failed (error: {err})') from err def run_script_with_context(script_path, cwd, context): From 7992483dc526375f3c2776fbebfe89f83f58746b Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Wed, 8 Jun 2022 18:03:16 +0300 Subject: [PATCH 217/274] Include caused exception details on reraise (PEP 3134) (vcs.py) --- cookiecutter/vcs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cookiecutter/vcs.py b/cookiecutter/vcs.py index 93ca2366a..e1a99e9bc 100644 --- a/cookiecutter/vcs.py +++ b/cookiecutter/vcs.py @@ -114,12 +114,12 @@ def clone(repo_url, checkout=None, clone_to_dir='.', no_input=False): raise RepositoryNotFound( f'The repository {repo_url} could not be found, ' 'have you made a typo?' - ) + ) from clone_error if any(error in output for error in BRANCH_ERRORS): raise RepositoryCloneFailed( f'The {checkout} branch of repository ' f'{repo_url} could not found, have you made a typo?' - ) + ) from clone_error logger.error('git clone failed with error: %s', output) raise From 86fc66f937adb6ae9d03b305a82bbf22334709a6 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Wed, 8 Jun 2022 18:03:29 +0300 Subject: [PATCH 218/274] Include caused exception details on reraise (PEP 3134) (prompt.py) --- cookiecutter/prompt.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/cookiecutter/prompt.py b/cookiecutter/prompt.py index d556528e1..ee19a339b 100644 --- a/cookiecutter/prompt.py +++ b/cookiecutter/prompt.py @@ -92,9 +92,9 @@ def process_json(user_value, default_value=None): try: user_dict = json.loads(user_value, object_pairs_hook=OrderedDict) - except Exception: + except Exception as error: # Leave it up to click to ask the user again - raise click.UsageError('Unable to decode to JSON.') + raise click.UsageError('Unable to decode to JSON.') from error if not isinstance(user_dict, dict): # Leave it up to click to ask the user again @@ -159,8 +159,7 @@ def render_variable(env, raw, cookiecutter_dict): template = env.from_string(raw) - rendered_template = template.render(cookiecutter=cookiecutter_dict) - return rendered_template + return template.render(cookiecutter=cookiecutter_dict) def prompt_choice_for_config(cookiecutter_dict, env, key, options, no_input): @@ -219,7 +218,7 @@ def prompt_for_config(context, no_input=False): cookiecutter_dict[key] = val except UndefinedError as err: msg = f"Unable to render variable '{key}'" - raise UndefinedVariableInTemplate(msg, err, context) + raise UndefinedVariableInTemplate(msg, err, context) from err # Second pass; handle the dictionaries. for key, raw in context['cookiecutter'].items(): @@ -238,6 +237,6 @@ def prompt_for_config(context, no_input=False): cookiecutter_dict[key] = val except UndefinedError as err: msg = f"Unable to render variable '{key}'" - raise UndefinedVariableInTemplate(msg, err, context) + raise UndefinedVariableInTemplate(msg, err, context) from err return cookiecutter_dict From b1d470135ef27427ac8800d2dab1a8ddafef1b0d Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Wed, 8 Jun 2022 18:13:47 +0300 Subject: [PATCH 219/274] Fix not closed files handlers --- tests/test_default_extensions.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_default_extensions.py b/tests/test_default_extensions.py index 2b03b7b23..d229e3d8c 100644 --- a/tests/test_default_extensions.py +++ b/tests/test_default_extensions.py @@ -26,7 +26,8 @@ def test_jinja2_time_extension(tmp_path): changelog_file = os.path.join(project_dir, 'HISTORY.rst') assert os.path.isfile(changelog_file) - changelog_lines = Path(changelog_file).open(encoding='utf-8').readlines() + with Path(changelog_file).open(encoding='utf-8') as f: + changelog_lines = f.readlines() expected_lines = [ 'History\n', @@ -57,6 +58,7 @@ def test_jinja2_uuid_extension(tmp_path): changelog_file = os.path.join(project_dir, 'id') assert os.path.isfile(changelog_file) - changelog_lines = Path(changelog_file).open(encoding='utf-8').readlines() + with Path(changelog_file).open(encoding='utf-8') as f: + changelog_lines = f.readlines() uuid.UUID(changelog_lines[0], version=4) From e162629ea6308e542520342d1337cbcc328f8b9b Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Thu, 9 Jun 2022 00:29:06 +0300 Subject: [PATCH 220/274] Extend sphinx config with type annotations from typehints --- docs/conf.py | 4 ++++ docs/requirements.txt | 1 + 2 files changed, 5 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 332153029..9c2ffdaf4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -55,6 +55,7 @@ 'sphinx_click.ext', 'myst_parser', 'sphinxcontrib.apidoc', + 'sphinx_autodoc_typehints', ] # Add any paths that contain templates here, relative to this directory. @@ -369,3 +370,6 @@ apidoc_output_dir = "." apidoc_toc_file = False apidoc_extra_args = ["-t", "_templates"] + +autodoc_member_order = "groupwise" +autodoc_typehints = "none" diff --git a/docs/requirements.txt b/docs/requirements.txt index cd980601c..582a2052e 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -4,3 +4,4 @@ myst-parser>=0.17.2 sphinx-autobuild>=2021.3.14 Sphinx>=4.5.0 sphinxcontrib-apidoc>=0.3.0 +sphinx-autodoc-typehints>=1.18.2 \ No newline at end of file From 2e2691ddfacff5a228d083d675d07e1ad77fad2d Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Thu, 9 Jun 2022 00:29:47 +0300 Subject: [PATCH 221/274] Find refactored and type annotated --- cookiecutter/find.py | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/cookiecutter/find.py b/cookiecutter/find.py index 054e286f4..526fd5adf 100644 --- a/cookiecutter/find.py +++ b/cookiecutter/find.py @@ -1,31 +1,27 @@ """Functions for finding Cookiecutter templates and other components.""" import logging import os +from pathlib import Path from cookiecutter.exceptions import NonTemplatedInputDirException logger = logging.getLogger(__name__) -def find_template(repo_dir): - """Determine which child directory of `repo_dir` is the project template. +def find_template(repo_dir: os.PathLike[str]) -> Path: + """Determine which child directory of ``repo_dir`` is the project template. :param repo_dir: Local directory of newly cloned repo. - :returns project_template: Relative path to project template. + :return: Relative path to project template. """ logger.debug('Searching %s for the project template.', repo_dir) - repo_dir_contents = os.listdir(repo_dir) - - project_template = None - for item in repo_dir_contents: - if 'cookiecutter' in item and '{{' in item and '}}' in item: - project_template = item + for str_path in os.listdir(repo_dir): + if 'cookiecutter' in str_path and '{{' in str_path and '}}' in str_path: + project_template = Path(repo_dir, str_path) break - - if project_template: - project_template = os.path.join(repo_dir, project_template) - logger.debug('The project template appears to be %s', project_template) - return project_template else: raise NonTemplatedInputDirException + + logger.debug('The project template appears to be %s', project_template) + return project_template From aebdc499127e1cfa350e1235c333b20c1f444c9e Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Thu, 9 Jun 2022 00:30:26 +0300 Subject: [PATCH 222/274] Update findpath tests --- tests/test_find.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_find.py b/tests/test_find.py index 761c02235..affbd806e 100644 --- a/tests/test_find.py +++ b/tests/test_find.py @@ -1,5 +1,5 @@ """Tests for `cookiecutter.find` module.""" -import os +from pathlib import Path import pytest @@ -9,12 +9,12 @@ @pytest.fixture(params=['fake-repo-pre', 'fake-repo-pre2']) def repo_dir(request): """Fixture returning path for `test_find_template` test.""" - return os.path.join('tests', request.param) + return Path('tests', request.param) def test_find_template(repo_dir): """Verify correctness of `find.find_template` path detection.""" template = find.find_template(repo_dir=repo_dir) - test_dir = os.path.join(repo_dir, '{{cookiecutter.repo_name}}') + test_dir = Path(repo_dir, '{{cookiecutter.repo_name}}') assert template == test_dir From 2a8731490517a48f1bbfe3c466431dea93bf3191 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Thu, 9 Jun 2022 01:28:21 +0300 Subject: [PATCH 223/274] Compatibility fix --- cookiecutter/find.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cookiecutter/find.py b/cookiecutter/find.py index 526fd5adf..409e4ce9a 100644 --- a/cookiecutter/find.py +++ b/cookiecutter/find.py @@ -8,7 +8,7 @@ logger = logging.getLogger(__name__) -def find_template(repo_dir: os.PathLike[str]) -> Path: +def find_template(repo_dir: "os.PathLike[str]") -> Path: """Determine which child directory of ``repo_dir`` is the project template. :param repo_dir: Local directory of newly cloned repo. From 4d127f87e565dbb4b39513dfd5fc91161e32cb4b Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Thu, 9 Jun 2022 03:36:05 +0300 Subject: [PATCH 224/274] Simplify make_sure_path_exists function --- cookiecutter/utils.py | 20 +++++++++----------- tests/test_utils.py | 4 ++-- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/cookiecutter/utils.py b/cookiecutter/utils.py index 6796849fb..e8709a8e8 100644 --- a/cookiecutter/utils.py +++ b/cookiecutter/utils.py @@ -1,15 +1,16 @@ """Helper functions used throughout Cookiecutter.""" import contextlib -import errno import logging import os import shutil import stat import sys +from pathlib import Path -from cookiecutter.prompt import read_user_yes_no from jinja2.ext import Extension +from cookiecutter.prompt import read_user_yes_no + logger = logging.getLogger(__name__) @@ -31,19 +32,16 @@ def rmtree(path): shutil.rmtree(path, onerror=force_delete) -def make_sure_path_exists(path): +def make_sure_path_exists(path: "os.PathLike[str]") -> None: """Ensure that a directory exists. - :param path: A directory path. + :param path: A directory tree path for creation. """ - logger.debug('Making sure path exists: %s', path) + logger.debug('Making sure path exists (Create tree if not exist): %s', path) try: - os.makedirs(path) - logger.debug('Created directory at: %s', path) - except OSError as exception: - if exception.errno != errno.EEXIST: - return False - return True + Path(path).mkdir(parents=True, exist_ok=True) + except OSError as error: + raise OSError(f'Unable to create replay directory at {path}') from error @contextlib.contextmanager diff --git a/tests/test_utils.py b/tests/test_utils.py index 5e93d5ad4..247bea7af 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -49,8 +49,8 @@ def test_make_sure_path_exists(tmp_path): existing_directory = tmp_path directory_to_create = Path(tmp_path, "not_yet_created") - assert utils.make_sure_path_exists(existing_directory) - assert utils.make_sure_path_exists(directory_to_create) + utils.make_sure_path_exists(existing_directory) + utils.make_sure_path_exists(directory_to_create) # Ensure by base system methods. assert existing_directory.is_dir() From 4cae4233f89b8458b785c62db5feadc5454d5d56 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Thu, 9 Jun 2022 03:39:48 +0300 Subject: [PATCH 225/274] Type annotate vcs.clone function --- cookiecutter/vcs.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/cookiecutter/vcs.py b/cookiecutter/vcs.py index e1a99e9bc..8e61417ac 100644 --- a/cookiecutter/vcs.py +++ b/cookiecutter/vcs.py @@ -2,7 +2,9 @@ import logging import os import subprocess # nosec +from pathlib import Path from shutil import which +from typing import Optional from cookiecutter.exceptions import ( RepositoryCloneFailed, @@ -54,7 +56,12 @@ def is_vcs_installed(repo_type): return bool(which(repo_type)) -def clone(repo_url, checkout=None, clone_to_dir='.', no_input=False): +def clone( + repo_url: str, + checkout: Optional[str] = None, + clone_to_dir: "os.PathLike[str]" = ".", + no_input: bool = False, +): """Clone a repo to the current directory. :param repo_url: Repo URL of unknown type. @@ -66,7 +73,7 @@ def clone(repo_url, checkout=None, clone_to_dir='.', no_input=False): :returns: str with path to the new directory of the repository. """ # Ensure that clone_to_dir exists - clone_to_dir = os.path.expanduser(clone_to_dir) + clone_to_dir = Path(clone_to_dir).expanduser() make_sure_path_exists(clone_to_dir) # identify the repo_type From 886cc8207c07b94d38d6ff3b600cc47037cb43cb Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Thu, 9 Jun 2022 03:40:39 +0300 Subject: [PATCH 226/274] Remove strings from vcs.clone tests --- tests/vcs/test_clone.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/vcs/test_clone.py b/tests/vcs/test_clone.py index bd19ef1ab..ab9598ed8 100644 --- a/tests/vcs/test_clone.py +++ b/tests/vcs/test_clone.py @@ -28,11 +28,11 @@ def test_clone_should_rstrip_trailing_slash_in_repo_url(mocker, clone_dir): autospec=True, ) - vcs.clone('https://github.com/foo/bar/', clone_to_dir=str(clone_dir), no_input=True) + vcs.clone('https://github.com/foo/bar/', clone_to_dir=clone_dir, no_input=True) mock_subprocess.assert_called_once_with( ['git', 'clone', 'https://github.com/foo/bar'], - cwd=str(clone_dir), + cwd=clone_dir, stderr=subprocess.STDOUT, ) @@ -114,13 +114,13 @@ def test_clone_should_invoke_vcs_command( branch = 'foobar' repo_dir = vcs.clone( - repo_url, checkout=branch, clone_to_dir=str(clone_dir), no_input=True + repo_url, checkout=branch, clone_to_dir=clone_dir, no_input=True ) assert repo_dir == expected_repo_dir mock_subprocess.assert_any_call( - [repo_type, 'clone', repo_url], cwd=str(clone_dir), stderr=subprocess.STDOUT + [repo_type, 'clone', repo_url], cwd=clone_dir, stderr=subprocess.STDOUT ) branch_info = [branch] From 68cdeb8f3d719cd777b27b6cf9f9a7fbf533543f Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Thu, 9 Jun 2022 03:42:16 +0300 Subject: [PATCH 227/274] Move OSError raising out of replay.dump (raise in make_sure_path_exists) --- cookiecutter/replay.py | 5 ++--- tests/replay/test_dump.py | 6 ++++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/cookiecutter/replay.py b/cookiecutter/replay.py index 9730e84da..d3c989879 100644 --- a/cookiecutter/replay.py +++ b/cookiecutter/replay.py @@ -16,10 +16,9 @@ def get_file_name(replay_dir, template_name): return os.path.join(replay_dir, file_name) -def dump(replay_dir, template_name, context): +def dump(replay_dir: "os.PathLike[str]", template_name: str, context: dict): """Write json data to file.""" - if not make_sure_path_exists(replay_dir): - raise OSError(f'Unable to create replay dir at {replay_dir}') + make_sure_path_exists(replay_dir) if not isinstance(template_name, str): raise TypeError('Template name is required to be of type str') diff --git a/tests/replay/test_dump.py b/tests/replay/test_dump.py index c757321b1..57ad8ee74 100644 --- a/tests/replay/test_dump.py +++ b/tests/replay/test_dump.py @@ -57,7 +57,9 @@ def mock_ensure_failure(mocker): Used to mock internal function and limit test scope. Always return expected value: False """ - return mocker.patch('cookiecutter.replay.make_sure_path_exists', return_value=False) + return mocker.patch( + 'cookiecutter.replay.make_sure_path_exists', side_effect=OSError + ) @pytest.fixture @@ -72,7 +74,7 @@ def mock_ensure_success(mocker): def test_ioerror_if_replay_dir_creation_fails(mock_ensure_failure, replay_test_dir): """Test that replay.dump raises when the replay_dir cannot be created.""" - with pytest.raises(IOError): + with pytest.raises(OSError): replay.dump(replay_test_dir, 'foo', {'cookiecutter': {'hello': 'world'}}) mock_ensure_failure.assert_called_once_with(replay_test_dir) From d958bdcbdfefa294c440fdd01485ca71ec3c2353 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Thu, 9 Jun 2022 03:43:25 +0300 Subject: [PATCH 228/274] Type annotate generate.render_and_create_dir for correct passing to make_sure_path_exists --- cookiecutter/generate.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/cookiecutter/generate.py b/cookiecutter/generate.py index 16d218c93..251d5f439 100644 --- a/cookiecutter/generate.py +++ b/cookiecutter/generate.py @@ -6,9 +6,9 @@ import shutil import warnings from collections import OrderedDict - +from pathlib import Path from binaryornot.check import is_binary -from jinja2 import FileSystemLoader +from jinja2 import FileSystemLoader, Environment from jinja2.exceptions import TemplateSyntaxError, UndefinedError from cookiecutter.environment import StrictEnvironment @@ -203,19 +203,23 @@ def generate_file(project_dir, infile, context, env, skip_if_file_exists=False): def render_and_create_dir( - dirname, context, output_dir, environment, overwrite_if_exists=False + dirname: str, + context: dict, + output_dir: "os.PathLike[str]", + environment: Environment, + overwrite_if_exists: bool = False, ): """Render name of a directory, create the directory, return its path.""" name_tmpl = environment.from_string(dirname) rendered_dirname = name_tmpl.render(**context) - dir_to_create = os.path.normpath(os.path.join(output_dir, rendered_dirname)) + dir_to_create = Path(output_dir, rendered_dirname) logger.debug( 'Rendered dir %s must exist in output_dir %s', dir_to_create, output_dir ) - output_dir_exists = os.path.exists(dir_to_create) + output_dir_exists = dir_to_create.exists() if output_dir_exists: if overwrite_if_exists: From 16051ff104188c40cf550c2a7b66fdb229fc27d9 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Thu, 9 Jun 2022 03:47:47 +0300 Subject: [PATCH 229/274] Type annotate zipfile.unzip --- cookiecutter/zipfile.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/cookiecutter/zipfile.py b/cookiecutter/zipfile.py index 241944e62..bab90a264 100644 --- a/cookiecutter/zipfile.py +++ b/cookiecutter/zipfile.py @@ -1,6 +1,8 @@ """Utility functions for handling and fetching repo archives in zip format.""" import os import tempfile +from pathlib import Path +from typing import Optional from zipfile import BadZipFile, ZipFile import requests @@ -10,7 +12,13 @@ from cookiecutter.utils import make_sure_path_exists, prompt_and_delete -def unzip(zip_uri, is_url, clone_to_dir='.', no_input=False, password=None): +def unzip( + zip_uri: str, + is_url: bool, + clone_to_dir: "os.PathLike[str]" = ".", + no_input: bool = False, + password: Optional[str] = None, +): """Download and unpack a zipfile at a given URI. This will download the zipfile to the cookiecutter repository, @@ -25,7 +33,7 @@ def unzip(zip_uri, is_url, clone_to_dir='.', no_input=False, password=None): :param password: The password to use when unpacking the repository. """ # Ensure that clone_to_dir exists - clone_to_dir = os.path.expanduser(clone_to_dir) + clone_to_dir = Path(clone_to_dir).expanduser() make_sure_path_exists(clone_to_dir) if is_url: From 759a6ac466dd06e24da318b331633390a672298e Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Thu, 9 Jun 2022 03:56:35 +0300 Subject: [PATCH 230/274] Update make_sure_path_exists tests --- tests/test_utils.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 247bea7af..f540442c0 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -65,14 +65,10 @@ def test_make_sure_path_exists_correctly_handle_os_error(mocker): Should return True if directory exist or created. Should return False if impossible to create directory (for example protected) """ - - def raiser(*args, **kwargs): - raise OSError() - - mocker.patch("os.makedirs", raiser) - uncreatable_directory = Path('protected_path') - - assert not utils.make_sure_path_exists(uncreatable_directory) + mocker.patch("pathlib.Path.mkdir", side_effect=OSError) + with pytest.raises(OSError) as err: + utils.make_sure_path_exists(Path('protected_path')) + assert str(err.value) == "Unable to create replay directory at protected_path" def test_work_in(tmp_path): From 7322abda4c696033ede36daacb64044663de3806 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Thu, 9 Jun 2022 03:59:11 +0300 Subject: [PATCH 231/274] Fix exception text --- cookiecutter/utils.py | 2 +- tests/test_utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cookiecutter/utils.py b/cookiecutter/utils.py index e8709a8e8..813f62cb1 100644 --- a/cookiecutter/utils.py +++ b/cookiecutter/utils.py @@ -41,7 +41,7 @@ def make_sure_path_exists(path: "os.PathLike[str]") -> None: try: Path(path).mkdir(parents=True, exist_ok=True) except OSError as error: - raise OSError(f'Unable to create replay directory at {path}') from error + raise OSError(f'Unable to create directory at {path}') from error @contextlib.contextmanager diff --git a/tests/test_utils.py b/tests/test_utils.py index f540442c0..fdd3692e2 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -68,7 +68,7 @@ def test_make_sure_path_exists_correctly_handle_os_error(mocker): mocker.patch("pathlib.Path.mkdir", side_effect=OSError) with pytest.raises(OSError) as err: utils.make_sure_path_exists(Path('protected_path')) - assert str(err.value) == "Unable to create replay directory at protected_path" + assert str(err.value) == "Unable to create directory at protected_path" def test_work_in(tmp_path): From b0a9ebb642b6348607c24ce9fd8ad6e74c0105c5 Mon Sep 17 00:00:00 2001 From: Andrey Shpak Date: Thu, 9 Jun 2022 12:28:57 +0300 Subject: [PATCH 232/274] Update cookiecutter/utils.py Co-authored-by: Jens W. Klein --- cookiecutter/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cookiecutter/utils.py b/cookiecutter/utils.py index 813f62cb1..d90b3a81d 100644 --- a/cookiecutter/utils.py +++ b/cookiecutter/utils.py @@ -37,7 +37,7 @@ def make_sure_path_exists(path: "os.PathLike[str]") -> None: :param path: A directory tree path for creation. """ - logger.debug('Making sure path exists (Create tree if not exist): %s', path) + logger.debug('Making sure path exists (creates tree if not exist): %s', path) try: Path(path).mkdir(parents=True, exist_ok=True) except OSError as error: From f57420816a3c0359adf5280eb2f3cbcbd29ec08d Mon Sep 17 00:00:00 2001 From: Alex Blackwell Date: Sun, 19 Jun 2022 05:13:32 -0700 Subject: [PATCH 233/274] Easy PR! Fix typos and add minor doc updates (#1741) Fix typos and add minor doc updates --- cookiecutter/exceptions.py | 2 +- cookiecutter/prompt.py | 2 +- cookiecutter/repository.py | 2 +- docs/tutorials/tutorial2.rst | 2 +- setup.cfg | 2 +- tests/conftest.py | 11 ++--------- tests/test_cli.py | 5 +---- 7 files changed, 8 insertions(+), 18 deletions(-) diff --git a/cookiecutter/exceptions.py b/cookiecutter/exceptions.py index 4acf6dc47..622e7c6ef 100644 --- a/cookiecutter/exceptions.py +++ b/cookiecutter/exceptions.py @@ -132,7 +132,7 @@ def __str__(self): class UnknownExtension(CookiecutterException): """ - Exception for un-importable extention. + Exception for un-importable extension. Raised when an environment is unable to import a required extension. """ diff --git a/cookiecutter/prompt.py b/cookiecutter/prompt.py index ee19a339b..f4c19c97a 100644 --- a/cookiecutter/prompt.py +++ b/cookiecutter/prompt.py @@ -222,7 +222,7 @@ def prompt_for_config(context, no_input=False): # Second pass; handle the dictionaries. for key, raw in context['cookiecutter'].items(): - # Skip private type dicts not ot be rendered. + # Skip private type dicts not to be rendered. if key.startswith('_') and not key.startswith('__'): continue diff --git a/cookiecutter/repository.py b/cookiecutter/repository.py index 5086e9b01..e407910fc 100644 --- a/cookiecutter/repository.py +++ b/cookiecutter/repository.py @@ -87,7 +87,7 @@ def determine_repo_dir( :param password: The password to use when extracting the repository. :param directory: Directory within repo where cookiecutter.json lives. :return: A tuple containing the cookiecutter template directory, and - a boolean descriving whether that directory should be cleaned up + a boolean describing whether that directory should be cleaned up after the template has been instantiated. :raises: `RepositoryNotFound` if a repository directory could not be found. """ diff --git a/docs/tutorials/tutorial2.rst b/docs/tutorials/tutorial2.rst index 202412537..d16f6b7fa 100644 --- a/docs/tutorials/tutorial2.rst +++ b/docs/tutorials/tutorial2.rst @@ -89,7 +89,7 @@ You can expect similar output: project_slug [test_web]: author [Anonymous]: Cookiecutter Developer -Resulting directory should be inside your work directory with a name that matches `project_slug` you defined. Inside that direcory there should be `index.html` with generated source: +Resulting directory should be inside your work directory with a name that matches `project_slug` you defined. Inside that directory there should be `index.html` with generated source: .. code-block:: html diff --git a/setup.cfg b/setup.cfg index 938b39de6..f32b1830b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,6 +16,6 @@ testpaths = tests addopts = -vvv --cov-report term-missing --cov=cookiecutter [doc8] -# TODO: Remove current max-line-lengh ignore in follow-up and adopt black limit. +# TODO: Remove current max-line-length ignore in follow-up and adopt black limit. # max-line-length = 88 ignore = D001 diff --git a/tests/conftest.py b/tests/conftest.py index e21071304..0990c07d3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,10 +12,6 @@ cookiecutters_dir: '{cookiecutters_dir}' replay_dir: '{replay_dir}' """ -# In YAML, double quotes mean to use escape sequences. -# Single quotes mean we will have unescaped backslahes. -# http://blogs.perl.org/users/tinita/2018/03/ -# strings-in-yaml---to-quote-or-not-to-quote.html @pytest.fixture(autouse=True) @@ -38,7 +34,7 @@ def backup_dir(original_dir, backup_dir): if not os.path.isdir(original_dir): return False - # Remove existing backups before backing up. If they exist, they're stale. + # Remove existing stale backups before backing up. if os.path.isdir(backup_dir): utils.rmtree(backup_dir) @@ -48,12 +44,9 @@ def backup_dir(original_dir, backup_dir): def restore_backup_dir(original_dir, backup_dir, original_dir_found): """Restore default contents.""" - # Carefully delete the created original_dir only in certain - # conditions. original_dir_is_dir = os.path.isdir(original_dir) if original_dir_found: - # Delete the created original_dir as long as a backup - # exists + # Delete original_dir if a backup exists if original_dir_is_dir and os.path.isdir(backup_dir): utils.rmtree(original_dir) else: diff --git a/tests/test_cli.py b/tests/test_cli.py index 39e924093..e0308f06e 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -543,10 +543,7 @@ def test_debug_list_installed_templates(cli_runner, debug_file, user_config_path """Verify --list-installed command correct invocation.""" fake_template_dir = os.path.dirname(os.path.abspath('fake-project')) os.makedirs(os.path.dirname(user_config_path)) - # In YAML, double quotes mean to use escape sequences. - # Single quotes mean we will have unescaped backslahes. - # http://blogs.perl.org/users/tinita/2018/03/ - # strings-in-yaml---to-quote-or-not-to-quote.html + # Single quotes in YAML will not parse escape codes (\). Path(user_config_path).write_text(f"cookiecutters_dir: '{fake_template_dir}'") Path("fake-project", "cookiecutter.json").write_text('{}') From 60a049ebf0014d0fd88567b95539025d174bdd42 Mon Sep 17 00:00:00 2001 From: Mike Taves Date: Mon, 20 Jun 2022 00:23:21 +1200 Subject: [PATCH 234/274] Remove universal bdist_wheel option; use "python -m build" (#1739) --- Makefile | 8 ++++---- setup.cfg | 3 --- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index 0392aeb33..293aedef6 100644 --- a/Makefile +++ b/Makefile @@ -99,19 +99,19 @@ submodules: ## Pull and update git submodules recursively .PHONY: release release: clean ## Package and upload release @echo "+ $@" - @python setup.py sdist bdist_wheel + @python -m build @twine upload -r $(PYPI_SERVER) dist/* .PHONY: sdist sdist: clean ## Build sdist distribution @echo "+ $@" - @python setup.py sdist + @python -m build --sdist @ls -l dist .PHONY: wheel -wheel: clean ## Build bdist_wheel distribution +wheel: clean ## Build wheel distribution @echo "+ $@" - @python setup.py bdist_wheel + @python -m build --wheel @ls -l dist .PHONY: help diff --git a/setup.cfg b/setup.cfg index f32b1830b..3f29fe9a0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -8,9 +8,6 @@ statistics = 1 # black official is 88 max-line-length = 88 -[bdist_wheel] -universal = 1 - [tool:pytest] testpaths = tests addopts = -vvv --cov-report term-missing --cov=cookiecutter From dc95dd25c41fe8a2edd221606b6a16e95ab084e3 Mon Sep 17 00:00:00 2001 From: Tim Gates Date: Mon, 4 Jul 2022 02:37:09 +1000 Subject: [PATCH 235/274] docs: fix simple typo, shat -> that (#1749) There is a small typo in docs/conf.py. Should read `that` rather than `shat`. --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 9c2ffdaf4..562baddb0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -327,7 +327,7 @@ # The format is a list of tuples containing the path and title. # epub_pre_files = [] -# HTML files shat should be inserted after the pages created by sphinx. +# HTML files that should be inserted after the pages created by sphinx. # The format is a list of tuples containing the path and title. # epub_post_files = [] From e9b3b84125d7cc71c858604c12ceea70e881883f Mon Sep 17 00:00:00 2001 From: segunb Date: Mon, 1 Aug 2022 01:17:25 +0100 Subject: [PATCH 236/274] Fixed minor typos in docs (#1753) Authored-by: Segun Babalola --- docs/installation.rst | 2 +- docs/overview.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index 068c6d9d9..995fd077b 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -14,7 +14,7 @@ Python interpreter Install Python for your operating system. On Windows and macOS this is usually necessary. -Most Linux distributions are coming with Python pre-installed. +Most Linux distributions come with Python pre-installed. Consult the official `Python documentation `_ for details. You can install the Python binaries from `python.org `_. diff --git a/docs/overview.rst b/docs/overview.rst index 6c75f77fe..026cb1bb1 100644 --- a/docs/overview.rst +++ b/docs/overview.rst @@ -3,13 +3,13 @@ Overview ======== Cookiecutter takes a template provided as a directory structure with template-files. -Templates can be in located in the filesystem, as a ZIP-file or on a VCS-Server (Git/Hg) like GitHub. +Templates can be located in the filesystem, as a ZIP-file or on a VCS-Server (Git/Hg) like GitHub. It reads a settings file and prompts the user interactively whether or not to change the settings. Then it takes both and generates an output directory structure from it. -Additional the template can provide code (Python or shell-script) to be executed before and after generation (pre-gen- and post-gen-hooks). +Additionally the template can provide code (Python or shell-script) to be executed before and after generation (pre-gen- and post-gen-hooks). Input From cf81d63bf3d82e1739db73bcbed6f1012890e33e Mon Sep 17 00:00:00 2001 From: Thomas Meckel <14177833+tmeckel@users.noreply.github.com> Date: Fri, 9 Sep 2022 14:04:10 +0200 Subject: [PATCH 237/274] feat: Add resolved template repository path as _repo_dir to the context (#1771) * Changes to cookiecutter/main.py: * Add resolved template repository path as _repo_dir to the context * Changes to tests/test_cli.py: * corrected test test_echo_undefined_variable_error to check for new context item _repo_dir Co-authored-by: Thomas Meckel --- cookiecutter/main.py | 3 +++ tests/test_cli.py | 1 + 2 files changed, 4 insertions(+) diff --git a/cookiecutter/main.py b/cookiecutter/main.py index cb81a0b94..ba1df1b6c 100644 --- a/cookiecutter/main.py +++ b/cookiecutter/main.py @@ -109,6 +109,9 @@ def cookiecutter( # include template dir or url in the context dict context['cookiecutter']['_template'] = template + # include repo dir or url in the context dict + context['cookiecutter']['_repo_dir'] = repo_dir + # include output+dir in the context dict context['cookiecutter']['_output_dir'] = os.path.abspath(output_dir) diff --git a/tests/test_cli.py b/tests/test_cli.py index e0308f06e..0364e2cef 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -398,6 +398,7 @@ def test_echo_undefined_variable_error(output_dir, cli_runner): 'github_username': 'hackebrot', 'project_slug': 'testproject', '_template': template_path, + '_repo_dir': template_path, '_output_dir': output_dir, } } From c83bffc02219f0d311570769f57bc9e8b5b4a63f Mon Sep 17 00:00:00 2001 From: Christo De Lange Date: Fri, 3 Feb 2023 10:26:16 -0500 Subject: [PATCH 238/274] get rid of nested .git --- docs/HelloCookieCutter1/.gitignore | 47 ++++++ docs/HelloCookieCutter1/Readme.rst | 159 ++++++++++++++++++ docs/HelloCookieCutter1/cookiecutter.json | 5 + .../{{cookiecutter.file_name}}.py | 1 + 4 files changed, 212 insertions(+) create mode 100644 docs/HelloCookieCutter1/.gitignore create mode 100644 docs/HelloCookieCutter1/Readme.rst create mode 100644 docs/HelloCookieCutter1/cookiecutter.json create mode 100644 docs/HelloCookieCutter1/{{cookiecutter.directory_name}}/{{cookiecutter.file_name}}.py diff --git a/docs/HelloCookieCutter1/.gitignore b/docs/HelloCookieCutter1/.gitignore new file mode 100644 index 000000000..cd2946ad7 --- /dev/null +++ b/docs/HelloCookieCutter1/.gitignore @@ -0,0 +1,47 @@ +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# ========================= +# Operating System Files +# ========================= + +# OSX +# ========================= + +.DS_Store +.AppleDouble +.LSOverride + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk diff --git a/docs/HelloCookieCutter1/Readme.rst b/docs/HelloCookieCutter1/Readme.rst new file mode 100644 index 000000000..4005a1bb2 --- /dev/null +++ b/docs/HelloCookieCutter1/Readme.rst @@ -0,0 +1,159 @@ +=========================================================== +Learn the Basics of Cookiecutter by Creating a Cookiecutter +=========================================================== + +The easiest way to understand what Cookiecutter does is to create a simple one +and see how it works. + +Cookiecutter takes a source directory tree and copies it into your new +project. It replaces all the names that it finds surrounded by *templating +tags* ``{{`` and ``}}`` with names that it finds in the file +``cookiecutter.json``. That's basically it. [1]_ + +The replaced names can be file names, directory names, and strings inside +files. + +With Cookiecutter, you can easily bootstrap a new project from a standard +form, which means you skip all the usual mistakes when starting a new +project. + +Before you can do anything in this example, you must have Python installed on +your machine. Go to the `Python Website `_ and follow +the instructions there. This includes the ``pip`` installer tool. Now run: + +.. code-block:: bash + + $ pip install cookiecutter + +Your First Cookiecutter +----------------------- + +To get started, create a directory somewhere on your computer. The name of +this directory will be the name of your Cookiecutter template, but it doesn't +constrain anything else---the generated project doesn't need to use the +template name, for example. Our project will be called ``HelloCookieCutter1``: + +.. code-block:: bash + + $ mkdir HelloCookieCutter1 + $ cd HelloCookieCutter1 + +Inside this directory, we create the directory tree to be copied into the +generated project. We want to generate a name for this directory, so we put +the directory name in templating tags ``{{`` and ``}}`` (yes, you type the +double-curly-braces onto the command line, just as you see them here): + +.. code-block:: bash + + $ mkdir {{cookiecutter.directory_name}} + $ cd {{cookiecutter.directory_name}} + +Anything inside templating tags can be placed inside a *namespace*. Here, by +putting ``directory_name`` inside the ``cookiecutter`` namespace, +``cookiecutter.directory_name`` will be looked up from the ``cookiecutter.json`` +file as the project is generated by Cookiecutter. + +Now we are inside the directory tree that will be copied. For the simplest +possible Cookiecutter template, we'll just include a single file. Again, we +want the file name to be looked up from ``cookiecutter.json``, so we name it +appropriately: + +.. code-block:: bash + + $ touch {{cookiecutter.file_name}}.py + +(``touch`` creates an empty file; you can just open it up in your editor). Now +edit the file so it contains: + +.. code-block:: python + + print("Hello, {{cookiecutter.greeting_recipient}}!") + +To finish, we create the ``cookiecutter.json`` file itself, so that +Cookiecutter can look up all our templated items. This file goes in our +``HelloCookieCutter1`` directory, and contains all the names we've used: + +.. code-block:: json + + { + "directory_name": "Hello", + "file_name": "Howdy", + "greeting_recipient": "Julie" + } + +Now we can actually run Cookiecutter and create a new project from our +template. Move to a directory where you want to create the new project. Then +run Cookiecutter and hand it the directory where the template lives. On my +(Windows, so the slashes go back instead of forward) machine, this happens to +be under the ``Git`` directory: + +.. code-block:: bash + + $ cookiecutter C:\Users\bruce\Documents\Git\HelloCookieCutter1 + directory_name [Hello]: + file_name [Howdy]: + greeting_recipient [Julie]: + +Cookiecutter tells us what the default name for each item is, and gives us the +option of replacing that name with something new. In this case, I just pressed +``Return`` for each one, to accept all the defaults. + +Now we have a generated directory called ``Hello``, containing a file +``Howdy.py``. When we run it: + +.. code-block:: bash + + $ cd Hello + $ python Howdy.py + Hello, Julie! + +Voila! Instant generated project! + +**Note**: The project we've created here happens to be Python, but +Cookiecutter is just replacing templated items with names it looks up in +``cookiecutter.json``, so you can produce projects of any kind, including +projects that aren't programs. + +This is nice, but what if you want to share your Cookiecutter template with +everyone on the Internet? The easiest way is to upload it to a version control +repository. As you might have guessed by the ``Git`` subdirectory, this +example is on GitHub. Conveniently, Cookiecutter can build a project directly +from an internet repository, like the one for this very example. For variety, +this time we'll replace the values from ``cookiecutter.json`` with our own: + +.. code-block:: bash + + $ cookiecutter https://github.com/BruceEckel/HelloCookieCutter1 + Cloning into 'HelloCookieCutter1'... + remote: Counting objects: 37, done. + Unpacking objects: 21% (8/37) + remote: Total 37 (delta 19), reused 21 (delta 3), pack-reused 0 + Unpacking objects: 100% (37/37), done. + Checking connectivity... done. + directory_name [Hello]: Fabulous + file_name [Howdy]: Zing + greeting_recipient [Julie]: Roscoe + + $ cd Fabulous + + $ python Zing.py + Hello, Roscoe! + +Same effect, but this time produced from the Internet! You'll notice that even +though it says ``Cloning into 'HelloCookieCutter1'...``, you don't see any +directory called ``HelloCookieCutter1`` in your local directory. Cookiecutter +has its own storage area for cookiecutters, which is in your home directory +in a subdirectory called ``.cookiecutters`` (the leading ``.`` hides the directory +on most operating systems). You don't need to do anything with this directory +but it can sometimes be useful to know where it is. + +Now if you ever find yourself duplicating effort when starting new projects, +you'll know how to eliminate that duplication using cookiecutter. But even +better, lots of people have created and published cookiecutters, so when you +are starting a new project, make sure you look at the `list of pre-defined +cookiecutters +`_ +first! + +.. [1] You can also run *hooks* before and/or after generation, but that's + more complex than what we want to cover here. diff --git a/docs/HelloCookieCutter1/cookiecutter.json b/docs/HelloCookieCutter1/cookiecutter.json new file mode 100644 index 000000000..ca7500fa1 --- /dev/null +++ b/docs/HelloCookieCutter1/cookiecutter.json @@ -0,0 +1,5 @@ +{ + "directory_name": "Hello", + "file_name": "Howdy", + "greeting_recipient": "Julie" +} \ No newline at end of file diff --git a/docs/HelloCookieCutter1/{{cookiecutter.directory_name}}/{{cookiecutter.file_name}}.py b/docs/HelloCookieCutter1/{{cookiecutter.directory_name}}/{{cookiecutter.file_name}}.py new file mode 100644 index 000000000..1d2ab57cb --- /dev/null +++ b/docs/HelloCookieCutter1/{{cookiecutter.directory_name}}/{{cookiecutter.file_name}}.py @@ -0,0 +1 @@ +print("Hello, {{cookiecutter.greeting_recipient}}!") \ No newline at end of file From c8e86c9a4f2efdf7239436f6440bc689e28dd875 Mon Sep 17 00:00:00 2001 From: Christo De Lange Date: Fri, 3 Feb 2023 11:26:48 -0500 Subject: [PATCH 239/274] Revert "get rid of nested .git" This reverts commit c83bffc02219f0d311570769f57bc9e8b5b4a63f. --- docs/HelloCookieCutter1/.gitignore | 47 ------ docs/HelloCookieCutter1/Readme.rst | 159 ------------------ docs/HelloCookieCutter1/cookiecutter.json | 5 - .../{{cookiecutter.file_name}}.py | 1 - 4 files changed, 212 deletions(-) delete mode 100644 docs/HelloCookieCutter1/.gitignore delete mode 100644 docs/HelloCookieCutter1/Readme.rst delete mode 100644 docs/HelloCookieCutter1/cookiecutter.json delete mode 100644 docs/HelloCookieCutter1/{{cookiecutter.directory_name}}/{{cookiecutter.file_name}}.py diff --git a/docs/HelloCookieCutter1/.gitignore b/docs/HelloCookieCutter1/.gitignore deleted file mode 100644 index cd2946ad7..000000000 --- a/docs/HelloCookieCutter1/.gitignore +++ /dev/null @@ -1,47 +0,0 @@ -# Windows image file caches -Thumbs.db -ehthumbs.db - -# Folder config file -Desktop.ini - -# Recycle Bin used on file shares -$RECYCLE.BIN/ - -# Windows Installer files -*.cab -*.msi -*.msm -*.msp - -# Windows shortcuts -*.lnk - -# ========================= -# Operating System Files -# ========================= - -# OSX -# ========================= - -.DS_Store -.AppleDouble -.LSOverride - -# Thumbnails -._* - -# Files that might appear in the root of a volume -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns - -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk diff --git a/docs/HelloCookieCutter1/Readme.rst b/docs/HelloCookieCutter1/Readme.rst deleted file mode 100644 index 4005a1bb2..000000000 --- a/docs/HelloCookieCutter1/Readme.rst +++ /dev/null @@ -1,159 +0,0 @@ -=========================================================== -Learn the Basics of Cookiecutter by Creating a Cookiecutter -=========================================================== - -The easiest way to understand what Cookiecutter does is to create a simple one -and see how it works. - -Cookiecutter takes a source directory tree and copies it into your new -project. It replaces all the names that it finds surrounded by *templating -tags* ``{{`` and ``}}`` with names that it finds in the file -``cookiecutter.json``. That's basically it. [1]_ - -The replaced names can be file names, directory names, and strings inside -files. - -With Cookiecutter, you can easily bootstrap a new project from a standard -form, which means you skip all the usual mistakes when starting a new -project. - -Before you can do anything in this example, you must have Python installed on -your machine. Go to the `Python Website `_ and follow -the instructions there. This includes the ``pip`` installer tool. Now run: - -.. code-block:: bash - - $ pip install cookiecutter - -Your First Cookiecutter ------------------------ - -To get started, create a directory somewhere on your computer. The name of -this directory will be the name of your Cookiecutter template, but it doesn't -constrain anything else---the generated project doesn't need to use the -template name, for example. Our project will be called ``HelloCookieCutter1``: - -.. code-block:: bash - - $ mkdir HelloCookieCutter1 - $ cd HelloCookieCutter1 - -Inside this directory, we create the directory tree to be copied into the -generated project. We want to generate a name for this directory, so we put -the directory name in templating tags ``{{`` and ``}}`` (yes, you type the -double-curly-braces onto the command line, just as you see them here): - -.. code-block:: bash - - $ mkdir {{cookiecutter.directory_name}} - $ cd {{cookiecutter.directory_name}} - -Anything inside templating tags can be placed inside a *namespace*. Here, by -putting ``directory_name`` inside the ``cookiecutter`` namespace, -``cookiecutter.directory_name`` will be looked up from the ``cookiecutter.json`` -file as the project is generated by Cookiecutter. - -Now we are inside the directory tree that will be copied. For the simplest -possible Cookiecutter template, we'll just include a single file. Again, we -want the file name to be looked up from ``cookiecutter.json``, so we name it -appropriately: - -.. code-block:: bash - - $ touch {{cookiecutter.file_name}}.py - -(``touch`` creates an empty file; you can just open it up in your editor). Now -edit the file so it contains: - -.. code-block:: python - - print("Hello, {{cookiecutter.greeting_recipient}}!") - -To finish, we create the ``cookiecutter.json`` file itself, so that -Cookiecutter can look up all our templated items. This file goes in our -``HelloCookieCutter1`` directory, and contains all the names we've used: - -.. code-block:: json - - { - "directory_name": "Hello", - "file_name": "Howdy", - "greeting_recipient": "Julie" - } - -Now we can actually run Cookiecutter and create a new project from our -template. Move to a directory where you want to create the new project. Then -run Cookiecutter and hand it the directory where the template lives. On my -(Windows, so the slashes go back instead of forward) machine, this happens to -be under the ``Git`` directory: - -.. code-block:: bash - - $ cookiecutter C:\Users\bruce\Documents\Git\HelloCookieCutter1 - directory_name [Hello]: - file_name [Howdy]: - greeting_recipient [Julie]: - -Cookiecutter tells us what the default name for each item is, and gives us the -option of replacing that name with something new. In this case, I just pressed -``Return`` for each one, to accept all the defaults. - -Now we have a generated directory called ``Hello``, containing a file -``Howdy.py``. When we run it: - -.. code-block:: bash - - $ cd Hello - $ python Howdy.py - Hello, Julie! - -Voila! Instant generated project! - -**Note**: The project we've created here happens to be Python, but -Cookiecutter is just replacing templated items with names it looks up in -``cookiecutter.json``, so you can produce projects of any kind, including -projects that aren't programs. - -This is nice, but what if you want to share your Cookiecutter template with -everyone on the Internet? The easiest way is to upload it to a version control -repository. As you might have guessed by the ``Git`` subdirectory, this -example is on GitHub. Conveniently, Cookiecutter can build a project directly -from an internet repository, like the one for this very example. For variety, -this time we'll replace the values from ``cookiecutter.json`` with our own: - -.. code-block:: bash - - $ cookiecutter https://github.com/BruceEckel/HelloCookieCutter1 - Cloning into 'HelloCookieCutter1'... - remote: Counting objects: 37, done. - Unpacking objects: 21% (8/37) - remote: Total 37 (delta 19), reused 21 (delta 3), pack-reused 0 - Unpacking objects: 100% (37/37), done. - Checking connectivity... done. - directory_name [Hello]: Fabulous - file_name [Howdy]: Zing - greeting_recipient [Julie]: Roscoe - - $ cd Fabulous - - $ python Zing.py - Hello, Roscoe! - -Same effect, but this time produced from the Internet! You'll notice that even -though it says ``Cloning into 'HelloCookieCutter1'...``, you don't see any -directory called ``HelloCookieCutter1`` in your local directory. Cookiecutter -has its own storage area for cookiecutters, which is in your home directory -in a subdirectory called ``.cookiecutters`` (the leading ``.`` hides the directory -on most operating systems). You don't need to do anything with this directory -but it can sometimes be useful to know where it is. - -Now if you ever find yourself duplicating effort when starting new projects, -you'll know how to eliminate that duplication using cookiecutter. But even -better, lots of people have created and published cookiecutters, so when you -are starting a new project, make sure you look at the `list of pre-defined -cookiecutters -`_ -first! - -.. [1] You can also run *hooks* before and/or after generation, but that's - more complex than what we want to cover here. diff --git a/docs/HelloCookieCutter1/cookiecutter.json b/docs/HelloCookieCutter1/cookiecutter.json deleted file mode 100644 index ca7500fa1..000000000 --- a/docs/HelloCookieCutter1/cookiecutter.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "directory_name": "Hello", - "file_name": "Howdy", - "greeting_recipient": "Julie" -} \ No newline at end of file diff --git a/docs/HelloCookieCutter1/{{cookiecutter.directory_name}}/{{cookiecutter.file_name}}.py b/docs/HelloCookieCutter1/{{cookiecutter.directory_name}}/{{cookiecutter.file_name}}.py deleted file mode 100644 index 1d2ab57cb..000000000 --- a/docs/HelloCookieCutter1/{{cookiecutter.directory_name}}/{{cookiecutter.file_name}}.py +++ /dev/null @@ -1 +0,0 @@ -print("Hello, {{cookiecutter.greeting_recipient}}!") \ No newline at end of file From 4883b51bcc733f2443ddb0f49b4e3b77eed30a29 Mon Sep 17 00:00:00 2001 From: Christo De Lange Date: Fri, 3 Feb 2023 11:26:48 -0500 Subject: [PATCH 240/274] Revert "get rid of nested .git" This reverts commit c83bffc02219f0d311570769f57bc9e8b5b4a63f. --- docs/HelloCookieCutter1/.gitignore | 47 ------ docs/HelloCookieCutter1/Readme.rst | 159 ------------------ docs/HelloCookieCutter1/cookiecutter.json | 5 - .../{{cookiecutter.file_name}}.py | 1 - 4 files changed, 212 deletions(-) delete mode 100644 docs/HelloCookieCutter1/.gitignore delete mode 100644 docs/HelloCookieCutter1/Readme.rst delete mode 100644 docs/HelloCookieCutter1/cookiecutter.json delete mode 100644 docs/HelloCookieCutter1/{{cookiecutter.directory_name}}/{{cookiecutter.file_name}}.py diff --git a/docs/HelloCookieCutter1/.gitignore b/docs/HelloCookieCutter1/.gitignore deleted file mode 100644 index cd2946ad7..000000000 --- a/docs/HelloCookieCutter1/.gitignore +++ /dev/null @@ -1,47 +0,0 @@ -# Windows image file caches -Thumbs.db -ehthumbs.db - -# Folder config file -Desktop.ini - -# Recycle Bin used on file shares -$RECYCLE.BIN/ - -# Windows Installer files -*.cab -*.msi -*.msm -*.msp - -# Windows shortcuts -*.lnk - -# ========================= -# Operating System Files -# ========================= - -# OSX -# ========================= - -.DS_Store -.AppleDouble -.LSOverride - -# Thumbnails -._* - -# Files that might appear in the root of a volume -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns - -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk diff --git a/docs/HelloCookieCutter1/Readme.rst b/docs/HelloCookieCutter1/Readme.rst deleted file mode 100644 index 4005a1bb2..000000000 --- a/docs/HelloCookieCutter1/Readme.rst +++ /dev/null @@ -1,159 +0,0 @@ -=========================================================== -Learn the Basics of Cookiecutter by Creating a Cookiecutter -=========================================================== - -The easiest way to understand what Cookiecutter does is to create a simple one -and see how it works. - -Cookiecutter takes a source directory tree and copies it into your new -project. It replaces all the names that it finds surrounded by *templating -tags* ``{{`` and ``}}`` with names that it finds in the file -``cookiecutter.json``. That's basically it. [1]_ - -The replaced names can be file names, directory names, and strings inside -files. - -With Cookiecutter, you can easily bootstrap a new project from a standard -form, which means you skip all the usual mistakes when starting a new -project. - -Before you can do anything in this example, you must have Python installed on -your machine. Go to the `Python Website `_ and follow -the instructions there. This includes the ``pip`` installer tool. Now run: - -.. code-block:: bash - - $ pip install cookiecutter - -Your First Cookiecutter ------------------------ - -To get started, create a directory somewhere on your computer. The name of -this directory will be the name of your Cookiecutter template, but it doesn't -constrain anything else---the generated project doesn't need to use the -template name, for example. Our project will be called ``HelloCookieCutter1``: - -.. code-block:: bash - - $ mkdir HelloCookieCutter1 - $ cd HelloCookieCutter1 - -Inside this directory, we create the directory tree to be copied into the -generated project. We want to generate a name for this directory, so we put -the directory name in templating tags ``{{`` and ``}}`` (yes, you type the -double-curly-braces onto the command line, just as you see them here): - -.. code-block:: bash - - $ mkdir {{cookiecutter.directory_name}} - $ cd {{cookiecutter.directory_name}} - -Anything inside templating tags can be placed inside a *namespace*. Here, by -putting ``directory_name`` inside the ``cookiecutter`` namespace, -``cookiecutter.directory_name`` will be looked up from the ``cookiecutter.json`` -file as the project is generated by Cookiecutter. - -Now we are inside the directory tree that will be copied. For the simplest -possible Cookiecutter template, we'll just include a single file. Again, we -want the file name to be looked up from ``cookiecutter.json``, so we name it -appropriately: - -.. code-block:: bash - - $ touch {{cookiecutter.file_name}}.py - -(``touch`` creates an empty file; you can just open it up in your editor). Now -edit the file so it contains: - -.. code-block:: python - - print("Hello, {{cookiecutter.greeting_recipient}}!") - -To finish, we create the ``cookiecutter.json`` file itself, so that -Cookiecutter can look up all our templated items. This file goes in our -``HelloCookieCutter1`` directory, and contains all the names we've used: - -.. code-block:: json - - { - "directory_name": "Hello", - "file_name": "Howdy", - "greeting_recipient": "Julie" - } - -Now we can actually run Cookiecutter and create a new project from our -template. Move to a directory where you want to create the new project. Then -run Cookiecutter and hand it the directory where the template lives. On my -(Windows, so the slashes go back instead of forward) machine, this happens to -be under the ``Git`` directory: - -.. code-block:: bash - - $ cookiecutter C:\Users\bruce\Documents\Git\HelloCookieCutter1 - directory_name [Hello]: - file_name [Howdy]: - greeting_recipient [Julie]: - -Cookiecutter tells us what the default name for each item is, and gives us the -option of replacing that name with something new. In this case, I just pressed -``Return`` for each one, to accept all the defaults. - -Now we have a generated directory called ``Hello``, containing a file -``Howdy.py``. When we run it: - -.. code-block:: bash - - $ cd Hello - $ python Howdy.py - Hello, Julie! - -Voila! Instant generated project! - -**Note**: The project we've created here happens to be Python, but -Cookiecutter is just replacing templated items with names it looks up in -``cookiecutter.json``, so you can produce projects of any kind, including -projects that aren't programs. - -This is nice, but what if you want to share your Cookiecutter template with -everyone on the Internet? The easiest way is to upload it to a version control -repository. As you might have guessed by the ``Git`` subdirectory, this -example is on GitHub. Conveniently, Cookiecutter can build a project directly -from an internet repository, like the one for this very example. For variety, -this time we'll replace the values from ``cookiecutter.json`` with our own: - -.. code-block:: bash - - $ cookiecutter https://github.com/BruceEckel/HelloCookieCutter1 - Cloning into 'HelloCookieCutter1'... - remote: Counting objects: 37, done. - Unpacking objects: 21% (8/37) - remote: Total 37 (delta 19), reused 21 (delta 3), pack-reused 0 - Unpacking objects: 100% (37/37), done. - Checking connectivity... done. - directory_name [Hello]: Fabulous - file_name [Howdy]: Zing - greeting_recipient [Julie]: Roscoe - - $ cd Fabulous - - $ python Zing.py - Hello, Roscoe! - -Same effect, but this time produced from the Internet! You'll notice that even -though it says ``Cloning into 'HelloCookieCutter1'...``, you don't see any -directory called ``HelloCookieCutter1`` in your local directory. Cookiecutter -has its own storage area for cookiecutters, which is in your home directory -in a subdirectory called ``.cookiecutters`` (the leading ``.`` hides the directory -on most operating systems). You don't need to do anything with this directory -but it can sometimes be useful to know where it is. - -Now if you ever find yourself duplicating effort when starting new projects, -you'll know how to eliminate that duplication using cookiecutter. But even -better, lots of people have created and published cookiecutters, so when you -are starting a new project, make sure you look at the `list of pre-defined -cookiecutters -`_ -first! - -.. [1] You can also run *hooks* before and/or after generation, but that's - more complex than what we want to cover here. diff --git a/docs/HelloCookieCutter1/cookiecutter.json b/docs/HelloCookieCutter1/cookiecutter.json deleted file mode 100644 index ca7500fa1..000000000 --- a/docs/HelloCookieCutter1/cookiecutter.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "directory_name": "Hello", - "file_name": "Howdy", - "greeting_recipient": "Julie" -} \ No newline at end of file diff --git a/docs/HelloCookieCutter1/{{cookiecutter.directory_name}}/{{cookiecutter.file_name}}.py b/docs/HelloCookieCutter1/{{cookiecutter.directory_name}}/{{cookiecutter.file_name}}.py deleted file mode 100644 index 1d2ab57cb..000000000 --- a/docs/HelloCookieCutter1/{{cookiecutter.directory_name}}/{{cookiecutter.file_name}}.py +++ /dev/null @@ -1 +0,0 @@ -print("Hello, {{cookiecutter.greeting_recipient}}!") \ No newline at end of file From 6285196adc576da28e81372cce45902bfbc1c980 Mon Sep 17 00:00:00 2001 From: Christo De Lange Date: Fri, 3 Feb 2023 12:02:38 -0500 Subject: [PATCH 241/274] fix: pre-commit; hooks style; bandit /tmp --- .pre-commit-config.yaml | 2 +- cookiecutter/hooks.py | 45 +++++++++++++++++++++++++++++------------ cookiecutter/main.py | 1 + 3 files changed, 34 insertions(+), 14 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 96d89ad9a..e5d1e7f52 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,7 +35,7 @@ repos: - id: check-xml - id: check-yaml exclude: "not_rendered.yml|invalid-config.yaml" - - repo: https://gitlab.com/pycqa/flake8 + - repo: https://github.com/PyCQA/flake8 rev: 4.0.1 hooks: - id: flake8 diff --git a/cookiecutter/hooks.py b/cookiecutter/hooks.py index 26ae2faa9..0fb60dde3 100644 --- a/cookiecutter/hooks.py +++ b/cookiecutter/hooks.py @@ -105,20 +105,39 @@ def run_script_with_context(script_path, cwd, context): with open(script_path, encoding='utf-8') as file: contents = file.read() - temp_name = None - with tempfile.NamedTemporaryFile(delete=False, mode='wb', suffix=extension) as temp: - env = StrictEnvironment(context=context, keep_trailing_newline=True) - template = env.from_string(contents) - output = template.render(**context) - if os.getenv('COOKIECUTTER_DEBUG_HOOKS', None): - import pathlib - temp = tempfile.NamedTemporaryFile(delete=False, mode='wb', suffix=extension, dir='/tmp', prefix=os.path.basename(_)+'+') - temp = pathlib.Path(temp.name) + temp_name = None # Just to make sure it's defined in this scope. + env = StrictEnvironment(context=context, keep_trailing_newline=True) + template = env.from_string(contents) + output = template.render(**context) + if os.getenv('COOKIECUTTER_DEBUG_HOOKS', "").lower() in ( + "1", + "true", + "yes", + "on", + "enabled", + ): + import pathlib + + with tempfile.NamedTemporaryFile( + delete=False, + mode='wb', + suffix=extension, + dir=tempfile.gettempdir(), + prefix=os.path.basename(_) + '+', + ) as temp: + debug_temp = pathlib.Path(temp.name) temp.unlink() - temp = pathlib.Path(os.path.join(temp.parent, temp.stem.split('+')[0]+temp.suffix)) - temp.write_text(output, encoding='utf-8') - temp_name = str(temp) - else: + debug_temp = pathlib.Path( + os.path.join( + debug_temp.parent, debug_temp.stem.split('+')[0] + debug_temp.suffix + ) + ) + debug_temp.write_text(output, encoding='utf-8') + temp_name = str(debug_temp) + else: + with tempfile.NamedTemporaryFile( + delete=False, mode='wb', suffix=extension + ) as temp: temp.write(output.encode('utf-8')) temp_name = temp.name diff --git a/cookiecutter/main.py b/cookiecutter/main.py index d4c6dbb99..0d733e0ec 100644 --- a/cookiecutter/main.py +++ b/cookiecutter/main.py @@ -118,6 +118,7 @@ def cookiecutter( dump(config_dict['replay_dir'], template_name, context) from cookiecutter import __version__ as cookiecutter__version__ + context['__version__'] = cookiecutter__version__ # Create project from local context and project template. with import_patch: From b19dbb88c44bbf232a298a609df67939b64c001b Mon Sep 17 00:00:00 2001 From: Christo De Lange Date: Fri, 3 Feb 2023 13:13:26 -0500 Subject: [PATCH 242/274] fix: Yeah ... no --- cookiecutter/hooks.py | 61 +++++++++++++++++++++---------------------- cookiecutter/main.py | 3 --- 2 files changed, 30 insertions(+), 34 deletions(-) diff --git a/cookiecutter/hooks.py b/cookiecutter/hooks.py index 0fb60dde3..8408bedc7 100644 --- a/cookiecutter/hooks.py +++ b/cookiecutter/hooks.py @@ -106,38 +106,37 @@ def run_script_with_context(script_path, cwd, context): contents = file.read() temp_name = None # Just to make sure it's defined in this scope. - env = StrictEnvironment(context=context, keep_trailing_newline=True) - template = env.from_string(contents) - output = template.render(**context) - if os.getenv('COOKIECUTTER_DEBUG_HOOKS', "").lower() in ( - "1", - "true", - "yes", - "on", - "enabled", - ): - import pathlib - - with tempfile.NamedTemporaryFile( - delete=False, - mode='wb', - suffix=extension, - dir=tempfile.gettempdir(), - prefix=os.path.basename(_) + '+', - ) as temp: - debug_temp = pathlib.Path(temp.name) - temp.unlink() - debug_temp = pathlib.Path( - os.path.join( - debug_temp.parent, debug_temp.stem.split('+')[0] + debug_temp.suffix + with tempfile.NamedTemporaryFile(delete=False, mode='wb', suffix=extension) as temp: + env = StrictEnvironment(context=context, keep_trailing_newline=True) + template = env.from_string(contents) + output = template.render(**context) + temp.write(output.encode('utf-8')) + if os.getenv('COOKIECUTTER_DEBUG_HOOKS', "").lower() in ( + "1", + "true", + "yes", + "on", + "enabled", + ): + import pathlib + + with tempfile.NamedTemporaryFile( + delete=False, + mode='wb', + suffix=extension, + dir=tempfile.gettempdir(), + prefix=os.path.basename(_) + '+', + ) as temp: + debug_temp = pathlib.Path(temp.name) + temp.unlink() + debug_temp = pathlib.Path( + os.path.join( + debug_temp.parent, debug_temp.stem.split('+')[0] + debug_temp.suffix + ) ) - ) - debug_temp.write_text(output, encoding='utf-8') - temp_name = str(debug_temp) - else: - with tempfile.NamedTemporaryFile( - delete=False, mode='wb', suffix=extension - ) as temp: + debug_temp.write_text(output, encoding='utf-8') + temp_name = str(debug_temp) + else: temp.write(output.encode('utf-8')) temp_name = temp.name diff --git a/cookiecutter/main.py b/cookiecutter/main.py index 0d733e0ec..ba1df1b6c 100644 --- a/cookiecutter/main.py +++ b/cookiecutter/main.py @@ -117,9 +117,6 @@ def cookiecutter( dump(config_dict['replay_dir'], template_name, context) - from cookiecutter import __version__ as cookiecutter__version__ - - context['__version__'] = cookiecutter__version__ # Create project from local context and project template. with import_patch: result = generate_files( From bd18d5eb1833f97071b6356e2252d930ca7b094b Mon Sep 17 00:00:00 2001 From: Christo De Lange Date: Fri, 3 Feb 2023 13:35:33 -0500 Subject: [PATCH 243/274] fix: double write --- cookiecutter/hooks.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cookiecutter/hooks.py b/cookiecutter/hooks.py index 8408bedc7..2dd74caa6 100644 --- a/cookiecutter/hooks.py +++ b/cookiecutter/hooks.py @@ -110,7 +110,6 @@ def run_script_with_context(script_path, cwd, context): env = StrictEnvironment(context=context, keep_trailing_newline=True) template = env.from_string(contents) output = template.render(**context) - temp.write(output.encode('utf-8')) if os.getenv('COOKIECUTTER_DEBUG_HOOKS', "").lower() in ( "1", "true", From 28532debaad097e2fb73269ef445dafe6b7ad08c Mon Sep 17 00:00:00 2001 From: Christo De Lange Date: Fri, 3 Feb 2023 13:38:25 -0500 Subject: [PATCH 244/274] fix: cannot unlink here --- cookiecutter/hooks.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cookiecutter/hooks.py b/cookiecutter/hooks.py index 2dd74caa6..3633a2b0a 100644 --- a/cookiecutter/hooks.py +++ b/cookiecutter/hooks.py @@ -125,9 +125,8 @@ def run_script_with_context(script_path, cwd, context): suffix=extension, dir=tempfile.gettempdir(), prefix=os.path.basename(_) + '+', - ) as temp: - debug_temp = pathlib.Path(temp.name) - temp.unlink() + ) as debug_temp: + debug_temp = pathlib.Path(debug_temp.name) debug_temp = pathlib.Path( os.path.join( debug_temp.parent, debug_temp.stem.split('+')[0] + debug_temp.suffix From c6bb701e49dbc931b1214ad85b60140484954439 Mon Sep 17 00:00:00 2001 From: Christo De Lange Date: Fri, 3 Feb 2023 13:56:00 -0500 Subject: [PATCH 245/274] fix: Better debug hooks test --- cookiecutter/hooks.py | 16 +++++++--------- tests/test_hooks.py | 25 +++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/cookiecutter/hooks.py b/cookiecutter/hooks.py index 3633a2b0a..26ef87c59 100644 --- a/cookiecutter/hooks.py +++ b/cookiecutter/hooks.py @@ -110,20 +110,17 @@ def run_script_with_context(script_path, cwd, context): env = StrictEnvironment(context=context, keep_trailing_newline=True) template = env.from_string(contents) output = template.render(**context) - if os.getenv('COOKIECUTTER_DEBUG_HOOKS', "").lower() in ( - "1", - "true", - "yes", - "on", - "enabled", - ): + debug_hooks_path = os.getenv('COOKIECUTTER_DEBUG_HOOKS', None) + if debug_hooks_path: import pathlib - + debug_hooks_path = pathlib.Path(debug_hooks_path) + if not debug_hooks_path.exists(): + debug_hooks_path = tempfile.gettempdir() with tempfile.NamedTemporaryFile( delete=False, mode='wb', suffix=extension, - dir=tempfile.gettempdir(), + dir=debug_hooks_path, prefix=os.path.basename(_) + '+', ) as debug_temp: debug_temp = pathlib.Path(debug_temp.name) @@ -134,6 +131,7 @@ def run_script_with_context(script_path, cwd, context): ) debug_temp.write_text(output, encoding='utf-8') temp_name = str(debug_temp) + sys.stderr.write(f"DEBUG: Hook {script_path} rendered to {debug_temp}") else: temp.write(output.encode('utf-8')) temp_name = temp.name diff --git a/tests/test_hooks.py b/tests/test_hooks.py index 9abd66af2..361a1a3f8 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -1,4 +1,8 @@ """Tests for `cookiecutter.hooks` module.""" +import pathlib +import shutil +import tempfile + import errno import os import stat @@ -216,6 +220,27 @@ def test_run_hook(self): hooks.run_hook('post_gen_project', tests_dir, {}) assert os.path.isfile(os.path.join(tests_dir, 'shell_post.txt')) + def test_run_hook_debug(self): + """Execute hook from specified template in specified output \ + directory.""" + tests_dir = os.path.join(self.repo_path, 'input{{hooks}}') + assert os.path.isdir(tempfile.gettempdir()), tempfile.gettempdir() + debug_hooks_dir = os.path.join(tempfile.gettempdir(), 'cookiecutter-debug-hooks') + if os.path.isdir(debug_hooks_dir): + shutil.rmtree(debug_hooks_dir) + os.mkdir(debug_hooks_dir) + os.environ['COOKIECUTTER_DEBUG_HOOKS'] = debug_hooks_dir + with utils.work_in(self.repo_path): + hooks.run_hook('pre_gen_project', tests_dir, {}) + assert os.path.isfile(os.path.join(tests_dir, 'python_pre.txt')) + assert os.path.isfile(os.path.join(tests_dir, 'shell_pre.txt')) + + hooks.run_hook('post_gen_project', tests_dir, {}) + assert os.path.isfile(os.path.join(tests_dir, 'shell_post.txt')) + del os.environ['COOKIECUTTER_DEBUG_HOOKS'] + hook_traces = list(pathlib.Path(debug_hooks_dir).glob('*')) + assert len(hook_traces) > 2, os.system("ls -l " + debug_hooks_dir) + def test_run_failing_hook(self): """Test correct exception raise if hook exit code is not zero.""" hook_path = os.path.join(self.hooks_path, 'pre_gen_project.py') From fe37978a1fde194754befbb4416c7c14bcdef127 Mon Sep 17 00:00:00 2001 From: Christo De Lange Date: Fri, 3 Feb 2023 13:59:23 -0500 Subject: [PATCH 246/274] fix: lint --- cookiecutter/hooks.py | 4 +++- tests/test_hooks.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/cookiecutter/hooks.py b/cookiecutter/hooks.py index 26ef87c59..f8e805a7e 100644 --- a/cookiecutter/hooks.py +++ b/cookiecutter/hooks.py @@ -113,6 +113,7 @@ def run_script_with_context(script_path, cwd, context): debug_hooks_path = os.getenv('COOKIECUTTER_DEBUG_HOOKS', None) if debug_hooks_path: import pathlib + debug_hooks_path = pathlib.Path(debug_hooks_path) if not debug_hooks_path.exists(): debug_hooks_path = tempfile.gettempdir() @@ -126,7 +127,8 @@ def run_script_with_context(script_path, cwd, context): debug_temp = pathlib.Path(debug_temp.name) debug_temp = pathlib.Path( os.path.join( - debug_temp.parent, debug_temp.stem.split('+')[0] + debug_temp.suffix + debug_temp.parent, + debug_temp.stem.split('+')[0] + debug_temp.suffix, ) ) debug_temp.write_text(output, encoding='utf-8') diff --git a/tests/test_hooks.py b/tests/test_hooks.py index 361a1a3f8..0b2f7bd77 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -225,7 +225,9 @@ def test_run_hook_debug(self): directory.""" tests_dir = os.path.join(self.repo_path, 'input{{hooks}}') assert os.path.isdir(tempfile.gettempdir()), tempfile.gettempdir() - debug_hooks_dir = os.path.join(tempfile.gettempdir(), 'cookiecutter-debug-hooks') + debug_hooks_dir = os.path.join( + tempfile.gettempdir(), 'cookiecutter-debug-hooks' + ) if os.path.isdir(debug_hooks_dir): shutil.rmtree(debug_hooks_dir) os.mkdir(debug_hooks_dir) From cc69716de830dd47e7b9bbdbd01c6824fc9581ed Mon Sep 17 00:00:00 2001 From: Christo De Lange Date: Fri, 3 Feb 2023 14:11:18 -0500 Subject: [PATCH 247/274] fix: hook test coverage --- cookiecutter/hooks.py | 1 + tests/test_hooks.py | 24 +++++++++++++----------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/cookiecutter/hooks.py b/cookiecutter/hooks.py index f8e805a7e..8551df32d 100644 --- a/cookiecutter/hooks.py +++ b/cookiecutter/hooks.py @@ -117,6 +117,7 @@ def run_script_with_context(script_path, cwd, context): debug_hooks_path = pathlib.Path(debug_hooks_path) if not debug_hooks_path.exists(): debug_hooks_path = tempfile.gettempdir() + os.environ['COOKIECUTTER_DEBUG_HOOKS'] = debug_hooks_path with tempfile.NamedTemporaryFile( delete=False, mode='wb', diff --git a/tests/test_hooks.py b/tests/test_hooks.py index 0b2f7bd77..497e1e1ae 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -231,17 +231,19 @@ def test_run_hook_debug(self): if os.path.isdir(debug_hooks_dir): shutil.rmtree(debug_hooks_dir) os.mkdir(debug_hooks_dir) - os.environ['COOKIECUTTER_DEBUG_HOOKS'] = debug_hooks_dir - with utils.work_in(self.repo_path): - hooks.run_hook('pre_gen_project', tests_dir, {}) - assert os.path.isfile(os.path.join(tests_dir, 'python_pre.txt')) - assert os.path.isfile(os.path.join(tests_dir, 'shell_pre.txt')) - - hooks.run_hook('post_gen_project', tests_dir, {}) - assert os.path.isfile(os.path.join(tests_dir, 'shell_post.txt')) - del os.environ['COOKIECUTTER_DEBUG_HOOKS'] - hook_traces = list(pathlib.Path(debug_hooks_dir).glob('*')) - assert len(hook_traces) > 2, os.system("ls -l " + debug_hooks_dir) + for location in (debug_hooks_dir, '1'): + os.environ['COOKIECUTTER_DEBUG_HOOKS'] = location + with utils.work_in(self.repo_path): + hooks.run_hook('pre_gen_project', tests_dir, {}) + assert os.path.isfile(os.path.join(tests_dir, 'python_pre.txt')) + assert os.path.isfile(os.path.join(tests_dir, 'shell_pre.txt')) + + hooks.run_hook('post_gen_project', tests_dir, {}) + assert os.path.isfile(os.path.join(tests_dir, 'shell_post.txt')) + hook_traces = list(pathlib.Path(os.environ['COOKIECUTTER_DEBUG_HOOKS']).glob('*')) + assert len(hook_traces) > 2, os.system("ls -l " + os.environ['COOKIECUTTER_DEBUG_HOOKS']) + del os.environ['COOKIECUTTER_DEBUG_HOOKS'] + shutil.rmtree(debug_hooks_dir) def test_run_failing_hook(self): """Test correct exception raise if hook exit code is not zero.""" From 129f50b81636d97624972ba1c86dc7643f13476c Mon Sep 17 00:00:00 2001 From: Christo De Lange Date: Fri, 3 Feb 2023 14:14:51 -0500 Subject: [PATCH 248/274] fix: lint --- tests/test_hooks.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/test_hooks.py b/tests/test_hooks.py index 497e1e1ae..03795cab5 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -237,11 +237,15 @@ def test_run_hook_debug(self): hooks.run_hook('pre_gen_project', tests_dir, {}) assert os.path.isfile(os.path.join(tests_dir, 'python_pre.txt')) assert os.path.isfile(os.path.join(tests_dir, 'shell_pre.txt')) - + hooks.run_hook('post_gen_project', tests_dir, {}) assert os.path.isfile(os.path.join(tests_dir, 'shell_post.txt')) - hook_traces = list(pathlib.Path(os.environ['COOKIECUTTER_DEBUG_HOOKS']).glob('*')) - assert len(hook_traces) > 2, os.system("ls -l " + os.environ['COOKIECUTTER_DEBUG_HOOKS']) + hook_traces = list( + pathlib.Path(os.environ['COOKIECUTTER_DEBUG_HOOKS']).glob('*') + ) + assert len(hook_traces) > 2, os.system( + "ls -l " + os.environ['COOKIECUTTER_DEBUG_HOOKS'] + ) del os.environ['COOKIECUTTER_DEBUG_HOOKS'] shutil.rmtree(debug_hooks_dir) From 3af76befe9886cc17bb84faa1da1893343dfb051 Mon Sep 17 00:00:00 2001 From: Christo De Lange Date: Fri, 3 Feb 2023 14:35:56 -0500 Subject: [PATCH 249/274] chore: cleanup crud --- .python-version | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .python-version diff --git a/.python-version b/.python-version deleted file mode 100644 index c8e988bc2..000000000 --- a/.python-version +++ /dev/null @@ -1 +0,0 @@ -cookiecutter From d00e4240992705361f932e9d4d0cd343065e9812 Mon Sep 17 00:00:00 2001 From: Christo De Lange Date: Fri, 3 Feb 2023 16:53:56 -0500 Subject: [PATCH 250/274] fix: Tests broken by progress since this feature was developed --- cookiecutter/context.py | 7 +++- .../{{cookiecutter.repo_name}}/file | 0 tests/test_context.py | 27 +++++++------- tests/test_main.py | 37 +++++++++++++++---- 4 files changed, 50 insertions(+), 21 deletions(-) create mode 100644 tests/test-generate-context-v2/min-v2-cookiecutter/{{cookiecutter.repo_name}}/file diff --git a/cookiecutter/context.py b/cookiecutter/context.py index 6818c673e..5fcf3ee2c 100644 --- a/cookiecutter/context.py +++ b/cookiecutter/context.py @@ -21,7 +21,9 @@ import logging import collections import json +import posix import re +import sys import click from jinja2 import Environment @@ -556,7 +558,10 @@ def load_context(json_object, no_input=False, verbose=True): break if verbose: - width, _ = click.get_terminal_size() + if sys.stdout.isatty(): + width, _ = posix.get_terminal_size() + else: + width = 80 click.echo('-' * width) context[variable.name] = deserialize(value) diff --git a/tests/test-generate-context-v2/min-v2-cookiecutter/{{cookiecutter.repo_name}}/file b/tests/test-generate-context-v2/min-v2-cookiecutter/{{cookiecutter.repo_name}}/file new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_context.py b/tests/test_context.py index 28bc5f184..3ea99cf31 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -583,19 +583,20 @@ def test_variable_str(): validation_flags=['ignorecase'], hide_input=True) - assert ':' in str(v) - assert "name='module_name'" in str(v) - assert "default='{{cookiecutter.plugin_name|lower|replace('-','_')}}'" in str(v) - assert "description='None'" in str(v) - assert "prompt='Please enter a name for your base python module'" in str(v) - assert "hide_input='True'" in str(v) - assert "var_type='string'" in str(v) - assert "skip_if=''" in str(v) - assert "prompt_user='True'" in str(v) - assert "choices='[]'" in str(v) - assert "validation='^[a-z_]+$'" in str(v) - assert "validation_flag_names='['ignorecase']'" in str(v) - assert "validation_flags='2'" in str(v) + str_v = str(v) + assert ':' in str_v + assert "name='module_name'" in str_v + assert "default='{{cookiecutter.plugin_name|lower|replace('-','_')}}'" in str_v + assert "description='None'" in str_v + assert "prompt='Please enter a name for your base python module'" in str_v + assert "hide_input='True'" in str_v + assert "var_type='string'" in str_v + assert "skip_if=''" in str_v + assert "prompt_user='True'" in str_v + assert "choices='[]'" in str_v + assert "validation='^[a-z_]+$'" in str_v + assert "validation_flag_names='['ignorecase']'" in str_v + assert "validation_flags='2'" in str_v or "validation_flags='re.IGNORECASE'" in str_v if sys.version_info >= (3, 4): assert "validate='re.compile('^[a-z_]+$', re.IGNORECASE)'" in str(v) diff --git a/tests/test_main.py b/tests/test_main.py index fc4eb96fa..c7f25d52a 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,4 +1,10 @@ """Collection of tests around cookiecutter's replay feature.""" +import collections +import os +import pathlib +import shutil + +from cookiecutter.find import find_template from cookiecutter.main import cookiecutter @@ -71,13 +77,28 @@ def test_version_2_load_context_call( for this test and call cookiecutter with '.' for the target template. """ monkeypatch.chdir('tests/test-generate-context-v2/min-v2-cookiecutter') - + if os.path.exists('test-repo'): + shutil.rmtree('test-repo') mock_replay_dump = mocker.patch('cookiecutter.main.dump') - mock_version_1_prompt_for_config = mocker.patch( - 'cookiecutter.main.prompt_for_config') - mock_version_2_load_context = mocker.patch( - 'cookiecutter.main.load_context') + counts = {} + def patch_load_context(counts): + counts['load_context'] = 0 + def load_context(json_object, no_input=False, verbose=True, counts=counts): + counts["load_context"] += 1 + return collections.OrderedDict({ + 'repo_name': 'test-repo', + }) + return load_context + + def patch_prompt_for_config(counts): + counts['prompt_for_config'] = 0 + def prompt_for_config(context, no_input=False): + counts["prompt_for_config"] += 1 + return {} + + mocker.patch('cookiecutter.main.prompt_for_config', patch_prompt_for_config(counts)) + mocker.patch('cookiecutter.main.load_context', patch_load_context(counts)) cookiecutter( '.', @@ -86,9 +107,11 @@ def test_version_2_load_context_call( config_file=user_config_file, ) - assert mock_version_1_prompt_for_config.call_count == 0 - assert mock_version_2_load_context.call_count == 1 + if os.path.exists('test-repo'): + shutil.rmtree('test-repo') assert mock_replay_dump.call_count == 1 + assert counts["load_context"] == 1 + assert counts["prompt_for_config"] == 0 def test_custom_replay_file(monkeypatch, mocker, user_config_file): From 09b5c9716f040a36d70cb0879fd170597e89e224 Mon Sep 17 00:00:00 2001 From: Christo De Lange Date: Fri, 3 Feb 2023 17:04:05 -0500 Subject: [PATCH 251/274] fix: lint checks --- cookiecutter/context.py | 81 +- cookiecutter/generate.py | 58 +- cookiecutter/main.py | 8 +- tests/test_context.py | 215 +++-- tests/test_generate_context_v2.py | 1338 +++++++++++++++++++---------- tests/test_main.py | 17 +- 6 files changed, 1132 insertions(+), 585 deletions(-) diff --git a/cookiecutter/context.py b/cookiecutter/context.py index 5fcf3ee2c..6b65c6b3a 100644 --- a/cookiecutter/context.py +++ b/cookiecutter/context.py @@ -67,8 +67,7 @@ def context_is_version_2(cookiecutter_context): # This really is not sufficient since a v1 context could define each of # these fields; perhaps a more thorough test would be to also check if the # 'variables' field was defined as a list of OrderedDict items. - if (cookiecutter_context.keys() & - SET_OF_REQUIRED_FIELDS) == SET_OF_REQUIRED_FIELDS: + if (cookiecutter_context.keys() & SET_OF_REQUIRED_FIELDS) == SET_OF_REQUIRED_FIELDS: return True else: return False @@ -186,16 +185,17 @@ def prompt_yes_no(variable, default): def prompt_choice(variable, default): """Returns prompt, default and callback for a choice variable""" choice_map = collections.OrderedDict( - (u'{}'.format(i), value) - for i, value in enumerate(variable.choices, 1) + (u'{}'.format(i), value) for i, value in enumerate(variable.choices, 1) ) choices = choice_map.keys() - prompt = u'\n'.join(( - variable.prompt, - u'\n'.join([u'{} - {}'.format(*c) for c in choice_map.items()]), - u'Choose from {}'.format(u', '.join(choices)), - )) + prompt = u'\n'.join( + ( + variable.prompt, + u'\n'.join([u'{} - {}'.format(*c) for c in choice_map.items()]), + u'Choose from {}'.format(u', '.join(choices)), + ) + ) default = str(variable.choices.index(default) + 1) user_choice = click.prompt( @@ -336,7 +336,9 @@ def __init__(self, name, default, **info): self.description = self.check_type('description', None, str) # -- PROMPT ---------------------------------------------------------- - self.prompt = self.check_type('prompt', DEFAULT_PROMPT.format(variable=self), str) + self.prompt = self.check_type( + 'prompt', DEFAULT_PROMPT.format(variable=self), str + ) # -- HIDE_INPUT ------------------------------------------------------ self.hide_input = self.check_type('hide_input', False, bool) @@ -345,9 +347,11 @@ def __init__(self, name, default, **info): self.var_type = info.get('type', 'string') if self.var_type not in VALID_TYPES: msg = 'Variable: {var_name} has an invalid type {var_type}. Valid types are: {types}' - raise ValueError(msg.format(var_type=self.var_type, - var_name=self.name, - types=VALID_TYPES)) + raise ValueError( + msg.format( + var_type=self.var_type, var_name=self.name, types=VALID_TYPES + ) + ) # -- SKIP_IF --------------------------------------------------------- self.skip_if = self.check_type('skip_if', '', str) @@ -380,7 +384,11 @@ def __init__(self, name, default, **info): self.choices = self.check_type('choices', [], list) if self.choices and default not in self.choices: msg = "Variable: {var_name} has an invalid default value {default} for choices: {choices}." - raise ValueError(msg.format(var_name=self.name, default=self.default, choices=self.choices)) + raise ValueError( + msg.format( + var_name=self.name, default=self.default, choices=self.choices + ) + ) # -- VALIDATION STARTS ----------------------------------------------- self.validation = self.check_type('validation', None, str) @@ -395,10 +403,15 @@ def __init__(self, name, default, **info): if vflag in REGEX_COMPILE_FLAGS.keys(): self.validation_flags |= REGEX_COMPILE_FLAGS[vflag] else: - msg = "Variable: {var_name} - Ignoring unkown RegEx validation Control Flag named '{flag}'\n" \ - "Legal flag names are: {names}" - logger.warn(msg.format(var_name=self.name, flag=vflag, - names=REGEX_COMPILE_FLAGS.keys())) + msg = ( + "Variable: {var_name} - Ignoring unkown RegEx validation Control Flag named '{flag}'\n" + "Legal flag names are: {names}" + ) + logger.warn( + msg.format( + var_name=self.name, flag=vflag, names=REGEX_COMPILE_FLAGS.keys() + ) + ) self.validation_flag_names.remove(vflag) self.validate = None @@ -407,8 +420,9 @@ def __init__(self, name, default, **info): self.validate = re.compile(self.validation, self.validation_flags) except re.error as e: msg = "Variable: {var_name} - Validation Setup Error: Invalid RegEx '{value}' - does not compile - {err}" - raise ValueError(msg.format(var_name=self.name, - value=self.validation, err=e)) + raise ValueError( + msg.format(var_name=self.name, value=self.validation, err=e) + ) # -- VALIDATION ENDS ------------------------------------------------- def __repr__(self): @@ -418,7 +432,11 @@ def __repr__(self): ) def __str__(self): - s = ["{key}='{value}'".format(key=key, value=self.__dict__[key]) for key in self.__dict__ if key != 'info'] + s = [ + "{key}='{value}'".format(key=key, value=self.__dict__[key]) + for key in self.__dict__ + if key != 'info' + ] return self.__repr__() + ':\n' + ',\n'.join(s) def check_type(self, option_name, option_default_value, option_type): @@ -431,7 +449,14 @@ def check_type(self, option_name, option_default_value, option_type): if option_value is not None: if not isinstance(option_value, option_type): msg = "Variable: '{var_name}' Option: '{opt_name}' requires a value of type {type_name}, but has a value of: {value}" - raise ValueError(msg.format(var_name=self.name, opt_name=option_name, type_name=option_type.__name__, value=option_value)) + raise ValueError( + msg.format( + var_name=self.name, + opt_name=option_name, + type_name=option_type.__name__, + value=option_value, + ) + ) return option_value @@ -498,7 +523,7 @@ def load_context(json_object, no_input=False, verbose=True): if True, no input prompts are made, all defaults are accepted. :param verbose: Emit maximum varible information. """ - env = Environment(extensions=['jinja2_time.TimeExtension']) + env = Environment(extensions=['jinja2_time.TimeExtension'], autoescape=True) context = collections.OrderedDict({}) skip_to_variable_name = None @@ -549,7 +574,9 @@ def load_context(json_object, no_input=False, verbose=True): if variable.validate.match(value): break else: - msg = "Input validation failure against regex: '{val_string}', try again!".format(val_string=variable.validation) + msg = "Input validation failure against regex: '{val_string}', try again!".format( + val_string=variable.validation + ) click.echo(msg) if variable.validation_msg: click.echo(variable.validation_msg) @@ -573,6 +600,10 @@ def load_context(json_object, no_input=False, verbose=True): skip_to_variable_name = variable.if_no_skip_to if skip_to_variable_name: - logger.warn("Processed all variables, but skip_to_variable_name '{}' was never found.".format(skip_to_variable_name)) + logger.warn( + "Processed all variables, but skip_to_variable_name '{}' was never found.".format( + skip_to_variable_name + ) + ) return context diff --git a/cookiecutter/generate.py b/cookiecutter/generate.py index 451b0260e..2e948494f 100644 --- a/cookiecutter/generate.py +++ b/cookiecutter/generate.py @@ -23,7 +23,7 @@ from cookiecutter.hooks import run_hook from cookiecutter.utils import make_sure_path_exists, rmtree, work_in -from .context import context_is_version_2 +from cookiecutter.context import context_is_version_2 logger = logging.getLogger(__name__) @@ -80,13 +80,15 @@ def apply_overwrites_to_context(context, overwrite_context): def apply_default_overwrites_to_context_v2(context, overwrite_default_context): - """ + """V2 context overwrites. + Modify the given version 2 context in place based on the overwrite_default_context. """ - for variable, overwrite in overwrite_default_context.items(): - var_dict = next((d for d in context['variables'] if d['name'] == variable), None) # noqa + var_dict = next( + (d for d in context['variables'] if d['name'] == variable), None + ) # noqa if var_dict: if 'choices' in var_dict.keys(): context_value = var_dict['choices'] @@ -108,7 +110,8 @@ def apply_default_overwrites_to_context_v2(context, overwrite_default_context): def resolve_changed_variable_names(context, variables_to_resolve): - """ + """Resolve changed variable names. + The variable names contained in the variables_to_resolve dictionary's key names have been over-written with keys' value. Check the entire context and update any other variable context fields that may still @@ -122,14 +125,18 @@ def resolve_changed_variable_names(context, variables_to_resolve): for field_name in variable.keys(): if isinstance(variable[field_name], str): if var_name_to_resolve in variable[field_name]: - variable[field_name] = variable[field_name].replace(var_name_to_resolve, new_var_name) # noqa + variable[field_name] = variable[field_name].replace( + var_name_to_resolve, new_var_name + ) # noqa elif isinstance(variable[field_name], list): # a choices field could have an str item to update for i, item in enumerate(variable[field_name]): if isinstance(item, str): if var_name_to_resolve in item: - variable[field_name][i] = item.replace(var_name_to_resolve, new_var_name) # noqa + variable[field_name][i] = item.replace( + var_name_to_resolve, new_var_name + ) # noqa def apply_overwrites_to_context_v2(context, extra_context): @@ -221,16 +228,25 @@ def apply_overwrites_to_context_v2(context, extra_context): except IndexError: replace_name = None - var_dict = next((d for d in context['variables'] if d['name'] == xtra_ctx_name), None) # noqa + var_dict = next( + (d for d in context['variables'] if d['name'] == xtra_ctx_name), + None, + ) # noqa if var_dict: # Since creation of new key/value pairs is NOT # desired, we only use a key that is common to both # the variables context and the extra context. - common_keys = [key for key in xtra_ctx_item.keys() if key in var_dict.keys()] # noqa + common_keys = [ + key + for key in xtra_ctx_item.keys() + if key in var_dict.keys() + ] # noqa for key in common_keys: if xtra_ctx_item[key] == '<>': if key in ['default']: - raise ValueError("Cannot remove mandatory 'default' field") # noqa + raise ValueError( + "Cannot remove mandatory 'default' field" + ) # noqa var_dict.pop(key, None) else: # normal field update @@ -240,31 +256,39 @@ def apply_overwrites_to_context_v2(context, extra_context): # house-keeping to do. The default/choices # house-keeping could effecively be no-ops if the # user did the correct thing. - if ('default' in common_keys) & ('choices' in var_dict.keys()): # noqa + if ('default' in common_keys) & ( + 'choices' in var_dict.keys() + ): # noqa # default updated, regardless if choices has been # updated, re-order choices based on default if var_dict['default'] in var_dict['choices']: - var_dict['choices'].remove(var_dict['default']) # noqa + var_dict['choices'].remove(var_dict['default']) # noqa var_dict['choices'].insert(0, var_dict['default']) - if ('default' not in common_keys) & ('choices' in common_keys): # noqa + if ('default' not in common_keys) & ( + 'choices' in common_keys + ): # noqa # choices updated, so update default based on # first location in choices var_dict['default'] = var_dict['choices'][0] if replace_name: - variable_names_to_resolve[xtra_ctx_name] = replace_name # noqa + variable_names_to_resolve[ + xtra_ctx_name + ] = replace_name # noqa var_dict['name'] = replace_name else: msg = "No variable found in context whose name matches extra context name '{name}'" # noqa raise ValueError(msg.format(name=xtra_ctx_name)) else: - msg = "Extra context dictionary item {item} is missing a 'name' key." # noqa + msg = "Extra context dictionary item {item} is missing a 'name' key." # noqa raise ValueError(msg.format(item=xtra_ctx_item)) else: - msg = "Extra context list item '{item}' is of type {t}, should be a dictionary." # noqa - raise ValueError(msg.format(item=str(xtra_ctx_item), t=type(xtra_ctx_item).__name__)) # noqa + msg = "Extra context list item '{item}' is of type {t}, should be a dictionary." # noqa + raise ValueError( + msg.format(item=str(xtra_ctx_item), t=type(xtra_ctx_item).__name__) + ) # noqa if variable_names_to_resolve: # At least one variable name has been over-written, if any diff --git a/cookiecutter/main.py b/cookiecutter/main.py index 20358f57c..adca601aa 100644 --- a/cookiecutter/main.py +++ b/cookiecutter/main.py @@ -17,7 +17,7 @@ from cookiecutter.repository import determine_repo_dir from cookiecutter.utils import rmtree -from .context import context_is_version_2, load_context +from cookiecutter.context import context_is_version_2, load_context logger = logging.getLogger(__name__) @@ -106,9 +106,9 @@ def cookiecutter( # prompt the user to manually configure at the command line. # except when 'no-input' flag is set if context_is_version_2(context['cookiecutter']): - context['cookiecutter'] = load_context(context[u'cookiecutter'], - no_input=no_input, - verbose=True) + context['cookiecutter'] = load_context( + context[u'cookiecutter'], no_input=no_input, verbose=True + ) else: with import_patch: context['cookiecutter'] = prompt_for_config(context, no_input) diff --git a/tests/test_context.py b/tests/test_context.py index 3ea99cf31..2d7b0db1e 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -20,9 +20,7 @@ from cookiecutter import context -from cookiecutter.exceptions import ( - ContextDecodingException -) +from cookiecutter.exceptions import ContextDecodingException import click @@ -44,7 +42,8 @@ def load_cookiecutter(cookiecutter_file): json_exc_message = str(e) our_exc_message = ( 'JSON decoding error while loading "{0}". Decoding' - ' error details: "{1}"'.format(full_fpath, json_exc_message)) + ' error details: "{1}"'.format(full_fpath, json_exc_message) + ) raise ContextDecodingException(our_exc_message) # Add the Python object to the context dictionary @@ -58,43 +57,45 @@ def load_cookiecutter(cookiecutter_file): def context_data_check(): context_all_reqs = ( { - 'cookiecutter_context': OrderedDict([ - ("name", "cookiecutter-pytest-plugin"), - ("cookiecutter_version", "2.0.0"), - ("variables", []) - ]) + 'cookiecutter_context': OrderedDict( + [ + ("name", "cookiecutter-pytest-plugin"), + ("cookiecutter_version", "2.0.0"), + ("variables", []), + ] + ) }, - True + True, ) context_missing_name = ( { - 'cookiecutter_context': OrderedDict([ - ("cookiecutter_version", "2.0.0"), - ("variables", []) - ]) + 'cookiecutter_context': OrderedDict( + [("cookiecutter_version", "2.0.0"), ("variables", [])] + ) }, - False + False, ) context_missing_cookiecutter_version = ( { - 'cookiecutter_context': OrderedDict([ - ("name", "cookiecutter-pytest-plugin"), - ("variables", []) - ]) + 'cookiecutter_context': OrderedDict( + [("name", "cookiecutter-pytest-plugin"), ("variables", [])] + ) }, - False + False, ) context_missing_variables = ( { - 'cookiecutter_context': OrderedDict([ - ("name", "cookiecutter-pytest-plugin"), - ("cookiecutter_version", "2.0.0"), - ]) + 'cookiecutter_context': OrderedDict( + [ + ("name", "cookiecutter-pytest-plugin"), + ("cookiecutter_version", "2.0.0"), + ] + ) }, - False + False, ) yield context_all_reqs @@ -126,21 +127,23 @@ def test_load_context_defaults(): assert cc_cfg['module_name'] == 'emoji' assert cc_cfg['license'] == 'MIT' assert cc_cfg['docs'] is False - assert 'docs_tool' not in cc_cfg.keys() # skip_if worked + assert 'docs_tool' not in cc_cfg.keys() # skip_if worked assert cc_cfg['year'] == time.strftime('%Y') assert cc_cfg['incept_year'] == 2017 assert cc_cfg['released'] is False assert cc_cfg['temperature'] == 77.3 assert cc_cfg['Release-GUID'] == UUID('04f5eaa9ee7345469dccffc538b27194') assert cc_cfg['extensions'] == "['jinja2_time.TimeExtension']" - assert cc_cfg['copy_with_out_render'] == "['*.html', '*not_rendered_dir', 'rendered_dir/not_rendered_file.ini']" - assert cc_cfg['fixtures'] == OrderedDict([('foo', - OrderedDict([('scope', 'session'), - ('autouse', True)])), - ('bar', - OrderedDict([('scope', 'function'), - ('autouse', - False)]))]) + assert ( + cc_cfg['copy_with_out_render'] + == "['*.html', '*not_rendered_dir', 'rendered_dir/not_rendered_file.ini']" + ) + assert cc_cfg['fixtures'] == OrderedDict( + [ + ('foo', OrderedDict([('scope', 'session'), ('autouse', True)])), + ('bar', OrderedDict([('scope', 'function'), ('autouse', False)])), + ] + ) @pytest.mark.usefixtures('clean_system') @@ -179,10 +182,10 @@ def test_load_context_defaults_skips_no_branch(): assert cc_cfg['project_config_format'] == 'toml' # not skipped assert cc_cfg['project_uses_existing_logging_facilities'] is False - assert cc_cfg['project_logging_enabled'] is True # not skipped + assert cc_cfg['project_logging_enabled'] is True # not skipped assert cc_cfg['project_console_logging_enabled'] is True # not skipped assert cc_cfg['project_console_logging_level'] == 'WARN' # not skipped - assert cc_cfg['project_file_logging_enabled'] is True # not skipped + assert cc_cfg['project_file_logging_enabled'] is True # not skipped assert 'project_file_logging_level' not in cc_cfg.keys() # do_if skipped @@ -210,7 +213,10 @@ def test_load_context_defaults_skips_unknown_variable_name_warning(caplog): for record in caplog.records: assert record.levelname == 'WARNING' - assert "Processed all variables, but skip_to_variable_name 'this_variable_name_is_not_in_the_list' was never found." in caplog.text + assert ( + "Processed all variables, but skip_to_variable_name 'this_variable_name_is_not_in_the_list' was never found." + in caplog.text + ) def test_prompt_string(mocker): @@ -251,7 +257,9 @@ def test_prompt_bool(mocker): m = mocker.Mock() m.side_effect = context.Variable - v = m.side_effect(name='flag', default=False, prompt='Enter a Flag', hide_input=False) + v = m.side_effect( + name='flag', default=False, prompt='Enter a Flag', hide_input=False + ) r = context.prompt_boolean(v, default=False) @@ -262,7 +270,7 @@ def test_prompt_bool(mocker): type=click.BOOL, ) - assert r # EXPECTED_VALUE + assert r # EXPECTED_VALUE def test_prompt_int(mocker): @@ -329,7 +337,9 @@ def test_prompt_uuid(mocker): m = mocker.Mock() m.side_effect = context.Variable - v = m.side_effect(name='uuid', default=None, prompt='Enter a UUID', hide_input=False) + v = m.side_effect( + name='uuid', default=None, prompt='Enter a UUID', hide_input=False + ) r = context.prompt_uuid(v, default=None) @@ -354,7 +364,9 @@ def test_prompt_json(monkeypatch, mocker): ) m = mocker.Mock() m.side_effect = context.Variable - v = m.side_effect(name='json', default=None, prompt='Enter Config', hide_input=False) + v = m.side_effect( + name='json', default=None, prompt='Enter Config', hide_input=False + ) r = context.prompt_json(v, default=None) @@ -369,11 +381,13 @@ def test_prompt_json_bad_json_decode_click_asks_again(mocker, capsys): mocker.patch( 'click.termui.visible_prompt_func', autospec=True, - side_effect=[EXPECTED_BAD_VALUE, EXPECTED_GOOD_VALUE] + side_effect=[EXPECTED_BAD_VALUE, EXPECTED_GOOD_VALUE], ) m = mocker.Mock() m.side_effect = context.Variable - v = m.side_effect(name='json', default=None, prompt='Enter Config', hide_input=False) + v = m.side_effect( + name='json', default=None, prompt='Enter Config', hide_input=False + ) r = context.prompt_json(v, default=None) @@ -395,7 +409,9 @@ def test_prompt_json_default(mocker): m = mocker.Mock() m.side_effect = context.Variable - v = m.side_effect(name='json', default=None, prompt='Enter Config', hide_input=False) + v = m.side_effect( + name='json', default=None, prompt='Enter Config', hide_input=False + ) r = context.prompt_json(v, default=cfg) @@ -422,7 +438,9 @@ def test_prompt_yes_no_default_no(mocker): m = mocker.Mock() m.side_effect = context.Variable - v = m.side_effect(name='enable_docs', default='n', prompt='Enable docs', hide_input=False) + v = m.side_effect( + name='enable_docs', default='n', prompt='Enable docs', hide_input=False + ) r = context.prompt_yes_no(v, default=False) @@ -433,7 +451,7 @@ def test_prompt_yes_no_default_no(mocker): type=click.BOOL, ) - assert r # EXPECTED_VALUE + assert r # EXPECTED_VALUE def test_prompt_yes_no_default_yes(mocker): @@ -448,7 +466,9 @@ def test_prompt_yes_no_default_yes(mocker): m = mocker.Mock() m.side_effect = context.Variable - v = m.side_effect(name='enable_docs', default='y', prompt='Enable docs', hide_input=False) + v = m.side_effect( + name='enable_docs', default='y', prompt='Enable docs', hide_input=False + ) r = context.prompt_yes_no(v, default=True) @@ -459,7 +479,7 @@ def test_prompt_yes_no_default_yes(mocker): type=click.BOOL, ) - assert r # EXPECTED_VALUE + assert r # EXPECTED_VALUE def test_prompt_choice(mocker): @@ -479,8 +499,13 @@ def test_prompt_choice(mocker): m = mocker.Mock() m.side_effect = context.Variable - v = m.side_effect(name='license', default=DEFAULT_LICENSE, choices=LICENSES, - prompt='Pick a License', hide_input=False) + v = m.side_effect( + name='license', + default=DEFAULT_LICENSE, + choices=LICENSES, + prompt='Pick a License', + hide_input=False, + ) r = context.prompt_choice(v, default=DEFAULT_LICENSE) @@ -500,10 +525,15 @@ def test_variable_invalid_default_choice(): CHOICES = ['green', 'red', 'blue', 'yellow'] with pytest.raises(ValueError) as excinfo: - context.Variable(name='badchoice', default='purple', type='string', - choices=CHOICES) + context.Variable( + name='badchoice', default='purple', type='string', choices=CHOICES + ) - assert 'Variable: badchoice has an invalid default value purple for choices: {choices}'.format(choices=CHOICES) in str(excinfo.value) + assert 'Variable: badchoice has an invalid default value purple for choices: {choices}'.format( + choices=CHOICES + ) in str( + excinfo.value + ) def test_variable_invalid_validation_control_flag_is_logged_and_removed(caplog): @@ -515,13 +545,20 @@ def test_variable_invalid_validation_control_flag_is_logged_and_removed(caplog): prompt="Please enter a name for your base python module", type='string', validation='^[a-z_]+$', - validation_flags=['ignorecase', 'forget', ], - hide_input=True) + validation_flags=[ + 'ignorecase', + 'forget', + ], + hide_input=True, + ) for record in caplog.records: assert record.levelname == 'WARNING' - assert "Variable: module_name - Ignoring unkown RegEx validation Control Flag named 'forget'" in caplog.text + assert ( + "Variable: module_name - Ignoring unkown RegEx validation Control Flag named 'forget'" + in caplog.text + ) assert v.validation_flag_names == ['ignorecase'] @@ -529,7 +566,7 @@ def test_variable_invalid_validation_control_flag_is_logged_and_removed(caplog): def test_variable_validation_compile_exception(): VAR_NAME = 'module_name' - BAD_REGEX_STRING = '^[a-z_+$' # Missing a closing square-bracket (]) + BAD_REGEX_STRING = '^[a-z_+$' # Missing a closing square-bracket (]) with pytest.raises(ValueError) as excinfo: context.Variable( @@ -539,9 +576,14 @@ def test_variable_validation_compile_exception(): type='string', validation=BAD_REGEX_STRING, validation_flags=['ignorecase'], - hide_input=True) + hide_input=True, + ) - assert "Variable: {var_name} - Validation Setup Error: Invalid RegEx '{value}' - does not compile - ".format(var_name=VAR_NAME, value=BAD_REGEX_STRING) in str(excinfo.value) + assert "Variable: {var_name} - Validation Setup Error: Invalid RegEx '{value}' - does not compile - ".format( + var_name=VAR_NAME, value=BAD_REGEX_STRING + ) in str( + excinfo.value + ) def test_variable_forces_no_prompt_for_private_variable_names(): @@ -553,7 +595,8 @@ def test_variable_forces_no_prompt_for_private_variable_names(): type='string', validation='^[a-z_]+$', validation_flags=['ignorecase'], - hide_input=True) + hide_input=True, + ) assert v.prompt_user is False @@ -567,7 +610,8 @@ def test_variable_repr(): type='string', validation='^[a-z_]+$', validation_flags=['ignorecase'], - hide_input=True) + hide_input=True, + ) assert repr(v) == "" @@ -581,7 +625,8 @@ def test_variable_str(): type='string', validation='^[a-z_]+$', validation_flags=['ignorecase'], - hide_input=True) + hide_input=True, + ) str_v = str(v) assert ':' in str_v @@ -596,7 +641,9 @@ def test_variable_str(): assert "choices='[]'" in str_v assert "validation='^[a-z_]+$'" in str_v assert "validation_flag_names='['ignorecase']'" in str_v - assert "validation_flags='2'" in str_v or "validation_flags='re.IGNORECASE'" in str_v + assert ( + "validation_flags='2'" in str_v or "validation_flags='re.IGNORECASE'" in str_v + ) if sys.version_info >= (3, 4): assert "validate='re.compile('^[a-z_]+$', re.IGNORECASE)'" in str(v) @@ -607,7 +654,7 @@ def test_variable_str(): def test_variable_option_raise_invalid_type_value_error(): VAR_NAME = 'module_name' - OPT_VALUE_OF_INCORRECT_TYPE = 12 # should be a string + OPT_VALUE_OF_INCORRECT_TYPE = 12 # should be a string with pytest.raises(ValueError) as excinfo: context.Variable( @@ -617,17 +664,21 @@ def test_variable_option_raise_invalid_type_value_error(): type='string', validation=OPT_VALUE_OF_INCORRECT_TYPE, validation_flags=['ignorecase'], - hide_input=True) + hide_input=True, + ) msg = "Variable: '{var_name}' Option: 'validation' requires a value of type str, but has a value of: {value}" - assert msg.format(var_name=VAR_NAME, value=OPT_VALUE_OF_INCORRECT_TYPE) in str(excinfo.value) + assert msg.format(var_name=VAR_NAME, value=OPT_VALUE_OF_INCORRECT_TYPE) in str( + excinfo.value + ) def test_cookiecutter_template_repr(): # name, cookiecutter_version, variables, **info - cct = context.CookiecutterTemplate('cookiecutter_template_repr_test', - cookiecutter_version='2.0.0', variables=[]) + cct = context.CookiecutterTemplate( + 'cookiecutter_template_repr_test', cookiecutter_version='2.0.0', variables=[] + ) assert repr(cct) == "" @@ -641,7 +692,7 @@ def test_load_context_with_input_chioces(mocker): mocker.patch( 'click.termui.visible_prompt_func', autospec=True, - side_effect=[INPUT_1, INPUT_2, INPUT_3] + side_effect=[INPUT_1, INPUT_2, INPUT_3], ) cc_cfg = context.load_context(cc['cookiecutter_choices'], no_input=False) @@ -659,7 +710,7 @@ def test_load_context_with_input_with_validation_success(mocker): mocker.patch( 'click.termui.visible_prompt_func', autospec=True, - side_effect=[INPUT_1, INPUT_2] + side_effect=[INPUT_1, INPUT_2], ) logger.debug(cc) @@ -679,7 +730,7 @@ def test_load_context_with_input_with_validation_failure(mocker, capsys): mocker.patch( 'click.termui.visible_prompt_func', autospec=True, - side_effect=[INPUT_1, INPUT_2, INPUT_3] + side_effect=[INPUT_1, INPUT_2, INPUT_3], ) cc_cfg = context.load_context(cc['cookiecutter_val_failure'], no_input=False) @@ -702,7 +753,7 @@ def test_load_context_with_input_with_validation_failure_msg(mocker, capsys): mocker.patch( 'click.termui.visible_prompt_func', autospec=True, - side_effect=[INPUT_1, INPUT_2, INPUT_3] + side_effect=[INPUT_1, INPUT_2, INPUT_3], ) cc_cfg = context.load_context(cc['cookiecutter_val_failure_msg'], no_input=False) @@ -725,10 +776,14 @@ def test_specify_if_yes_skip_to_without_yes_no_type(): field and the variable type is not 'yes+no' """ with pytest.raises(ValueError) as excinfo: - context.Variable(name='author', default='JKR', type='string', - if_yes_skip_to='roman') + context.Variable( + name='author', default='JKR', type='string', if_yes_skip_to='roman' + ) - assert "Variable: 'author' specifies 'if_yes_skip_to' field, but variable not of type 'yes_no'" in str(excinfo.value) + assert ( + "Variable: 'author' specifies 'if_yes_skip_to' field, but variable not of type 'yes_no'" + in str(excinfo.value) + ) def test_specify_if_no_skip_to_without_yes_no_type(): @@ -737,7 +792,11 @@ def test_specify_if_no_skip_to_without_yes_no_type(): field and the variable type is not 'yes+no' """ with pytest.raises(ValueError) as excinfo: - context.Variable(name='author', default='JKR', type='string', - if_no_skip_to='roman') + context.Variable( + name='author', default='JKR', type='string', if_no_skip_to='roman' + ) - assert "Variable: 'author' specifies 'if_no_skip_to' field, but variable not of type 'yes_no'" in str(excinfo.value) + assert ( + "Variable: 'author' specifies 'if_no_skip_to' field, but variable not of type 'yes_no'" + in str(excinfo.value) + ) diff --git a/tests/test_generate_context_v2.py b/tests/test_generate_context_v2.py index 61088d51a..1edcbb75d 100644 --- a/tests/test_generate_context_v2.py +++ b/tests/test_generate_context_v2.py @@ -18,111 +18,180 @@ def context_data(): context = ( + {'context_file': 'tests/test-generate-context-v2/test.json'}, { - 'context_file': 'tests/test-generate-context-v2/test.json' - }, - { - "test": OrderedDict([ - ("name", "cookiecutter-pytest-plugin"), - ("cookiecutter_version", "2.0.0"), - ("variables", [ - OrderedDict([ - ("name", "full_name"), - ("default", "J. Paul Getty"), - ("prompt", "What's your full name?"), - ("description", "Please enter your full name. It will be displayed on the README file and used for the PyPI package definition."), - ("type", "string")]), - OrderedDict([ - ("name", "email"), - ("default", "jpg@rich.de"), - ("prompt", "What's your email?"), - ("description", "Please enter an email address for the meta information in setup.py."), - ("type", "string")]), - ]) - ]) - } + "test": OrderedDict( + [ + ("name", "cookiecutter-pytest-plugin"), + ("cookiecutter_version", "2.0.0"), + ( + "variables", + [ + OrderedDict( + [ + ("name", "full_name"), + ("default", "J. Paul Getty"), + ("prompt", "What's your full name?"), + ( + "description", + "Please enter your full name. It will be displayed on the README file and used for the PyPI package definition.", + ), + ("type", "string"), + ] + ), + OrderedDict( + [ + ("name", "email"), + ("default", "jpg@rich.de"), + ("prompt", "What's your email?"), + ( + "description", + "Please enter an email address for the meta information in setup.py.", + ), + ("type", "string"), + ] + ), + ], + ), + ] + ) + }, ) context_with_default = ( { 'context_file': 'tests/test-generate-context-v2/test.json', - 'default_context': {'full_name': 'James Patrick Morgan', 'this_key_ignored': 'not_in_context'} - }, - { - "test": OrderedDict([ - ("name", "cookiecutter-pytest-plugin"), - ("cookiecutter_version", "2.0.0"), - ("variables", [ - OrderedDict([ - ("name", "full_name"), - ("default", "James Patrick Morgan"), - ("prompt", "What's your full name?"), - ("description", "Please enter your full name. It will be displayed on the README file and used for the PyPI package definition."), - ("type", "string")]), - OrderedDict([ - ("name", "email"), - ("default", "jpg@rich.de"), - ("prompt", "What's your email?"), - ("description", "Please enter an email address for the meta information in setup.py."), - ("type", "string")]), - ]) - ]) - } + 'default_context': { + 'full_name': 'James Patrick Morgan', + 'this_key_ignored': 'not_in_context', + }, + }, + { + "test": OrderedDict( + [ + ("name", "cookiecutter-pytest-plugin"), + ("cookiecutter_version", "2.0.0"), + ( + "variables", + [ + OrderedDict( + [ + ("name", "full_name"), + ("default", "James Patrick Morgan"), + ("prompt", "What's your full name?"), + ( + "description", + "Please enter your full name. It will be displayed on the README file and used for the PyPI package definition.", + ), + ("type", "string"), + ] + ), + OrderedDict( + [ + ("name", "email"), + ("default", "jpg@rich.de"), + ("prompt", "What's your email?"), + ( + "description", + "Please enter an email address for the meta information in setup.py.", + ), + ("type", "string"), + ] + ), + ], + ), + ] + ) + }, ) context_with_extra = ( { 'context_file': 'tests/test-generate-context-v2/test.json', - 'extra_context': {'email': 'jpm@chase.bk'} - }, - { - "test": OrderedDict([ - ("name", "cookiecutter-pytest-plugin"), - ("cookiecutter_version", "2.0.0"), - ("variables", [ - OrderedDict([ - ("name", "full_name"), - ("default", "J. Paul Getty"), - ("prompt", "What's your full name?"), - ("description", "Please enter your full name. It will be displayed on the README file and used for the PyPI package definition."), - ("type", "string")]), - OrderedDict([ - ("name", "email"), - ("default", "jpm@chase.bk"), - ("prompt", "What's your email?"), - ("description", "Please enter an email address for the meta information in setup.py."), - ("type", "string")]), - ]) - ]) - } + 'extra_context': {'email': 'jpm@chase.bk'}, + }, + { + "test": OrderedDict( + [ + ("name", "cookiecutter-pytest-plugin"), + ("cookiecutter_version", "2.0.0"), + ( + "variables", + [ + OrderedDict( + [ + ("name", "full_name"), + ("default", "J. Paul Getty"), + ("prompt", "What's your full name?"), + ( + "description", + "Please enter your full name. It will be displayed on the README file and used for the PyPI package definition.", + ), + ("type", "string"), + ] + ), + OrderedDict( + [ + ("name", "email"), + ("default", "jpm@chase.bk"), + ("prompt", "What's your email?"), + ( + "description", + "Please enter an email address for the meta information in setup.py.", + ), + ("type", "string"), + ] + ), + ], + ), + ] + ) + }, ) context_with_default_and_extra = ( { 'context_file': 'tests/test-generate-context-v2/test.json', 'default_context': {'full_name': 'Alpha Gamma Five'}, - 'extra_context': {'email': 'agamma5@universe.open'} - }, - { - "test": OrderedDict([ - ("name", "cookiecutter-pytest-plugin"), - ("cookiecutter_version", "2.0.0"), - ("variables", [ - OrderedDict([ - ("name", "full_name"), - ("default", "Alpha Gamma Five"), - ("prompt", "What's your full name?"), - ("description", "Please enter your full name. It will be displayed on the README file and used for the PyPI package definition."), - ("type", "string")]), - OrderedDict([ - ("name", "email"), - ("default", "agamma5@universe.open"), - ("prompt", "What's your email?"), - ("description", "Please enter an email address for the meta information in setup.py."), - ("type", "string")]), - ]) - ]) - } + 'extra_context': {'email': 'agamma5@universe.open'}, + }, + { + "test": OrderedDict( + [ + ("name", "cookiecutter-pytest-plugin"), + ("cookiecutter_version", "2.0.0"), + ( + "variables", + [ + OrderedDict( + [ + ("name", "full_name"), + ("default", "Alpha Gamma Five"), + ("prompt", "What's your full name?"), + ( + "description", + "Please enter your full name. It will be displayed on the README file and used for the PyPI package definition.", + ), + ("type", "string"), + ] + ), + OrderedDict( + [ + ("name", "email"), + ("default", "agamma5@universe.open"), + ("prompt", "What's your email?"), + ( + "description", + "Please enter an email address for the meta information in setup.py.", + ), + ("type", "string"), + ] + ), + ], + ), + ] + ) + }, ) context_choices_with_default = ( @@ -131,18 +200,34 @@ def context_data(): 'default_context': {'license': 'Apache2'}, }, { - "test_choices": OrderedDict([ - ("name", "cookiecutter-pytest-plugin"), - ("cookiecutter_version", "2.0.0"), - ("variables", [ - OrderedDict([ - ("name", "license"), - ("default", "Apache2"), - ("choices", ["Apache2", "MIT", "BSD3", "GNU-GPL3", "Mozilla2"]), - ])] - ) - ]) - } + "test_choices": OrderedDict( + [ + ("name", "cookiecutter-pytest-plugin"), + ("cookiecutter_version", "2.0.0"), + ( + "variables", + [ + OrderedDict( + [ + ("name", "license"), + ("default", "Apache2"), + ( + "choices", + [ + "Apache2", + "MIT", + "BSD3", + "GNU-GPL3", + "Mozilla2", + ], + ), + ] + ) + ], + ), + ] + ) + }, ) context_choices_with_default_not_in_choices = ( @@ -151,25 +236,42 @@ def context_data(): 'default_context': {'orientation': 'landscape'}, }, { - "test": OrderedDict([ - ("name", "cookiecutter-pytest-plugin"), - ("cookiecutter_version", "2.0.0"), - ("variables", [ - OrderedDict([ - ("name", "full_name"), - ("default", "J. Paul Getty"), - ("prompt", "What's your full name?"), - ("description", "Please enter your full name. It will be displayed on the README file and used for the PyPI package definition."), - ("type", "string")]), - OrderedDict([ - ("name", "email"), - ("default", "jpg@rich.de"), - ("prompt", "What's your email?"), - ("description", "Please enter an email address for the meta information in setup.py."), - ("type", "string")]), - ]) - ]) - } + "test": OrderedDict( + [ + ("name", "cookiecutter-pytest-plugin"), + ("cookiecutter_version", "2.0.0"), + ( + "variables", + [ + OrderedDict( + [ + ("name", "full_name"), + ("default", "J. Paul Getty"), + ("prompt", "What's your full name?"), + ( + "description", + "Please enter your full name. It will be displayed on the README file and used for the PyPI package definition.", + ), + ("type", "string"), + ] + ), + OrderedDict( + [ + ("name", "email"), + ("default", "jpg@rich.de"), + ("prompt", "What's your email?"), + ( + "description", + "Please enter an email address for the meta information in setup.py.", + ), + ("type", "string"), + ] + ), + ], + ), + ] + ) + }, ) yield context yield context_with_default @@ -200,7 +302,8 @@ def test_generate_context_extra_ctx_invalid(): generate.generate_context( context_file='tests/test-generate-context-v2/test.json', default_context=None, - extra_context='should_be_a_list_or_a_dictionary') + extra_context='should_be_a_list_or_a_dictionary', + ) msg = "Extra context must be a dictionary or a list of dictionaries!" assert msg in str(excinfo.value) @@ -212,14 +315,13 @@ def test_generate_context_extra_ctx_list_item_not_dict(): Test error condition when extra context is a list, but not a list that contains a dictionary. """ - xtra_context = [ - 'a_string', 'here_too' - ] + xtra_context = ['a_string', 'here_too'] with pytest.raises(ValueError) as excinfo: generate.generate_context( context_file='tests/test-generate-context-v2/test.json', default_context=None, - extra_context=xtra_context) + extra_context=xtra_context, + ) msg = "Extra context list item 'a_string' is of type str, should be a dictionary." assert msg in str(excinfo.value) @@ -237,14 +339,15 @@ def test_generate_context_extra_ctx_list_item_dict_missing_name_field(): "default": "Robert Lewis", "prompt": "What's the author's name?", "description": "Please enter the author's full name.", - "type": "string" + "type": "string", } ] with pytest.raises(ValueError) as excinfo: generate.generate_context( context_file='tests/test-generate-context-v2/test.json', default_context=None, - extra_context=xtra_context) + extra_context=xtra_context, + ) msg = "is missing a 'name' key." assert msg in str(excinfo.value) @@ -262,14 +365,15 @@ def test_generate_context_extra_ctx_list_item_dict_no_name_field_match(): "default": "Robert Lewis", "prompt": "What's the author's name?", "description": "Please enter the author's full name.", - "type": "string" + "type": "string", } ] with pytest.raises(ValueError) as excinfo: generate.generate_context( context_file='tests/test-generate-context-v2/test.json', default_context=None, - extra_context=xtra_context) + extra_context=xtra_context, + ) msg = "No variable found in context whose name matches extra context name 'author_name'" assert msg in str(excinfo.value) @@ -287,28 +391,42 @@ def gen_context_data_inputs_expected(): 'description': 'Enter jazzy email...', 'extra_field': 'extra_field_value', } - ] - }, - { - "test": OrderedDict([ - ("name", "cookiecutter-pytest-plugin"), - ("cookiecutter_version", "2.0.0"), - ("variables", [ - OrderedDict([ - ("name", "full_name"), - ("default", "J. Paul Getty"), - ("prompt", "What's your full name?"), - ("description", "Please enter your full name. It will be displayed on the README file and used for the PyPI package definition."), - ("type", "string")]), - OrderedDict([ - ("name", "email"), - ("default", "miles.davis@jazz.gone"), - ("prompt", "What's your email?"), - ("description", "Enter jazzy email..."), - ("type", "string")]), - ]) - ]) - } + ], + }, + { + "test": OrderedDict( + [ + ("name", "cookiecutter-pytest-plugin"), + ("cookiecutter_version", "2.0.0"), + ( + "variables", + [ + OrderedDict( + [ + ("name", "full_name"), + ("default", "J. Paul Getty"), + ("prompt", "What's your full name?"), + ( + "description", + "Please enter your full name. It will be displayed on the README file and used for the PyPI package definition.", + ), + ("type", "string"), + ] + ), + OrderedDict( + [ + ("name", "email"), + ("default", "miles.davis@jazz.gone"), + ("prompt", "What's your email?"), + ("description", "Enter jazzy email..."), + ("type", "string"), + ] + ), + ], + ), + ] + ) + }, ) # Empty extra context precipitates no ill effect context_with_valid_extra_1 = ( @@ -325,33 +443,62 @@ def gen_context_data_inputs_expected(): # ] }, { - "representative": OrderedDict([ - ("name", "cc-representative"), - ("cookiecutter_version", "2.0.0"), - ("variables", [ - OrderedDict([ - ("name", "director_credit"), - ("default", True), - ("prompt", "Is there a director credit on this film?"), - ("description", "Directors take credit for most of their films, usually..."), - ("type", "boolean") - ]), - OrderedDict([ - ("name", "director_name"), - ("default", "Allan Smithe"), - ("prompt", "What's the Director's full name?"), - ("prompt_user", True), - ("description", "The default director is not proud of their work, we hope you are."), - ("hide_input", False), - ("choices", ["Allan Smithe", "Ridley Scott", "Victor Fleming", "John Houston"]), - ("validation", "^[a-z][A-Z]+$"), - ("validation_flags", ["verbose", "ascii"]), - ("skip_if", "{{cookiecutter.director_credit == False}}"), - ("type", "string") - ]) - ]) - ]) - } + "representative": OrderedDict( + [ + ("name", "cc-representative"), + ("cookiecutter_version", "2.0.0"), + ( + "variables", + [ + OrderedDict( + [ + ("name", "director_credit"), + ("default", True), + ( + "prompt", + "Is there a director credit on this film?", + ), + ( + "description", + "Directors take credit for most of their films, usually...", + ), + ("type", "boolean"), + ] + ), + OrderedDict( + [ + ("name", "director_name"), + ("default", "Allan Smithe"), + ("prompt", "What's the Director's full name?"), + ("prompt_user", True), + ( + "description", + "The default director is not proud of their work, we hope you are.", + ), + ("hide_input", False), + ( + "choices", + [ + "Allan Smithe", + "Ridley Scott", + "Victor Fleming", + "John Houston", + ], + ), + ("validation", "^[a-z][A-Z]+$"), + ("validation_flags", ["verbose", "ascii"]), + ( + "skip_if", + "{{cookiecutter.director_credit == False}}", + ), + ("type", "string"), + ] + ), + ], + ), + ] + ) + }, ) # Test the ability to change the variable's name field (since it is used @@ -370,36 +517,61 @@ def gen_context_data_inputs_expected(): 'name': 'director_name', 'skip_if': '<>', }, - - ] - }, - { - "representative": OrderedDict([ - ("name", "cc-representative"), - ("cookiecutter_version", "2.0.0"), - ("variables", [ - OrderedDict([ - ("name", "producer_credit"), - ("default", True), - ("prompt", "Is there a producer credit on this film?"), - ("description", "There are usually a lot of producers..."), - ("type", "boolean") - ]), - OrderedDict([ - ("name", "director_name"), - ("default", "Allan Smithe"), - ("prompt", "What's the Director's full name?"), - ("prompt_user", True), - ("description", "The default director is not proud of their work, we hope you are."), - ("hide_input", False), - ("choices", ["Allan Smithe", "Ridley Scott", "Victor Fleming", "John Houston"]), - ("validation", "^[a-z][A-Z]+$"), - ("validation_flags", ["verbose", "ascii"]), - ("type", "string") - ]) - ]) - ]) - } + ], + }, + { + "representative": OrderedDict( + [ + ("name", "cc-representative"), + ("cookiecutter_version", "2.0.0"), + ( + "variables", + [ + OrderedDict( + [ + ("name", "producer_credit"), + ("default", True), + ( + "prompt", + "Is there a producer credit on this film?", + ), + ( + "description", + "There are usually a lot of producers...", + ), + ("type", "boolean"), + ] + ), + OrderedDict( + [ + ("name", "director_name"), + ("default", "Allan Smithe"), + ("prompt", "What's the Director's full name?"), + ("prompt_user", True), + ( + "description", + "The default director is not proud of their work, we hope you are.", + ), + ("hide_input", False), + ( + "choices", + [ + "Allan Smithe", + "Ridley Scott", + "Victor Fleming", + "John Houston", + ], + ), + ("validation", "^[a-z][A-Z]+$"), + ("validation_flags", ["verbose", "ascii"]), + ("type", "string"), + ] + ), + ], + ), + ] + ) + }, ) # Test the ability to change the variable's name field (since it is used # to identify the variable to be modifed) with extra context and to also @@ -414,36 +586,66 @@ def gen_context_data_inputs_expected(): 'prompt': 'Is there a producer credit on this film?', 'description': 'There are usually a lot of producers...', }, - ] - }, - { - "representative_2B": OrderedDict([ - ("name", "cc-representative"), - ("cookiecutter_version", "2.0.0"), - ("variables", [ - OrderedDict([ - ("name", "producer_credit"), - ("default", True), - ("prompt", "Is there a producer credit on this film?"), - ("description", "There are usually a lot of producers..."), - ("type", "boolean") - ]), - OrderedDict([ - ("name", "director_name"), - ("default", "Allan Smithe"), - ("prompt", "What's the Director's full name?"), - ("prompt_user", True), - ("description", "The default director is not proud of their work, we hope you are."), - ("hide_input", False), - ("choices", ["Allan Smithe", "Ridley Scott", "Victor Fleming", "John Houston", "{{cookiecutter.producer_credit}}"]), - ("validation", "^[a-z][A-Z]+$"), - ("validation_flags", ["verbose", "ascii"]), - ("skip_if", "{{cookiecutter.producer_credit == False}}"), - ("type", "string") - ]) - ]) - ]) - } + ], + }, + { + "representative_2B": OrderedDict( + [ + ("name", "cc-representative"), + ("cookiecutter_version", "2.0.0"), + ( + "variables", + [ + OrderedDict( + [ + ("name", "producer_credit"), + ("default", True), + ( + "prompt", + "Is there a producer credit on this film?", + ), + ( + "description", + "There are usually a lot of producers...", + ), + ("type", "boolean"), + ] + ), + OrderedDict( + [ + ("name", "director_name"), + ("default", "Allan Smithe"), + ("prompt", "What's the Director's full name?"), + ("prompt_user", True), + ( + "description", + "The default director is not proud of their work, we hope you are.", + ), + ("hide_input", False), + ( + "choices", + [ + "Allan Smithe", + "Ridley Scott", + "Victor Fleming", + "John Houston", + "{{cookiecutter.producer_credit}}", + ], + ), + ("validation", "^[a-z][A-Z]+$"), + ("validation_flags", ["verbose", "ascii"]), + ( + "skip_if", + "{{cookiecutter.producer_credit == False}}", + ), + ("type", "string"), + ] + ), + ], + ), + ] + ) + }, ) # Test changing variable's name field value, default field, prompt field, @@ -459,36 +661,65 @@ def gen_context_data_inputs_expected(): 'description': 'There are usually a lot of producers...', 'type': "int", } - ] - }, - { - "representative": OrderedDict([ - ("name", "cc-representative"), - ("cookiecutter_version", "2.0.0"), - ("variables", [ - OrderedDict([ - ("name", "producer_credits"), - ("default", 2), - ("prompt", "How many producers does this film have?"), - ("description", "There are usually a lot of producers..."), - ("type", "int") - ]), - OrderedDict([ - ("name", "director_name"), - ("default", "Allan Smithe"), - ("prompt", "What's the Director's full name?"), - ("prompt_user", True), - ("description", "The default director is not proud of their work, we hope you are."), - ("hide_input", False), - ("choices", ["Allan Smithe", "Ridley Scott", "Victor Fleming", "John Houston"]), - ("validation", "^[a-z][A-Z]+$"), - ("validation_flags", ["verbose", "ascii"]), - ("skip_if", "{{cookiecutter.producer_credits == False}}"), - ("type", "string") - ]) - ]) - ]) - } + ], + }, + { + "representative": OrderedDict( + [ + ("name", "cc-representative"), + ("cookiecutter_version", "2.0.0"), + ( + "variables", + [ + OrderedDict( + [ + ("name", "producer_credits"), + ("default", 2), + ( + "prompt", + "How many producers does this film have?", + ), + ( + "description", + "There are usually a lot of producers...", + ), + ("type", "int"), + ] + ), + OrderedDict( + [ + ("name", "director_name"), + ("default", "Allan Smithe"), + ("prompt", "What's the Director's full name?"), + ("prompt_user", True), + ( + "description", + "The default director is not proud of their work, we hope you are.", + ), + ("hide_input", False), + ( + "choices", + [ + "Allan Smithe", + "Ridley Scott", + "Victor Fleming", + "John Houston", + ], + ), + ("validation", "^[a-z][A-Z]+$"), + ("validation_flags", ["verbose", "ascii"]), + ( + "skip_if", + "{{cookiecutter.producer_credits == False}}", + ), + ("type", "string"), + ] + ), + ], + ), + ] + ) + }, ) # Test changing choices field without changing the default, but default # does not change because the first position in choices matches default @@ -498,39 +729,76 @@ def gen_context_data_inputs_expected(): 'extra_context': [ { 'name': 'director_name', - 'choices': ['Allan Smithe', 'Ridley Scott', 'Victor Fleming', 'John Houston', - 'John Ford', 'Billy Wilder'], + 'choices': [ + 'Allan Smithe', + 'Ridley Scott', + 'Victor Fleming', + 'John Houston', + 'John Ford', + 'Billy Wilder', + ], } - ] - }, - { - "representative": OrderedDict([ - ("name", "cc-representative"), - ("cookiecutter_version", "2.0.0"), - ("variables", [ - OrderedDict([ - ("name", "director_credit"), - ("default", True), - ("prompt", "Is there a director credit on this film?"), - ("description", "Directors take credit for most of their films, usually..."), - ("type", "boolean") - ]), - OrderedDict([ - ("name", "director_name"), - ("default", "Allan Smithe"), - ("prompt", "What's the Director's full name?"), - ("prompt_user", True), - ("description", "The default director is not proud of their work, we hope you are."), - ("hide_input", False), - ("choices", ['Allan Smithe', 'Ridley Scott', 'Victor Fleming', 'John Houston', 'John Ford', 'Billy Wilder']), - ("validation", "^[a-z][A-Z]+$"), - ("validation_flags", ["verbose", "ascii"]), - ("skip_if", "{{cookiecutter.director_credit == False}}"), - ("type", "string") - ]) - ]) - ]) - } + ], + }, + { + "representative": OrderedDict( + [ + ("name", "cc-representative"), + ("cookiecutter_version", "2.0.0"), + ( + "variables", + [ + OrderedDict( + [ + ("name", "director_credit"), + ("default", True), + ( + "prompt", + "Is there a director credit on this film?", + ), + ( + "description", + "Directors take credit for most of their films, usually...", + ), + ("type", "boolean"), + ] + ), + OrderedDict( + [ + ("name", "director_name"), + ("default", "Allan Smithe"), + ("prompt", "What's the Director's full name?"), + ("prompt_user", True), + ( + "description", + "The default director is not proud of their work, we hope you are.", + ), + ("hide_input", False), + ( + "choices", + [ + 'Allan Smithe', + 'Ridley Scott', + 'Victor Fleming', + 'John Houston', + 'John Ford', + 'Billy Wilder', + ], + ), + ("validation", "^[a-z][A-Z]+$"), + ("validation_flags", ["verbose", "ascii"]), + ( + "skip_if", + "{{cookiecutter.director_credit == False}}", + ), + ("type", "string"), + ] + ), + ], + ), + ] + ) + }, ) # Test changing choices field and changing the default context_with_valid_extra_5 = ( @@ -540,39 +808,76 @@ def gen_context_data_inputs_expected(): { 'name': 'director_name', 'default': 'John Ford', - 'choices': ['Allan Smithe', 'Ridley Scott', 'Victor Fleming', 'John Houston', - 'John Ford', 'Billy Wilder'], + 'choices': [ + 'Allan Smithe', + 'Ridley Scott', + 'Victor Fleming', + 'John Houston', + 'John Ford', + 'Billy Wilder', + ], } - ] - }, - { - "representative": OrderedDict([ - ("name", "cc-representative"), - ("cookiecutter_version", "2.0.0"), - ("variables", [ - OrderedDict([ - ("name", "director_credit"), - ("default", True), - ("prompt", "Is there a director credit on this film?"), - ("description", "Directors take credit for most of their films, usually..."), - ("type", "boolean") - ]), - OrderedDict([ - ("name", "director_name"), - ("default", "John Ford"), - ("prompt", "What's the Director's full name?"), - ("prompt_user", True), - ("description", "The default director is not proud of their work, we hope you are."), - ("hide_input", False), - ("choices", ['John Ford', 'Allan Smithe', 'Ridley Scott', 'Victor Fleming', 'John Houston', 'Billy Wilder']), - ("validation", "^[a-z][A-Z]+$"), - ("validation_flags", ["verbose", "ascii"]), - ("skip_if", "{{cookiecutter.director_credit == False}}"), - ("type", "string") - ]) - ]) - ]) - } + ], + }, + { + "representative": OrderedDict( + [ + ("name", "cc-representative"), + ("cookiecutter_version", "2.0.0"), + ( + "variables", + [ + OrderedDict( + [ + ("name", "director_credit"), + ("default", True), + ( + "prompt", + "Is there a director credit on this film?", + ), + ( + "description", + "Directors take credit for most of their films, usually...", + ), + ("type", "boolean"), + ] + ), + OrderedDict( + [ + ("name", "director_name"), + ("default", "John Ford"), + ("prompt", "What's the Director's full name?"), + ("prompt_user", True), + ( + "description", + "The default director is not proud of their work, we hope you are.", + ), + ("hide_input", False), + ( + "choices", + [ + 'John Ford', + 'Allan Smithe', + 'Ridley Scott', + 'Victor Fleming', + 'John Houston', + 'Billy Wilder', + ], + ), + ("validation", "^[a-z][A-Z]+$"), + ("validation_flags", ["verbose", "ascii"]), + ( + "skip_if", + "{{cookiecutter.director_credit == False}}", + ), + ("type", "string"), + ] + ), + ], + ), + ] + ) + }, ) # Test changing the default, but not the choices field, yet seeing choices field re-ordered # to put default value in first location @@ -584,36 +889,66 @@ def gen_context_data_inputs_expected(): 'name': 'director_name', 'default': 'John Ford', } - ] - }, - { - "representative": OrderedDict([ - ("name", "cc-representative"), - ("cookiecutter_version", "2.0.0"), - ("variables", [ - OrderedDict([ - ("name", "director_credit"), - ("default", True), - ("prompt", "Is there a director credit on this film?"), - ("description", "Directors take credit for most of their films, usually..."), - ("type", "boolean") - ]), - OrderedDict([ - ("name", "director_name"), - ("default", "John Ford"), - ("prompt", "What's the Director's full name?"), - ("prompt_user", True), - ("description", "The default director is not proud of their work, we hope you are."), - ("hide_input", False), - ("choices", ['John Ford', 'Allan Smithe', 'Ridley Scott', 'Victor Fleming', 'John Houston']), - ("validation", "^[a-z][A-Z]+$"), - ("validation_flags", ["verbose", "ascii"]), - ("skip_if", "{{cookiecutter.director_credit == False}}"), - ("type", "string") - ]) - ]) - ]) - } + ], + }, + { + "representative": OrderedDict( + [ + ("name", "cc-representative"), + ("cookiecutter_version", "2.0.0"), + ( + "variables", + [ + OrderedDict( + [ + ("name", "director_credit"), + ("default", True), + ( + "prompt", + "Is there a director credit on this film?", + ), + ( + "description", + "Directors take credit for most of their films, usually...", + ), + ("type", "boolean"), + ] + ), + OrderedDict( + [ + ("name", "director_name"), + ("default", "John Ford"), + ("prompt", "What's the Director's full name?"), + ("prompt_user", True), + ( + "description", + "The default director is not proud of their work, we hope you are.", + ), + ("hide_input", False), + ( + "choices", + [ + 'John Ford', + 'Allan Smithe', + 'Ridley Scott', + 'Victor Fleming', + 'John Houston', + ], + ), + ("validation", "^[a-z][A-Z]+$"), + ("validation_flags", ["verbose", "ascii"]), + ( + "skip_if", + "{{cookiecutter.director_credit == False}}", + ), + ("type", "string"), + ] + ), + ], + ), + ] + ) + }, ) # Test changing choices field without changing the default, but default # does get changee because the first position in choices field chagned @@ -623,39 +958,76 @@ def gen_context_data_inputs_expected(): 'extra_context': [ { 'name': 'director_name', - 'choices': ['Billy Wilder', 'Allan Smithe', 'Ridley Scott', 'Victor Fleming', 'John Houston', - 'John Ford'], + 'choices': [ + 'Billy Wilder', + 'Allan Smithe', + 'Ridley Scott', + 'Victor Fleming', + 'John Houston', + 'John Ford', + ], } - ] - }, - { - "representative": OrderedDict([ - ("name", "cc-representative"), - ("cookiecutter_version", "2.0.0"), - ("variables", [ - OrderedDict([ - ("name", "director_credit"), - ("default", True), - ("prompt", "Is there a director credit on this film?"), - ("description", "Directors take credit for most of their films, usually..."), - ("type", "boolean") - ]), - OrderedDict([ - ("name", "director_name"), - ("default", "Billy Wilder"), - ("prompt", "What's the Director's full name?"), - ("prompt_user", True), - ("description", "The default director is not proud of their work, we hope you are."), - ("hide_input", False), - ("choices", ['Billy Wilder', 'Allan Smithe', 'Ridley Scott', 'Victor Fleming', 'John Houston', 'John Ford']), - ("validation", "^[a-z][A-Z]+$"), - ("validation_flags", ["verbose", "ascii"]), - ("skip_if", "{{cookiecutter.director_credit == False}}"), - ("type", "string") - ]) - ]) - ]) - } + ], + }, + { + "representative": OrderedDict( + [ + ("name", "cc-representative"), + ("cookiecutter_version", "2.0.0"), + ( + "variables", + [ + OrderedDict( + [ + ("name", "director_credit"), + ("default", True), + ( + "prompt", + "Is there a director credit on this film?", + ), + ( + "description", + "Directors take credit for most of their films, usually...", + ), + ("type", "boolean"), + ] + ), + OrderedDict( + [ + ("name", "director_name"), + ("default", "Billy Wilder"), + ("prompt", "What's the Director's full name?"), + ("prompt_user", True), + ( + "description", + "The default director is not proud of their work, we hope you are.", + ), + ("hide_input", False), + ( + "choices", + [ + 'Billy Wilder', + 'Allan Smithe', + 'Ridley Scott', + 'Victor Fleming', + 'John Houston', + 'John Ford', + ], + ), + ("validation", "^[a-z][A-Z]+$"), + ("validation_flags", ["verbose", "ascii"]), + ( + "skip_if", + "{{cookiecutter.director_credit == False}}", + ), + ("type", "string"), + ] + ), + ], + ), + ] + ) + }, ) # Test changing the default value with a value that is not in choices, # we should see the choice first position get updated. @@ -667,36 +1039,66 @@ def gen_context_data_inputs_expected(): 'name': 'director_name', 'default': 'Peter Sellers', } - ] - }, - { - "representative": OrderedDict([ - ("name", "cc-representative"), - ("cookiecutter_version", "2.0.0"), - ("variables", [ - OrderedDict([ - ("name", "director_credit"), - ("default", True), - ("prompt", "Is there a director credit on this film?"), - ("description", "Directors take credit for most of their films, usually..."), - ("type", "boolean") - ]), - OrderedDict([ - ("name", "director_name"), - ("default", "Peter Sellers"), - ("prompt", "What's the Director's full name?"), - ("prompt_user", True), - ("description", "The default director is not proud of their work, we hope you are."), - ("hide_input", False), - ("choices", ["Peter Sellers", "Allan Smithe", "Ridley Scott", "Victor Fleming", "John Houston"]), - ("validation", "^[a-z][A-Z]+$"), - ("validation_flags", ["verbose", "ascii"]), - ("skip_if", "{{cookiecutter.director_credit == False}}"), - ("type", "string") - ]) - ]) - ]) - } + ], + }, + { + "representative": OrderedDict( + [ + ("name", "cc-representative"), + ("cookiecutter_version", "2.0.0"), + ( + "variables", + [ + OrderedDict( + [ + ("name", "director_credit"), + ("default", True), + ( + "prompt", + "Is there a director credit on this film?", + ), + ( + "description", + "Directors take credit for most of their films, usually...", + ), + ("type", "boolean"), + ] + ), + OrderedDict( + [ + ("name", "director_name"), + ("default", "Peter Sellers"), + ("prompt", "What's the Director's full name?"), + ("prompt_user", True), + ( + "description", + "The default director is not proud of their work, we hope you are.", + ), + ("hide_input", False), + ( + "choices", + [ + "Peter Sellers", + "Allan Smithe", + "Ridley Scott", + "Victor Fleming", + "John Houston", + ], + ), + ("validation", "^[a-z][A-Z]+$"), + ("validation_flags", ["verbose", "ascii"]), + ( + "skip_if", + "{{cookiecutter.director_credit == False}}", + ), + ("type", "string"), + ] + ), + ], + ), + ] + ) + }, ) yield context_with_valid_extra_0 yield context_with_valid_extra_1 @@ -711,10 +1113,12 @@ def gen_context_data_inputs_expected(): @pytest.mark.usefixtures('clean_system') -@pytest.mark.parametrize('input_params, expected_context', - gen_context_data_inputs_expected()) +@pytest.mark.parametrize( + 'input_params, expected_context', gen_context_data_inputs_expected() +) def test_generate_context_with_extra_context_dictionary( - input_params, expected_context, monkeypatch): + input_params, expected_context, monkeypatch +): """ Test the generated context with extra content overwrite to multiple fields, with creation of new fields NOT allowed. @@ -726,35 +1130,60 @@ def context_data_2(): context_with_valid_extra_2_A = ( { 'context_file': 'tests/test-generate-context-v2/representative.json', - }, { - "representative": OrderedDict([ - ("name", "cc-representative"), - ("cookiecutter_version", "2.0.0"), - ("variables", [ - OrderedDict([ - ("name", "director_credit"), - ("default", True), - ("prompt", "Is there a director credit on this film?"), - ("description", "Directors take credit for most of their films, usually..."), - ("type", "boolean") - ]), - OrderedDict([ - ("name", "director_name"), - ("default", "Allan Smithe"), - ("prompt", "What's the Director's full name?"), - ("prompt_user", True), - ("description", "The default director is not proud of their work, we hope you are."), - ("hide_input", False), - ("choices", ["Allan Smithe", "Ridley Scott", "Victor Fleming", "John Houston"]), - ("validation", "^[a-z][A-Z]+$"), - ("validation_flags", ["verbose", "ascii"]), - ("type", "string") - ]) - ]) - ]) - } + "representative": OrderedDict( + [ + ("name", "cc-representative"), + ("cookiecutter_version", "2.0.0"), + ( + "variables", + [ + OrderedDict( + [ + ("name", "director_credit"), + ("default", True), + ( + "prompt", + "Is there a director credit on this film?", + ), + ( + "description", + "Directors take credit for most of their films, usually...", + ), + ("type", "boolean"), + ] + ), + OrderedDict( + [ + ("name", "director_name"), + ("default", "Allan Smithe"), + ("prompt", "What's the Director's full name?"), + ("prompt_user", True), + ( + "description", + "The default director is not proud of their work, we hope you are.", + ), + ("hide_input", False), + ( + "choices", + [ + "Allan Smithe", + "Ridley Scott", + "Victor Fleming", + "John Houston", + ], + ), + ("validation", "^[a-z][A-Z]+$"), + ("validation_flags", ["verbose", "ascii"]), + ("type", "string"), + ] + ), + ], + ), + ] + ) + }, ) @@ -777,6 +1206,7 @@ def test_raise_exception_when_attempting_to_remove_mandatory_field(): generate.generate_context( context_file='tests/test-generate-context-v2/representative.json', default_context=None, - extra_context=xtra_context) + extra_context=xtra_context, + ) assert "Cannot remove mandatory 'default' field" in str(excinfo.value) diff --git a/tests/test_main.py b/tests/test_main.py index c7f25d52a..0af183596 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,10 +1,8 @@ """Collection of tests around cookiecutter's replay feature.""" import collections import os -import pathlib import shutil -from cookiecutter.find import find_template from cookiecutter.main import cookiecutter @@ -68,8 +66,7 @@ def test_replay_load_template_name( ) -def test_version_2_load_context_call( - monkeypatch, mocker, user_config_file): +def test_version_2_load_context_call(monkeypatch, mocker, user_config_file): """Check that the version 2 load_context() is called. Change the current working directory temporarily to @@ -82,17 +79,23 @@ def test_version_2_load_context_call( mock_replay_dump = mocker.patch('cookiecutter.main.dump') counts = {} + def patch_load_context(counts): counts['load_context'] = 0 + def load_context(json_object, no_input=False, verbose=True, counts=counts): counts["load_context"] += 1 - return collections.OrderedDict({ - 'repo_name': 'test-repo', - }) + return collections.OrderedDict( + { + 'repo_name': 'test-repo', + } + ) + return load_context def patch_prompt_for_config(counts): counts['prompt_for_config'] = 0 + def prompt_for_config(context, no_input=False): counts["prompt_for_config"] += 1 return {} From 392ee47e535a644c84e4c5b7e838bfccffdb2bf6 Mon Sep 17 00:00:00 2001 From: Christo De Lange Date: Fri, 3 Feb 2023 18:15:27 -0500 Subject: [PATCH 252/274] fix: Cover all cases 100% --- cookiecutter/context.py | 8 +- tests/test_context.py | 2 +- tests/test_generate_context_v2.py | 133 +++++++++++++++++++++++++++++- 3 files changed, 135 insertions(+), 8 deletions(-) diff --git a/cookiecutter/context.py b/cookiecutter/context.py index 6b65c6b3a..6120b8d08 100644 --- a/cookiecutter/context.py +++ b/cookiecutter/context.py @@ -21,9 +21,8 @@ import logging import collections import json -import posix import re -import sys +import shutil import click from jinja2 import Environment @@ -585,10 +584,7 @@ def load_context(json_object, no_input=False, verbose=True): break if verbose: - if sys.stdout.isatty(): - width, _ = posix.get_terminal_size() - else: - width = 80 + width, _ = shutil.get_terminal_size() click.echo('-' * width) context[variable.name] = deserialize(value) diff --git a/tests/test_context.py b/tests/test_context.py index 2d7b0db1e..79b594d2b 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -683,7 +683,7 @@ def test_cookiecutter_template_repr(): assert repr(cct) == "" -def test_load_context_with_input_chioces(mocker): +def test_load_context_with_input_choices(mocker): cc = load_cookiecutter('tests/test-context/cookiecutter_choices.json') INPUT_1 = 'E.R. Uber' diff --git a/tests/test_generate_context_v2.py b/tests/test_generate_context_v2.py index 1edcbb75d..c2cc6de91 100644 --- a/tests/test_generate_context_v2.py +++ b/tests/test_generate_context_v2.py @@ -230,6 +230,43 @@ def context_data(): }, ) + context_choices_with_extra = ( + { + 'context_file': 'tests/test-generate-context-v2/test_choices.json', + 'default_context': {'license': 'Apache2'}, + 'extra_context': {'license': 'MIT'}, + }, + { + "test_choices": OrderedDict( + [ + ("name", "cookiecutter-pytest-plugin"), + ("cookiecutter_version", "2.0.0"), + ( + "variables", + [ + OrderedDict( + [ + ("name", "license"), + ("default", "MIT"), + ( + "choices", + [ + "MIT", + "Apache2", + "BSD3", + "GNU-GPL3", + "Mozilla2", + ], + ), + ] + ) + ], + ), + ] + ) + }, + ) + context_choices_with_default_not_in_choices = ( { 'context_file': 'tests/test-generate-context-v2/test.json', @@ -278,9 +315,88 @@ def context_data(): yield context_with_extra yield context_with_default_and_extra yield context_choices_with_default + yield context_choices_with_extra yield context_choices_with_default_not_in_choices +def context_data_value_errors(): + context_choices_with_default_value_error = ( + { + 'context_file': 'tests/test-generate-context-v2/test_choices.json', + 'default_context': [{'license': 'MIT'}], + }, + { + "test_choices": OrderedDict( + [ + ("name", "cookiecutter-pytest-plugin"), + ("cookiecutter_version", "2.0.0"), + ( + "variables", + [ + OrderedDict( + [ + ("name", "license"), + ("default", "MIT"), + ( + "choices", + [ + "MIT", + "Apache2", + "BSD3", + "GNU-GPL3", + "Mozilla2", + ], + ), + ] + ) + ], + ), + ] + ) + }, + False + ) + context_choices_with_extra_value_error = ( + { + 'context_file': 'tests/test-generate-context-v2/test_choices.json', + 'default_context': {'license': 'Apache2'}, + 'extra_context': [{'license': 'MIT'}], + }, + { + "test_choices": OrderedDict( + [ + ("name", "cookiecutter-pytest-plugin"), + ("cookiecutter_version", "2.0.0"), + ( + "variables", + [ + OrderedDict( + [ + ("name", "license"), + ("default", "MIT"), + ( + "choices", + [ + "MIT", + "Apache2", + "BSD3", + "GNU-GPL3", + "Mozilla2", + ], + ), + ] + ) + ], + ), + ] + ) + }, + True + ) + yield context_choices_with_default_value_error + yield context_choices_with_extra_value_error + + @pytest.mark.usefixtures('clean_system') @pytest.mark.parametrize('input_params, expected_context', context_data()) def test_generate_context(input_params, expected_context): @@ -288,7 +404,22 @@ def test_generate_context(input_params, expected_context): Test the generated context for several input parameters against the according expected context. """ - assert generate.generate_context(**input_params) == expected_context + generated_context = generate.generate_context(**input_params) + assert generated_context == expected_context + + +@pytest.mark.usefixtures('clean_system') +@pytest.mark.parametrize('input_params, expected_context, raise_exception', context_data_value_errors()) +def test_generate_context_value_error(input_params, expected_context, raise_exception): + """ + Test the generated context for several input parameters against the + according expected context. + """ + if raise_exception: + with pytest.raises(ValueError) as excinfo: + generate.generate_context(**input_params) + else: + generate.generate_context(**input_params) @pytest.mark.usefixtures('clean_system') From b9a7985a8eccf613dc65bf89997b9a67fbc41d73 Mon Sep 17 00:00:00 2001 From: Christo De Lange Date: Fri, 3 Feb 2023 18:20:23 -0500 Subject: [PATCH 253/274] fix: lint --- tests/test_generate_context_v2.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/test_generate_context_v2.py b/tests/test_generate_context_v2.py index c2cc6de91..223130977 100644 --- a/tests/test_generate_context_v2.py +++ b/tests/test_generate_context_v2.py @@ -354,7 +354,7 @@ def context_data_value_errors(): ] ) }, - False + False, ) context_choices_with_extra_value_error = ( { @@ -391,7 +391,7 @@ def context_data_value_errors(): ] ) }, - True + True, ) yield context_choices_with_default_value_error yield context_choices_with_extra_value_error @@ -409,7 +409,9 @@ def test_generate_context(input_params, expected_context): @pytest.mark.usefixtures('clean_system') -@pytest.mark.parametrize('input_params, expected_context, raise_exception', context_data_value_errors()) +@pytest.mark.parametrize( + 'input_params, expected_context, raise_exception', context_data_value_errors() +) def test_generate_context_value_error(input_params, expected_context, raise_exception): """ Test the generated context for several input parameters against the @@ -419,7 +421,7 @@ def test_generate_context_value_error(input_params, expected_context, raise_exce with pytest.raises(ValueError) as excinfo: generate.generate_context(**input_params) else: - generate.generate_context(**input_params) + generate.generate_context(**input_params) @pytest.mark.usefixtures('clean_system') From 8bc3edaee367307f1834f0136e0953fb062fe676 Mon Sep 17 00:00:00 2001 From: Christo De Lange Date: Fri, 3 Feb 2023 18:22:01 -0500 Subject: [PATCH 254/274] fix: workflow trigger --- .github/workflows/tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a84a59992..a63151166 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,6 +4,7 @@ on: push: branches: - master + - main tags: - "*" pull_request: From 3908278986cb88a185094cb0950ebb8059532834 Mon Sep 17 00:00:00 2001 From: Christo De Lange Date: Fri, 3 Feb 2023 18:27:30 -0500 Subject: [PATCH 255/274] fix: gah --- .gitmodules | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .gitmodules diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..e0372d2b5 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "docs/HelloCookieCutter1"] + path = docs/HelloCookieCutter1 + url = https://github.com/BruceEckel/HelloCookieCutter1 From 4bb0ef8be218899e2e73a12faa5fe4024d722d3f Mon Sep 17 00:00:00 2001 From: Christo De Lange Date: Fri, 3 Feb 2023 18:43:50 -0500 Subject: [PATCH 256/274] docs --- cookiecutter/generate.py | 25 +++++++++++-------------- docs/cookiecutter.rst | 6 +++--- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/cookiecutter/generate.py b/cookiecutter/generate.py index 2e948494f..d60cd4d4e 100644 --- a/cookiecutter/generate.py +++ b/cookiecutter/generate.py @@ -140,8 +140,10 @@ def resolve_changed_variable_names(context, variables_to_resolve): def apply_overwrites_to_context_v2(context, extra_context): - """ - Modify the given version 2 context in place based on extra_context. + """Modify the given version 2 context in place based on extra_context. + + :parameter context: cookiecutter context. + :parameter extra_context: optional dictionary of key/value pairs to The extra_context parameter may be a dictionary or a list of dictionaries. @@ -168,20 +170,17 @@ def apply_overwrites_to_context_v2(context, extra_context): Changing the 'name' field requires a special syntax. Because the algorithm chosen to find a variable’s dictionary entry in the variables list of OrderDicts uses the variable’s ‘name’ field; it could not be used to - simultaneously hold a new ‘name’ field value. Therefore the following - extra context dictionary entry snytax was introduced to allow the ‘name’ + simultaneously hold a new ‘name’ field value. Therefor the following + extra context dictionary entry sytax was introduced to allow the ‘name’ field of a variable to be changed: - { - 'name': 'CURRENT_VARIABLE_NAME::NEW_VARIABLE_NAME', - } + { 'name': 'CURRENT_VARIABLE_NAME::NEW_VARIABLE_NAME',} So, for example, to change a variable’s ‘name’ field from ‘director_credit’ to ‘producer_credit’, would require: - { - 'name': 'director_credit::producer_credit', - } + { 'name': 'director_credit::producer_credit', } + Removing a Field from a Variable -------------------------------- @@ -191,10 +190,8 @@ def apply_overwrites_to_context_v2(context, extra_context): In order to accomplish this a remove field token is used in the extra context as follows: - { - 'name': 'director_cut', - 'skip_if': '<>', - } + { 'name': 'director_cut', + 'skip_if': '<>', } In the example above, the extra context overwrite results in the variable named ‘director_cut’ having it’s ‘skip_if’ field removed. diff --git a/docs/cookiecutter.rst b/docs/cookiecutter.rst index 9e63654c9..f1086a079 100644 --- a/docs/cookiecutter.rst +++ b/docs/cookiecutter.rst @@ -25,9 +25,9 @@ cookiecutter.context module --------------------------- .. automodule:: cookiecutter.context - :members: - :undoc-members: - :show-inheritance: + :members: + :undoc-members: + :show-inheritance: cookiecutter.environment module ------------------------------- From 4881a9c53f1e4c893f4705f23264876a811cf09f Mon Sep 17 00:00:00 2001 From: Christo De Lange Date: Fri, 3 Feb 2023 18:51:13 -0500 Subject: [PATCH 257/274] fix: submodule --- .gitmodules => gitmodules | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .gitmodules => gitmodules (100%) diff --git a/.gitmodules b/gitmodules similarity index 100% rename from .gitmodules rename to gitmodules From 057a3cd6eb1190e548251920696eb6fc281ba888 Mon Sep 17 00:00:00 2001 From: Christo De Lange Date: Fri, 3 Feb 2023 18:54:20 -0500 Subject: [PATCH 258/274] chore: fix submodule --- .gitmodules | 3 +++ docs/HelloCookieCutter1 | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 .gitmodules diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..e0372d2b5 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "docs/HelloCookieCutter1"] + path = docs/HelloCookieCutter1 + url = https://github.com/BruceEckel/HelloCookieCutter1 diff --git a/docs/HelloCookieCutter1 b/docs/HelloCookieCutter1 index 1e4c68092..239ea6928 160000 --- a/docs/HelloCookieCutter1 +++ b/docs/HelloCookieCutter1 @@ -1 +1 @@ -Subproject commit 1e4c680929339bec24cf5c306a1c34e2e4821946 +Subproject commit 239ea692896301eaa280dd407fdd4d5c55cf6998 From 9a3a7bc0e67a56f52a4589f1ea027da3811fdaeb Mon Sep 17 00:00:00 2001 From: Christo De Lange Date: Fri, 3 Feb 2023 19:01:22 -0500 Subject: [PATCH 259/274] fix: obsolete submodule --- .gitmodules | 3 --- gitmodules | 3 --- 2 files changed, 6 deletions(-) delete mode 100644 .gitmodules delete mode 100644 gitmodules diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index e0372d2b5..000000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "docs/HelloCookieCutter1"] - path = docs/HelloCookieCutter1 - url = https://github.com/BruceEckel/HelloCookieCutter1 diff --git a/gitmodules b/gitmodules deleted file mode 100644 index e0372d2b5..000000000 --- a/gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "docs/HelloCookieCutter1"] - path = docs/HelloCookieCutter1 - url = https://github.com/BruceEckel/HelloCookieCutter1 From 9562d775e119fdc0b4e359eb2ca9aae0b6708b56 Mon Sep 17 00:00:00 2001 From: Christo De Lange Date: Fri, 3 Feb 2023 19:02:30 -0500 Subject: [PATCH 260/274] fix: scrub docs --- docs/HelloCookieCutter1 | 1 - 1 file changed, 1 deletion(-) delete mode 160000 docs/HelloCookieCutter1 diff --git a/docs/HelloCookieCutter1 b/docs/HelloCookieCutter1 deleted file mode 160000 index 239ea6928..000000000 --- a/docs/HelloCookieCutter1 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 239ea692896301eaa280dd407fdd4d5c55cf6998 From ad9e7ba2a221318a19d226578bd39d31b1d0c737 Mon Sep 17 00:00:00 2001 From: Christo De Lange Date: Fri, 3 Feb 2023 19:43:29 -0500 Subject: [PATCH 261/274] chore: Cover the deeper cases --- cookiecutter/generate.py | 2 +- .../representative-var.json | 26 +++ tests/test_generate_context_v2.py | 166 ++++++++++++++++++ 3 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 tests/test-generate-context-v2/representative-var.json diff --git a/cookiecutter/generate.py b/cookiecutter/generate.py index d60cd4d4e..f6eb3b2e8 100644 --- a/cookiecutter/generate.py +++ b/cookiecutter/generate.py @@ -130,7 +130,7 @@ def resolve_changed_variable_names(context, variables_to_resolve): ) # noqa elif isinstance(variable[field_name], list): - # a choices field could have an str item to update + # a choices field could have a str item to update for i, item in enumerate(variable[field_name]): if isinstance(item, str): if var_name_to_resolve in item: diff --git a/tests/test-generate-context-v2/representative-var.json b/tests/test-generate-context-v2/representative-var.json new file mode 100644 index 000000000..45acc36a5 --- /dev/null +++ b/tests/test-generate-context-v2/representative-var.json @@ -0,0 +1,26 @@ +{ + "name": "cc-representative", + "cookiecutter_version": "2.0.0", + "variables" : [ + { + "name": "director_credit", + "default": true, + "prompt": "Is there a director credit on this film?", + "description": "Directors take credit for most of their films, usually...", + "type": "boolean" + }, + { + "name": "director_name", + "default": "{{cookiecutter.director_credit}}", + "prompt": "What's the Director's full name?", + "prompt_user": true, + "description": "The default director is not proud of their work, we hope you are.", + "hide_input": false, + "choices": ["Allan Smithe", "Ridley Scott", "Victor Fleming", "John Houston"], + "validation": "^[a-z][A-Z]+$", + "validation_flags": ["verbose", "ascii"], + "skip_if": "{{cookiecutter.director_credit == False}}", + "type": "string" + } + ] +} diff --git a/tests/test_generate_context_v2.py b/tests/test_generate_context_v2.py index 223130977..0b06330a2 100644 --- a/tests/test_generate_context_v2.py +++ b/tests/test_generate_context_v2.py @@ -1245,6 +1245,158 @@ def gen_context_data_inputs_expected(): yield context_with_valid_extra_8 +def gen_context_data_inputs_expected_var(): + # Test the ability to change the variable's name field (since it is used + # to identify the variable to be modifed) with extra context and to remove + # a key from the context via the removal token: '<>' + context_with_valid_extra_2 = ( + { + 'context_file': 'tests/test-generate-context-v2/representative.json', + 'extra_context': [ + { + 'name': 'director_credit::producer_credit', + 'prompt': 'Is there a producer credit on this film?', + 'description': 'There are usually a lot of producers...', + }, + { + 'name': 'director_name', + 'skip_if': '<>', + }, + ], + }, + { + "representative": OrderedDict( + [ + ("name", "cc-representative"), + ("cookiecutter_version", "2.0.0"), + ( + "variables", + [ + OrderedDict( + [ + ("name", "producer_credit"), + ("default", True), + ( + "prompt", + "Is there a producer credit on this film?", + ), + ( + "description", + "There are usually a lot of producers...", + ), + ("type", "boolean"), + ] + ), + OrderedDict( + [ + ("name", "director_name"), + ("default", "Allan Smithe"), + ("prompt", "What's the Director's full name?"), + ("prompt_user", True), + ( + "description", + "The default director is not proud of their work, we hope you are.", + ), + ("hide_input", False), + ( + "choices", + [ + "Allan Smithe", + "Ridley Scott", + "Victor Fleming", + "John Houston", + ], + ), + ("validation", "^[a-z][A-Z]+$"), + ("validation_flags", ["verbose", "ascii"]), + ("type", "string"), + ] + ), + ], + ), + ] + ) + }, + ) + # Test the ability to change the variable's name field (since it is used + # to identify the variable to be modifed) with extra context and to also + # test that any other references in other variables that might use the + # original variable name get updated as well. + context_with_valid_extra_2_B = ( + { + 'context_file': 'tests/test-generate-context-v2/representative_2B.json', + 'extra_context': [ + { + 'name': 'director_credit::producer_credit', + 'prompt': 'Is there a producer credit on this film?', + 'description': 'There are usually a lot of producers...', + }, + ], + }, + { + "representative_2B": OrderedDict( + [ + ("name", "cc-representative"), + ("cookiecutter_version", "2.0.0"), + ( + "variables", + [ + OrderedDict( + [ + ("name", "producer_credit"), + ("default", True), + ( + "prompt", + "Is there a producer credit on this film?", + ), + ( + "description", + "There are usually a lot of producers...", + ), + ("type", "boolean"), + ] + ), + OrderedDict( + [ + ("name", "director_name"), + ("default", "Allan Smithe"), + ("prompt", "What's the Director's full name?"), + ("prompt_user", True), + ( + "description", + "The default director is not proud of their work, we hope you are.", + ), + ("hide_input", False), + ( + "choices", + [ + "Allan Smithe", + "Ridley Scott", + "Victor Fleming", + "John Houston", + "{{cookiecutter.producer_credit}}", + ], + ), + ("validation", "^[a-z][A-Z]+$"), + ("validation_flags", ["verbose", "ascii"]), + ( + "skip_if", + "{{cookiecutter.producer_credit == False}}", + ), + ("type", "string"), + ] + ), + ], + ), + ] + ) + }, + ) + + yield context_with_valid_extra_2 + yield context_with_valid_extra_2_B + + @pytest.mark.usefixtures('clean_system') @pytest.mark.parametrize( 'input_params, expected_context', gen_context_data_inputs_expected() @@ -1259,6 +1411,20 @@ def test_generate_context_with_extra_context_dictionary( assert generate.generate_context(**input_params) == expected_context +@pytest.mark.usefixtures('clean_system') +@pytest.mark.parametrize( + 'input_params, expected_context', gen_context_data_inputs_expected_var() +) +def test_generate_context_with_extra_context_dictionary_var( + input_params, expected_context, monkeypatch +): + """ + Test the generated context with extra content overwrite to multiple fields, + with creation of new fields NOT allowed. + """ + assert generate.generate_context(**input_params) == expected_context + + def context_data_2(): context_with_valid_extra_2_A = ( { From 63633df02dde81f9112a5b15e4aed8a137c93aa0 Mon Sep 17 00:00:00 2001 From: Christo De Lange Date: Fri, 3 Feb 2023 19:56:25 -0500 Subject: [PATCH 262/274] fix: coverage --- tests/test-generate-context-v2/representative-var.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test-generate-context-v2/representative-var.json b/tests/test-generate-context-v2/representative-var.json index 45acc36a5..a83ee70c5 100644 --- a/tests/test-generate-context-v2/representative-var.json +++ b/tests/test-generate-context-v2/representative-var.json @@ -4,7 +4,7 @@ "variables" : [ { "name": "director_credit", - "default": true, + "default": false, "prompt": "Is there a director credit on this film?", "description": "Directors take credit for most of their films, usually...", "type": "boolean" From 8e705be4f9049c6c704bd40bcafe50bee55aa60e Mon Sep 17 00:00:00 2001 From: Christo De Lange Date: Sat, 4 Feb 2023 08:32:35 -0500 Subject: [PATCH 263/274] fix: Cover the uncovered branches --- cookiecutter/context.py | 3 +- setup.cfg | 2 +- tests/test-context/cookiecutter.json | 12 ++ tests/test-context/cookiecutter_skips_1.json | 13 ++ .../representative-director.json | 36 ++++++ .../representative-var.json | 6 +- .../test_choices-miss.json | 17 +++ tests/test_context.py | 19 +++ tests/test_generate_context_v2.py | 113 +++++++++++++++++- 9 files changed, 214 insertions(+), 7 deletions(-) create mode 100644 tests/test-generate-context-v2/representative-director.json create mode 100644 tests/test-generate-context-v2/test_choices-miss.json diff --git a/cookiecutter/context.py b/cookiecutter/context.py index 6120b8d08..41d3c5156 100644 --- a/cookiecutter/context.py +++ b/cookiecutter/context.py @@ -540,7 +540,8 @@ def load_context(json_object, no_input=False, verbose=True): if variable.skip_if: skip_template = env.from_string(variable.skip_if) - if skip_template.render(cookiecutter=context) == 'True': + skip_value = skip_template.render(cookiecutter=context) + if skip_value == 'True': continue if variable.do_if: diff --git a/setup.cfg b/setup.cfg index 3f29fe9a0..f3b8719a7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -10,7 +10,7 @@ max-line-length = 88 [tool:pytest] testpaths = tests -addopts = -vvv --cov-report term-missing --cov=cookiecutter +addopts = -vvv --cov-report=html --cov-report=xml --cov-branch --cov-fail-under=100 --cov-report term-missing --cov=cookiecutter [doc8] # TODO: Remove current max-line-length ignore in follow-up and adopt black limit. diff --git a/tests/test-context/cookiecutter.json b/tests/test-context/cookiecutter.json index 72f91ed22..eb746bb4d 100644 --- a/tests/test-context/cookiecutter.json +++ b/tests/test-context/cookiecutter.json @@ -88,6 +88,18 @@ ], "skip_if": "{{cookiecutter.docs == False}}" }, + { + "name": "docs_tool_skip", + "default": "mkdocs", + "prompt": "Which tool do you want to choose for generating docs?", + "description": "There are a number of options for documentation generators. Please choose one. We will create a separate folder for you", + "type": "string", + "choices": [ + "mkdocs", + "sphinx" + ], + "skip_if": "{{cookiecutter.docs == True}}" + }, { "name": "year", "default": "{% now 'utc', '%Y' %}", diff --git a/tests/test-context/cookiecutter_skips_1.json b/tests/test-context/cookiecutter_skips_1.json index 6e78c1c8a..523ee217f 100644 --- a/tests/test-context/cookiecutter_skips_1.json +++ b/tests/test-context/cookiecutter_skips_1.json @@ -57,6 +57,19 @@ ], "do_if": "{{cookiecutter.project_logging_enabled == True}}" }, + { + "name": "project_console_logging_level_skip", + "default": "WARN", + "prompt": "Select the minimum logging level to log to the console.", + "type": "string", + "choices": [ + "WARN", + "INFO", + "DEBUG", + "ERROR" + ], + "skip_if": "{{cookiecutter.project_logging_enabled == False}}" + }, { "name": "project_file_logging_enabled", "default": true, diff --git a/tests/test-generate-context-v2/representative-director.json b/tests/test-generate-context-v2/representative-director.json new file mode 100644 index 000000000..51b9490b0 --- /dev/null +++ b/tests/test-generate-context-v2/representative-director.json @@ -0,0 +1,36 @@ +{ + "name": "cc-representative", + "cookiecutter_version": "2.0.0", + "variables" : [ + { + "name": "director_credit", + "default": true, + "prompt": "Is there a director credit on this film?", + "description": "Directors take credit for most of their films, usually...", + "type": "boolean" + }, + { + "name": "director_exists", + "default": false, + "prompt": "Is there a Director?", + "prompt_user": true, + "description": "The director exists.", + "hide_input": false, + "choices": [true, false], + "type": "boolean" + }, + { + "name": "director_name", + "default": "Allan Smithe", + "prompt": "What's the Director's full name?", + "prompt_user": true, + "description": "The default director is not proud of their work, we hope you are.", + "hide_input": false, + "choices": ["Allan Smithe", "Ridley Scott", "Victor Fleming", "John Houston"], + "validation": "^[a-z][A-Z]+$", + "validation_flags": ["verbose", "ascii"], + "skip_if": "{{cookiecutter.director_credit == False}}", + "type": "string" + } + ] +} diff --git a/tests/test-generate-context-v2/representative-var.json b/tests/test-generate-context-v2/representative-var.json index a83ee70c5..3be4d675a 100644 --- a/tests/test-generate-context-v2/representative-var.json +++ b/tests/test-generate-context-v2/representative-var.json @@ -4,19 +4,19 @@ "variables" : [ { "name": "director_credit", - "default": false, + "default": true, "prompt": "Is there a director credit on this film?", "description": "Directors take credit for most of their films, usually...", "type": "boolean" }, { "name": "director_name", - "default": "{{cookiecutter.director_credit}}", + "default": "Allan Smithe", "prompt": "What's the Director's full name?", "prompt_user": true, "description": "The default director is not proud of their work, we hope you are.", "hide_input": false, - "choices": ["Allan Smithe", "Ridley Scott", "Victor Fleming", "John Houston"], + "choices": ["Allan Smithe", "Ridley Scott", "Victor Fleming", "John Houston","{{cookiecutter.director_name}}"], "validation": "^[a-z][A-Z]+$", "validation_flags": ["verbose", "ascii"], "skip_if": "{{cookiecutter.director_credit == False}}", diff --git a/tests/test-generate-context-v2/test_choices-miss.json b/tests/test-generate-context-v2/test_choices-miss.json new file mode 100644 index 000000000..5ced20b25 --- /dev/null +++ b/tests/test-generate-context-v2/test_choices-miss.json @@ -0,0 +1,17 @@ +{ + "name": "test_choices-miss", + "cookiecutter_version": "2.0.0", + "variables" : [ + { + "name": "license", + "default": "Apache2", + "choices": [ + "MIT", + "BSD3", + "GNU-GPL3", + "Apache2", + "Mozilla2" + ] + } + ] +} diff --git a/tests/test_context.py b/tests/test_context.py index 79b594d2b..868fd05bf 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -702,6 +702,25 @@ def test_load_context_with_input_choices(mocker): assert cc_cfg['license'] == 'MIT' +def test_load_context_with_input_choices_no_verbose(mocker): + cc = load_cookiecutter('tests/test-context/cookiecutter_choices.json') + + INPUT_1 = 'E.R. Uber' + INPUT_2 = 'eruber@gmail.com' + INPUT_3 = '2' # 'MIT' + mocker.patch( + 'click.termui.visible_prompt_func', + autospec=True, + side_effect=[INPUT_1, INPUT_2, INPUT_3], + ) + + cc_cfg = context.load_context(cc['cookiecutter_choices'], no_input=False, verbose=False) + + assert cc_cfg['full_name'] == INPUT_1 + assert cc_cfg['email'] == INPUT_2 + assert cc_cfg['license'] == 'MIT' + + def test_load_context_with_input_with_validation_success(mocker): cc = load_cookiecutter('tests/test-context/cookiecutter_val_success.json') diff --git a/tests/test_generate_context_v2.py b/tests/test_generate_context_v2.py index 0b06330a2..1f45f8dc6 100644 --- a/tests/test_generate_context_v2.py +++ b/tests/test_generate_context_v2.py @@ -319,6 +319,83 @@ def context_data(): yield context_choices_with_default_not_in_choices +def context_data_misses(): + context_choices_with_default = ( + { + 'context_file': 'tests/test-generate-context-v2/test_choices-miss.json', + 'default_context': {'license': 'Cherokee'}, + }, + { + "test_choices-miss": OrderedDict( + [ + ("name", "test_choices-miss"), + ("cookiecutter_version", "2.0.0"), + ( + "variables", + [ + OrderedDict( + [ + ("name", "license"), + ("default", "Apache2"), + ( + "choices", + [ + "MIT", + "BSD3", + "GNU-GPL3", + "Apache2", + "Mozilla2", + ], + ), + ] + ) + ], + ), + ] + ) + }, + ) + + context_choices_with_extra = ( + { + 'context_file': 'tests/test-generate-context-v2/test_choices-miss.json', + 'extra_context': {'license': 'MIT'}, + }, + { + "test_choices-miss": OrderedDict( + [ + ("name", "test_choices-miss"), + ("cookiecutter_version", "2.0.0"), + ( + "variables", + [ + OrderedDict( + [ + ("name", "license"), + ("default", "MIT"), + ( + "choices", + [ + "MIT", + "BSD3", + "GNU-GPL3", + "Apache2", + "Mozilla2", + ], + ), + ] + ) + ], + ), + ] + ) + }, + ) + + yield context_choices_with_default + yield context_choices_with_extra + + def context_data_value_errors(): context_choices_with_default_value_error = ( { @@ -408,6 +485,17 @@ def test_generate_context(input_params, expected_context): assert generated_context == expected_context +@pytest.mark.usefixtures('clean_system') +@pytest.mark.parametrize('input_params, expected_context', context_data_misses()) +def test_generate_context_misses(input_params, expected_context): + """ + Test the generated context for several input parameters against the + according expected context. + """ + generated_context = generate.generate_context(**input_params) + assert generated_context == expected_context + + @pytest.mark.usefixtures('clean_system') @pytest.mark.parametrize( 'input_params, expected_context, raise_exception', context_data_value_errors() @@ -639,7 +727,7 @@ def gen_context_data_inputs_expected(): # a key from the context via the removal token: '<>' context_with_valid_extra_2 = ( { - 'context_file': 'tests/test-generate-context-v2/representative.json', + 'context_file': 'tests/test-generate-context-v2/representative-director.json', 'extra_context': [ { 'name': 'director_credit::producer_credit', @@ -653,7 +741,7 @@ def gen_context_data_inputs_expected(): ], }, { - "representative": OrderedDict( + "representative-director": OrderedDict( [ ("name", "cc-representative"), ("cookiecutter_version", "2.0.0"), @@ -675,6 +763,27 @@ def gen_context_data_inputs_expected(): ("type", "boolean"), ] ), + OrderedDict( + [ + ("name", "director_exists"), + ("default", False), + ("prompt", "Is there a Director?"), + ("prompt_user", True), + ( + "description", + "The director exists.", + ), + ("hide_input", False), + ( + "choices", + [ + True, + False, + ], + ), + ("type", "boolean"), + ] + ), OrderedDict( [ ("name", "director_name"), From 5225f5f45407b015e89c30c4f7a2fc4dcf75405a Mon Sep 17 00:00:00 2001 From: Christo De Lange Date: Sat, 4 Feb 2023 08:35:23 -0500 Subject: [PATCH 264/274] fix: lint --- tests/test_context.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_context.py b/tests/test_context.py index 868fd05bf..4cc32feaf 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -714,7 +714,9 @@ def test_load_context_with_input_choices_no_verbose(mocker): side_effect=[INPUT_1, INPUT_2, INPUT_3], ) - cc_cfg = context.load_context(cc['cookiecutter_choices'], no_input=False, verbose=False) + cc_cfg = context.load_context( + cc['cookiecutter_choices'], no_input=False, verbose=False + ) assert cc_cfg['full_name'] == INPUT_1 assert cc_cfg['email'] == INPUT_2 From f7c2acf0ae5364b8c2414a8757ee110e7f60d6a0 Mon Sep 17 00:00:00 2001 From: Christo De Lange Date: Sat, 4 Feb 2023 08:44:02 -0500 Subject: [PATCH 265/274] fix: All new versions hit this - bypassing for py3.8 --- cookiecutter/context.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cookiecutter/context.py b/cookiecutter/context.py index 41d3c5156..5e8f45b12 100644 --- a/cookiecutter/context.py +++ b/cookiecutter/context.py @@ -582,7 +582,8 @@ def load_context(json_object, no_input=False, verbose=True): click.echo(variable.validation_msg) else: # no validation defined - break + # Only py3.8 does not detect coverage here + break # pragma: no cover if verbose: width, _ = shutil.get_terminal_size() From 7d4c852f806c9b9acf5fe807744b2e8fad71e79a Mon Sep 17 00:00:00 2001 From: Christo De Lange Date: Sat, 4 Feb 2023 08:51:26 -0500 Subject: [PATCH 266/274] fix: Cheat the peephole optimizer for py3.8 --- cookiecutter/context.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cookiecutter/context.py b/cookiecutter/context.py index 5e8f45b12..389cda653 100644 --- a/cookiecutter/context.py +++ b/cookiecutter/context.py @@ -581,9 +581,12 @@ def load_context(json_object, no_input=False, verbose=True): if variable.validation_msg: click.echo(variable.validation_msg) else: + # Assign a random variable here to disable the peephole + # optimizer so that coverage can see this line. + # See bpo-2506 for more information. + no_peephole_opt = None # no validation defined - # Only py3.8 does not detect coverage here - break # pragma: no cover + break if verbose: width, _ = shutil.get_terminal_size() From 0724f7c480758843888cf103c544443c9a4aa445 Mon Sep 17 00:00:00 2001 From: Christo De Lange Date: Sat, 4 Feb 2023 09:29:29 -0500 Subject: [PATCH 267/274] Fix jsonschema refs --- setup.cfg | 2 +- setup.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 95eb909d2..34eedfd32 100644 --- a/setup.cfg +++ b/setup.cfg @@ -67,7 +67,7 @@ install_requires = jinja2-time>=0.2.0 python-slugify>=4.0.0 requests>=2.23.0 - jsonschema>=3.2.0' + jsonschema>=3.2.0 [options.entry_points] console_scripts = diff --git a/setup.py b/setup.py index d61fbd6ba..47f00a7e7 100644 --- a/setup.py +++ b/setup.py @@ -14,6 +14,7 @@ 'jinja2-time>=0.2.0', 'python-slugify>=4.0.0', 'requests>=2.23.0', + 'jsonschema>=3.2.0', ] setup( From 7004717618101b2701b7ad9ededad7650a589823 Mon Sep 17 00:00:00 2001 From: Christo De Lange <111363591+christokur@users.noreply.github.com> Date: Sat, 4 Feb 2023 13:48:15 -0500 Subject: [PATCH 268/274] fix: Minor merge issues before working on tests --- cookiecutter/context.py | 15 +++--- cookiecutter/main.py | 14 +++--- tests/test_context.py | 77 +++++++++++++++++++++++-------- tests/test_generate_context_v2.py | 24 ++++++++-- tests/test_main.py | 27 ----------- 5 files changed, 95 insertions(+), 62 deletions(-) diff --git a/cookiecutter/context.py b/cookiecutter/context.py index 06b71632b..35917b6d3 100644 --- a/cookiecutter/context.py +++ b/cookiecutter/context.py @@ -132,7 +132,6 @@ def prompt_int(variable, default): def prompt_float(variable, default): """Prompts the user for float.""" - return click.prompt( variable.prompt, default=default, @@ -143,7 +142,6 @@ def prompt_float(variable, default): def prompt_uuid(variable, default): """Prompts the user for a uuid.""" - return click.prompt( variable.prompt, default=default, @@ -155,13 +153,16 @@ def prompt_uuid(variable, default): def prompt_json(variable, default): """Prompts the user for a JSON entry.""" # The JSON object from cookiecutter.json might be very large - # We only show 'default' + # We only show 'default' default_json = 'default' def process_json(user_value): try: - return json.loads(user_value, object_pairs_hook=collections.OrderedDict,) + return json.loads( + user_value, + object_pairs_hook=collections.OrderedDict, + ) except ValueError: # json.decoder.JSONDecodeError raised in Python 3.5, 3.6 # but it inherits from ValueError which is raised in Python 3.4 @@ -441,7 +442,8 @@ def __init__(self, name: str, type: str, **info): def __repr__(self): """Provide a representation with variable name.""" return "<{class_name} {variable_name}>".format( - class_name=self.__class__.__name__, variable_name=self.name, + class_name=self.__class__.__name__, + variable_name=self.name, ) def __str__(self): @@ -504,7 +506,8 @@ def __init__(self, template, requires=None, extensions=None, **kwargs): def __repr__(self): """Provide a classname with template name.""" return "<{class_name} {template_name}>".format( - class_name=self.__class__.__name__, template_name=self.name, + class_name=self.__class__.__name__, + template_name=self.name, ) def __iter__(self): diff --git a/cookiecutter/main.py b/cookiecutter/main.py index 517c0a98d..5fdabd086 100644 --- a/cookiecutter/main.py +++ b/cookiecutter/main.py @@ -96,13 +96,13 @@ def cookiecutter( else: context_file = os.path.join(repo_dir, 'cookiecutter.json') logger.debug('context_file is %s', context_file) - + context = generate_context( context_file=context_file, default_context=config_dict['default_context'], extra_context=extra_context, ) - + # prompt the user to manually configure at the command line. # except when 'no-input' flag is set if infer_schema_version(context['cookiecutter']) in ['2.0']: @@ -111,18 +111,18 @@ def cookiecutter( ) else: context['cookiecutter'] = prompt_for_config(context, no_input) - + # include template dir or url in the context dict context['cookiecutter']['_template'] = template - + # include repo dir or url in the context dict context['cookiecutter']['_repo_dir'] = repo_dir - + # include output+dir in the context dict context['cookiecutter']['_output_dir'] = os.path.abspath(output_dir) - + dump(config_dict['replay_dir'], template_name, context) - + # Create project from local context and project template. result = generate_files( repo_dir=repo_dir, diff --git a/tests/test_context.py b/tests/test_context.py index dc28d74a4..a917a902e 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -168,7 +168,9 @@ def test_prompt_string(mocker): expected_value = 'Input String' mock_prompt = mocker.patch( - 'cookiecutter.prompt.click.prompt', autospec=True, return_value=expected_value, + 'cookiecutter.prompt.click.prompt', + autospec=True, + return_value=expected_value, ) m = mocker.Mock() @@ -180,7 +182,10 @@ def test_prompt_string(mocker): r = context.prompt_string(v, default='Alpha') assert mock_prompt.call_args == mocker.call( - v.prompt, default='Alpha', hide_input=v.hide_input, type=click.STRING, + v.prompt, + default='Alpha', + hide_input=v.hide_input, + type=click.STRING, ) assert r == expected_value @@ -191,7 +196,9 @@ def test_prompt_bool(mocker): expected_value = True mock_prompt = mocker.patch( - 'cookiecutter.prompt.click.prompt', autospec=True, return_value=expected_value, + 'cookiecutter.prompt.click.prompt', + autospec=True, + return_value=expected_value, ) m = mocker.Mock() @@ -203,7 +210,10 @@ def test_prompt_bool(mocker): r = context.prompt_boolean(v, default=False) assert mock_prompt.call_args == mocker.call( - v.prompt, default=False, hide_input=v.hide_input, type=click.BOOL, + v.prompt, + default=False, + hide_input=v.hide_input, + type=click.BOOL, ) assert r # expected_value @@ -214,7 +224,9 @@ def test_prompt_int(mocker): expected_value = 777 mock_prompt = mocker.patch( - 'cookiecutter.prompt.click.prompt', autospec=True, return_value=expected_value, + 'cookiecutter.prompt.click.prompt', + autospec=True, + return_value=expected_value, ) m = mocker.Mock() @@ -226,7 +238,10 @@ def test_prompt_int(mocker): r = context.prompt_int(v, default=1000) assert mock_prompt.call_args == mocker.call( - v.prompt, default=1000, hide_input=v.hide_input, type=click.INT, + v.prompt, + default=1000, + hide_input=v.hide_input, + type=click.INT, ) assert r == expected_value @@ -237,7 +252,9 @@ def test_prompt_float(mocker): expected_value = 3.14 mock_prompt = mocker.patch( - 'cookiecutter.prompt.click.prompt', autospec=True, return_value=expected_value, + 'cookiecutter.prompt.click.prompt', + autospec=True, + return_value=expected_value, ) m = mocker.Mock() @@ -249,7 +266,10 @@ def test_prompt_float(mocker): r = context.prompt_float(v, default=3.0) assert mock_prompt.call_args == mocker.call( - v.prompt, default=3.0, hide_input=v.hide_input, type=click.FLOAT, + v.prompt, + default=3.0, + hide_input=v.hide_input, + type=click.FLOAT, ) assert r == expected_value @@ -260,7 +280,9 @@ def test_prompt_uuid(mocker): expected_value = '931ef56c3e7b45eea0427bac386f0a98' mock_prompt = mocker.patch( - 'cookiecutter.prompt.click.prompt', autospec=True, return_value=expected_value, + 'cookiecutter.prompt.click.prompt', + autospec=True, + return_value=expected_value, ) m = mocker.Mock() @@ -272,7 +294,10 @@ def test_prompt_uuid(mocker): r = context.prompt_uuid(v, default=None) assert mock_prompt.call_args == mocker.call( - v.prompt, default=None, hide_input=v.hide_input, type=click.UUID, + v.prompt, + default=None, + hide_input=v.hide_input, + type=click.UUID, ) assert r == expected_value @@ -283,7 +308,9 @@ def test_prompt_json(monkeypatch, mocker): expected_value = '{"port": 67888, "colors": ["red", "green", "blue"]}' mocker.patch( - 'click.termui.visible_prompt_func', autospec=True, return_value=expected_value, + 'click.termui.visible_prompt_func', + autospec=True, + return_value=expected_value, ) m = mocker.Mock() m.side_effect = context.Variable @@ -325,7 +352,9 @@ def test_prompt_json_default(mocker): cfg = '{"port": 67888, "colors": ["red", "green", "blue"]}' mock_prompt = mocker.patch( - 'cookiecutter.prompt.click.prompt', autospec=True, return_value=expected_value, + 'cookiecutter.prompt.click.prompt', + autospec=True, + return_value=expected_value, ) m = mocker.Mock() @@ -352,7 +381,9 @@ def test_prompt_yes_no_default_no(mocker): expected_value = 'y' mock_prompt = mocker.patch( - 'cookiecutter.prompt.click.prompt', autospec=True, return_value=expected_value, + 'cookiecutter.prompt.click.prompt', + autospec=True, + return_value=expected_value, ) m = mocker.Mock() @@ -368,7 +399,10 @@ def test_prompt_yes_no_default_no(mocker): r = context.prompt_yes_no(v, default=False) assert mock_prompt.call_args == mocker.call( - v.prompt, default='n', hide_input=v.hide_input, type=click.BOOL, + v.prompt, + default='n', + hide_input=v.hide_input, + type=click.BOOL, ) assert r # expected_value @@ -379,7 +413,9 @@ def test_prompt_yes_no_default_yes(mocker): expected_value = 'y' mock_prompt = mocker.patch( - 'cookiecutter.prompt.click.prompt', autospec=True, return_value=expected_value, + 'cookiecutter.prompt.click.prompt', + autospec=True, + return_value=expected_value, ) m = mocker.Mock() @@ -395,7 +431,10 @@ def test_prompt_yes_no_default_yes(mocker): r = context.prompt_yes_no(v, default=True) assert mock_prompt.call_args == mocker.call( - v.prompt, default='y', hide_input=v.hide_input, type=click.BOOL, + v.prompt, + default='y', + hide_input=v.hide_input, + type=click.BOOL, ) assert r # expected_value @@ -411,7 +450,9 @@ def test_prompt_choice(mocker): expected_license = 'MIT' mocker.patch( - 'cookiecutter.prompt.click.prompt', autospec=True, return_value=expected_value, + 'cookiecutter.prompt.click.prompt', + autospec=True, + return_value=expected_value, ) m = mocker.Mock() @@ -535,7 +576,7 @@ def test_variable_str(): assert "validation='^[a-z_]+$'" in v_str assert "validation_flag_names='['ignorecase']'" in v_str assert ("validation_flags='2'" in v_str) | (".IGNORECASE" in v_str) - + if sys.version_info >= (3, 4): assert "validate='re.compile('^[a-z_]+$', re.IGNORECASE)'" in v_str else: diff --git a/tests/test_generate_context_v2.py b/tests/test_generate_context_v2.py index 0b7e621ac..1eb0b91c9 100644 --- a/tests/test_generate_context_v2.py +++ b/tests/test_generate_context_v2.py @@ -635,7 +635,10 @@ def gen_context_data_inputs_expected(): 'prompt': 'Is there a producer credit on this film?', 'description': 'There are usually a lot of producers...', }, - {'name': 'director_name', 'skip_if': '<>',}, + { + 'name': 'director_name', + 'skip_if': '<>', + }, ], }, {"representative": expected_file2_v1}, @@ -797,7 +800,12 @@ def gen_context_data_inputs_expected(): context_with_valid_extra_6 = ( { 'context_file': 'tests/test-generate-context-v2/representative.json', - 'extra_context': [{'name': 'director_name', 'default': 'John Ford',}], + 'extra_context': [ + { + 'name': 'director_name', + 'default': 'John Ford', + } + ], }, {"representative": expected_file2_v5}, ) @@ -827,7 +835,12 @@ def gen_context_data_inputs_expected(): context_with_valid_extra_8 = ( { 'context_file': 'tests/test-generate-context-v2/representative.json', - 'extra_context': [{'name': 'director_name', 'default': 'Peter Sellers',}], + 'extra_context': [ + { + 'name': 'director_name', + 'default': 'Peter Sellers', + } + ], }, {"representative": expected_file2_v7}, ) @@ -1034,7 +1047,10 @@ def test_raise_exception_when_attempting_to_remove_mandatory_field(): used to specify which variable to remove. """ xtra_context = [ - {'name': 'director_name', 'default': '<>',}, + { + 'name': 'director_name', + 'default': '<>', + }, ] with pytest.raises(ValueError) as excinfo: diff --git a/tests/test_main.py b/tests/test_main.py index 5f857343c..0af183596 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -134,30 +134,3 @@ def test_custom_replay_file(monkeypatch, mocker, user_config_file): '.', 'custom-replay-file', ) - - -def test_version_2_load_context_call(monkeypatch, mocker, user_config_file): - """Check that the version 2 load_context() is called. - - Change the current working directory temporarily to - 'tests/test-generate-context-v2/min-v2-cookiecutter' - for this test and call cookiecutter with '.' for the target template. - """ - monkeypatch.chdir('tests/test-generate-context-v2/min-v2-cookiecutter') - - mock_replay_dump = mocker.patch('cookiecutter.main.dump') - - mock_version_1_prompt_for_config = mocker.patch( - 'cookiecutter.main.prompt_for_config' - ) - mock_version_2_load_context = mocker.patch('cookiecutter.main.load_context') - - mocker.patch('cookiecutter.main.generate_files') - - cookiecutter( - '.', no_input=True, replay=False, config_file=user_config_file, - ) - - assert mock_version_1_prompt_for_config.call_count == 0 - assert mock_version_2_load_context.call_count == 1 - assert mock_replay_dump.call_count == 1 From 3ad378d94d3296b7546fd0db33bacf11f89cc690 Mon Sep 17 00:00:00 2001 From: Christo De Lange <111363591+christokur@users.noreply.github.com> Date: Sat, 4 Feb 2023 14:11:00 -0500 Subject: [PATCH 269/274] fix: Working on tests --- cookiecutter/context.py | 3 +- tests/test_context.py | 22 -------------- tests/test_generate_context_v2.py | 48 ++++++++++++++++--------------- 3 files changed, 27 insertions(+), 46 deletions(-) diff --git a/cookiecutter/context.py b/cookiecutter/context.py index 35917b6d3..9bf5a8fd8 100644 --- a/cookiecutter/context.py +++ b/cookiecutter/context.py @@ -14,6 +14,7 @@ https://github.com/hackebrot/cookiecutter/tree/new-context-format """ +import shutil import collections import json @@ -602,7 +603,7 @@ def jinja_render(string): # for prompt esthetics if verbose: - width, _ = click.get_terminal_size() + width, _ = shutil.get_terminal_size() click.echo('-' * width) # updating the skipping variables for the continuation diff --git a/tests/test_context.py b/tests/test_context.py index a917a902e..c8e7711db 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -583,28 +583,6 @@ def test_variable_str(): assert "validate='<_sre.SRE_Pattern object at" in v_str -def test_variable_option_raise_invalid_type_value_error(): - - VAR_NAME = 'module_name' - OPT_VALUE_OF_INCORRECT_TYPE = 12 # should be a string - - with pytest.raises(ValueError) as excinfo: - context.Variable( - VAR_NAME, - "{{cookiecutter.plugin_name|lower|replace('-','_')}}", - prompt="Please enter a name for your base python module", - type='string', - validation=OPT_VALUE_OF_INCORRECT_TYPE, - validation_flags=['ignorecase'], - hide_input=True, - ) - - msg = "Variable: '{var_name}' Option: 'validation' requires a value of type str, but has a value of: {value}" - assert msg.format(var_name=VAR_NAME, value=OPT_VALUE_OF_INCORRECT_TYPE) in str( - excinfo.value - ) - - def test_cookiecutter_template_repr(): # name, cookiecutter_version, variables, **info diff --git a/tests/test_generate_context_v2.py b/tests/test_generate_context_v2.py index 1eb0b91c9..4fbfa7eb9 100644 --- a/tests/test_generate_context_v2.py +++ b/tests/test_generate_context_v2.py @@ -317,6 +317,16 @@ def context_data_serializer(): yield context_choices_with_default_not_in_choices +@pytest.mark.usefixtures('clean_system') +@pytest.mark.parametrize('input_params, expected_context', context_data_serializer()) +def test_generate_context(input_params, expected_context): + """ + Test the generated context for several input parameters against the + according expected context. + """ + assert generate.generate_context(**input_params) == expected_context + + def context_data_misses(): context_choices_with_default = ( { @@ -370,7 +380,7 @@ def context_data_misses(): OrderedDict( [ ("name", "license"), - ("default", "MIT"), + ("default", "Apache2"), ( "choices", [ @@ -394,6 +404,17 @@ def context_data_misses(): yield context_choices_with_extra +@pytest.mark.usefixtures('clean_system') +@pytest.mark.parametrize('input_params, expected_context', context_data_misses()) +def test_generate_context_misses(input_params, expected_context): + """ + Test the generated context for several input parameters against the + according expected context. + """ + generated_context = generate.generate_context(**input_params) + assert generated_context == expected_context + + def context_data_value_errors(): context_choices_with_default_value_error = ( { @@ -429,13 +450,13 @@ def context_data_value_errors(): ] ) }, - False, + True, ) context_choices_with_extra_value_error = ( { 'context_file': 'tests/test-generate-context-v2/test_choices.json', 'default_context': {'license': 'Apache2'}, - 'extra_context': [{'license': 'MIT'}], + 'extra_context': [{'name': 'license', 'default': 'MIT'}], }, { "test_choices": OrderedDict( @@ -466,31 +487,12 @@ def context_data_value_errors(): ] ) }, - True, + False, ) yield context_choices_with_default_value_error yield context_choices_with_extra_value_error -def test_generate_context(input_params, expected_context): - """ - Test the generated context for several input parameters against the - according expected context. - """ - assert generate.generate_context(**input_params) == expected_context - - -@pytest.mark.usefixtures('clean_system') -@pytest.mark.parametrize('input_params, expected_context', context_data_misses()) -def test_generate_context_misses(input_params, expected_context): - """ - Test the generated context for several input parameters against the - according expected context. - """ - generated_context = generate.generate_context(**input_params) - assert generated_context == expected_context - - @pytest.mark.usefixtures('clean_system') @pytest.mark.parametrize( 'input_params, expected_context, raise_exception', context_data_value_errors() From 61427f60dc1094842d2da2c6c05ffb721d9629a3 Mon Sep 17 00:00:00 2001 From: Christo De Lange <111363591+christokur@users.noreply.github.com> Date: Sun, 5 Feb 2023 10:59:57 -0500 Subject: [PATCH 270/274] fix: Fix tests and improve coverage --- cookiecutter/context.py | 2 +- setup.cfg | 2 +- .../cookiecutter-no-requires.json | 154 ++++++ tests/test_context.py | 35 ++ tests/test_generate_context_v2.py | 443 ------------------ tests/test_read_user_dict.py | 14 +- 6 files changed, 202 insertions(+), 448 deletions(-) create mode 100644 tests/test-context/cookiecutter-no-requires.json diff --git a/cookiecutter/context.py b/cookiecutter/context.py index 9bf5a8fd8..960c8cfb1 100644 --- a/cookiecutter/context.py +++ b/cookiecutter/context.py @@ -479,7 +479,7 @@ def __init__(self, template, requires=None, extensions=None, **kwargs): self.extensions = extensions if self.requirements: - self.cookiecutter_version = self.requirements.get('cookiecutter') + self.cookiecutter_version = self.requirements.get('cookiecutter', None) if self.cookiecutter_version: validate_requirement( self.cookiecutter_version, diff --git a/setup.cfg b/setup.cfg index 34eedfd32..29242fa98 100644 --- a/setup.cfg +++ b/setup.cfg @@ -85,7 +85,7 @@ max-line-length = 88 [tool:pytest] testpaths = tests -addopts = -vvv --cov-report=html --cov-report=xml --cov-branch --cov-fail-under=100 --cov-report term-missing --cov=cookiecutter +addopts = --cov-report=html --cov-report=xml --cov-branch --cov-fail-under=100 --cov-report term-missing --cov=cookiecutter [doc8] # TODO: Remove current max-line-length ignore in follow-up and adopt black limit. diff --git a/tests/test-context/cookiecutter-no-requires.json b/tests/test-context/cookiecutter-no-requires.json new file mode 100644 index 000000000..32523b078 --- /dev/null +++ b/tests/test-context/cookiecutter-no-requires.json @@ -0,0 +1,154 @@ +{ + "version": "2.0", + "requires": { + "python": ">=3" + }, + "jinja": { + "optimized": true, + "extensions": [ + "cookiecutter.extensions.SlugifyExtension", + "jinja2_time.TimeExtension" + ] + }, + "template": { + "name": "cookiecutter-pytest-plugin", + "version": "0.1", + "description": "a cookiecutter to create pytest plugins with ease.", + "authors": [ + "Raphael Pierzina ", + "Audrey Roy Greenfeld " + ], + "license": "MIT", + "keywords": [ + "pytest", + "python", + "plugin" + ], + "url": "https://github.com/pytest-dev/cookiecutter-pytest-plugin", + "variables": [ + { + "name": "full_name", + "default": "Raphael Pierzina", + "prompt": "What's your full name?", + "description": "Please enter your full name. It will be displayed on the README file and used for the PyPI package definition.", + "type": "string" + }, + { + "name": "email", + "default": "raphael@hackebrot.de", + "prompt": "What's your email?", + "description": "Please enter an email address for the meta information in setup.py.", + "type": "string" + }, + { + "name": "secret_token", + "prompt": "Please enter your secret token", + "type": "string", + "hide_input": true + }, + { + "name": "plugin_name", + "default": "emoji", + "prompt": "What should be the name for your plugin?", + "description": "Please enter a name for your plugin. We will prepend the name with 'pytest-'", + "type": "string" + }, + { + "name": "module_name", + "default": "{{cookiecutter.plugin_name|lower|replace('-','_')}}", + "prompt": "Please enter a name for your base python module", + "type": "string", + "validation": "^[a-z_]+$" + }, + { + "name": "license", + "default": "MIT", + "prompt": "Please choose a license!", + "description": "Cookiecutter will add an according LICENSE file for you and set the according classifier in setup.py.", + "type": "string", + "choices": [ + "MIT", + "BSD-3", + "GNU GPL v3.0", + "Apache Software License 2.0", + "Mozilla Public License 2.0" + ] + }, + { + "name": "docs", + "default": false, + "prompt": "Do you want to generate a base for docs?", + "description": "Would you like to generate documentation for your plugin? You will be able to choose from a number of generators.", + "type": "yes_no" + }, + { + "name": "docs_tool", + "default": "mkdocs", + "prompt": "Which tool do you want to choose for generating docs?", + "description": "There are a number of options for documentation generators. Please choose one. We will create a separate folder for you", + "type": "string", + "choices": [ + "mkdocs", + "sphinx" + ], + "skip_if": "{{cookiecutter.docs == False}}" + }, + { + "name": "year", + "default": "{% now 'utc', '%Y' %}", + "prompt_user": false, + "type": "string" + }, + { + "name": "incept_year", + "default": 2017, + "prompt_user": false, + "type": "int" + }, + { + "name": "released", + "default": false, + "prompt_user": false, + "type": "boolean" + }, + { + "name": "temperature", + "default": 77.3, + "prompt_user": false, + "type": "float" + }, + { + "name": "Release-GUID", + "default": "04f5eaa9ee7345469dccffc538b27194", + "prompt_user": false, + "type": "uuid" + }, + { + "name": "copy_with_out_render", + "default": [ + "*.html", + "*not_rendered_dir", + "rendered_dir/not_rendered_file.ini" + ], + "prompt_user": false, + "type": "string" + }, + { + "name": "fixtures", + "default": { + "foo": { + "scope": "session", + "autouse": true + }, + "bar": { + "scope": "function", + "autouse": false + } + }, + "description": "Please enter a valid JSON string to set up fixtures for your plugin.", + "prompt_user": true, + "type": "json" + } + ] + } +} diff --git a/tests/test_context.py b/tests/test_context.py index c8e7711db..806421a05 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -90,6 +90,41 @@ def test_load_context_defaults(): ) +@pytest.mark.usefixtures('clean_system') +def test_load_context_defaults_no_requires(): + + cc = load_cookiecutter('tests/test-context/cookiecutter-no-requires.json') + cc_cfg = context.load_context(cc['cookiecutter-no-requires'], no_input=True, verbose=False) + + assert cc_cfg['full_name'] == 'Raphael Pierzina' + assert cc_cfg['email'] == 'raphael@hackebrot.de' + assert cc_cfg['plugin_name'] == 'emoji' + assert cc_cfg['module_name'] == 'emoji' + assert cc_cfg['license'] == 'MIT' + assert cc_cfg['docs'] is False + assert 'docs_tool' not in cc_cfg.keys() # skip_if worked + assert cc_cfg['year'] == time.strftime('%Y') + assert cc_cfg['incept_year'] == 2017 + assert cc_cfg['released'] is False + assert cc_cfg['temperature'] == 77.3 + assert cc_cfg['Release-GUID'] == UUID('04f5eaa9ee7345469dccffc538b27194').hex + assert cc_cfg['_extensions'] == [ + 'cookiecutter.extensions.SlugifyExtension', + 'jinja2_time.TimeExtension', + ] + assert cc_cfg['_jinja2_env_vars'] == {"optimized": True} + assert ( + cc_cfg['copy_with_out_render'] + == "['*.html', '*not_rendered_dir', 'rendered_dir/not_rendered_file.ini']" + ) + assert cc_cfg['fixtures'] == OrderedDict( + [ + ('foo', OrderedDict([('scope', 'session'), ('autouse', True)])), + ('bar', OrderedDict([('scope', 'function'), ('autouse', False)])), + ] + ) + + @pytest.mark.usefixtures('clean_system') def test_load_context_defaults_skips_branch(): """ diff --git a/tests/test_generate_context_v2.py b/tests/test_generate_context_v2.py index 4fbfa7eb9..d0c8228a4 100644 --- a/tests/test_generate_context_v2.py +++ b/tests/test_generate_context_v2.py @@ -597,449 +597,6 @@ def test_generate_context_extra_ctx_list_item_dict_no_name_field_match(): assert msg in str(excinfo.value) -def gen_context_data_inputs_expected(): - """ - Creates a generator of combination: - ((input file, additional params), expected output) - """ - context_with_valid_extra_0 = ( - { - 'context_file': 'tests/test-generate-context-v2/test.json', - 'extra_context': [ - { - 'name': 'email', - 'default': 'miles.davis@jazz.gone', - 'description': 'Enter jazzy email...', - 'extra_field': 'extra_field_value', - } - ], - }, - {"test": expected_file1_v4}, - ) - # Empty extra context precipitates no ill effect - context_with_valid_extra_1 = ( - { - 'context_file': 'tests/test-generate-context-v2/representative.json', - 'extra_context': [], - }, - {"representative": expected_file2_v0}, - ) - - # Test the ability to change the variable's name field (since it is used - # to identify the variable to be modifed) with extra context and to remove - # a key from the context via the removal token: '<>' - context_with_valid_extra_2 = ( - { - 'context_file': 'tests/test-generate-context-v2/representative-director.json', - 'extra_context': [ - { - 'name': 'director_credit::producer_credit', - 'prompt': 'Is there a producer credit on this film?', - 'description': 'There are usually a lot of producers...', - }, - { - 'name': 'director_name', - 'skip_if': '<>', - }, - ], - }, - {"representative": expected_file2_v1}, - ) - # Test the ability to change the variable's name field (since it is used - # to identify the variable to be modifed) with extra context and to also - context_with_valid_extra_2_B = ( - { - 'context_file': 'tests/test-generate-context-v2/representative_2B.json', - 'extra_context': [ - { - 'name': 'director_credit::producer_credit', - 'prompt': 'Is there a producer credit on this film?', - 'description': 'There are usually a lot of producers...', - }, - ], - }, - { - "representative_2B": OrderedDict( - [ - ("version", "2.0"), - ( - "requires", - OrderedDict([("cookiecutter", ">1"), ("python", ">=3.0")]), - ), - ( - "template", - OrderedDict( - [ - ("name", "cc-representative"), - ( - "variables", - [ - OrderedDict( - [ - ("name", "producer_credit"), - ("default", True), - ( - "prompt", - "Is there a producer credit on this film?", - ), - ( - "description", - "There are usually a lot of producers...", - ), - ("type", "boolean"), - ] - ), - OrderedDict( - [ - ("name", "director_name"), - ("default", "Allan Smithe"), - ( - "prompt", - "What's the Director's full name?", - ), - ("prompt_user", True), - ( - "description", - "The default director is not proud " - "of their work, we hope you are.", - ), - ("hide_input", False), - ( - "choices", - [ - "Allan Smithe", - "Ridley Scott", - "Victor Fleming", - "John Houston", - "{{cookiecutter.producer_credit}}", - ], - ), - ("validation", "^[a-z][A-Z]+$"), - ( - "validation_flags", - ["verbose", "ascii"], - ), - ( - "skip_if", - "{{cookiecutter.producer_credit == False}}", - ), - ("type", "string"), - ] - ), - ], - ), - ] - ), - ), - ] - ) - }, - ) - # test that any other references in other variables that might use the - # original variable name get updated as well. - - # Test changing variable's name field value, default field, prompt field, - # and changing the type - context_with_valid_extra_3 = ( - { - 'context_file': 'tests/test-generate-context-v2/representative.json', - 'extra_context': [ - { - 'name': 'director_credit::producer_credits', - 'default': 2, - 'prompt': 'How many producers does this film have?', - 'description': 'There are usually a lot of producers...', - 'type': "int", - } - ], - }, - {"representative": expected_file2_v2}, - ) - # Test changing choices field without changing the default, but default - # does not change because the first position in choices matches default - context_with_valid_extra_4 = ( - { - 'context_file': 'tests/test-generate-context-v2/representative.json', - 'extra_context': [ - { - 'name': 'director_name', - 'choices': [ - 'Allan Smithe', - 'Ridley Scott', - 'Victor Fleming', - 'John Houston', - 'John Ford', - 'Billy Wilder', - ], - } - ], - }, - {"representative": expected_file2_v3}, - ) - # Test changing choices field and changing the default - context_with_valid_extra_5 = ( - { - 'context_file': 'tests/test-generate-context-v2/representative.json', - 'extra_context': [ - { - 'name': 'director_name', - 'default': 'John Ford', - 'choices': [ - 'Allan Smithe', - 'Ridley Scott', - 'Victor Fleming', - 'John Houston', - 'John Ford', - 'Billy Wilder', - ], - } - ], - }, - {"representative": expected_file2_v4}, - ) - # Test changing the default, but not the choices field, yet seeing choices field re-ordered - # to put default value in first location - context_with_valid_extra_6 = ( - { - 'context_file': 'tests/test-generate-context-v2/representative.json', - 'extra_context': [ - { - 'name': 'director_name', - 'default': 'John Ford', - } - ], - }, - {"representative": expected_file2_v5}, - ) - # Test changing choices field without changing the default, but default - # does get changee because the first position in choices field chagned - context_with_valid_extra_7 = ( - { - 'context_file': 'tests/test-generate-context-v2/representative.json', - 'extra_context': [ - { - 'name': 'director_name', - 'choices': [ - 'Billy Wilder', - 'Allan Smithe', - 'Ridley Scott', - 'Victor Fleming', - 'John Houston', - 'John Ford', - ], - } - ], - }, - {"representative": expected_file2_v6}, - ) - # Test changing the default value with a value that is not in choices, - # we should see the choice first position get updated. - context_with_valid_extra_8 = ( - { - 'context_file': 'tests/test-generate-context-v2/representative.json', - 'extra_context': [ - { - 'name': 'director_name', - 'default': 'Peter Sellers', - } - ], - }, - {"representative": expected_file2_v7}, - ) - yield context_with_valid_extra_0 - yield context_with_valid_extra_1 - yield context_with_valid_extra_2 - yield context_with_valid_extra_2_B - yield context_with_valid_extra_3 - yield context_with_valid_extra_4 - yield context_with_valid_extra_5 - yield context_with_valid_extra_6 - yield context_with_valid_extra_7 - yield context_with_valid_extra_8 - - -def gen_context_data_inputs_expected_var(): - # Test the ability to change the variable's name field (since it is used - # to identify the variable to be modifed) with extra context and to remove - # a key from the context via the removal token: '<>' - context_with_valid_extra_2 = ( - { - 'context_file': 'tests/test-generate-context-v2/representative.json', - 'extra_context': [ - { - 'name': 'director_credit::producer_credit', - 'prompt': 'Is there a producer credit on this film?', - 'description': 'There are usually a lot of producers...', - }, - { - 'name': 'director_name', - 'skip_if': '<>', - }, - ], - }, - { - "representative": OrderedDict( - [ - ("name", "cc-representative"), - ("cookiecutter_version", "2.0.0"), - ( - "variables", - [ - OrderedDict( - [ - ("name", "producer_credit"), - ("default", True), - ( - "prompt", - "Is there a producer credit on this film?", - ), - ( - "description", - "There are usually a lot of producers...", - ), - ("type", "boolean"), - ] - ), - OrderedDict( - [ - ("name", "director_name"), - ("default", "Allan Smithe"), - ("prompt", "What's the Director's full name?"), - ("prompt_user", True), - ( - "description", - "The default director is not proud of their work, we hope you are.", - ), - ("hide_input", False), - ( - "choices", - [ - "Allan Smithe", - "Ridley Scott", - "Victor Fleming", - "John Houston", - ], - ), - ("validation", "^[a-z][A-Z]+$"), - ("validation_flags", ["verbose", "ascii"]), - ("type", "string"), - ] - ), - ], - ), - ] - ) - }, - ) - # Test the ability to change the variable's name field (since it is used - # to identify the variable to be modifed) with extra context and to also - # test that any other references in other variables that might use the - # original variable name get updated as well. - context_with_valid_extra_2_B = ( - { - 'context_file': 'tests/test-generate-context-v2/representative_2B.json', - 'extra_context': [ - { - 'name': 'director_credit::producer_credit', - 'prompt': 'Is there a producer credit on this film?', - 'description': 'There are usually a lot of producers...', - }, - ], - }, - { - "representative_2B": OrderedDict( - [ - ("name", "cc-representative"), - ("cookiecutter_version", "2.0.0"), - ( - "variables", - [ - OrderedDict( - [ - ("name", "producer_credit"), - ("default", True), - ( - "prompt", - "Is there a producer credit on this film?", - ), - ( - "description", - "There are usually a lot of producers...", - ), - ("type", "boolean"), - ] - ), - OrderedDict( - [ - ("name", "director_name"), - ("default", "Allan Smithe"), - ("prompt", "What's the Director's full name?"), - ("prompt_user", True), - ( - "description", - "The default director is not proud of their work, we hope you are.", - ), - ("hide_input", False), - ( - "choices", - [ - "Allan Smithe", - "Ridley Scott", - "Victor Fleming", - "John Houston", - "{{cookiecutter.producer_credit}}", - ], - ), - ("validation", "^[a-z][A-Z]+$"), - ("validation_flags", ["verbose", "ascii"]), - ( - "skip_if", - "{{cookiecutter.producer_credit == False}}", - ), - ("type", "string"), - ] - ), - ], - ), - ] - ) - }, - ) - - yield context_with_valid_extra_2 - yield context_with_valid_extra_2_B - - -@pytest.mark.usefixtures('clean_system') -@pytest.mark.parametrize( - 'input_params, expected_context', gen_context_data_inputs_expected() -) -def test_generate_context_with_extra_context_dictionary( - input_params, expected_context, monkeypatch -): - """ - Test the generated context with extra content overwrite to multiple fields, - with creation of new fields NOT allowed. - """ - assert OrderedDict(generate.generate_context(**input_params)) == OrderedDict( - expected_context - ) - - -@pytest.mark.usefixtures('clean_system') -@pytest.mark.parametrize( - 'input_params, expected_context', gen_context_data_inputs_expected_var() -) -def test_generate_context_with_extra_context_dictionary_var( - input_params, expected_context, monkeypatch -): - """ - Test the generated context with extra content overwrite to multiple fields, - with creation of new fields NOT allowed. - """ - assert generate.generate_context(**input_params) == expected_context - - @pytest.mark.usefixtures('clean_system') def test_raise_exception_when_attempting_to_remove_mandatory_field(): """ diff --git a/tests/test_read_user_dict.py b/tests/test_read_user_dict.py index 0ce50efc2..7d8a2063a 100644 --- a/tests/test_read_user_dict.py +++ b/tests/test_read_user_dict.py @@ -1,6 +1,6 @@ """Test `process_json`, `read_user_dict` functions in `cookiecutter.prompt`.""" -import click import pytest +import click.testing from cookiecutter.prompt import ( process_json, @@ -106,11 +106,19 @@ def test_should_not_load_json_from_sentinel(mocker): 'cookiecutter.prompt.json.loads', autospec=True, return_value={} ) + +def test_should_not_call_process_json_default_value(mocker, monkeypatch): + """Make sure that `process_json` is not called when using default value.""" + mock_process_json = mocker.patch( + 'cookiecutter.prompt.process_json', autospec=True, return_value='default') + runner = click.testing.CliRunner() - with runner.isolation(input="\n"): + with runner.isolation(input="\n") as streams: read_user_dict('name', {'project_slug': 'pytest-plugin'}) + stdout, stderr = streams + assert not stdout.getvalue().decode().strip() == 'name [default]:\n' - mock_json_loads.assert_not_called() + mock_process_json.assert_not_called() @pytest.mark.parametrize("input", ["\n", "default\n"]) From 91452961dea9e3d6eef90f382878a9c74d03288c Mon Sep 17 00:00:00 2001 From: Christo De Lange <111363591+christokur@users.noreply.github.com> Date: Sun, 5 Feb 2023 11:16:18 -0500 Subject: [PATCH 271/274] fix: Test missed overwrite in default and extra contexts --- .../test_choices-miss.json | 33 +++-- tests/test_generate_context_v2.py | 137 +++++++++--------- 2 files changed, 88 insertions(+), 82 deletions(-) diff --git a/tests/test-generate-context-v2/test_choices-miss.json b/tests/test-generate-context-v2/test_choices-miss.json index 5ced20b25..b1ff8e3aa 100644 --- a/tests/test-generate-context-v2/test_choices-miss.json +++ b/tests/test-generate-context-v2/test_choices-miss.json @@ -1,17 +1,20 @@ { - "name": "test_choices-miss", - "cookiecutter_version": "2.0.0", - "variables" : [ - { - "name": "license", - "default": "Apache2", - "choices": [ - "MIT", - "BSD3", - "GNU-GPL3", - "Apache2", - "Mozilla2" - ] - } - ] + "version": "2.0", + "template": { + "name": "test_choices-miss", + "variables": [ + { + "name": "license", + "type": "string", + "default": "MIT", + "choices": [ + "MIT", + "BSD3", + "GNU-GPL3", + "Apache2", + "Mozilla2" + ] + } + ] + } } diff --git a/tests/test_generate_context_v2.py b/tests/test_generate_context_v2.py index d0c8228a4..c24014bc7 100644 --- a/tests/test_generate_context_v2.py +++ b/tests/test_generate_context_v2.py @@ -309,26 +309,8 @@ def context_data_serializer(): }, {"test": expected_file1_v0}, ) - yield context - yield context_with_default - yield context_with_extra - yield context_with_default_and_extra - yield context_choices_with_default - yield context_choices_with_default_not_in_choices - - -@pytest.mark.usefixtures('clean_system') -@pytest.mark.parametrize('input_params, expected_context', context_data_serializer()) -def test_generate_context(input_params, expected_context): - """ - Test the generated context for several input parameters against the - according expected context. - """ - assert generate.generate_context(**input_params) == expected_context - -def context_data_misses(): - context_choices_with_default = ( + context_choices_with_default_misses = ( { 'context_file': 'tests/test-generate-context-v2/test_choices-miss.json', 'default_context': {'license': 'Cherokee'}, @@ -336,35 +318,43 @@ def context_data_misses(): { "test_choices-miss": OrderedDict( [ - ("name", "test_choices-miss"), - ("cookiecutter_version", "2.0.0"), + ("version", "2.0"), ( - "variables", - [ - OrderedDict( - [ - ("name", "license"), - ("default", "Apache2"), - ( - "choices", - [ - "MIT", - "BSD3", - "GNU-GPL3", - "Apache2", - "Mozilla2", - ], - ), - ] - ) - ], + "template", + OrderedDict( + [ + ("name", "test_choices-miss"), + ( + "variables", + [ + OrderedDict( + [ + ("name", "license"), + ("type", "string"), + ("default", "MIT"), + ( + "choices", + [ + "MIT", + "BSD3", + "GNU-GPL3", + "Apache2", + "Mozilla2", + ], + ), + ] + ) + ], + ), + ] + ), ), ] ) }, ) - context_choices_with_extra = ( + context_choices_with_extra_misses = ( { 'context_file': 'tests/test-generate-context-v2/test_choices-miss.json', 'extra_context': {'license': 'MIT'}, @@ -372,47 +362,60 @@ def context_data_misses(): { "test_choices-miss": OrderedDict( [ - ("name", "test_choices-miss"), - ("cookiecutter_version", "2.0.0"), + ("version", "2.0"), ( - "variables", - [ - OrderedDict( - [ - ("name", "license"), - ("default", "Apache2"), - ( - "choices", - [ - "MIT", - "BSD3", - "GNU-GPL3", - "Apache2", - "Mozilla2", - ], - ), - ] - ) - ], + "template", + OrderedDict( + [ + ("name", "test_choices-miss"), + ( + "variables", + [ + OrderedDict( + [ + ("name", "license"), + ("type", "string"), + ("default", "MIT"), + ( + "choices", + [ + "MIT", + "BSD3", + "GNU-GPL3", + "Apache2", + "Mozilla2", + ], + ), + ] + ) + ], + ), + ] + ), ), ] ) }, ) + yield context + yield context_with_default + yield context_with_extra + yield context_with_default_and_extra yield context_choices_with_default - yield context_choices_with_extra + yield context_choices_with_default_not_in_choices + yield context_choices_with_default_misses + yield context_choices_with_extra_misses @pytest.mark.usefixtures('clean_system') -@pytest.mark.parametrize('input_params, expected_context', context_data_misses()) -def test_generate_context_misses(input_params, expected_context): +@pytest.mark.parametrize('input_params, expected_context', context_data_serializer()) +def test_generate_context(input_params, expected_context): """ Test the generated context for several input parameters against the according expected context. """ - generated_context = generate.generate_context(**input_params) - assert generated_context == expected_context + assert generate.generate_context(**input_params) == expected_context def context_data_value_errors(): From f6ada27425b626890aafa6d212a12242ff45fd6c Mon Sep 17 00:00:00 2001 From: Christo De Lange <111363591+christokur@users.noreply.github.com> Date: Sun, 5 Feb 2023 11:44:11 -0500 Subject: [PATCH 272/274] fix: Coverage of resolve_changed_variable_names --- tests/test_generate_context_v2.py | 236 +++++++++++++++++++++++++++--- 1 file changed, 215 insertions(+), 21 deletions(-) diff --git a/tests/test_generate_context_v2.py b/tests/test_generate_context_v2.py index c24014bc7..84d0370bb 100644 --- a/tests/test_generate_context_v2.py +++ b/tests/test_generate_context_v2.py @@ -233,9 +233,9 @@ def context_data_serializer(): context_with_default = ( { - 'context_file': 'tests/test-generate-context-v2/test.json', + 'context_file': 'tests/test-generate-context-v2/test.json', 'default_context': { - 'full_name': 'James Patrick Morgan', + 'full_name': 'James Patrick Morgan', 'this_key_ignored': 'not_in_context', }, }, @@ -244,7 +244,7 @@ def context_data_serializer(): context_with_extra = ( { - 'context_file': 'tests/test-generate-context-v2/test.json', + 'context_file': 'tests/test-generate-context-v2/test.json', 'extra_context': {'email': 'jpm@chase.bk'}, }, {"test": expected_file1_v2}, @@ -252,7 +252,7 @@ def context_data_serializer(): context_choices_with_default = ( { - 'context_file': 'tests/test-generate-context-v2/test_choices.json', + 'context_file': 'tests/test-generate-context-v2/test_choices.json', 'default_context': {'license': 'Apache2'}, }, { @@ -295,16 +295,16 @@ def context_data_serializer(): ) context_with_default_and_extra = ( { - 'context_file': 'tests/test-generate-context-v2/test.json', + 'context_file': 'tests/test-generate-context-v2/test.json', 'default_context': {'full_name': 'Alpha Gamma Five'}, - 'extra_context': {'email': 'agamma5@universe.open'}, + 'extra_context': {'email': 'agamma5@universe.open'}, }, {"test": expected_file1_v3}, ) context_choices_with_default_not_in_choices = ( { - 'context_file': 'tests/test-generate-context-v2/test.json', + 'context_file': 'tests/test-generate-context-v2/test.json', 'default_context': {'orientation': 'landscape'}, }, {"test": expected_file1_v0}, @@ -312,7 +312,7 @@ def context_data_serializer(): context_choices_with_default_misses = ( { - 'context_file': 'tests/test-generate-context-v2/test_choices-miss.json', + 'context_file': 'tests/test-generate-context-v2/test_choices-miss.json', 'default_context': {'license': 'Cherokee'}, }, { @@ -356,7 +356,7 @@ def context_data_serializer(): context_choices_with_extra_misses = ( { - 'context_file': 'tests/test-generate-context-v2/test_choices-miss.json', + 'context_file': 'tests/test-generate-context-v2/test_choices-miss.json', 'extra_context': {'license': 'MIT'}, }, { @@ -421,7 +421,7 @@ def test_generate_context(input_params, expected_context): def context_data_value_errors(): context_choices_with_default_value_error = ( { - 'context_file': 'tests/test-generate-context-v2/test_choices.json', + 'context_file': 'tests/test-generate-context-v2/test_choices.json', 'default_context': [{'license': 'MIT'}], }, { @@ -457,9 +457,9 @@ def context_data_value_errors(): ) context_choices_with_extra_value_error = ( { - 'context_file': 'tests/test-generate-context-v2/test_choices.json', + 'context_file': 'tests/test-generate-context-v2/test_choices.json', 'default_context': {'license': 'Apache2'}, - 'extra_context': [{'name': 'license', 'default': 'MIT'}], + 'extra_context': [{'name': 'license', 'default': 'MIT'}], }, { "test_choices": OrderedDict( @@ -557,10 +557,10 @@ def test_generate_context_extra_ctx_list_item_dict_missing_name_field(): xtra_context = [ { "shouldbename": "author_name", - "default": "Robert Lewis", - "prompt": "What's the author's name?", - "description": "Please enter the author's full name.", - "type": "string", + "default": "Robert Lewis", + "prompt": "What's the author's name?", + "description": "Please enter the author's full name.", + "type": "string", } ] with pytest.raises(ValueError) as excinfo: @@ -582,11 +582,11 @@ def test_generate_context_extra_ctx_list_item_dict_no_name_field_match(): """ xtra_context = [ { - "name": "author_name", - "default": "Robert Lewis", - "prompt": "What's the author's name?", + "name": "author_name", + "default": "Robert Lewis", + "prompt": "What's the author's name?", "description": "Please enter the author's full name.", - "type": "string", + "type": "string", } ] with pytest.raises(ValueError) as excinfo: @@ -610,7 +610,7 @@ def test_raise_exception_when_attempting_to_remove_mandatory_field(): """ xtra_context = [ { - 'name': 'director_name', + 'name': 'director_name', 'default': '<>', }, ] @@ -623,3 +623,197 @@ def test_raise_exception_when_attempting_to_remove_mandatory_field(): ) assert "Cannot remove mandatory 'default' field" in str(excinfo.value) + + +def gen_context_data_inputs_expected_var(): + # Test the ability to change the variable's name field (since it is used + # to identify the variable to be modifed) with extra context and to remove + # a key from the context via the removal token: '<>' + context_with_valid_extra_2 = ( + { + 'context_file': 'tests/test-generate-context-v2/representative.json', + 'extra_context': [ + { + 'name': 'director_credit::producer_credit', + 'prompt': 'Is there a producer credit on this film?', + 'description': 'There are usually a lot of producers...', + }, + { + 'name': 'director_name', + 'skip_if': '<>', + }, + ], + }, + OrderedDict( + [ + ( + "representative", OrderedDict( + [ + ('version', '2.0'), + ( + "template", OrderedDict( + [ + ("name", "cc-representative"), + ( + "variables", + [ + OrderedDict( + [ + ("name", "producer_credit"), + ("default", True), + ( + "prompt", + "Is there a producer credit on this film?", + ), + ( + "description", + "There are usually a lot of producers...", + ), + ("type", "boolean"), + ] + ), + OrderedDict( + [ + ("name", "director_name"), + ("default", "Allan Smithe"), + ("prompt", "What's the Director's full name?"), + ("prompt_user", True), + ( + "description", + "The default director is not proud of their work, we hope you are.", + ), + ("hide_input", False), + ( + "choices", + [ + "Allan Smithe", + "Ridley Scott", + "Victor Fleming", + "John Houston", + ], + ), + ("validation", "^[a-z][A-Z]+$"), + ("validation_flags", ["verbose", "ascii"]), + ("type", "string"), + ] + ), + ], + ), + ] + ) + ), + ] + ) + ) + ] + ) + ) + # Test the ability to change the variable's name field (since it is used + # to identify the variable to be modifed) with extra context and to also + # test that any other references in other variables that might use the + # original variable name get updated as well. + context_with_valid_extra_2_B = ( + { + 'context_file': 'tests/test-generate-context-v2/representative_2B.json', + 'extra_context': [ + { + 'name': 'director_credit::producer_credit', + 'prompt': 'Is there a producer credit on this film?', + 'description': 'There are usually a lot of producers...', + }, + ], + }, + OrderedDict( + [ + ( + "representative_2B", OrderedDict( + [ + ('version', '2.0'), + ( + 'requires', OrderedDict( + [ + ('cookiecutter', '>1'), + ('python', '>=3.0') + ] + ) + ), + ( + "template", OrderedDict( + [ + ("name", "cc-representative"), + ( + "variables", + [ + OrderedDict( + [ + ("name", "producer_credit"), + ("default", True), + ( + "prompt", + "Is there a producer credit on this film?", + ), + ( + "description", + "There are usually a lot of producers...", + ), + ("type", "boolean"), + ] + ), + OrderedDict( + [ + ("name", "director_name"), + ("default", "Allan Smithe"), + ("prompt", "What's the Director's full name?"), + ("prompt_user", True), + ( + "description", + "The default director is not proud of their work, we hope you are.", + ), + ("hide_input", False), + ( + "choices", + [ + "Allan Smithe", + "Ridley Scott", + "Victor Fleming", + "John Houston", + "{{cookiecutter.producer_credit}}", + ], + ), + ("validation", "^[a-z][A-Z]+$"), + ("validation_flags", ["verbose", "ascii"]), + ( + "skip_if", + "{{cookiecutter.producer_credit == False}}", + ), + ("type", "string"), + ] + ), + ], + ), + ] + ) + ) + ] + ) + ) + ] + ) + ) + + yield context_with_valid_extra_2 + yield context_with_valid_extra_2_B + + +@pytest.mark.usefixtures('clean_system') +@pytest.mark.parametrize( + 'input_params, expected_context', gen_context_data_inputs_expected_var() +) +def test_generate_context_with_extra_context_dictionary_var( + input_params, expected_context, monkeypatch +): + """ + Test the generated context with extra content overwrite to multiple fields, + with creation of new fields NOT allowed. + """ + assert generate.generate_context(**input_params) == expected_context From 0c042df1d9a3d0540eda534aeb00beab15057b7d Mon Sep 17 00:00:00 2001 From: Christo De Lange <111363591+christokur@users.noreply.github.com> Date: Sun, 5 Feb 2023 12:21:04 -0500 Subject: [PATCH 273/274] fix: test_generate_context_with_extra_context_dictionary --- .../representative-director.json | 77 ++-- tests/test_context.py | 4 +- tests/test_generate_context_v2.py | 406 +++++++++++++++--- tests/test_read_user_dict.py | 3 +- 4 files changed, 407 insertions(+), 83 deletions(-) diff --git a/tests/test-generate-context-v2/representative-director.json b/tests/test-generate-context-v2/representative-director.json index 51b9490b0..708e653c7 100644 --- a/tests/test-generate-context-v2/representative-director.json +++ b/tests/test-generate-context-v2/representative-director.json @@ -1,36 +1,49 @@ { + "version": "2.0", + "template": { "name": "cc-representative", - "cookiecutter_version": "2.0.0", - "variables" : [ - { - "name": "director_credit", - "default": true, - "prompt": "Is there a director credit on this film?", - "description": "Directors take credit for most of their films, usually...", - "type": "boolean" - }, - { - "name": "director_exists", - "default": false, - "prompt": "Is there a Director?", - "prompt_user": true, - "description": "The director exists.", - "hide_input": false, - "choices": [true, false], - "type": "boolean" - }, - { - "name": "director_name", - "default": "Allan Smithe", - "prompt": "What's the Director's full name?", - "prompt_user": true, - "description": "The default director is not proud of their work, we hope you are.", - "hide_input": false, - "choices": ["Allan Smithe", "Ridley Scott", "Victor Fleming", "John Houston"], - "validation": "^[a-z][A-Z]+$", - "validation_flags": ["verbose", "ascii"], - "skip_if": "{{cookiecutter.director_credit == False}}", - "type": "string" - } + "variables": [ + { + "name": "director_credit", + "default": true, + "prompt": "Is there a director credit on this film?", + "description": "Directors take credit for most of their films, usually...", + "type": "boolean" + }, + { + "name": "director_exists", + "default": false, + "prompt": "Is there a Director?", + "prompt_user": true, + "description": "The director exists.", + "hide_input": false, + "choices": [ + true, + false + ], + "type": "boolean" + }, + { + "name": "director_name", + "default": "Allan Smithe", + "prompt": "What's the Director's full name?", + "prompt_user": true, + "description": "The default director is not proud of their work, we hope you are.", + "hide_input": false, + "choices": [ + "Allan Smithe", + "Ridley Scott", + "Victor Fleming", + "John Houston" + ], + "validation": "^[a-z][A-Z]+$", + "validation_flags": [ + "verbose", + "ascii" + ], + "skip_if": "{{cookiecutter.director_credit == False}}", + "type": "string" + } ] + } } diff --git a/tests/test_context.py b/tests/test_context.py index 806421a05..58149da1c 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -94,7 +94,9 @@ def test_load_context_defaults(): def test_load_context_defaults_no_requires(): cc = load_cookiecutter('tests/test-context/cookiecutter-no-requires.json') - cc_cfg = context.load_context(cc['cookiecutter-no-requires'], no_input=True, verbose=False) + cc_cfg = context.load_context( + cc['cookiecutter-no-requires'], no_input=True, verbose=False + ) assert cc_cfg['full_name'] == 'Raphael Pierzina' assert cc_cfg['email'] == 'raphael@hackebrot.de' diff --git a/tests/test_generate_context_v2.py b/tests/test_generate_context_v2.py index 84d0370bb..c2750e337 100644 --- a/tests/test_generate_context_v2.py +++ b/tests/test_generate_context_v2.py @@ -220,6 +220,23 @@ 'John Houston', ] +expected_file2_v8 = deepcopy(expected_file2_v1) +expected_file2_v8['template']['variables'].insert( + 1, + OrderedDict( + [ + ('name', 'director_exists'), + ('default', False), + ('prompt', 'Is there a Director?'), + ('prompt_user', True), + ('description', 'The director exists.'), + ('hide_input', False), + ('choices', [True, False]), + ('type', 'boolean'), + ] + ), +) + def context_data_serializer(): """ @@ -233,9 +250,9 @@ def context_data_serializer(): context_with_default = ( { - 'context_file': 'tests/test-generate-context-v2/test.json', + 'context_file': 'tests/test-generate-context-v2/test.json', 'default_context': { - 'full_name': 'James Patrick Morgan', + 'full_name': 'James Patrick Morgan', 'this_key_ignored': 'not_in_context', }, }, @@ -244,7 +261,7 @@ def context_data_serializer(): context_with_extra = ( { - 'context_file': 'tests/test-generate-context-v2/test.json', + 'context_file': 'tests/test-generate-context-v2/test.json', 'extra_context': {'email': 'jpm@chase.bk'}, }, {"test": expected_file1_v2}, @@ -252,7 +269,7 @@ def context_data_serializer(): context_choices_with_default = ( { - 'context_file': 'tests/test-generate-context-v2/test_choices.json', + 'context_file': 'tests/test-generate-context-v2/test_choices.json', 'default_context': {'license': 'Apache2'}, }, { @@ -295,16 +312,16 @@ def context_data_serializer(): ) context_with_default_and_extra = ( { - 'context_file': 'tests/test-generate-context-v2/test.json', + 'context_file': 'tests/test-generate-context-v2/test.json', 'default_context': {'full_name': 'Alpha Gamma Five'}, - 'extra_context': {'email': 'agamma5@universe.open'}, + 'extra_context': {'email': 'agamma5@universe.open'}, }, {"test": expected_file1_v3}, ) context_choices_with_default_not_in_choices = ( { - 'context_file': 'tests/test-generate-context-v2/test.json', + 'context_file': 'tests/test-generate-context-v2/test.json', 'default_context': {'orientation': 'landscape'}, }, {"test": expected_file1_v0}, @@ -312,7 +329,7 @@ def context_data_serializer(): context_choices_with_default_misses = ( { - 'context_file': 'tests/test-generate-context-v2/test_choices-miss.json', + 'context_file': 'tests/test-generate-context-v2/test_choices-miss.json', 'default_context': {'license': 'Cherokee'}, }, { @@ -356,7 +373,7 @@ def context_data_serializer(): context_choices_with_extra_misses = ( { - 'context_file': 'tests/test-generate-context-v2/test_choices-miss.json', + 'context_file': 'tests/test-generate-context-v2/test_choices-miss.json', 'extra_context': {'license': 'MIT'}, }, { @@ -421,7 +438,7 @@ def test_generate_context(input_params, expected_context): def context_data_value_errors(): context_choices_with_default_value_error = ( { - 'context_file': 'tests/test-generate-context-v2/test_choices.json', + 'context_file': 'tests/test-generate-context-v2/test_choices.json', 'default_context': [{'license': 'MIT'}], }, { @@ -457,9 +474,9 @@ def context_data_value_errors(): ) context_choices_with_extra_value_error = ( { - 'context_file': 'tests/test-generate-context-v2/test_choices.json', + 'context_file': 'tests/test-generate-context-v2/test_choices.json', 'default_context': {'license': 'Apache2'}, - 'extra_context': [{'name': 'license', 'default': 'MIT'}], + 'extra_context': [{'name': 'license', 'default': 'MIT'}], }, { "test_choices": OrderedDict( @@ -557,10 +574,10 @@ def test_generate_context_extra_ctx_list_item_dict_missing_name_field(): xtra_context = [ { "shouldbename": "author_name", - "default": "Robert Lewis", - "prompt": "What's the author's name?", - "description": "Please enter the author's full name.", - "type": "string", + "default": "Robert Lewis", + "prompt": "What's the author's name?", + "description": "Please enter the author's full name.", + "type": "string", } ] with pytest.raises(ValueError) as excinfo: @@ -582,11 +599,11 @@ def test_generate_context_extra_ctx_list_item_dict_no_name_field_match(): """ xtra_context = [ { - "name": "author_name", - "default": "Robert Lewis", - "prompt": "What's the author's name?", + "name": "author_name", + "default": "Robert Lewis", + "prompt": "What's the author's name?", "description": "Please enter the author's full name.", - "type": "string", + "type": "string", } ] with pytest.raises(ValueError) as excinfo: @@ -610,7 +627,7 @@ def test_raise_exception_when_attempting_to_remove_mandatory_field(): """ xtra_context = [ { - 'name': 'director_name', + 'name': 'director_name', 'default': '<>', }, ] @@ -631,15 +648,15 @@ def gen_context_data_inputs_expected_var(): # a key from the context via the removal token: '<>' context_with_valid_extra_2 = ( { - 'context_file': 'tests/test-generate-context-v2/representative.json', + 'context_file': 'tests/test-generate-context-v2/representative.json', 'extra_context': [ { - 'name': 'director_credit::producer_credit', - 'prompt': 'Is there a producer credit on this film?', + 'name': 'director_credit::producer_credit', + 'prompt': 'Is there a producer credit on this film?', 'description': 'There are usually a lot of producers...', }, { - 'name': 'director_name', + 'name': 'director_name', 'skip_if': '<>', }, ], @@ -647,11 +664,13 @@ def gen_context_data_inputs_expected_var(): OrderedDict( [ ( - "representative", OrderedDict( + "representative", + OrderedDict( [ ('version', '2.0'), ( - "template", OrderedDict( + "template", + OrderedDict( [ ("name", "cc-representative"), ( @@ -676,7 +695,10 @@ def gen_context_data_inputs_expected_var(): [ ("name", "director_name"), ("default", "Allan Smithe"), - ("prompt", "What's the Director's full name?"), + ( + "prompt", + "What's the Director's full name?", + ), ("prompt_user", True), ( "description", @@ -693,20 +715,23 @@ def gen_context_data_inputs_expected_var(): ], ), ("validation", "^[a-z][A-Z]+$"), - ("validation_flags", ["verbose", "ascii"]), + ( + "validation_flags", + ["verbose", "ascii"], + ), ("type", "string"), ] ), ], ), ] - ) + ), ), ] - ) + ), ) ] - ) + ), ) # Test the ability to change the variable's name field (since it is used # to identify the variable to be modifed) with extra context and to also @@ -714,11 +739,11 @@ def gen_context_data_inputs_expected_var(): # original variable name get updated as well. context_with_valid_extra_2_B = ( { - 'context_file': 'tests/test-generate-context-v2/representative_2B.json', + 'context_file': 'tests/test-generate-context-v2/representative_2B.json', 'extra_context': [ { - 'name': 'director_credit::producer_credit', - 'prompt': 'Is there a producer credit on this film?', + 'name': 'director_credit::producer_credit', + 'prompt': 'Is there a producer credit on this film?', 'description': 'There are usually a lot of producers...', }, ], @@ -726,19 +751,19 @@ def gen_context_data_inputs_expected_var(): OrderedDict( [ ( - "representative_2B", OrderedDict( + "representative_2B", + OrderedDict( [ ('version', '2.0'), ( - 'requires', OrderedDict( - [ - ('cookiecutter', '>1'), - ('python', '>=3.0') - ] - ) + 'requires', + OrderedDict( + [('cookiecutter', '>1'), ('python', '>=3.0')] + ), ), ( - "template", OrderedDict( + "template", + OrderedDict( [ ("name", "cc-representative"), ( @@ -763,7 +788,10 @@ def gen_context_data_inputs_expected_var(): [ ("name", "director_name"), ("default", "Allan Smithe"), - ("prompt", "What's the Director's full name?"), + ( + "prompt", + "What's the Director's full name?", + ), ("prompt_user", True), ( "description", @@ -781,7 +809,10 @@ def gen_context_data_inputs_expected_var(): ], ), ("validation", "^[a-z][A-Z]+$"), - ("validation_flags", ["verbose", "ascii"]), + ( + "validation_flags", + ["verbose", "ascii"], + ), ( "skip_if", "{{cookiecutter.producer_credit == False}}", @@ -792,13 +823,13 @@ def gen_context_data_inputs_expected_var(): ], ), ] - ) - ) + ), + ), ] - ) + ), ) ] - ) + ), ) yield context_with_valid_extra_2 @@ -817,3 +848,280 @@ def test_generate_context_with_extra_context_dictionary_var( with creation of new fields NOT allowed. """ assert generate.generate_context(**input_params) == expected_context + + +def gen_context_data_inputs_expected(): + """ + Creates a generator of combination: + ((input file, additional params), expected output) + """ + context_with_valid_extra_0 = ( + { + 'context_file': 'tests/test-generate-context-v2/test.json', + 'extra_context': [ + { + 'name': 'email', + 'default': 'miles.davis@jazz.gone', + 'description': 'Enter jazzy email...', + 'extra_field': 'extra_field_value', + } + ], + }, + {"test": expected_file1_v4}, + ) + # Empty extra context precipitates no ill effect + context_with_valid_extra_1 = ( + { + 'context_file': 'tests/test-generate-context-v2/representative.json', + 'extra_context': [], + }, + {"representative": expected_file2_v0}, + ) + + # Test the ability to change the variable's name field (since it is used + # to identify the variable to be modifed) with extra context and to remove + # a key from the context via the removal token: '<>' + context_with_valid_extra_2 = ( + { + 'context_file': 'tests/test-generate-context-v2/representative-director.json', + 'extra_context': [ + { + 'name': 'director_credit::producer_credit', + 'prompt': 'Is there a producer credit on this film?', + 'description': 'There are usually a lot of producers...', + }, + { + 'name': 'director_name', + 'skip_if': '<>', + }, + ], + }, + {"representative-director": expected_file2_v8}, + ) + # Test the ability to change the variable's name field (since it is used + # to identify the variable to be modifed) with extra context and to also + context_with_valid_extra_2_B = ( + { + 'context_file': 'tests/test-generate-context-v2/representative_2B.json', + 'extra_context': [ + { + 'name': 'director_credit::producer_credit', + 'prompt': 'Is there a producer credit on this film?', + 'description': 'There are usually a lot of producers...', + }, + ], + }, + { + "representative_2B": OrderedDict( + [ + ("version", "2.0"), + ( + "requires", + OrderedDict([("cookiecutter", ">1"), ("python", ">=3.0")]), + ), + ( + "template", + OrderedDict( + [ + ("name", "cc-representative"), + ( + "variables", + [ + OrderedDict( + [ + ("name", "producer_credit"), + ("default", True), + ( + "prompt", + "Is there a producer credit on this film?", + ), + ( + "description", + "There are usually a lot of producers...", + ), + ("type", "boolean"), + ] + ), + OrderedDict( + [ + ("name", "director_name"), + ("default", "Allan Smithe"), + ( + "prompt", + "What's the Director's full name?", + ), + ("prompt_user", True), + ( + "description", + "The default director is not proud " + "of their work, we hope you are.", + ), + ("hide_input", False), + ( + "choices", + [ + "Allan Smithe", + "Ridley Scott", + "Victor Fleming", + "John Houston", + "{{cookiecutter.producer_credit}}", + ], + ), + ("validation", "^[a-z][A-Z]+$"), + ( + "validation_flags", + ["verbose", "ascii"], + ), + ( + "skip_if", + "{{cookiecutter.producer_credit == False}}", + ), + ("type", "string"), + ] + ), + ], + ), + ] + ), + ), + ] + ) + }, + ) + # test that any other references in other variables that might use the + # original variable name get updated as well. + + # Test changing variable's name field value, default field, prompt field, + # and changing the type + context_with_valid_extra_3 = ( + { + 'context_file': 'tests/test-generate-context-v2/representative.json', + 'extra_context': [ + { + 'name': 'director_credit::producer_credits', + 'default': 2, + 'prompt': 'How many producers does this film have?', + 'description': 'There are usually a lot of producers...', + 'type': "int", + } + ], + }, + {"representative": expected_file2_v2}, + ) + # Test changing choices field without changing the default, but default + # does not change because the first position in choices matches default + context_with_valid_extra_4 = ( + { + 'context_file': 'tests/test-generate-context-v2/representative.json', + 'extra_context': [ + { + 'name': 'director_name', + 'choices': [ + 'Allan Smithe', + 'Ridley Scott', + 'Victor Fleming', + 'John Houston', + 'John Ford', + 'Billy Wilder', + ], + } + ], + }, + {"representative": expected_file2_v3}, + ) + # Test changing choices field and changing the default + context_with_valid_extra_5 = ( + { + 'context_file': 'tests/test-generate-context-v2/representative.json', + 'extra_context': [ + { + 'name': 'director_name', + 'default': 'John Ford', + 'choices': [ + 'Allan Smithe', + 'Ridley Scott', + 'Victor Fleming', + 'John Houston', + 'John Ford', + 'Billy Wilder', + ], + } + ], + }, + {"representative": expected_file2_v4}, + ) + # Test changing the default, but not the choices field, yet seeing choices field re-ordered + # to put default value in first location + context_with_valid_extra_6 = ( + { + 'context_file': 'tests/test-generate-context-v2/representative.json', + 'extra_context': [ + { + 'name': 'director_name', + 'default': 'John Ford', + } + ], + }, + {"representative": expected_file2_v5}, + ) + # Test changing choices field without changing the default, but default + # does get changee because the first position in choices field chagned + context_with_valid_extra_7 = ( + { + 'context_file': 'tests/test-generate-context-v2/representative.json', + 'extra_context': [ + { + 'name': 'director_name', + 'choices': [ + 'Billy Wilder', + 'Allan Smithe', + 'Ridley Scott', + 'Victor Fleming', + 'John Houston', + 'John Ford', + ], + } + ], + }, + {"representative": expected_file2_v6}, + ) + # Test changing the default value with a value that is not in choices, + # we should see the choice first position get updated. + context_with_valid_extra_8 = ( + { + 'context_file': 'tests/test-generate-context-v2/representative.json', + 'extra_context': [ + { + 'name': 'director_name', + 'default': 'Peter Sellers', + } + ], + }, + {"representative": expected_file2_v7}, + ) + yield context_with_valid_extra_0 + yield context_with_valid_extra_1 + yield context_with_valid_extra_2 + yield context_with_valid_extra_2_B + yield context_with_valid_extra_3 + yield context_with_valid_extra_4 + yield context_with_valid_extra_5 + yield context_with_valid_extra_6 + yield context_with_valid_extra_7 + yield context_with_valid_extra_8 + + +@pytest.mark.usefixtures('clean_system') +@pytest.mark.parametrize( + 'input_params, expected_context', gen_context_data_inputs_expected() +) +def test_generate_context_with_extra_context_dictionary( + input_params, expected_context, monkeypatch +): + """ + Test the generated context with extra content overwrite to multiple fields, + with creation of new fields NOT allowed. + """ + assert OrderedDict(generate.generate_context(**input_params)) == OrderedDict( + expected_context + ) diff --git a/tests/test_read_user_dict.py b/tests/test_read_user_dict.py index 7d8a2063a..409b2bffc 100644 --- a/tests/test_read_user_dict.py +++ b/tests/test_read_user_dict.py @@ -110,7 +110,8 @@ def test_should_not_load_json_from_sentinel(mocker): def test_should_not_call_process_json_default_value(mocker, monkeypatch): """Make sure that `process_json` is not called when using default value.""" mock_process_json = mocker.patch( - 'cookiecutter.prompt.process_json', autospec=True, return_value='default') + 'cookiecutter.prompt.process_json', autospec=True, return_value='default' + ) runner = click.testing.CliRunner() with runner.isolation(input="\n") as streams: From 969f88cca1cc574a30ba3b569b61099a0443e084 Mon Sep 17 00:00:00 2001 From: Christo De Lange <111363591+christokur@users.noreply.github.com> Date: Sun, 5 Feb 2023 12:22:36 -0500 Subject: [PATCH 274/274] fix: test_should_not_load_json_from_sentinel --- tests/test_read_user_dict.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_read_user_dict.py b/tests/test_read_user_dict.py index 409b2bffc..b4227a43b 100644 --- a/tests/test_read_user_dict.py +++ b/tests/test_read_user_dict.py @@ -106,6 +106,12 @@ def test_should_not_load_json_from_sentinel(mocker): 'cookiecutter.prompt.json.loads', autospec=True, return_value={} ) + runner = click.testing.CliRunner() + with runner.isolation(input="\n"): + read_user_dict('name', {'project_slug': 'pytest-plugin'}) + + mock_json_loads.assert_not_called() + def test_should_not_call_process_json_default_value(mocker, monkeypatch): """Make sure that `process_json` is not called when using default value."""