Source code for et_micc.expand

# -*- coding: utf-8 -*-
"""
Module et_micc.expand
=====================

Helper functions for dealing with *Cookiecutter* templates.
"""

import os, shutil, platform
from pathlib import Path
import json

import click
from cookiecutter.main import cookiecutter

import et_micc.logger

EXIT_OVERWRITE = -3


[docs]def resolve_template(template): """Compose the absolute path of a template.""" if template.startswith('~') or template.startswith(os.sep): pass # absolute path elif os.sep in template: # reative path template = Path.cwd() / template else: # just the template name template = Path(__file__).parent / 'templates' / template if not template.exists(): raise AssertionError(f"Inexisting template {template}") return template
[docs]def set_preferences(micc_file): """Set the preferences in *micc_file*. (This function requires user interaction!) :param Path micc_file: path to a json file. """ with micc_file.open() as f: preferences = json.load(f) for parameter,description in preferences.items(): if not description['default'].startswith('{{ '): answer = click.prompt(**description) preferences[parameter]['default'] = answer with micc_file.open(mode='w') as f: json.dump(preferences,f) return preferences
[docs]def get_preferences(micc_file): """Get the preferences from *micc_file*. (This function requires user interaction if no *micc_file* was provided!) :param Path micc_file: path to a json file. """ if micc_file.samefile('.'): # There is no et_micc file with preferences yet. dotmicc = Path().home() / '.et_micc' dotmicc.mkdir(exist_ok=True) dotmicc_miccfile = dotmicc / 'micc.json' if dotmicc_miccfile.exists(): preferences = get_preferences(dotmicc_miccfile) else: micc_file_template = Path(__file__).parent / 'micc.json' shutil.copyfile(str(micc_file_template),str(dotmicc_miccfile)) preferences = set_preferences(dotmicc_miccfile) else: with micc_file.open() as f: preferences = json.load(f) return preferences
[docs]def get_template_parameters(preferences): """Get the template parameters from the preferences. :param dict|Path preferenes: :returns: dict of (parameter name,parameter value) pairs. """ if isinstance(preferences,dict): template_parameters = {} for parameter,description in preferences.items(): template_parameters[parameter] = description['default'] elif isinstance(preferences,Path): with preferences.open() as f: template_parameters = json.load(f) else: raise RuntimeError() return template_parameters
[docs]def expand_templates(options): """Expand a list of cookiecutter :py:obj:`templates` in directory :py:obj:`project_path`. Expanding templates may require overwriting pre-existing files. *Micc* handles this situation in different ways: * If :py:obj:`options.overwrite` equals :py:const:`False` the exansion will fail without overwriting any pre-existing files. The project is not modified. A warning is produced. This is the default. To continue, rerun the command with one of the two options below. * If :py:obj:`options.overwrite` equals :py:const:`True` the exansion will overwrite any pre-existing files without backup, and produce a warning, listing the overwritten files. * If :py:obj:`options.backup` equals :py:const:`True` pre-existing files will be backed up (.bak) before the new files are expanded. If anything went wrong, you can inspect the backup files, and correct the errors manually. :param types.SimpleNamespace options: namespace object with options accepted by et_micc commands. Relevant attributes are * templates: ordered list of (paths to) cookiecutter templates that will be expanded as they appear. The template parameters are propagated from each template to the next. * **verbosity** * **project_path**: Path to the project on which the command operates. * **template_parameters**: extra template parameters not read from *micc_file* """ templates = options.templates if not isinstance(templates, list): templates = [templates] project_path = options.project_path project_path.mkdir(parents=True, exist_ok=True) output_dir = project_path.parent micc_logger = options.logger # list existing files that would be overwritten if options.overwrite==True existing_files = {} for template in templates: template = resolve_template(template) # write a cookiecutter.json file in the cookiecutter template directory cookiecutter_json = template / 'cookiecutter.json' with open(cookiecutter_json,'w') as f: json.dump(options.template_parameters, f, indent=2) # run cookiecutter in an empty temporary directory to check if there are any # existing project files that would be overwritten. tmp = output_dir / '_cookiecutter_tmp_' if tmp.exists(): shutil.rmtree(tmp) tmp.mkdir(parents=True, exist_ok=True) # expand the Cookiecutter template in a temporary directory, cookiecutter( str(template) , no_input=True , overwrite_if_exists=True , output_dir=str(tmp) ) # find out if there are any files that would be overwritten. os_name = platform.system() for root, _, files in os.walk(tmp): if root==tmp: continue else: root2 = os.path.relpath(root,tmp) for f in files: if os_name=="Darwin" and f==".DS_Store": continue file = output_dir / root2 / f if file.exists(): if not template in existing_files: existing_files[template] = [] existing_files[template].append(file) if existing_files: if options.backup: micc_logger.warning("Pre-existing files that will be backed up ('--backup' specified):\n") micc_logger.indent(2) for files in existing_files.values(): for src in files: src = str(src) dst = src + '.bak' shutil.copyfile(src, dst) micc_logger.warning(f"{src} -> {dst}") micc_logger.dedent() elif not options.overwrite: micc_logger.warning("Pre-existing files that would be overwritten:\n") micc_logger.indent(2) for files in existing_files.values(): for src in files: micc_logger.warning(str(src)) micc_logger.dedent() click.secho("Aborting because 'overwrite==False'.\n" " Rerun the command with the '--backup' flag to first backup these files (*.bak).\n" " Rerun the command with the '--overwrite' flag to overwrite these files without backup.\n" "Aborting." , fg='bright_red' ) return EXIT_OVERWRITE else: micc_logger.warning(f"'--overwrite' specified: pre-existing files will be overwritten WITHOUT backup:\n") for files in existing_files.values(): for src in files: micc_logger.warning(f" overwriting {src}") # Now we can safely overwrite pre-existing files. micc_logger.debug(f"Expanding templates using these parameters:\n{json.dumps(options.template_parameters,indent=2)}") for template in templates: template = resolve_template(template) micc_logger.debug(f"Expanding template {template}.") cookiecutter( str(template) , no_input=True , overwrite_if_exists=True , output_dir=str(output_dir) ) # Clean up (see issue #7) cookiecutter_json = template / 'cookiecutter.json' cookiecutter_json.unlink() # Clean up if tmp.exists(): shutil.rmtree(str(tmp)) return 0
#eof