PK!ۏcopier/__init__.py"""Copier (previously known as "Voodoo") """ from .main import * # noqa from .tools import * # noqa from .version import __version__ # noqa PK! 2 copier/cli.py#!/usr/bin/env python import argparse from hashlib import sha512 from os import urandom import sys try: from .main import copy from .version import __version__ except Exception: from copier.main import copy from copier.version import __version__ 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 or sys.argv[1] == "help": parser.print_help(sys.stderr) print() sys.exit(1) if sys.argv[1] == "version": sys.stdout.write(__version__) print() 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!Jؼcopier/main.pyimport datetime import filecmp import os import re import shutil import subprocess from pathlib import Path from . import vcs from .user_data import get_user_data from .tools import ( copy_file, get_jinja_renderer, get_name_filter, make_folder, printf, prompt_bool, STYLE_OK, STYLE_IGNORE, STYLE_DANGER, STYLE_WARNING, ) __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=None, include=None, tasks=None, 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. - tasks (list): Optional lists of commands to run in order after finishing the copy. Like in the templates files, you can use variables on the commands that will be replaced by the real values before running the command. If one of the commands fail, the rest of them will not run. - 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, include=include, tasks=tasks, 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 resolve_paths(src_path, dst_path): try: src_path = Path(src_path).resolve() except FileNotFoundError: raise ValueError("Project template not found") if not src_path.exists(): raise ValueError("Project template not found") if not src_path.is_dir(): raise ValueError("The project template must be a folder") return src_path, Path(dst_path).resolve() def copy_local( src_path, dst_path, data, *, exclude=None, include=None, tasks=None, envops=None, **flags ): src_path, dst_path = resolve_paths(src_path, dst_path) user_data = get_user_data(src_path, **flags) user_exclude = user_data.pop("_exclude", None) if exclude is None: exclude = user_exclude or DEFAULT_EXCLUDE user_include = user_data.pop("_include", None) if include is None: include = user_include or DEFAULT_INCLUDE user_tasks = user_data.pop("_tasks", None) if tasks is None: tasks = user_tasks or [] must_filter = get_name_filter(exclude, include) data.update(user_data) data.setdefault("folder_name", dst_path.name) render = get_jinja_renderer(src_path, data, envops) if not flags["quiet"]: print("") # padding space for folder, _, files in os.walk(str(src_path)): rel_folder = folder.replace(str(src_path), "", 1).lstrip(os.path.sep) rel_folder = render.string(rel_folder) if must_filter(rel_folder): continue folder = Path(folder) rel_folder = Path(rel_folder) for src_name in files: dst_name = re.sub(RE_TMPL, "", src_name) dst_name = render.string(dst_name) rel_path = rel_folder / dst_name if must_filter(rel_path): continue source_path = folder / src_name render_file(dst_path, rel_path, source_path, render, **flags) if not flags["quiet"]: print("") # padding space if tasks: run_tasks(dst_path, render, tasks) def render_file(dst_path, rel_path, source_path, render, **flags): """Process or copy a file of the skeleton. """ final_path = dst_path.resolve() / rel_path if not flags["pretend"]: make_folder(final_path.parent) if source_path.suffix == ".tmpl": content = render(source_path) else: content = None display_path = str(rel_path).replace("." + os.path.sep, ".", 1) if not final_path.exists(): if not flags["quiet"]: printf("create", display_path, style=STYLE_OK) else: if file_is_identical(source_path, final_path, content): if not flags["quiet"]: printf("identical", display_path, style=STYLE_IGNORE) return if not overwrite_file(display_path, source_path, final_path, content, **flags): return if flags["pretend"]: return if content is None: copy_file(source_path, final_path) else: final_path.write_text(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(str(path1), str(path2), shallow=False) def file_has_this_content(path, content): return content == path.read_text() def overwrite_file(display_path, source_path, final_path, content, **flags): if not flags["quiet"]: printf("conflict", display_path, style=STYLE_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"]: printf("force" if overwrite else "skip", display_path, style=STYLE_WARNING) return overwrite def run_tasks(dst_path, render, tasks): dst_path = str(dst_path) for task in tasks: task = render.string(task) subprocess.run(task, shell=True, check=True, cwd=dst_path) PK!g}copier/tools.pyfrom fnmatch import fnmatch from functools import reduce import errno import os import shutil import unicodedata import jinja2 from jinja2.sandbox import SandboxedEnvironment import colorama from colorama import Fore, Style _all__ = ( "STYLE_OK", "STYLE_WARNING", "STYLE_IGNORE", "STYLE_DANGER", "printf", "prompt", "prompt_bool", ) colorama.init() STYLE_OK = [Fore.GREEN, Style.BRIGHT] STYLE_WARNING = [Fore.YELLOW, Style.BRIGHT] STYLE_IGNORE = [Fore.CYAN] STYLE_DANGER = [Fore.RED, Style.BRIGHT] def printf(action, msg="", style=None, indent=12): action = action.rjust(indent, " ") if not style: return action + msg out = style + [action, Fore.RESET, Style.RESET_ALL, " ", msg] print(*out, sep="") 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, ) def make_folder(folder, pretend=False): if not folder.exists(): try: os.makedirs(str(folder)) except OSError as e: if e.errno != errno.EEXIST: raise def copy_file(src, dst): shutil.copy2(str(src), str(dst)) # 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 = str(fullpath) \ .replace(self.src_path, "", 1) \ .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. """ # Jinja <= 2.10 does not work with `pathlib.Path`s src_path = str(src_path) _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(str(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!n6ܩ copier/user_data.pyimport json from pathlib import Path from ruamel.yaml import YAML from .tools import printf, prompt, STYLE_WARNING __all__ = ("get_user_data", ) yaml = YAML(typ="safe", pure=True) INDENT = " " def load_yaml_data(src_path, quiet=False): yaml_path = Path(src_path) / "copier.yml" if not yaml_path.exists(): return {} yaml_src = yaml_path.read_text() 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("") printf("INVALID", msg=yaml_path, style=STYLE_WARNING, indent=0) print("-" * 42) print(e) print("-" * 42) return {} def load_json_data(src_path, quiet=False, warning=True): json_path = Path(src_path) / "copier.json" if not json_path.exists(): return load_old_json_data(src_path, quiet=quiet, warning=warning) json_src = json_path.read_text() try: return json.loads(json_src) except ValueError as e: if not quiet: print("") printf("INVALID", msg=json_path, style=STYLE_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 3.0 json_path = Path(src_path) / "voodoo.json" if not json_path.exists(): return {} if warning and not quiet: print("") printf( "WARNING", msg="`voodoo.json` is deprecated. " + "Replace it with a `copier.yml` or `copier.json`.", style=STYLE_WARNING, indent=10, ) json_src = json_path.read_text() try: return json.loads(json_src) except ValueError as e: if not quiet: print("") printf("INVALID", msg=json_path, style=STYLE_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 SPECIAL_KEYS = ("_exclude", "_include") 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: if key in SPECIAL_KEYS: continue default = default_user_data[key] user_data[key] = prompt(INDENT + " {0}?".format(key), default) print("\n" + INDENT + "-" * 42) return user_data PK!Sng 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): url = str(url) # In case we have got a `pathlib.Path` 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!OX PPcopier/version.pyimport pkg_resources __version__ = pkg_resources.require("copier")[0].version PK!Hx'&)'copier-2.2.2.dist-info/entry_points.txtN+I/N.,()J/L-Pz9VEy\\PK!aꈃo|"R]XUCvk!_VV)z"_>~|?=** o,W!Bl;4=&)!K[hKHʮxǔǭ{mEGtxi SowlORa BJ]+T)C'q(Ic wUͩwl ]\TI?tħaHdjYd3P^/鹶(-46Hmb;za3I4TŢuJUO)*s]hm*v$ W)q+tY뼩!Kk`1@*(@:(Iݸ*x|~.@TJ\\LJ+!N 'D;X.ߪB32P ڃ8u1z`%*oic{xqEU)2iᲮ,X!5]f'U(2)7LTXma~ӕSv2s6e@ U  r0'oV\(h>&awL^yNCb½CQZ|3te f:h+gع7Rưr~i<9Z^ʍg" |Jf3q~_y 5ws!(0N.yؕ\rN F kB4%/Hl/+GF[ Mxm5Q*q{@}xX҇#xBZ?Z"0lM 7SK8֗ ^OVZy%d U|T-6Wב8'ƗWqe|饻v㮮6ʒʵ,)@`g;]r`TgLEKCwx FW ,zkv{}Sܝ)u=\ PKr ߓբg27p~Q&ta 8RyZ&(K.#ӽt"mNT/BFR0ىc^-4 \ap-x>̺ޅwS^_ѵq@)e̫AZzߤ1PFX扣MבٻDr mfWIWbj2e wv!W3QAfE9]z'̏]j7}~{_.? O Oi\a*z] 'p 꾸ktqV>~Iv<w^S{ HVe@Q>ݪ^oD{aׁ -q ޛ8SqKZa0rO!5pm `sXϰm.]KkY!\g>|,SXѱã竺]Bw`K,qoqh:C3-o$:"B, lA#t"l.lkdsv eHϷz5&8,OHtH_D=~Jxfk FO V?cGf# t݅|t\榎AJO %_!` _KэeaA,\ _^W*IPK!H˪83|copier-2.2.2.dist-info/RECORDuˎ@}? m1 ZZi6h T!U|Ʉs_N70o?"HxL % {t_.w:dTrZU)nF{V.cI^M2BmMaߑa̫(lR3 R'0Zxݨ]r F )1=kf;@Hegb\%`]Թ@ R<׃^վbg#yg M^,l9/{kwñ(%A l+;0"fO_g$==؅mZd[bF$o įs/~[Um|Odc&rXYwuHCŒ<Zp˿ #Nq!dIG:k] 8J@|vZB96~0]H;2+ṊvWD{Rq@kB9X[%}0jz?.ˊ"φY3GJ:HY;z6,y{)(nd9i[S7PK!ۏcopier/__init__.pyPK! 2 큿copier/cli.pyPK!Jؼ copier/main.pyPK!g}%copier/tools.pyPK!n6ܩ :copier/user_data.pyPK!Sng WFcopier/vcs.pyPK!OX PP{Icopier/version.pyPK!Hx'&)'Icopier-2.2.2.dist-info/entry_points.txtPK!