diff --git a/CHANGELOG.md b/CHANGELOG.md index 2017b18..1d58697 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ Note: Breaking changes between versions are indicated by "💥". +## Unreleased + +- [Feature] Add `config render` command + ## 3.11.0 (2020-01-14) - [Feature] Add support for simple, YAML-based plugins diff --git a/tests/test_env.py b/tests/test_env.py index 15f9b39..369d5df 100644 --- a/tests/test_env.py +++ b/tests/test_env.py @@ -25,6 +25,14 @@ class EnvTests(unittest.TestCase): self.assertIn(template_name, renderer.environment.loader.list_templates()) self.assertNotIn(template_name, templates) + def test_is_binary_file(self): + self.assertTrue(env.is_binary_file("/home/somefile.ico")) + + def test_find_path(self): + renderer = env.Renderer({}, [env.TEMPLATES_ROOT]) + path = renderer.find_path("local/docker-compose.yml") + self.assertTrue(os.path.exists(path)) + def test_pathjoin(self): self.assertEqual( "/tmp/env/target/dummy", env.pathjoin("/tmp", "target", "dummy") diff --git a/tutor/commands/config.py b/tutor/commands/config.py index 3270272..fe74bab 100644 --- a/tutor/commands/config.py +++ b/tutor/commands/config.py @@ -59,6 +59,28 @@ def save(context, interactive, set_, unset): env.save(context.root, config) +@click.command(help="Render a template folder with eventual extra configuration files") +@click.option( + "-x", + "--extra-config", + "extra_configs", + multiple=True, + type=click.Path(exists=True, resolve_path=True, dir_okay=False), + help="Load extra configuration file (can be used multiple times)", +) +@click.argument("src", type=click.Path(exists=True, resolve_path=True)) +@click.argument("dst") +@click.pass_obj +def render(context, extra_configs, src, dst): + config = tutor_config.load(context.root) + for extra_config in extra_configs: + tutor_config.merge(config, tutor_config.load_file(extra_config), force=True) + + renderer = env.Renderer(config, [src]) + renderer.render_all_to(dst) + fmt.echo_info("Templates rendered to {}".format(dst)) + + @click.command(help="Print the project root") @click.pass_obj def printroot(context): @@ -77,5 +99,6 @@ def printvalue(context, key): config_command.add_command(save) +config_command.add_command(render) config_command.add_command(printroot) config_command.add_command(printvalue) diff --git a/tutor/config.py b/tutor/config.py index 4b46ac1..7ef1951 100644 --- a/tutor/config.py +++ b/tutor/config.py @@ -56,6 +56,11 @@ def load_defaults(): return serialize.load(env.read("config.yml")) +def load_file(path): + with open(path) as f: + return serialize.load(f.read()) + + def load_current(root, defaults): """ Load the configuration currently stored on disk. diff --git a/tutor/env.py b/tutor/env.py index c0623ff..0e31abb 100644 --- a/tutor/env.py +++ b/tutor/env.py @@ -14,6 +14,7 @@ from .__about__ import __version__ TEMPLATES_ROOT = pkg_resources.resource_filename("tutor", "templates") VERSION_FILENAME = "version" +BIN_FILE_EXTENSIONS = [".ico", ".ttf", ".png", ".jpg"] class Renderer: @@ -38,6 +39,7 @@ class Renderer: def __init__(self, config, template_roots): self.config = deepcopy(config) + self.template_roots = template_roots # Create environment environment = jinja2.Environment( @@ -68,6 +70,13 @@ class Renderer: """ yield from self.iter_templates_in(subdir + "/") + def find_path(self, path): + for templates_root in self.template_roots: + full_path = os.path.join(templates_root, path) + if os.path.exists(full_path): + return full_path + raise ValueError("Template path does not exist") + def patch(self, name, separator="\n", suffix=""): """ Render calls to {{ patch("...") }} in environment templates from plugin patches. @@ -93,11 +102,17 @@ class Renderer: return self.__render(template) def render_file(self, path): + if is_binary_file(path): + # Don't try to render binary files + with open(self.find_path(path), "rb") as f: + return f.read() + try: template = self.environment.get_template(path) except Exception: fmt.echo_error("Error loading template " + path) raise + try: return self.__render(template) except (jinja2.exceptions.TemplateError, exceptions.TutorError): @@ -107,6 +122,12 @@ class Renderer: fmt.echo_error("Unknown error rendering template " + path) raise + def render_all_to(self, root): + for template in self.iter_templates_in(): + rendered = self.render_file(template) + dst = os.path.join(root, template) + write_to(rendered, dst) + def __render(self, template): try: return template.render(**self.config) @@ -165,8 +186,14 @@ def save_all_from(prefix, root, config): def write_to(content, path): + """ + Content can be either str or bytes. + """ + open_mode = "w" + if isinstance(content, bytes): + open_mode += "b" utils.ensure_file_directory_exists(path) - with open(path, "w") as of: + with open(path, open_mode) as of: of.write(content) @@ -262,10 +289,15 @@ def is_part_of_env(path): is_excluded = False is_excluded = is_excluded or basename.startswith(".") or basename.endswith(".pyc") is_excluded = is_excluded or basename == "__pycache__" - is_excluded = is_excluded or "partials" in parts + is_excluded = is_excluded or "partials" in parts or ".git" in parts return not is_excluded +def is_binary_file(path): + ext = os.path.splitext(path)[1] + return ext in BIN_FILE_EXTENSIONS + + def template_path(*path, templates_root=TEMPLATES_ROOT): """ Return the template file's absolute path.