Source code for nengo_bones.templates

"""Handles the processing of nengo-bones templates using jinja2."""

from collections import defaultdict, OrderedDict
import os
import stat
import warnings

try:
    from black import FileMode, format_str, TargetVersion

    HAS_BLACK = True
except ImportError:
    HAS_BLACK = False
import jinja2


[docs]class BonesTemplate: """ A templated file known to Nengo Bones. The only necessary information is the output filename, relative to the ``templates`` directory, which means that the output filename for the Sphinx configuration is ``docs/conf.py``. Other attributes are determined from the output filename. Parameters ---------- output_file : str Filename for the rendered output file. env : ``jinja2.Environment`` Initialized jinja environment for loading/rendering templates. Attributes ---------- env : ``jinja2.Environment`` Initialized jinja environment for loading/rendering templates. output_file : str Filename for the rendered output file. section : str The heading for the section in the config file containing config options specific to the template being rendered. template_file : str Filename for the input template file. """ __slots__ = ("env", "output_file", "section", "template_file") extra_render_data = defaultdict(list) def __init__(self, output_file, env): self.output_file = output_file self.env = env section = output_file.lstrip(".") section = section.replace(".", "_") section = section.replace("/", "_") section = section.replace("-", "_") section = section.lower() self.section = section self.template_file = "%s.template" % (output_file,)
[docs] @classmethod def add_render_data(cls, filename): """ Register functions that add template-specific render data. For example: .. testcode:: @nengo_bones.templates.BonesTemplate.add_render_data("my_new_template") def add_my_new_template_data(data): data["attr"] = "val" ... """ def _add_render_data(func): cls.extra_render_data[filename].append(func) return func return _add_render_data
[docs] def get_render_data(self, config): """ Construct the ``data`` that will be used to render this template. This method creates a new dictionary so the original ``config`` is not modified. Additionally, certain sections have processing done to them in addition to flattening out the section to the top-level of the config. Parameters ---------- config : dict Dictionary containing configuration values. Returns ------- data : dict A dictionary that can be passed to `.render` and `.render_to_file`. """ data = {} # TODO: separate "top-level" config into its own section? data.update(config) data.update(config[self.section]) # Add special options for specific sections for adder in self.extra_render_data[self.section]: adder(data) return data
[docs] def render(self, **data): """ Render this template to a string. Parameters ---------- data : dict Will be passed on to the ``template.render`` function. """ rendered = self.env.get_template(self.template_file).render(**data) # Format Python templates with black if HAS_BLACK: if self.output_file.endswith(".py"): black_mode = FileMode( target_versions={ TargetVersion.PY35, TargetVersion.PY36, TargetVersion.PY37, } ) rendered = format_str(rendered, mode=black_mode) else: warnings.warn( "Black not installed, rendered template may not be formatted correctly" ) return rendered
[docs] def render_to_file(self, output_dir, output_name=None, **data): """ Render a template to file. .. note:: Rendered shell scripts (files with the ``.sh extension``) are automatically marked as executable. Parameters ---------- output_dir : str Directory in which the rendered file should be placed. output_name : str, optional An alternative filename for the rendered file. This overrides the class's internal ``output_file`` attribute. data : dict Will be passed on to the ``render`` function. """ if output_name is None: output_name = self.output_file output_path = os.path.join(output_dir, output_name) if not os.path.exists(os.path.dirname(output_path)): os.makedirs(os.path.dirname(output_path)) with open(output_path, "w") as f: f.write(self.render(**data)) # We mark all `.sh` files as executable if output_name.endswith(".sh"): st = os.stat(output_path) os.chmod(output_path, st.st_mode | stat.S_IEXEC)
[docs]@BonesTemplate.add_render_data("travis_yml") def add_travis_data(data): """Add travis.yml-specific entries to the 'data' dict.""" jobs = data["travis_yml"]["jobs"] for job in jobs: # shortcuts for setting environment variables if "env" not in job: job["env"] = OrderedDict() for var in ("script", "test_args"): if var in job: job["env"][var] = job.pop(var)
[docs]@BonesTemplate.add_render_data("manifest_in") def add_manifest_data(data): """Add MANIFEST.in-specific entries to the 'data' dict.""" data["custom"] = data["manifest_in"]
def _get_extras(sect_data, extra_types): extras = OrderedDict() # We iterate over sect_data (rather than extra_types) so that order is preserved for key in list(sect_data): if key in extra_types: extras[key] = (extra_types[key], sect_data.pop(key)) return extras
[docs]@BonesTemplate.add_render_data("setup_py") def add_setup_py_data(data): """Add setup.py-specific entries to the 'data' dict.""" data["extras"] = _get_extras( data["setup_py"], extra_types={ "classifiers": "list", "py_modules": "list", "entry_points": "dict", "package_data": "dict", }, )
[docs]@BonesTemplate.add_render_data("setup_cfg") def add_setup_cfg_data(data): """Add setup.cfg-specific entries to the 'data' dict.""" pytest_data = data["setup_cfg"]["pytest"] pytest_data["extras"] = _get_extras( pytest_data, extra_types={ "allclose_tolerances": "list", "filterwarnings": "list", "nengo_neurons": "list", "nengo_simulator": "str", "nengo_simloader": "str", "nengo_test_unsupported": "dict", "plt_dirname": "str", "plt_filename_drop": "list", "rng_salt": "str", "xfail_strict": "str", }, )
[docs]def load_env(): """Creates a jinja environment for loading/rendering templates.""" bones_toplevel = os.path.normpath(os.path.dirname(__file__)) # Load overridden templates first. # Builtins are referenced with templates/*.template override_dirs = [] override_dirs.append(".templates") override_dirs.append(bones_toplevel) override_loader = jinja2.FileSystemLoader(override_dirs) # If those fail, use the builtins builtin_loader = jinja2.FileSystemLoader(os.path.join(bones_toplevel, "templates")) env = jinja2.Environment( loader=jinja2.ChoiceLoader([override_loader, builtin_loader]), trim_blocks=True, lstrip_blocks=True, keep_trailing_newline=True, ) env.filters["rstrip"] = lambda s, chars: s.rstrip(chars) return env