Source code for facio.template

# -*- coding: utf-8 -*-

"""
.. module:: facio.start
   :synopsis: Process the users template using Jninja2 rendering it
              out into the current working directory.
"""

import fnmatch
import os
import re
import shutil

from codecs import open
from facio.base import BaseFacio
from facio.exceptions import FacioException
from facio.state import state
from facio.vcs import GitVCS, MercurialVCS

try:
    from jinja2 import Environment, FileSystemLoader
except ImportError:  # pragma: no cover
    raise FacioException('Jinja2 must be installed to use Facio')


# Regex for extracting context variable name from file or directory name
get_var_name_pattern = re.compile(r'\{\{(\w+)\}\}')


[docs]class Template(BaseFacio): COPY_ATTEMPT_LIMIT = 5 COPY_ATTEMPT = 1 def __init__(self, origin): """ Constructor for Template Class sets the project template origin. It also sets the default ignore globs. :param origin: The origin path to the template :type origin: str """ self.origin = origin # Update copy ignore globs with standard ignore patterns self.update_copy_ignore_globs([ '.git', '.hg', '.svn', '.DS_Store', 'Thumbs.db', ]) # Update render ignore globs self.update_render_ignore_globs([ '*.png', '*.gif', '*.jpeg', '*.jpg', ])
[docs] def update_copy_ignore_globs(self, globs): """ Update the ignore glob patterns to include the list provided. ** Usage: ** .. code-block:: python from facio.template import Template t = Template('foo', '/path/to/foo') globs = [ '*.png', '*.gif', ] t.update_copy_ignore_globs(globs) :param globs: A list of globs :type globs: list """ try: self.copy_ignore_globs += globs except AttributeError: if not isinstance(globs, list): self.copy_ignore_globs = [] else: self.copy_ignore_globs = globs except TypeError: raise FacioException('Failed to add {0} to ignore globs ' 'list'.format(globs))
[docs] def get_copy_ignore_globs(self): """ Returns ignore globs list at time of call. :returns: list """ try: return self.copy_ignore_globs except AttributeError: return []
[docs] def update_render_ignore_globs(self, globs): """ Update the render ignore glob patterns to include the list provided. ** Usage: ** .. code-block:: python from facio.template import Template t = Template('foo', '/path/to/foo') globs = [ '*.png', '*.gif', ] t.update_render_ignore_globs(globs) :param globs: A list of globs :type globs: list """ try: self.render_ignore_globs += globs except AttributeError: if not isinstance(globs, list): self.render_ignore_globs = [] else: self.render_ignore_globs = globs except TypeError: raise FacioException('Failed to add {0} to ignore globs ' 'list'.format(globs))
[docs] def get_render_ignore_globs(self): """ Returns ignore globs list at time of call. :returns: list """ try: return self.render_ignore_globs except AttributeError: return []
[docs] def get_render_ignore_files(self, files): """ Returns a list of files to ignore for rendering based on ``get_render_ignore_globs`` patterns. :param files: List of files to check against :type files: list :returns: list -- list of filenames """ ignores = [] for pattern in self.get_render_ignore_globs(): for filename in fnmatch.filter(files, pattern): ignores.append(filename) return ignores
[docs] def copy(self, callback=None): """ Copy template from origin path to ``state.get_project_root()``. :param callback: A callback function to be called after copy is complete :type callback: function -- default None :returns: bool """ self.out('Copying {0} to {1}'.format( self.origin, state.get_project_root())) ignore = shutil.ignore_patterns(*self.get_copy_ignore_globs()) try: shutil.copytree(self.origin, state.get_project_root(), ignore=ignore) except shutil.Error: raise FacioException('Failed to copy {0} to {1}'.format( self.origin, state.get_project_root())) except OSError: # If we get an OSError either the template path does not exist or # the project root already exists. Check the later first and then # check if the template path is git+ or hg+ and clone, finally # raising exceptions if not os.path.isdir(state.get_project_root()): supported_vcs = [ ('git+', GitVCS), ('hg+', MercurialVCS), ] for prefix, cls in supported_vcs: if self.origin.startswith(prefix): vcs = cls(self.origin) new_path = vcs.clone() if not new_path: raise FacioException( 'New path to template not returned by ' '{0}.clone()'.format(vcs.__class__.__name__)) self.origin = new_path break else: # Loop feel through so path is not prefixed with git+ or # +hg so it must be a path that does not exist raise FacioException('{0} does not exist'.format( self.origin)) # The loop broke so we can call self.copy again if self.COPY_ATTEMPT <= self.COPY_ATTEMPT_LIMIT: self.COPY_ATTEMPT += 1 self.copy(callback=vcs.remove_tmp_dir) else: raise FacioException('Failed to copy template after ' '{0} attempts'.format( self.COPY_ATTEMPT)) else: # project root exists, raise exception raise FacioException('{0} already exists'.format( state.get_project_root())) # Call callback if callable if callable(callback): callback( origin=self.origin, destination=state.get_project_root()) return True
[docs] def rename_direcories(self): """ Renames directories that are named after context variables, for example: ``{{PROJECT_NAME}}``. :returns: generator """ for root, dirs, files in os.walk(state.get_project_root()): for directory in fnmatch.filter(dirs, '*{{*}}*'): var_name = get_var_name_pattern.findall(directory)[0] var_value = state.get_context_variable(var_name) if var_value: old_path = os.path.join(root, directory) new_path = os.path.join(root, var_value) shutil.move(old_path, new_path) yield (old_path, new_path)
[docs] def rename_files(self): """ Rename files that are named after context variables, for example: ``{{PROJECT_NAME}}.py`` :returns: generator """ for root, dirs, files in os.walk(state.get_project_root()): for filename in fnmatch.filter(files, '*{{*}}*'): var_name = get_var_name_pattern.findall(filename)[0] var_value = state.get_context_variable(var_name) if var_value: name, ext = os.path.splitext(filename) old_path = os.path.join(root, filename) new_path = os.path.join(root, '{0}{1}'.format( var_value, ext)) shutil.move(old_path, new_path) yield (old_path, new_path)
[docs] def rename(self): """ Runs the two rename files and rename directories methods. """ for old, new in self.rename_direcories(): self.out('Renaming {0} to {1}'.format(old, new)) for old, new in self.rename_files(): self.out('Renaming {0} to {1}'.format(old, new))
[docs] def render(self): """ Reads the template and uses Jinja 2 to replace context variables with their real values. """ variables = state.get_context_variables() for root, dirs, files in os.walk(state.get_project_root()): jinja_loader = FileSystemLoader(root) jinja_environment = Environment(loader=jinja_loader) ignores = self.get_render_ignore_files(files) for filename in files: if filename not in ignores: path = os.path.join(root, filename) try: template = jinja_environment.get_template(filename) rendered = template.render(variables) except: import sys e = sys.exc_info()[1] self.warning('Failed to render {0}: {1}'.format( path, e)) else: with open(path, 'w', encoding='utf8') as handler: handler.write(rendered)

Related Topics