diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f011c5..e4c6267 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Note: Breaking changes between versions are indicated by "💥". ## Unreleased +- [Bugfix] Fix template rendering for Windows users. - [Improvement] Switch to `bcrypt` for htpasswd password generation, for better portability on Windows. - [Improvement] In the openedx production docker image, add some jitter to the gunicorn worker restart process to prevent all workers from restarting at the same time. diff --git a/tests/test_env.py b/tests/test_env.py index 2122df7..a544b8b 100644 --- a/tests/test_env.py +++ b/tests/test_env.py @@ -28,9 +28,9 @@ class EnvTests(unittest.TestCase): def test_is_binary_file(self): self.assertTrue(env.is_binary_file("/home/somefile.ico")) - def test_find_path(self): + def test_find_os_path(self): renderer = env.Renderer({}, [env.TEMPLATES_ROOT]) - path = renderer.find_path("local/docker-compose.yml") + path = renderer.find_os_path("local/docker-compose.yml") self.assertTrue(os.path.exists(path)) def test_pathjoin(self): diff --git a/tutor/commands/config.py b/tutor/commands/config.py index bc11f85..cc7eb13 100644 --- a/tutor/commands/config.py +++ b/tutor/commands/config.py @@ -101,8 +101,10 @@ def printvalue(context, key): try: # Note that this will incorrectly print None values fmt.echo(config[key]) - except KeyError: - raise exceptions.TutorError("Missing configuration value: {}".format(key)) + except KeyError as e: + raise exceptions.TutorError( + "Missing configuration value: {}".format(key) + ) from e config_command.add_command(save) diff --git a/tutor/env.py b/tutor/env.py index d364ccd..5ccfbea 100644 --- a/tutor/env.py +++ b/tutor/env.py @@ -57,8 +57,11 @@ class Renderer: environment.globals["TUTOR_VERSION"] = __version__ self.environment = environment - def iter_templates_in(self, *path): - prefix = "/".join(path) + def iter_templates_in(self, *prefix): + """ + The elements of `prefix` must contain only "/", and not os.sep. + """ + prefix = "/".join(prefix) for template in self.environment.loader.list_templates(): if template.startswith(prefix) and self.is_part_of_env(template): yield template @@ -88,7 +91,8 @@ class Renderer: is_excluded = is_excluded or ignore_folder in parts return not is_excluded - def find_path(self, path): + def find_os_path(self, template_name): + path = template_name.replace("/", os.sep) for templates_root in self.template_roots: full_path = os.path.join(templates_root, path) if os.path.exists(full_path): @@ -119,35 +123,41 @@ class Renderer: template = self.environment.from_string(text) return self.__render(template) - def render_file(self, path): + def render_template(self, template_name): """ Render a template file. Return the corresponding string. If it's a binary file (as indicated by its path), return bytes. + + The template_name *always* uses "/" separators, and is not os-dependent. Do not pass the result of + os.path.join(...) to this function. """ - if is_binary_file(path): + if is_binary_file(template_name): # Don't try to render binary files - with open(self.find_path(path), "rb") as f: + with open(self.find_os_path(template_name), "rb") as f: return f.read() try: - template = self.environment.get_template(path) + template = self.environment.get_template(template_name) except Exception: - fmt.echo_error("Error loading template " + path) + fmt.echo_error("Error loading template " + template_name) raise try: return self.__render(template) except (jinja2.exceptions.TemplateError, exceptions.TutorError): - fmt.echo_error("Error rendering template " + path) + fmt.echo_error("Error rendering template " + template_name) raise except Exception: - fmt.echo_error("Unknown error rendering template " + path) + fmt.echo_error("Unknown error rendering template " + template_name) 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) + def render_all_to(self, root, *prefix): + """ + `prefix` can be used to limit the templates to render. + """ + for template_name in self.iter_templates_in(*prefix): + rendered = self.render_template(template_name) + dst = os.path.join(root, template_name.replace("/", os.sep)) write_to(rendered, dst) def __render(self, template): @@ -207,13 +217,10 @@ def save_plugin_templates(plugin, root, config): def save_all_from(prefix, root, config): """ Render the templates that start with `prefix` and store them with the same - hierarchy at `root`. + hierarchy at `root`. Here, `prefix` can be the result of os.path.join(...). """ renderer = Renderer.instance(config) - for template in renderer.iter_templates_in(prefix): - rendered = renderer.render_file(template) - dst = os.path.join(root, template) - write_to(rendered, dst) + renderer.render_all_to(root, prefix.replace(os.sep, "/")) def write_to(content, path): @@ -233,8 +240,8 @@ def render_file(config, *path): Return the rendered contents of a template. """ renderer = Renderer.instance(config) - file_path = os.path.join(*path) - return renderer.render_file(file_path) + template_name = "/".join(path) + return renderer.render_template(template_name) def render_dict(config):