From 68d23c5dd50535d4d59684e0070110ee0e923df3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Behmo?= Date: Sat, 11 May 2019 19:31:18 +0200 Subject: [PATCH] Env module refactoring Better naming and tests. --- tests/test_env.py | 24 +++++++++ tutor/env.py | 133 +++++++++++++++++++++++++++++++++------------- 2 files changed, 120 insertions(+), 37 deletions(-) create mode 100644 tests/test_env.py diff --git a/tests/test_env.py b/tests/test_env.py new file mode 100644 index 0000000..8344d7d --- /dev/null +++ b/tests/test_env.py @@ -0,0 +1,24 @@ +import unittest + +from tutor import env +from tutor import exceptions + + +class EnvTests(unittest.TestCase): + def test_walk_templates(self): + templates = list(env.walk_templates("local")) + self.assertIn("local/docker-compose.yml", templates) + + def test_pathjoin(self): + self.assertEqual( + "/tmp/env/target/dummy", env.pathjoin("/tmp", "target", "dummy") + ) + self.assertEqual("/tmp/env/dummy", env.pathjoin("/tmp", "dummy")) + + def test_render_str(self): + self.assertEqual( + "hello world", env.render_str({"name": "world"}, "hello {{ name }}") + ) + + def test_render_str_missing_configuration(self): + self.assertRaises(exceptions.TutorError, env.render_str, {}, "hello {{ name }}") diff --git a/tutor/env.py b/tutor/env.py index 4ae10a8..7b862b2 100644 --- a/tutor/env.py +++ b/tutor/env.py @@ -13,32 +13,74 @@ TEMPLATES_ROOT = os.path.join(os.path.dirname(__file__), "templates") VERSION_FILENAME = "version" +class Renderer: + ENVIRONMENT = None + + @classmethod + def environment(cls): + if not cls.ENVIRONMENT: + cls.ENVIRONMENT = jinja2.Environment( + loader=jinja2.FileSystemLoader(TEMPLATES_ROOT), + undefined=jinja2.StrictUndefined, + ) + return cls.ENVIRONMENT + + @classmethod + def render_str(cls, config, text): + template_globals = dict( + RAND8=utils.random_string(8), RAND24=utils.random_string(24), **config + ) + template = cls.environment().from_string(text, globals=template_globals) + try: + return template.render() + except jinja2.exceptions.UndefinedError as e: + raise exceptions.TutorError( + "Missing configuration value: {}".format(e.args[0]) + ) + + def render_full(root, config): """ Render the full environment, including version information. """ - for target in ["android", "apps", "k8s", "local", "webui"]: - render_target(root, config, target) - copy_target(root, "build") + for subdir in ["android", "apps", "k8s", "local", "webui"]: + render_subdir(subdir, root, config) + copy_subdir("build", root) with open(pathjoin(root, VERSION_FILENAME), "w") as f: f.write(__version__) -def render_target(root, config, target): +def render_subdir(subdir, root, config): """ - Render the templates located in `target` and store them with the same + Render the templates located in `subdir` and store them with the same hierarchy at `root`. """ - for src, dst in walk_templates(root, target): - rendered = render_file(config, src) + for path in walk_templates(subdir): + dst = pathjoin(root, path) + rendered = render_file(config, path) + ensure_file_directory_exists(dst) with open(dst, "w") as of: of.write(rendered) +def ensure_file_directory_exists(path): + """ + Create file's base directory if it does not exist. + """ + directory = os.path.dirname(path) + if not os.path.exists(directory): + os.makedirs(directory) + + def render_file(config, path): + """ + Return the rendered contents of a template. + """ with codecs.open(path, encoding="utf-8") as fi: try: return render_str(config, fi.read()) + except jinja2.exceptions.UndefinedError: + raise except jinja2.exceptions.TemplateError: print("Error rendering template", path) raise @@ -74,31 +116,32 @@ def render_str(config, text): Return: substituted (str) """ - template = jinja2.Template(text, undefined=jinja2.StrictUndefined) - try: - return template.render( - RAND8=utils.random_string(8), RAND24=utils.random_string(24), **config - ) - except jinja2.exceptions.UndefinedError as e: - raise exceptions.TutorError("Missing configuration value: {}".format(e.args[0])) + return Renderer.render_str(config, text) -def copy_target(root, target): +def copy_subdir(subdir, root): """ - Copy the templates located in `path` and store them with the same hierarchy - at `root`. + Copy the templates located in `subdir` and store them with the same hierarchy + at `root`. No rendering is done here. """ - for src, dst in walk_templates(root, target): + for path in walk_templates(subdir): + src = os.path.join(TEMPLATES_ROOT, path) + dst = pathjoin(root, path) + ensure_file_directory_exists(dst) shutil.copy(src, dst) def is_up_to_date(root): + """ + Check if the currently rendered version is equal to the current tutor version. + """ return version(root) == __version__ def version(root): """ - Return the current environment version. + Return the current environment version. If the current environment has no version, + return "0". """ path = pathjoin(root, VERSION_FILENAME) if not os.path.exists(path): @@ -108,36 +151,33 @@ def version(root): def read(*path): """ - Read template content located at `path`. + Read raw content of template located at `path`. """ src = template_path(*path) with codecs.open(src, encoding="utf-8") as fi: return fi.read() -def walk_templates(root, target): +def walk_templates(subdir): """ - Iterate on the template files from `templates/target`. + Iterate on the template files from `templates/`. Yield: - src: template path - dst: destination path inside root + path: template path relative to the template root """ - target_root = template_path(target) - for dirpath, _, filenames in os.walk(target_root): + for dirpath, _, filenames in os.walk(template_path(subdir)): if not is_part_of_env(dirpath): continue - dst_dir = pathjoin(root, target, os.path.relpath(dirpath, target_root)) - if not os.path.exists(dst_dir): - os.makedirs(dst_dir) for filename in filenames: - src = os.path.join(dirpath, filename) - dst = os.path.join(dst_dir, filename) - if is_part_of_env(src): - yield src, dst + path = os.path.join(os.path.relpath(dirpath, TEMPLATES_ROOT), filename) + if is_part_of_env(path): + yield path def is_part_of_env(path): + """ + Determines whether a file should be rendered or not. + """ basename = os.path.basename(path) return not ( basename.startswith(".") @@ -147,16 +187,35 @@ def is_part_of_env(path): def template_path(*path): + """ + Return the template file's absolute path. + """ return os.path.join(TEMPLATES_ROOT, *path) def data_path(root, *path): - return os.path.join(os.path.abspath(root), "data", *path) + """ + Return the file's absolute path inside the data directory. + """ + return os.path.join(root_dir(root), "data", *path) -def pathjoin(root, target, *path): - return os.path.join(base_dir(root), target, *path) +def pathjoin(root, *path): + """ + Return the file's absolute path inside the environment. + """ + return os.path.join(base_dir(root), *path) def base_dir(root): - return os.path.join(root, "env") + """ + Return the environment base directory. + """ + return os.path.join(root_dir(root), "env") + + +def root_dir(root): + """ + Return the project root directory. + """ + return os.path.abspath(root)