from os import path
from shutil import copy2
import codecs
import fnmatch
import os
import re
import six
import stat
from jinja2 import Environment, StrictUndefined
jinja2_env = Environment(
block_start_string="{{%",
block_end_string="%}}",
variable_start_string="{{{",
variable_end_string="}}}",
trim_blocks=True,
undefined=StrictUndefined,
)
jinja2_renderer = lambda s, v: jinja2_env.from_string(s).render(parse_variables(v))
python_formatting_renderer = lambda s, v: s % v
DEFAULT_IGNORED_FILES = ['.mrbob.ini', '.DS_Store']
DEFAULT_IGNORED_DIRECTORIES = []
def parse_variables(variables):
d = dict()
for key, value in variables.items():
keys = key.split('.')
new_d = None
for k in keys[:-1]:
if new_d is None:
if k not in d:
d[k] = dict()
new_d = d[k]
else:
if k not in new_d:
new_d[k] = dict()
new_d = new_d[k]
if new_d is None:
d[keys[-1]] = value
else:
new_d[keys[-1]] = value
return dict(d)
def matches_any(filename, patterns):
result = any(fnmatch.fnmatch(filename, pat) for pat in patterns)
return result
[docs]def render_structure(fs_source_root, fs_target_root, variables, verbose,
renderer, ignored_files, ignored_directories):
"""Recursively copies the given filesystem path `fs_source_root_ to a target directory `fs_target_root`.
Any files ending in `.bob` are rendered as templates using the given
renderer using the variables dictionary, thereby losing the `.bob` suffix.
strings wrapped in `+` signs in file- or directory names will be replaced
with values from the variables, i.e. a file named `+name+.py.bob` given a
dictionary {'name': 'bar'} would be rendered as `bar.py`.
"""
ignored_files.extend(DEFAULT_IGNORED_FILES)
ignored_directories.extend(DEFAULT_IGNORED_DIRECTORIES)
if not isinstance(fs_source_root, six.text_type): # pragma: no cover
fs_source_root = six.u(fs_source_root)
for fs_source_dir, local_directories, local_files in os.walk(fs_source_root, topdown=True):
fs_target_dir = path.abspath(path.join(fs_target_root, path.relpath(fs_source_dir, fs_source_root)))
local_directories[:] = [d for d in local_directories if not matches_any(d, ignored_directories)]
for local_file in local_files:
if matches_any(local_file, ignored_files):
continue
render_template(
path.join(fs_source_dir, local_file),
render_filename(fs_target_dir, variables),
variables,
verbose,
renderer,
)
for local_directory in local_directories:
abs_dir = render_filename(path.join(fs_target_dir, local_directory), variables)
if not path.exists(abs_dir):
if verbose:
print(six.u("mkdir %s") % abs_dir)
os.mkdir(abs_dir)
def render_template(fs_source, fs_target_dir, variables, verbose, renderer):
filename = path.split(fs_source)[1]
if filename.endswith('.bob'):
filename = filename.split('.bob')[0]
fs_target_path = path.join(fs_target_dir, render_filename(filename, variables))
if verbose:
print(six.u("Rendering %s to %s") % (fs_source, fs_target_path))
fs_source_mode = stat.S_IMODE(os.stat(fs_source).st_mode)
with codecs.open(fs_source, 'r', 'utf-8') as f:
source_output = f.read()
output = renderer(source_output, variables)
# append newline due to jinja2 bug, see https://github.com/iElectric/mr.bob/issues/30
if source_output.endswith('\n') and not output.endswith('\n'):
output += '\n'
with codecs.open(fs_target_path, 'w', 'utf-8') as fs_target:
fs_target.write(output)
os.chmod(fs_target_path, fs_source_mode)
else:
fs_target_path = path.join(fs_target_dir, render_filename(filename, variables))
if verbose:
print(six.u("Copying %s to %s") % (fs_source, fs_target_path))
copy2(fs_source, fs_target_path)
return path.join(fs_target_dir, filename)
def render_filename(filename, variables):
variables_regex = re.compile(r"\+[^+%s]+\+" % re.escape(os.sep))
replaceables = variables_regex.findall(filename)
for replaceable in replaceables:
actual_replaceable = replaceable.replace('+', '')
if actual_replaceable in variables:
filename = filename.replace(replaceable, variables[actual_replaceable])
else:
raise KeyError('%s key part of filename %s was not found in variables %s' % (actual_replaceable, filename, variables))
return filename