Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

python 3.5+ updates and tidyups #1805

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS.md
Expand Up @@ -171,6 +171,7 @@
- Tom Forbes ([@orf](https://github.com/orf))
- Xie Yanbo ([@xyb](https://github.com/xyb))
- Maxim Ivanov ([@ivanovmg](https://github.com/ivanovmg))
- Mark Mayo ([@marksmayo](https://github.com/marksmayo/))

## Backers

Expand Down
1 change: 0 additions & 1 deletion __main__.py
@@ -1,6 +1,5 @@
"""Allow cookiecutter to be executable from a checkout or zip file."""
import runpy


if __name__ == "__main__":
runpy.run_module("cookiecutter", run_name="__main__")
1 change: 0 additions & 1 deletion cookiecutter/__main__.py
@@ -1,6 +1,5 @@
"""Allow cookiecutter to be executable through `python -m cookiecutter`."""
from cookiecutter.cli import main


if __name__ == "__main__": # pragma: no cover
main(prog_name="cookiecutter")
42 changes: 24 additions & 18 deletions cookiecutter/cli.py
Expand Up @@ -7,20 +7,16 @@
import click

from cookiecutter import __version__
from cookiecutter.exceptions import (
ContextDecodingException,
FailedHookException,
InvalidModeException,
InvalidZipRepository,
OutputDirExistsException,
RepositoryCloneFailed,
RepositoryNotFound,
UndefinedVariableInTemplate,
UnknownExtension,
)
from cookiecutter.config import get_user_config
from cookiecutter.exceptions import (ContextDecodingException,
FailedHookException, InvalidModeException,
InvalidZipRepository,
OutputDirExistsException,
RepositoryCloneFailed, RepositoryNotFound,
UndefinedVariableInTemplate,
UnknownExtension)
from cookiecutter.log import configure_logger
from cookiecutter.main import cookiecutter
from cookiecutter.config import get_user_config


def version_msg():
Expand All @@ -36,7 +32,7 @@ def validate_extra_context(ctx, param, value):
if '=' not in string:
raise click.BadParameter(
f"EXTRA_CONTEXT should contain items of the form key=value; "
f"'{string}' doesn't match that form"
f"'{string}' doesn't match that form",
)

# Convert tuple -- e.g.: ('program_name=foobar', 'startsecs=66')
Expand All @@ -51,15 +47,15 @@ def list_installed_templates(default_config, passed_config_file):
if not os.path.exists(cookiecutter_folder):
click.echo(
f"Error: Cannot list installed templates. "
f"Folder does not exist: {cookiecutter_folder}"
f"Folder does not exist: {cookiecutter_folder}",
)
sys.exit(-1)

template_names = [
folder
for folder in os.listdir(cookiecutter_folder)
if os.path.exists(
os.path.join(cookiecutter_folder, folder, 'cookiecutter.json')
os.path.join(cookiecutter_folder, folder, 'cookiecutter.json'),
)
]
click.echo(f'{len(template_names)} installed templates: ')
Expand Down Expand Up @@ -89,7 +85,11 @@ def list_installed_templates(default_config, passed_config_file):
'for advanced repositories with multi templates in it',
)
@click.option(
'-v', '--verbose', is_flag=True, help='Print debug information', default=False
'-v',
'--verbose',
is_flag=True,
help='Print debug information',
default=False,
)
@click.option(
'--replay',
Expand Down Expand Up @@ -124,7 +124,10 @@ def list_installed_templates(default_config, passed_config_file):
help='Where to output the generated project dir into',
)
@click.option(
'--config-file', type=click.Path(), default=None, help='User configuration file'
'--config-file',
type=click.Path(),
default=None,
help='User configuration file',
)
@click.option(
'--default-config',
Expand All @@ -144,7 +147,10 @@ def list_installed_templates(default_config, passed_config_file):
help='Accept pre/post hooks',
)
@click.option(
'-l', '--list-installed', is_flag=True, help='List currently installed templates.'
'-l',
'--list-installed',
is_flag=True,
help='List currently installed templates.',
)
@click.option(
'--keep-project-on-failure',
Expand Down
10 changes: 5 additions & 5 deletions cookiecutter/config.py
Expand Up @@ -6,7 +6,8 @@

import yaml

from cookiecutter.exceptions import ConfigDoesNotExistException, InvalidConfiguration
from cookiecutter.exceptions import (ConfigDoesNotExistException,
InvalidConfiguration)

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -63,7 +64,7 @@ def get_config(config_path):
yaml_dict = yaml.safe_load(file_handle)
except yaml.YAMLError as e:
raise InvalidConfiguration(
f'Unable to parse YAML file {config_path}.'
f'Unable to parse YAML file {config_path}.',
) from e

config_dict = merge_configs(DEFAULT_CONFIG, yaml_dict)
Expand Down Expand Up @@ -112,9 +113,8 @@ def get_user_config(config_file=None, default_config=False):
if os.path.exists(USER_CONFIG_PATH):
logger.debug("Loading config from %s.", USER_CONFIG_PATH)
return get_config(USER_CONFIG_PATH)
else:
logger.debug("User config not found. Loading default config.")
return copy.copy(DEFAULT_CONFIG)
logger.debug("User config not found. Loading default config.")
return copy.copy(DEFAULT_CONFIG)
else:
# There is a config environment variable. Try to load it.
# Do not check for existence, so invalid file paths raise an error.
Expand Down
65 changes: 45 additions & 20 deletions cookiecutter/generate.py
Expand Up @@ -7,18 +7,17 @@
import warnings
from collections import OrderedDict
from pathlib import Path

from binaryornot.check import is_binary
from jinja2 import FileSystemLoader, Environment
from jinja2 import Environment, FileSystemLoader
from jinja2.exceptions import TemplateSyntaxError, UndefinedError

from cookiecutter.environment import StrictEnvironment
from cookiecutter.exceptions import (
ContextDecodingException,
FailedHookException,
NonTemplatedInputDirException,
OutputDirExistsException,
UndefinedVariableInTemplate,
)
from cookiecutter.exceptions import (ContextDecodingException,
FailedHookException,
NonTemplatedInputDirException,
OutputDirExistsException,
UndefinedVariableInTemplate)
from cookiecutter.find import find_template
from cookiecutter.hooks import run_hook
from cookiecutter.utils import make_sure_path_exists, rmtree, work_in
Expand Down Expand Up @@ -66,7 +65,7 @@ def apply_overwrites_to_context(context, overwrite_context):
else:
raise ValueError(
f"{overwrite} provided for choice variable {variable}, "
f"but the choices are {context_value}."
f"but the choices are {context_value}.",
)
elif isinstance(context_value, dict) and isinstance(overwrite, dict):
# Partially overwrite some keys in original dict
Expand All @@ -78,7 +77,9 @@ def apply_overwrites_to_context(context, overwrite_context):


def generate_context(
context_file='cookiecutter.json', default_context=None, extra_context=None
context_file='cookiecutter.json',
default_context=None,
extra_context=None,
):
"""Generate the context for a Cookiecutter project template.

Expand Down Expand Up @@ -216,15 +217,18 @@ def render_and_create_dir(
dir_to_create = Path(output_dir, rendered_dirname)

logger.debug(
'Rendered dir %s must exist in output_dir %s', dir_to_create, output_dir
'Rendered dir %s must exist in output_dir %s',
dir_to_create,
output_dir,
)

output_dir_exists = dir_to_create.exists()

if output_dir_exists:
if overwrite_if_exists:
logger.debug(
'Output directory %s already exists, overwriting it', dir_to_create
'Output directory %s already exists, overwriting it',
dir_to_create,
)
else:
msg = f'Error: "{dir_to_create}" directory already exists'
Expand All @@ -239,12 +243,15 @@ def ensure_dir_is_templated(dirname):
"""Ensure that dirname is a templated directory name."""
if '{{' in dirname and '}}' in dirname:
return True
else:
raise NonTemplatedInputDirException
raise NonTemplatedInputDirException


def _run_hook_from_repo_dir(
repo_dir, hook_name, project_dir, context, delete_project_on_failure
repo_dir,
hook_name,
project_dir,
context,
delete_project_on_failure,
):
"""Run hook from repo directory, clean project directory if hook fails.

Expand Down Expand Up @@ -302,7 +309,11 @@ def generate_files(
env = StrictEnvironment(context=context, keep_trailing_newline=True, **envvars)
try:
project_dir, output_directory_created = render_and_create_dir(
unrendered_dir, context, output_dir, env, overwrite_if_exists
unrendered_dir,
context,
output_dir,
env,
overwrite_if_exists,
)
except UndefinedError as err:
msg = f"Unable to create project directory '{unrendered_dir}'"
Expand All @@ -324,7 +335,11 @@ def generate_files(

if accept_hooks:
_run_hook_from_repo_dir(
repo_dir, 'pre_gen_project', project_dir, context, delete_project_on_failure
repo_dir,
'pre_gen_project',
project_dir,
context,
delete_project_on_failure,
)

with work_in(template_dir):
Expand Down Expand Up @@ -368,7 +383,11 @@ def generate_files(
unrendered_dir = os.path.join(project_dir, root, d)
try:
render_and_create_dir(
unrendered_dir, context, output_dir, env, overwrite_if_exists
unrendered_dir,
context,
output_dir,
env,
overwrite_if_exists,
)
except UndefinedError as err:
if delete_project_on_failure:
Expand All @@ -384,14 +403,20 @@ def generate_files(
outfile_rendered = outfile_tmpl.render(**context)
outfile = os.path.join(project_dir, outfile_rendered)
logger.debug(
'Copying file %s to %s without rendering', infile, outfile
'Copying file %s to %s without rendering',
infile,
outfile,
)
shutil.copyfile(infile, outfile)
shutil.copymode(infile, outfile)
continue
try:
generate_file(
project_dir, infile, context, env, skip_if_file_exists
project_dir,
infile,
context,
env,
skip_if_file_exists,
)
except UndefinedError as err:
if delete_project_on_failure:
Expand Down
4 changes: 2 additions & 2 deletions cookiecutter/hooks.py
Expand Up @@ -83,12 +83,12 @@ def run_script(script_path, cwd='.'):
exit_status = proc.wait()
if exit_status != EXIT_SUCCESS:
raise FailedHookException(
f'Hook script failed (exit status: {exit_status})'
f'Hook script failed (exit status: {exit_status})',
)
except OSError as err:
if err.errno == errno.ENOEXEC:
raise FailedHookException(
'Hook script failed, might be an empty file or missing a shebang'
'Hook script failed, might be an empty file or missing a shebang',
) from err
raise FailedHookException(f'Hook script failed (error: {err})') from err

Expand Down
2 changes: 1 addition & 1 deletion cookiecutter/main.py
Expand Up @@ -4,10 +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 copy import copy

from cookiecutter.config import get_user_config
from cookiecutter.exceptions import InvalidModeException
Expand Down
29 changes: 20 additions & 9 deletions cookiecutter/prompt.py
Expand Up @@ -69,11 +69,14 @@ def read_user_choice(var_name, options):
f"Select {var_name}:",
"\n".join(choice_lines),
f"Choose from {', '.join(choices)}",
)
),
)

user_choice = click.prompt(
prompt, type=click.Choice(choices), default=default, show_choices=False
prompt,
type=click.Choice(choices),
default=default,
show_choices=False,
)
return choice_map[user_choice]

Expand Down Expand Up @@ -145,16 +148,18 @@ def render_variable(env, raw, cookiecutter_dict):
"""
if raw is None or isinstance(raw, bool):
return raw
elif isinstance(raw, dict):
if isinstance(raw, dict):
return {
render_variable(env, k, cookiecutter_dict): render_variable(
env, v, cookiecutter_dict
env,
v,
cookiecutter_dict,
)
for k, v in raw.items()
}
elif isinstance(raw, list):
if isinstance(raw, list):
return [render_variable(env, v, cookiecutter_dict) for v in raw]
elif not isinstance(raw, str):
if not isinstance(raw, str):
raw = str(raw)

template = env.from_string(raw)
Expand Down Expand Up @@ -189,22 +194,28 @@ def prompt_for_config(context, no_input=False):
if key.startswith('_') and not key.startswith('__'):
cookiecutter_dict[key] = raw
continue
elif key.startswith('__'):
if key.startswith('__'):
cookiecutter_dict[key] = render_variable(env, raw, cookiecutter_dict)
continue

try:
if isinstance(raw, list):
# We are dealing with a choice variable
val = prompt_choice_for_config(
cookiecutter_dict, env, key, raw, no_input
cookiecutter_dict,
env,
key,
raw,
no_input,
)
cookiecutter_dict[key] = val
elif isinstance(raw, bool):
# We are dealing with a boolean variable
if no_input:
cookiecutter_dict[key] = render_variable(
env, raw, cookiecutter_dict
env,
raw,
cookiecutter_dict,
)
else:
cookiecutter_dict[key] = read_user_yes_no(key, raw)
Expand Down