PK!eCq,aacopier/__init__.py"""Copier (previously known as "Voodoo") """ from .main import * # noqa __version__ = '2.0.1' PK!^n copier/cli.py#!/usr/bin/env python import argparse from hashlib import sha512 from os import urandom import sys try: from copier import __version__, copy except ImportError: from . import __version__, copy parser = argparse.ArgumentParser( description='Create a new project from a template', usage='copier project_template_path destination_path [options]', epilog=( 'Warning! Use only trusted project templates as they might ' 'execute code with the same level of access as your user.' ) ) parser.add_argument('--version', action='version', version=__version__) parser.add_argument('source', help='The path of the project template') parser.add_argument('dest', help='The path of where to create the new project') parser.add_argument( '--exclude', nargs='*', help=( 'A list of names or shell-style patterns matching files or folders ' 'that must not be copied.' )) parser.add_argument( '--include', nargs='*', help=( 'A list of names or shell-style patterns matching files or folders that ' 'must be included, even if its name are in the `exclude` list.' )) parser.add_argument( '--pretend', action='store_true', dest='pretend', help='Run but do not make any changes') parser.add_argument( '--force', action='store_true', dest='force', help='Overwrite files that already exist, without asking') parser.add_argument( '--skip', action='store_true', dest='skip', help='Skip files that already exist, without asking') parser.add_argument( '--quiet', action='store_true', dest='quiet', help='Suppress status output') def run(): # pragma:no cover if len(sys.argv) == 1: parser.print_help(sys.stderr) sys.exit(1) args = parser.parse_args() kwargs = vars(args) data = {'make_secret': lambda: sha512(urandom(48)).hexdigest()} copy(kwargs.pop('source'), kwargs.pop('dest'), data=data, **kwargs) if __name__ == '__main__': run() PK!copier/main.pyimport datetime import filecmp import os import re import shutil from . import tools, vcs from .user_data import get_user_data, prompt_bool __all__ = ( 'copy', 'copy_local', ) # Files of the template to exclude from the final project DEFAULT_EXCLUDE = ( 'copier.yml', '~*', '*.py[co]', '__pycache__', '__pycache__/*', '.git', '.git/*', '.DS_Store', '.svn' ) DEFAULT_INCLUDE = () DEFAULT_DATA = { 'now': datetime.datetime.utcnow, } def copy( src_path, dst_path, data=None, *, exclude=DEFAULT_EXCLUDE, include=DEFAULT_INCLUDE, envops=None, pretend=False, force=False, skip=False, quiet=False ): """ Uses the template in src_path to generate a new project at dst_path. Arguments: - src_path (str): Absolute path to the project skeleton. May be a version control system URL - dst_path (str): Absolute path to where to render the skeleton - data (dict): Optional. Data to be passed to the templates in addtion to the user data from a `copier.json`. - exclude (list): A list of names or shell-style patterns matching files or folders that must not be copied. - include (list): A list of names or shell-style patterns matching files or folders that must be included, even if its name are in the `exclude` list. Eg: `['.gitignore']`. The default is an empty list. - envops (dict): Extra options for the Jinja template environment. - pretend (bool): Run but do not make any changes - force (bool): Overwrite files that already exist, without asking - skip (bool): Skip files that already exist, without asking - quiet (bool): Suppress the status output """ repo = vcs.get_repo(src_path) if repo: src_path = vcs.clone(repo) _data = DEFAULT_DATA.copy() _data.update(data or {}) try: copy_local( src_path, dst_path, data=_data, exclude=exclude or DEFAULT_EXCLUDE, include=include or DEFAULT_INCLUDE, envops=envops, pretend=pretend, force=force, skip=skip, quiet=quiet, ) finally: if repo: shutil.rmtree(src_path) RE_TMPL = re.compile(r'\.tmpl$', re.IGNORECASE) def copy_local( src_path, dst_path, data, *, exclude, include, envops, **flags ): if not os.path.exists(src_path): raise ValueError('Project template not found') if not os.path.isdir(src_path): raise ValueError('The project template must be a folder') user_data = get_user_data(src_path, **flags) data.update(user_data) data.setdefault('folder_name', os.path.basename(dst_path)) must_filter = tools.get_name_filter(exclude, include) render = tools.get_jinja_renderer(src_path, data, envops) if not flags['quiet']: print('') # padding space for folder, _, files in os.walk(src_path): rel_folder = folder.replace(src_path, '').lstrip(os.path.sep) rel_folder = render.string(rel_folder) if must_filter(rel_folder): continue for src_name in files: dst_name = re.sub(RE_TMPL, '', src_name) dst_name = render.string(dst_name) rel_path = os.path.join(rel_folder, dst_name) if must_filter(rel_path): continue render_file( dst_path, rel_folder, folder, src_name, dst_name, render, **flags, ) if not flags['quiet']: print('') # padding space def render_file( dst_path, rel_folder, folder, src_name, dst_name, render, **flags ): """Process or copy a file of the skeleton. """ source_path = os.path.join(folder, src_name) display_path = os.path.join(rel_folder, dst_name).lstrip('.').lstrip(os.path.sep) final_path = tools.make_folder( dst_path, rel_folder, dst_name, pretend=flags['pretend'] ) if src_name.endswith('.tmpl'): content = render(source_path) else: content = None if not os.path.exists(final_path): if not flags['quiet']: tools.print_format('create', display_path, color=tools.COLOR_OK) else: if file_is_identical(source_path, final_path, content): if not flags['quiet']: tools.print_format( 'identical', display_path, color=tools.COLOR_IGNORE, bright=None ) return if not overwrite_file(display_path, source_path, final_path, content, **flags): return if flags['pretend']: return if content is None: tools.copy_file(source_path, final_path) else: tools.write_file(final_path, content) def file_is_identical(source_path, final_path, content): if content is None: return files_are_identical(source_path, final_path) return file_has_this_content(final_path, content) def files_are_identical(path1, path2): return filecmp.cmp(path1, path2, shallow=False) def file_has_this_content(path, content): return content == tools.read_file(path) def overwrite_file(display_path, source_path, final_path, content, **flags): if not flags['quiet']: tools.print_format('conflict', display_path, color=tools.COLOR_DANGER) if flags['force']: overwrite = True elif flags['skip']: overwrite = False else: # pragma:no cover msg = ' Overwrite {}? (y/n)'.format(final_path) overwrite = prompt_bool(msg, default=True) if not flags['quiet']: tools.print_format( 'force' if overwrite else 'skip', display_path, color=tools.COLOR_WARNING, ) return overwrite PK!ȣFFcopier/tools.pyfrom fnmatch import fnmatch from functools import reduce import errno import io import os import shutil import unicodedata from colorama import Fore, Back, Style import jinja2 from jinja2.sandbox import SandboxedEnvironment COLOR_OK = 'green' COLOR_WARNING = 'yellow' COLOR_IGNORE = 'cyan' COLOR_DANGER = 'red' def format_message(action, msg='', color='', on_color='', bright=True, indent=12): """Format message.""" action = action.rjust(indent, ' ') color = getattr(Fore, color.upper(), '') on_color = getattr(Back, on_color.upper(), '') style = Style.BRIGHT if bright else Style.DIM if bright is False else '' return ''.join([ color, on_color, style, action, Fore.RESET, Back.RESET, Style.RESET_ALL, ' ', msg, ]) def print_format(*args, **kwargs): """Like format_message but prints it.""" print(format_message(*args, **kwargs)) def make_folder(*lpath, pretend=False): path = os.path.join(*lpath) path = os.path.abspath(path) if pretend: return path if not os.path.exists(path): try: os.makedirs(os.path.dirname(path)) except OSError as e: if e.errno != errno.EEXIST: raise return path def read_file(path, mode='rt'): with io.open(path, mode=mode) as file: return file.read() def write_file(path, content, mode='wt'): with io.open(path, mode=mode) as file: file.write(content) copy_file = shutil.copy2 # The default env options for jinja2 DEFAULT_ENV_OPTIONS = { 'autoescape': True, 'block_start_string': '[%', 'block_end_string': '%]', 'variable_start_string': '[[', 'variable_end_string': ']]', 'keep_trailing_newline': True, } class Renderer(object): def __init__(self, env, src_path, data): self.env = env self.src_path = src_path self.data = data def __call__(self, fullpath): relpath = fullpath.replace(self.src_path, '').lstrip(os.path.sep) tmpl = self.env.get_template(relpath) return tmpl.render(**self.data) def string(self, string): tmpl = self.env.from_string(string) return tmpl.render(**self.data) def get_jinja_renderer(src_path, data, envops=None): """Returns a function that can render a Jinja template. """ _envops = DEFAULT_ENV_OPTIONS.copy() _envops.update(envops or {}) _envops.setdefault('loader', jinja2.FileSystemLoader(src_path)) # We want to minimize the risk of hidden malware in the templates # so we use the SandboxedEnvironment instead of the regular one. # Of couse we still have the post-copy tasks to worry about, but at least # they are more visible to the final user. env = SandboxedEnvironment(**_envops) return Renderer(env=env, src_path=src_path, data=data) def normalize(text, form='NFD'): """Normalize unicode text. Uses the NFD algorithm by default.""" return unicodedata.normalize(form, text) def get_name_filter(exclude, include): """Returns a function that evaluates if a file or folder name must be filtered out. The compared paths are first converted to unicode and decomposed. This is neccesary because the way PY2.* `os.walk` read unicode paths in different filesystems. For instance, in OSX, it returns a decomposed unicode string. In those systems, u'ñ' is read as `\u0303` instead of `\xf1`. """ exclude = [normalize(f) for f in exclude] include = [normalize(f) for f in include] def fullmatch(path, pattern): path = normalize(path) name = os.path.basename(path) return fnmatch(name, pattern) or fnmatch(path, pattern) def must_be_filtered(name): return reduce( lambda r, pattern: r or fullmatch(name, pattern), exclude, False ) def must_be_included(name): return reduce( lambda r, pattern: r or fullmatch(name, pattern), include, False ) def must_filter(path): return must_be_filtered(path) and not must_be_included(path) return must_filter PK!3ycopier/user_data.pyimport json import os from ruamel.yaml import YAML from .tools import read_file, print_format, COLOR_WARNING __all__ = ( 'get_user_data', 'prompt', 'prompt_bool', ) yaml = YAML(typ="safe", pure=True) INDENT = ' ' def load_yaml_data(src_path, quiet=False): yaml_path = os.path.join(src_path, 'copier.yml') if not os.path.exists(yaml_path): return {} yaml_src = read_file(yaml_path) try: data = yaml.load(yaml_src) # The YAML parser can too permissive if not isinstance(data, dict): data = {} return data except Exception as e: if not quiet: print('') print_format('INVALID', msg=yaml_path, color=COLOR_WARNING, indent=0) print('-' * 42) print(e) print('-' * 42) return {} def load_json_data(src_path, quiet=False, warning=True): json_path = os.path.join(src_path, 'copier.json') if not os.path.exists(json_path): return load_old_json_data(src_path, quiet=quiet, warning=warning) json_src = read_file(json_path) try: return json.loads(json_src) except ValueError as e: if not quiet: print('') print_format('INVALID', msg=json_path, color=COLOR_WARNING, indent=0) print('-' * 42) print(e) print('-' * 42) return {} def load_old_json_data(src_path, quiet=False, warning=True): # TODO: Remove on version 2.2 json_path = os.path.join(src_path, 'voodoo.json') if not os.path.exists(json_path): return {} if warning and not quiet: print('') print_format( 'WARNING', msg='`voodoo.json` is deprecated. ' + 'Replace it with a `copier.yml` or `copier.json`.', color=COLOR_WARNING, indent=10 ) json_src = read_file(json_path) try: return json.loads(json_src) except ValueError as e: if not quiet: print('') print_format('INVALID', msg=json_path, color=COLOR_WARNING, indent=0) print('-' * 42) print(e) print('-' * 42) return {} def load_default_data(src_path, quiet=False, warning=True): data = load_yaml_data(src_path, quiet=quiet) if not data: data = load_json_data(src_path, quiet=quiet, warning=warning) return data def get_user_data(src_path, **flags): # pragma:no cover """Query to user for information needed as per the template's ``copier.yml``. """ default_user_data = load_default_data(src_path, quiet=flags['quiet']) if flags['force'] or not default_user_data: return default_user_data print('') user_data = {} for key in default_user_data: default = default_user_data[key] user_data[key] = prompt(INDENT + ' {0}?'.format(key), default) print('\n' + INDENT + '-' * 42) return user_data no_value = object() def required(value): if not value: raise ValueError() return value def prompt(text, default=no_value, validator=required, **kwargs): """ Prompt for a value from the command line. A default value can be provided, which will be used if no text is entered by the user. The value can be validated, and possibly changed by supplying a validator function. Any extra keyword arguments to this function will be passed along to the validator. If the validator raises a ValueError, the error message will be printed and the user asked to supply another value. """ text += ' [%s] ' % default if default is not no_value else ' ' while True: resp = input(text) if resp == '' and default is not no_value: resp = default try: return validator(resp, **kwargs) except ValueError as e: if str(e): print(str(e)) def prompt_bool(question, default=False, yes_choices=None, no_choices=None): """Prompt for a true/false yes/no boolean value""" yes_choices = yes_choices or ('y', 'yes', 't', 'true', 'on', '1') no_choices = no_choices or ('n', 'no', 'f', 'false', 'off', '0') def validator(value): value = value.lower() if value in yes_choices: return True if value in no_choices: return False raise ValueError('Enter yes/no. y/n, true/false, on/off') return prompt( question, default=yes_choices[0] if default else no_choices[0], validator=validator) PK!As copier/vcs.pyimport re import tempfile import shutil import subprocess __all__ = ('get_repo', 'clone') GIT_PREFIX = ('git@', 'git://', 'git+') GIT_POSTFIX = ('.git', ) RE_GITHUB = re.compile(r'^gh:/?') RE_GITLAB = re.compile(r'^gl:/?') def get_repo(url): if not (url.endswith(GIT_POSTFIX) or url.startswith(GIT_PREFIX)): return None if url.startswith('git+'): url = url[4:] url = re.sub(RE_GITHUB, 'https://github.com/', url) url = re.sub(RE_GITLAB, 'https://gitlab.com/', url) return url def clone(url): location = tempfile.mkdtemp() shutil.rmtree(location) # Path must not exists subprocess.check_call(['git', 'clone', url, location]) return location PK!Hx'&)'copier-2.0.1.dist-info/entry_points.txtN+I/N.,()J/L-Pz9VEy\\PK!9L%6uuԪQy0['YL/-g|4fXT\)DCKo4Ž:#o_{?[ŭ%q'n*Q;vk-N36ws%6jGQ+ج-sAj Aߐ!!Ӌ#6k;N} SpG)*j![ q0H׋"pyzz\t?=[.tV܈L)TR8'ȒB~ zS9y8z  @"Wg2EC X/O,t1rT1o sh+67jِ[MƗ]sⓛo {5_>Zv)*l*9[ E,m|SmA6H'ɷU$Cmn-[!tl8TQY2^x2)խXk&\0 J%VByy%:h;Zޭ.}&<%SG-DM I悷NbuM!o^k@feL|BE=5!0iVhf5GTDľjjb 18M@&/zEvZ0V2fk'QcS$ɲ,$5)Pbjq~"4@8xY||٦ljCEI["{p{@触NjYO#ڷ#WQ; >"lkn1NٵSck[h<0P0u[?k )4Þ7 U:VB,QzJVcn)(7w䒣DܳxR eKP>؀I =j%KFkP9z;b^IМosxU|u]JSsE2o%mКR@/ ]ZbSu1˭z:@uTcߝt$</깃8C#+zfx%f:pyl[4=. 4v`a;#J'bŢub7Q/g!> W)byk!KsHy˲Ưl&ލ;,p#yE]]c?f['omLAUBqZ|S\ĊpǤxovG&m4j?H!e ?n`a]}6B/$O7 ` kPA; [! RV11QlŃ7T~|z2MWɻ5b0Nʶ}zfvvD)Ǔ%x6{ۚ(l 'j=% '<}8~wt|yx>ꗊo?¥N76x\I.V63TdۍPf{)JjhF? g}4)qp؃`1zGgsU1[!$ _6JXY-ѝ)d"ʩ_JcYQޓܺWП XLxnJ}2}f6Y뻌RإPjl]iY˗Mk+ sVz5WGb<=(<"HM= Ɩbl2@S]m"5hoc:%o#BG#3z!ξi'[K8*=#9V`[ X+6-m4q߯OBWJߏX?a9^D? zgyxm Ć I?PK!H 3copier-2.0.1.dist-info/RECORDuَ@ّy@V6Q|Ѐb~hL:'ܸD(AhVOTw8Q8Fތ9{ cvvxRٻQ=A Ұ<%0<·-ʽS5 *s\ 1";tYmkza4r ._?Vz2́@^ZTF91O,rl܁VEBJ`wiDoֲYvo 9-JXQMaݢD(A^ {6:>k\4 oOtgWxM 3cgI:ç3ŤP]et32'J|?(Y:=]ZaܭzQ7җ`wxwǦ[j|lmԇZ=vzMwakkHpb| ˽XOt8*ӄweQJ-H;;.#zMMpcA򃴁j>U囦-7a %N]^վmW|uަ~s]t\>PK!eCq,aacopier/__init__.pyPK!^n 큑copier/cli.pyPK!}copier/main.pyPK!ȣFF) copier/tools.pyPK!3y0copier/user_data.pyPK!As Bcopier/vcs.pyPK!Hx'&)'zEcopier-2.0.1.dist-info/entry_points.txtPK!