Add `config render` command

This is going to be useful for using custom themes with user-defined
variables.
This commit is contained in:
Régis Behmo 2020-01-16 15:40:38 +01:00
parent c4c12b0ab8
commit 72e23f3f96
5 changed files with 74 additions and 2 deletions

View File

@ -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

View File

@ -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")

View File

@ -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)

View File

@ -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.

View File

@ -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.