From 238d70510375dce9fa8641e75dd52cd11244ed3b Mon Sep 17 00:00:00 2001 From: Charlie Liu Date: Wed, 14 Nov 2018 17:42:16 -0500 Subject: [PATCH 1/8] =?UTF-8?q?=E2=9C=A8=20Add=20new=20target=20and=20temp?= =?UTF-8?q?late=20global=20variables?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This adds new global variables for the target and template files. Closes https://github.com/moremoban/moban/issues/29 --- moban/engine.py | 4 ++++ tests/fixtures/globals/nested.template | 1 + tests/fixtures/globals/variables.template | 3 +++ tests/fixtures/globals/variables.yml | 1 + tests/test_engine.py | 22 ++++++++++++++++++++++ 5 files changed, 31 insertions(+) create mode 100644 tests/fixtures/globals/nested.template create mode 100644 tests/fixtures/globals/variables.template create mode 100644 tests/fixtures/globals/variables.yml diff --git a/moban/engine.py b/moban/engine.py index 571b9e10..8ba66a91 100644 --- a/moban/engine.py +++ b/moban/engine.py @@ -50,6 +50,8 @@ def __init__(self, template_dirs, context_dirs): def render_to_file(self, template_file, data_file, output_file): template = self.jj2_environment.get_template(template_file) data = self.context.get_data(data_file) + template.globals['__target__'] = output_file + template.globals['__template__'] = template.name reporter.report_templating(template_file, output_file) rendered_content = template.render(**data) @@ -96,6 +98,8 @@ def _file_permissions_copy(self, template_file, output_file): utils.file_permissions_copy(true_template_file, output_file) def _apply_template(self, template, data, output): + template.globals['__target__'] = output + template.globals['__template__'] = template.name temp_file_path = get_template_path(self.template_dirs, template) rendered_content = template.render(**data) rendered_content = utils.strip_off_trailing_new_lines(rendered_content) diff --git a/tests/fixtures/globals/nested.template b/tests/fixtures/globals/nested.template new file mode 100644 index 00000000..f28bb6c4 --- /dev/null +++ b/tests/fixtures/globals/nested.template @@ -0,0 +1 @@ +{% include 'variables.template' %} \ No newline at end of file diff --git a/tests/fixtures/globals/variables.template b/tests/fixtures/globals/variables.template new file mode 100644 index 00000000..f2576e70 --- /dev/null +++ b/tests/fixtures/globals/variables.template @@ -0,0 +1,3 @@ +template: {{ __template__ }} +target: {{ __target__ }} +{{ test }} \ No newline at end of file diff --git a/tests/fixtures/globals/variables.yml b/tests/fixtures/globals/variables.yml new file mode 100644 index 00000000..a0067b10 --- /dev/null +++ b/tests/fixtures/globals/variables.yml @@ -0,0 +1 @@ +test: here \ No newline at end of file diff --git a/tests/test_engine.py b/tests/test_engine.py index 9d885945..f8a173e4 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -110,3 +110,25 @@ def test_globals(): content = output_file.read() eq_(content, "world\n\ntest") os.unlink(output) + + +def test_global_template_variables(): + output = "test.txt" + path = os.path.join("tests", "fixtures", "globals") + engine = Engine([path], path) + engine.render_to_file("variables.template", "variables.yml", output) + with open(output, "r") as output_file: + content = output_file.read() + eq_(content, "template: variables.template\ntarget: test.txt\nhere") + os.unlink(output) + + +def test_nested_global_template_variables(): + output = "test.txt" + path = os.path.join("tests", "fixtures", "globals") + engine = Engine([path], path) + engine.render_to_file("nested.template", "variables.yml", output) + with open(output, "r") as output_file: + content = output_file.read() + eq_(content, "template: nested.template\ntarget: test.txt\nhere") + os.unlink(output) From 16cce07296ad0e7d0bb8e4f36845af966d15811e Mon Sep 17 00:00:00 2001 From: Charlie Liu Date: Thu, 15 Nov 2018 12:32:21 -0500 Subject: [PATCH 2/8] =?UTF-8?q?=F0=9F=93=9A=20Update=20changelog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .moban.cd/changelog.yml | 6 ++++++ CHANGELOG.rst | 9 +++++++++ setup.py | 4 +--- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/.moban.cd/changelog.yml b/.moban.cd/changelog.yml index d899c1e7..392eed67 100644 --- a/.moban.cd/changelog.yml +++ b/.moban.cd/changelog.yml @@ -1,6 +1,12 @@ name: moban organisation: moremoban releases: +- changes: + - action: Added + details: + - "global variables to store the target and template file names in the jinja2 engine" + date: 14-11-2018 + version: 0.3.4 - changes: - action: Added details: diff --git a/CHANGELOG.rst b/CHANGELOG.rst index bfa01b0c..f189ae90 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,15 @@ Change log ================================================================================ +0.3.4 - 14-11-2018 +-------------------------------------------------------------------------------- + +Added +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +#. global variables to store the target and template file names in the jinja2 + engine + 0.3.3 - 05-11-2018 -------------------------------------------------------------------------------- diff --git a/setup.py b/setup.py index f79b17a5..3d20e792 100644 --- a/setup.py +++ b/setup.py @@ -5,9 +5,7 @@ import sys import codecs from shutil import rmtree - -from setuptools import Command, setup, find_packages - +from setuptools import setup, find_packages, Command PY2 = sys.version_info[0] == 2 PY26 = PY2 and sys.version_info[1] < 7 From bec2a49c47457671ffe0784e3d8019dfc0565809 Mon Sep 17 00:00:00 2001 From: jaska Date: Fri, 16 Nov 2018 16:19:32 +0000 Subject: [PATCH 3/8] code refactoring (#129) * :hammer: code refactoring * :hammer: format code * :hammer: pump up the version * :green_heart: fix the problem with the import. mysteriously, moban.extensions works but relocating it to moban.jinja2.extension wont do * :hammer: code refactoring * :sparkles: code refactoring * :hammer: code refactoring on jinja2 engine * :hammer: code refactoring * :hammer: load handle_bar by default for now * :green_heart: include pybars3 as temp requirements to pass unit tests * :bug: fix a typo * :hammer: allow base engine to have constructor that has more shared code * :tructor: rename engine_factory * :hammer: code refactor jinja2 engine * :fire: remove duplicated find template file * :fire: remove useless function * :fire: remove logging --- .moban.cd/moban.yml | 5 +- docs/conf.py | 29 +++- moban/_version.py | 2 +- moban/base_engine.py | 20 --- moban/engine_factory.py | 104 ------------ moban/engine_handlebars.py | 93 +++------- moban/extensions.py | 40 +---- moban/{filters => jinja2}/__init__.py | 0 moban/{ => jinja2}/engine.py | 72 +++----- moban/{tests => jinja2/filters}/__init__.py | 0 moban/{ => jinja2}/filters/github.py | 0 moban/{ => jinja2}/filters/repr.py | 0 moban/{ => jinja2}/filters/text.py | 0 moban/jinja2/tests/__init__.py | 0 moban/{ => jinja2}/tests/files.py | 0 moban/main.py | 6 +- moban/mobanfile.py | 2 +- moban/plugins.py | 159 ++++++++++++++++-- moban/strategy.py | 45 +++++ moban/utils.py | 19 ++- setup.py | 9 +- test.sh | 2 +- .../test_command_line_options.py | 129 +++++++------- tests/test_context.py | 2 +- tests/test_engine.py | 6 +- tests/test_filter_github.py | 2 +- tests/test_filter_repr.py | 2 +- tests/test_template.py | 18 +- tests/test_text_filter.py | 2 +- tests/test_utils.py | 5 +- 30 files changed, 375 insertions(+), 398 deletions(-) delete mode 100644 moban/engine_factory.py rename moban/{filters => jinja2}/__init__.py (100%) rename moban/{ => jinja2}/engine.py (61%) rename moban/{tests => jinja2/filters}/__init__.py (100%) rename moban/{ => jinja2}/filters/github.py (100%) rename moban/{ => jinja2}/filters/repr.py (100%) rename moban/{ => jinja2}/filters/text.py (100%) create mode 100644 moban/jinja2/tests/__init__.py rename moban/{ => jinja2}/tests/files.py (100%) create mode 100644 moban/strategy.py diff --git a/.moban.cd/moban.yml b/.moban.cd/moban.yml index a36be3eb..061a144a 100644 --- a/.moban.cd/moban.yml +++ b/.moban.cd/moban.yml @@ -3,8 +3,8 @@ organisation: moremoban author: C. W. contact: wangc_2011@hotmail.com license: MIT -version: 0.3.3 -current_version: 0.3.3 +version: 0.3.4 +current_version: 0.3.4 release: 0.3.3 branch: master command_line_interface: "moban" @@ -19,5 +19,6 @@ dependencies: - jinja2>=2.7.1 - lml==0.0.4 - crayons + - pybars3 description: Yet another jinja2 cli command for static text generation scm_host: github.com diff --git a/docs/conf.py b/docs/conf.py index 2a3d7184..b4ffa141 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -30,7 +30,7 @@ # The short X.Y version version = u'0.3.3' # The full version, including alpha/beta/rc tags -release = u'0.3.3' +release = u'0.3.4' # -- General configuration --------------------------------------------------- @@ -42,12 +42,7 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.doctest', - 'sphinx.ext.intersphinx', - 'sphinx.ext.viewcode', -] +extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode',] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -74,7 +69,7 @@ exclude_patterns = [] # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = None # -- Options for HTML output ------------------------------------------------- @@ -162,6 +157,24 @@ 'Miscellaneous'), ] + +# -- Options for Epub output ------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = project + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +# +# epub_identifier = '' + +# A unique identification for the text. +# +# epub_uid = '' + +# A list of files that should not be packed into the epub file. +epub_exclude_files = ['search.html'] + # -- Extension configuration ------------------------------------------------- # -- Options for intersphinx extension --------------------------------------- diff --git a/moban/_version.py b/moban/_version.py index 96dc8dbe..a55fecff 100644 --- a/moban/_version.py +++ b/moban/_version.py @@ -1,2 +1,2 @@ -__version__ = "0.3.3" +__version__ = "0.3.4" __author__ = "C. W." diff --git a/moban/base_engine.py b/moban/base_engine.py index 12b30ff2..e69de29b 100644 --- a/moban/base_engine.py +++ b/moban/base_engine.py @@ -1,20 +0,0 @@ -import moban.reporter as reporter - - -class BaseEngine(object): - def __init__(self): - self.templated_count = 0 - self.file_count = 0 - - def report(self): - if self.templated_count == 0: - reporter.report_no_action() - elif self.templated_count == self.file_count: - reporter.report_full_run(self.file_count) - else: - reporter.report_partial_run( - self.templated_count, self.file_count - ) - - def number_of_templated_files(self): - return self.templated_count diff --git a/moban/engine_factory.py b/moban/engine_factory.py deleted file mode 100644 index 4f072da7..00000000 --- a/moban/engine_factory.py +++ /dev/null @@ -1,104 +0,0 @@ -import os -from collections import defaultdict - -import moban.utils as utils -import moban.constants as constants -import moban.exceptions as exceptions -from lml.plugin import PluginManager - - -class EngineFactory(PluginManager): - def __init__(self): - super(EngineFactory, self).__init__( - constants.TEMPLATE_ENGINE_EXTENSION - ) - - def get_engine(self, template_type): - return self.load_me_now(template_type) - - def all_types(self): - return list(self.registry.keys()) - - def raise_exception(self, key): - raise exceptions.NoThirdPartyEngine(key) - - -class Context(object): - def __init__(self, context_dirs): - verify_the_existence_of_directories(context_dirs) - self.context_dirs = context_dirs - self.__cached_environ_variables = dict( - (key, os.environ[key]) for key in os.environ - ) - - def get_data(self, file_name): - file_extension = os.path.splitext(file_name)[1] - if file_extension == ".json": - data = utils.open_json(self.context_dirs, file_name) - elif file_extension in [".yml", ".yaml"]: - data = utils.open_yaml(self.context_dirs, file_name) - utils.merge(data, self.__cached_environ_variables) - else: - raise exceptions.IncorrectDataInput - return data - - -class Strategy(object): - DATA_FIRST = 1 - TEMPLATE_FIRST = 2 - - def __init__(self, array_of_param_tuple): - self.data_file_index = defaultdict(list) - self.template_file_index = defaultdict(list) - self.tuples = array_of_param_tuple - - def process(self): - for (template_file, data_file, output_file) in self.tuples: - _append_to_array_item_to_dictionary_key( - self.data_file_index, data_file, (template_file, output_file) - ) - _append_to_array_item_to_dictionary_key( - self.template_file_index, - template_file, - (data_file, output_file), - ) - - def what_to_do(self): - choice = Strategy.DATA_FIRST - if self.data_file_index == {}: - choice = Strategy.TEMPLATE_FIRST - elif self.template_file_index != {}: - data_files = len(self.data_file_index) - template_files = len(self.template_file_index) - if data_files > template_files: - choice = Strategy.TEMPLATE_FIRST - return choice - - -def _append_to_array_item_to_dictionary_key(adict, key, array_item): - if array_item in adict[key]: - raise exceptions.MobanfileGrammarException( - constants.MESSAGE_SYNTAX_ERROR % (array_item, key) - ) - else: - adict[key].append(array_item) - - -def verify_the_existence_of_directories(dirs): - if not isinstance(dirs, list): - dirs = [dirs] - - for directory in dirs: - if os.path.exists(directory): - continue - should_I_ignore = ( - constants.DEFAULT_CONFIGURATION_DIRNAME in directory - or constants.DEFAULT_TEMPLATE_DIRNAME in directory - ) - if should_I_ignore: - # ignore - pass - else: - raise exceptions.DirectoryNotFound( - constants.MESSAGE_DIR_NOT_EXIST % os.path.abspath(directory) - ) diff --git a/moban/engine_handlebars.py b/moban/engine_handlebars.py index f7c32fd8..e7486d47 100644 --- a/moban/engine_handlebars.py +++ b/moban/engine_handlebars.py @@ -1,78 +1,29 @@ -import os import sys +from lml.plugin import PluginInfo + import moban.utils as utils import moban.reporter as reporter import moban.constants as constants -import moban.exceptions as exceptions -from lml.plugin import PluginInfo -from moban.base_engine import BaseEngine -from moban.engine_factory import ( - Context, - verify_the_existence_of_directories, - Strategy, -) from moban import plugins from pybars import Compiler -@PluginInfo( - constants.TEMPLATE_ENGINE_EXTENSION, tags=["handlebars", "hbs"] -) -class EngineHandlebars(BaseEngine): - def __init__(self, template_dirs, context_dirs): - BaseEngine.__init__(self) - plugins.refresh_plugins() - template_dirs = list( - plugins.expand_template_directories(template_dirs)) - verify_the_existence_of_directories(template_dirs) - context_dirs = plugins.expand_template_directory(context_dirs) - self.context = Context(context_dirs) - self.template_dirs = template_dirs - - def find_template_file(self, template_file): - for directory in self.template_dirs: - if os.path.exists(os.path.join(directory, template_file)): - return os.path.abspath(os.path.join(directory, template_file)) - raise exceptions.FileNotFound(template_file) - - def render_to_files(self, array_of_param_tuple): - sta = Strategy(array_of_param_tuple) - sta.process() - choice = sta.what_to_do() - if choice == Strategy.DATA_FIRST: - self._render_with_finding_data_first(sta.data_file_index) - else: - self._render_with_finding_template_first(sta.template_file_index) - - def _file_permissions_copy(self, template_file, output_file): - true_template_file = template_file - for a_template_dir in self.template_dirs: - true_template_file = os.path.join(a_template_dir, template_file) - if os.path.exists(true_template_file): - break - utils.file_permissions_copy(true_template_file, output_file) +@PluginInfo(constants.TEMPLATE_ENGINE_EXTENSION, tags=["handlebars", "hbs"]) +class EngineHandlebars(plugins.BaseEngine): def render_to_file(self, template_file, data_file, output_file): - template_file = self.find_template_file(template_file) - with open(template_file, "r") as source: - if sys.version_info[0] < 3: - template = Compiler().compile(unicode(source.read())) # noqa - else: - template = Compiler().compile(source.read()) data = self.context.get_data(data_file) + template_file, template = self._get_hbr_template(template_file) + self._apply_template(template_file, template, data, output_file) reporter.report_templating(template_file, output_file) - rendered_content = ''.join(template(data)) - utils.write_file_out(output_file, rendered_content) - self._file_permissions_copy(template_file, output_file) - def _render_with_finding_template_first(self, template_file_index): for (template_file, data_output_pairs) in template_file_index.items(): - template_file = self.find_template_file(template_file) + template_file, template = self._get_hbr_template(template_file) for (data_file, output) in data_output_pairs: data = self.context.get_data(data_file) - self._apply_template(template_file, data, output) + self._apply_template(template_file, template, data, output) reporter.report_templating(template_file, output) self.templated_count += 1 self.file_count += 1 @@ -81,23 +32,31 @@ def _render_with_finding_data_first(self, data_file_index): for (data_file, template_output_pairs) in data_file_index.items(): data = self.context.get_data(data_file) for (template_file, output) in template_output_pairs: - template_file = self.find_template_file(template_file) - self._apply_template(template_file, data, output) + template_file, template = self._get_hbr_template(template_file) + self._apply_template(template_file, template, data, output) reporter.report_templating(template_file, output) self.templated_count += 1 self.file_count += 1 - def _apply_template(self, template, data, output): - template_file = self.find_template_file(template) - with open(template_file, "r") as source: - if sys.version_info[0] < 3: - template = Compiler().compile(unicode(source.read())) # noqa - else: - template = Compiler().compile(source.read()) - rendered_content = ''.join(template(data)) + def _apply_template(self, template_file, template, data, output): + rendered_content = "".join(template(data)) rendered_content = utils.strip_off_trailing_new_lines(rendered_content) rendered_content = rendered_content.encode("utf-8") utils.write_file_out( output, rendered_content, strip=False, encode=False ) utils.file_permissions_copy(template_file, output) + + def _get_hbr_template(self, template_file): + actual_template_file = self.find_template_file(template_file) + with open(actual_template_file, "r") as source: + if sys.version_info[0] < 3: + hbr_template = Compiler().compile( + unicode(source.read()) # noqa: F821 + ) + else: + hbr_template = Compiler().compile(source.read()) + return actual_template_file, hbr_template + + def find_template_file(self, template_file): + return utils.get_template_path(self.template_dirs, template_file) diff --git a/moban/extensions.py b/moban/extensions.py index 61e174c7..7b21dbfd 100644 --- a/moban/extensions.py +++ b/moban/extensions.py @@ -1,21 +1,6 @@ -from lml.plugin import PluginInfo, PluginManager +from lml.plugin import PluginInfo -import moban.constants as constants - - -class PluginMixin: - def get_all(self): - for name in self.registry.keys(): - # only the first matching one is returned - the_filter = self.load_me_now(name) - yield (name, the_filter) - - -class JinjaFilterManager(PluginManager, PluginMixin): - def __init__(self): - super(JinjaFilterManager, self).__init__( - constants.JINJA_FILTER_EXTENSION - ) +from moban import constants class JinjaFilter(PluginInfo): @@ -26,11 +11,6 @@ def tags(self): yield self.cls.__name__ -class JinjaTestManager(PluginManager, PluginMixin): - def __init__(self): - super(JinjaTestManager, self).__init__(constants.JINJA_TEST_EXTENSION) - - class JinjaTest(PluginInfo): def __init__(self, test_name=None): super(JinjaTest, self).__init__(constants.JINJA_TEST_EXTENSION) @@ -48,22 +28,6 @@ def jinja_tests(**keywords): JinjaTest(key)(value) -class JinjaGlobalsManager(PluginManager, PluginMixin): - def __init__(self): - super(JinjaGlobalsManager, self).__init__( - constants.JINJA_GLOBALS_EXTENSION - ) - - def jinja_global(identifier, dict_obj): plugin = PluginInfo(constants.JINJA_GLOBALS_EXTENSION, tags=[identifier]) plugin(dict_obj) - - -class LibraryManager(PluginManager): - def __init__(self): - super(LibraryManager, self).__init__(constants.LIBRARY_EXTENSION) - - def resource_path_of(self, library_name): - library = self.get_a_plugin(library_name) - return library.resources_path diff --git a/moban/filters/__init__.py b/moban/jinja2/__init__.py similarity index 100% rename from moban/filters/__init__.py rename to moban/jinja2/__init__.py diff --git a/moban/engine.py b/moban/jinja2/engine.py similarity index 61% rename from moban/engine.py rename to moban/jinja2/engine.py index 8ba66a91..5e1c1bdb 100644 --- a/moban/engine.py +++ b/moban/jinja2/engine.py @@ -6,29 +6,18 @@ import moban.utils as utils import moban.reporter as reporter import moban.constants as constants +from moban import plugins from moban.utils import get_template_path from moban.hashstore import HASH_STORE -from moban.base_engine import BaseEngine -from moban.engine_factory import ( - Context, - verify_the_existence_of_directories, - Strategy -) -from moban import plugins @PluginInfo( constants.TEMPLATE_ENGINE_EXTENSION, tags=["jinja2", "jinja", "jj2", "j2"] ) -class Engine(BaseEngine): +class Engine(plugins.BaseEngine): def __init__(self, template_dirs, context_dirs): - BaseEngine.__init__(self) - plugins.refresh_plugins() - template_dirs = list( - plugins.expand_template_directories(template_dirs)) - verify_the_existence_of_directories(template_dirs) - context_dirs = plugins.expand_template_directory(context_dirs) - template_loader = FileSystemLoader(template_dirs) + super(Engine, self).__init__(template_dirs, context_dirs) + template_loader = FileSystemLoader(self.template_dirs) self.jj2_environment = Environment( loader=template_loader, keep_trailing_newline=True, @@ -44,35 +33,20 @@ def __init__(self, template_dirs, context_dirs): for global_name, dict_obj in plugins.GLOBALS.get_all(): self.jj2_environment.globals[global_name] = dict_obj - self.context = Context(context_dirs) - self.template_dirs = template_dirs - def render_to_file(self, template_file, data_file, output_file): - template = self.jj2_environment.get_template(template_file) data = self.context.get_data(data_file) - template.globals['__target__'] = output_file - template.globals['__template__'] = template.name + template_file, template = self._get_jinja2_template(template_file) + self._apply_template(template_file, template, data, output_file) reporter.report_templating(template_file, output_file) - rendered_content = template.render(**data) - utils.write_file_out(output_file, rendered_content) - self._file_permissions_copy(template_file, output_file) - - def render_to_files(self, array_of_param_tuple): - sta = Strategy(array_of_param_tuple) - sta.process() - choice = sta.what_to_do() - if choice == Strategy.DATA_FIRST: - self._render_with_finding_data_first(sta.data_file_index) - else: - self._render_with_finding_template_first(sta.template_file_index) - def _render_with_finding_template_first(self, template_file_index): for (template_file, data_output_pairs) in template_file_index.items(): - template = self.jj2_environment.get_template(template_file) + template_file, template = self._get_jinja2_template(template_file) for (data_file, output) in data_output_pairs: data = self.context.get_data(data_file) - flag = self._apply_template(template, data, output) + flag = self._apply_template( + template_file, template, data, output + ) if flag: reporter.report_templating(template_file, output) self.templated_count += 1 @@ -82,8 +56,12 @@ def _render_with_finding_data_first(self, data_file_index): for (data_file, template_output_pairs) in data_file_index.items(): data = self.context.get_data(data_file) for (template_file, output) in template_output_pairs: - template = self.jj2_environment.get_template(template_file) - flag = self._apply_template(template, data, output) + template_file, template = self._get_jinja2_template( + template_file + ) + flag = self._apply_template( + template_file, template, data, output + ) if flag: reporter.report_templating(template_file, output) self.templated_count += 1 @@ -97,19 +75,25 @@ def _file_permissions_copy(self, template_file, output_file): break utils.file_permissions_copy(true_template_file, output_file) - def _apply_template(self, template, data, output): - template.globals['__target__'] = output - template.globals['__template__'] = template.name - temp_file_path = get_template_path(self.template_dirs, template) + def _apply_template(self, template_file, template, data, output): + template.globals["__target__"] = output + template.globals["__template__"] = template.name rendered_content = template.render(**data) rendered_content = utils.strip_off_trailing_new_lines(rendered_content) rendered_content = rendered_content.encode("utf-8") flag = HASH_STORE.is_file_changed( - output, rendered_content, temp_file_path + output, rendered_content, template_file ) if flag: utils.write_file_out( output, rendered_content, strip=False, encode=False ) - utils.file_permissions_copy(temp_file_path, output) + utils.file_permissions_copy(template_file, output) return flag + + def _get_jinja2_template(self, template_file): + actual_template_file = get_template_path( + self.template_dirs, template_file + ) + template = self.jj2_environment.get_template(template_file) + return actual_template_file, template diff --git a/moban/tests/__init__.py b/moban/jinja2/filters/__init__.py similarity index 100% rename from moban/tests/__init__.py rename to moban/jinja2/filters/__init__.py diff --git a/moban/filters/github.py b/moban/jinja2/filters/github.py similarity index 100% rename from moban/filters/github.py rename to moban/jinja2/filters/github.py diff --git a/moban/filters/repr.py b/moban/jinja2/filters/repr.py similarity index 100% rename from moban/filters/repr.py rename to moban/jinja2/filters/repr.py diff --git a/moban/filters/text.py b/moban/jinja2/filters/text.py similarity index 100% rename from moban/filters/text.py rename to moban/jinja2/filters/text.py diff --git a/moban/jinja2/tests/__init__.py b/moban/jinja2/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/moban/tests/files.py b/moban/jinja2/tests/files.py similarity index 100% rename from moban/tests/files.py rename to moban/jinja2/tests/files.py diff --git a/moban/main.py b/moban/main.py index 6d5b6dd6..8a6d9dda 100644 --- a/moban/main.py +++ b/moban/main.py @@ -8,7 +8,6 @@ :license: MIT License, see LICENSE for more details """ - import sys import argparse @@ -16,9 +15,9 @@ import moban.constants as constants import moban.mobanfile as mobanfile import moban.exceptions as exceptions +from moban import plugins from moban.utils import merge, open_yaml from moban.hashstore import HASH_STORE -from moban import plugins def main(): @@ -134,7 +133,8 @@ def handle_command_line(options): if options[constants.LABEL_TEMPLATE] is None: raise exceptions.NoTemplate(constants.ERROR_NO_TEMPLATE) engine_class = plugins.ENGINES.get_engine( - options[constants.LABEL_TEMPLATE_TYPE]) + options[constants.LABEL_TEMPLATE_TYPE] + ) engine = engine_class( options[constants.LABEL_TMPL_DIRS], options[constants.LABEL_CONFIG_DIR] ) diff --git a/moban/mobanfile.py b/moban/mobanfile.py index b52c040b..b6919007 100644 --- a/moban/mobanfile.py +++ b/moban/mobanfile.py @@ -7,6 +7,7 @@ import moban.reporter as reporter import moban.constants as constants +from moban import plugins from moban.utils import ( merge, git_clone, @@ -15,7 +16,6 @@ expand_directories, ) from moban.copier import Copier -from moban import plugins try: from urllib.parse import urlparse diff --git a/moban/plugins.py b/moban/plugins.py index 9de12bd4..b5a18458 100644 --- a/moban/plugins.py +++ b/moban/plugins.py @@ -1,32 +1,81 @@ import os -from moban import utils + from lml.loader import scan_plugins_regex +from lml.plugin import PluginManager + +import moban.reporter as reporter +from moban import utils, constants, exceptions +from moban.strategy import Strategy + + +class PluginMixin: + def get_all(self): + for name in self.registry.keys(): + # only the first matching one is returned + the_filter = self.load_me_now(name) + yield (name, the_filter) + + +class JinjaFilterManager(PluginManager, PluginMixin): + def __init__(self): + super(JinjaFilterManager, self).__init__( + constants.JINJA_FILTER_EXTENSION + ) + + +class JinjaTestManager(PluginManager, PluginMixin): + def __init__(self): + super(JinjaTestManager, self).__init__(constants.JINJA_TEST_EXTENSION) + + +class JinjaGlobalsManager(PluginManager, PluginMixin): + def __init__(self): + super(JinjaGlobalsManager, self).__init__( + constants.JINJA_GLOBALS_EXTENSION + ) -from moban.extensions import ( - JinjaTestManager, - JinjaFilterManager, - JinjaGlobalsManager -) -from moban.extensions import LibraryManager -from moban.engine_factory import EngineFactory -from moban.constants import MOBAN_ALL -LIBRARIES = LibraryManager() FILTERS = JinjaFilterManager() TESTS = JinjaTestManager() GLOBALS = JinjaGlobalsManager() -ENGINES = EngineFactory() BUILTIN_EXENSIONS = [ - "moban.filters.repr", - "moban.filters.github", - "moban.filters.text", - "moban.tests.files", + "moban.jinja2.filters.repr", + "moban.jinja2.filters.github", + "moban.jinja2.filters.text", + "moban.jinja2.tests.files", + "moban.jinja2.engine", + "moban.engine_handlebars", ] -def refresh_plugins(): - scan_plugins_regex(MOBAN_ALL, "moban", None, BUILTIN_EXENSIONS) +class LibraryManager(PluginManager): + def __init__(self): + super(LibraryManager, self).__init__(constants.LIBRARY_EXTENSION) + + def resource_path_of(self, library_name): + library = self.get_a_plugin(library_name) + return library.resources_path + + +class EngineFactory(PluginManager): + def __init__(self): + super(EngineFactory, self).__init__( + constants.TEMPLATE_ENGINE_EXTENSION + ) + + def get_engine(self, template_type): + return self.load_me_now(template_type) + + def all_types(self): + return list(self.registry.keys()) + + def raise_exception(self, key): + raise exceptions.NoThirdPartyEngine(key) + + +LIBRARIES = LibraryManager() +ENGINES = EngineFactory() def expand_template_directories(dirs): @@ -65,3 +114,79 @@ def expand_template_directory(directory): # local template path translated_directory = os.path.abspath(directory) return translated_directory + + +class Context(object): + def __init__(self, context_dirs): + verify_the_existence_of_directories(context_dirs) + self.context_dirs = context_dirs + self.__cached_environ_variables = dict( + (key, os.environ[key]) for key in os.environ + ) + + def get_data(self, file_name): + file_extension = os.path.splitext(file_name)[1] + if file_extension == ".json": + data = utils.open_json(self.context_dirs, file_name) + elif file_extension in [".yml", ".yaml"]: + data = utils.open_yaml(self.context_dirs, file_name) + utils.merge(data, self.__cached_environ_variables) + else: + raise exceptions.IncorrectDataInput + return data + + +class BaseEngine(object): + def __init__(self, template_dirs, context_dirs): + refresh_plugins() + template_dirs = list(expand_template_directories(template_dirs)) + verify_the_existence_of_directories(template_dirs) + context_dirs = expand_template_directory(context_dirs) + self.context = Context(context_dirs) + self.template_dirs = template_dirs + self.templated_count = 0 + self.file_count = 0 + + def report(self): + if self.templated_count == 0: + reporter.report_no_action() + elif self.templated_count == self.file_count: + reporter.report_full_run(self.file_count) + else: + reporter.report_partial_run(self.templated_count, self.file_count) + + def number_of_templated_files(self): + return self.templated_count + + def render_to_files(self, array_of_param_tuple): + sta = Strategy(array_of_param_tuple) + sta.process() + choice = sta.what_to_do() + if choice == Strategy.DATA_FIRST: + self._render_with_finding_data_first(sta.data_file_index) + else: + self._render_with_finding_template_first(sta.template_file_index) + + +def refresh_plugins(): + scan_plugins_regex(constants.MOBAN_ALL, "moban", None, BUILTIN_EXENSIONS) + + +def verify_the_existence_of_directories(dirs): + if not isinstance(dirs, list): + dirs = [dirs] + + for directory in dirs: + if os.path.exists(directory): + continue + should_I_ignore = ( + constants.DEFAULT_CONFIGURATION_DIRNAME in directory + or constants.DEFAULT_TEMPLATE_DIRNAME in directory + ) + if should_I_ignore: + # ignore + pass + else: + raise exceptions.DirectoryNotFound( + constants.MESSAGE_DIR_NOT_EXIST % os.path.abspath(directory) + ) diff --git a/moban/strategy.py b/moban/strategy.py new file mode 100644 index 00000000..3e9db349 --- /dev/null +++ b/moban/strategy.py @@ -0,0 +1,45 @@ +from collections import defaultdict + +import moban.constants as constants +import moban.exceptions as exceptions + + +class Strategy(object): + DATA_FIRST = 1 + TEMPLATE_FIRST = 2 + + def __init__(self, array_of_param_tuple): + self.data_file_index = defaultdict(list) + self.template_file_index = defaultdict(list) + self.tuples = array_of_param_tuple + + def process(self): + for (template_file, data_file, output_file) in self.tuples: + _append_to_array_item_to_dictionary_key( + self.data_file_index, data_file, (template_file, output_file) + ) + _append_to_array_item_to_dictionary_key( + self.template_file_index, + template_file, + (data_file, output_file), + ) + + def what_to_do(self): + choice = Strategy.DATA_FIRST + if self.data_file_index == {}: + choice = Strategy.TEMPLATE_FIRST + elif self.template_file_index != {}: + data_files = len(self.data_file_index) + template_files = len(self.template_file_index) + if data_files > template_files: + choice = Strategy.TEMPLATE_FIRST + return choice + + +def _append_to_array_item_to_dictionary_key(adict, key, array_item): + if array_item in adict[key]: + raise exceptions.MobanfileGrammarException( + constants.MESSAGE_SYNTAX_ERROR % (array_item, key) + ) + else: + adict[key].append(array_item) diff --git a/moban/utils.py b/moban/utils.py index 5d404d53..09f0c5e9 100644 --- a/moban/utils.py +++ b/moban/utils.py @@ -1,11 +1,11 @@ import os import re import sys +import json import stat import errno import yaml -import json import moban.reporter as reporter import moban.constants as constants import moban.exceptions as exceptions @@ -116,6 +116,7 @@ def expand_directories(file_list, template_dirs): def file_permissions_copy(source, dest): source_permissions = file_permissions(source) dest_permissions = file_permissions(dest) + if source_permissions != dest_permissions: os.chmod(dest, source_permissions) @@ -189,19 +190,19 @@ def git_clone(repos, submodule=False): def get_template_path(template_dirs, template): temp_dir = "" + for a_dir in template_dirs: template_file_exists = os.path.exists( - os.path.join(a_dir, template.filename) - ) and os.path.isfile(os.path.join(a_dir, template.filename)) + os.path.join(a_dir, template) + ) and os.path.isfile(os.path.join(a_dir, template)) if template_file_exists: temp_dir = a_dir - break - - temp_file_path = os.path.join( - os.getcwd(), os.path.join(temp_dir, template.filename) - ) - return temp_file_path + temp_file_path = os.path.join( + os.getcwd(), os.path.join(temp_dir, template) + ) + return temp_file_path + raise exceptions.FileNotFound def get_repo_name(repo_url): diff --git a/setup.py b/setup.py index 3d20e792..fb593dff 100644 --- a/setup.py +++ b/setup.py @@ -5,13 +5,15 @@ import sys import codecs from shutil import rmtree -from setuptools import setup, find_packages, Command + +from setuptools import Command, setup, find_packages + PY2 = sys.version_info[0] == 2 PY26 = PY2 and sys.version_info[1] < 7 NAME = 'moban' AUTHOR = 'C. W.' -VERSION = '0.3.3' +VERSION = '0.3.4' EMAIL = 'wangc_2011@hotmail.com' LICENSE = 'MIT' ENTRY_POINTS = { @@ -26,9 +28,9 @@ DOWNLOAD_URL = '%s/archive/0.3.3.tar.gz' % URL FILES = ['README.rst', 'CONTRIBUTORS.rst', 'CHANGELOG.rst'] KEYWORDS = [ + 'python', 'jinja2', 'moban', - 'python' ] CLASSIFIERS = [ @@ -48,6 +50,7 @@ 'jinja2>=2.7.1', 'lml==0.0.4', 'crayons', + 'pybars3', ] SETUP_COMMANDS = {} diff --git a/test.sh b/test.sh index cb4d8b7f..3fb012dc 100644 --- a/test.sh +++ b/test.sh @@ -3,4 +3,4 @@ pip freeze cd tests/moban-mako python setup.py install cd ../../ -nosetests --with-cov --with-doctest --doctest-extension=.rst --cover-package moban --cover-package tests && flake8 . --exclude=.moban.d --ignore=E203,E121,E123,E126,E226,E24,E704,W503,W504 +nosetests --with-cov --with-doctest --doctest-extension=.rst --cover-package moban --cover-package tests && flake8 . --exclude=.moban.d,docs --ignore=E203,E121,E123,E126,E226,E24,E704,W503,W504 diff --git a/tests/integration_tests/test_command_line_options.py b/tests/integration_tests/test_command_line_options.py index 0bab4889..fd8184f2 100644 --- a/tests/integration_tests/test_command_line_options.py +++ b/tests/integration_tests/test_command_line_options.py @@ -3,7 +3,6 @@ from shutil import copyfile from mock import patch -from moban.main import main from nose.tools import eq_, raises, assert_raises @@ -13,15 +12,11 @@ def setUp(self): with open(self.config_file, "w") as f: f.write("hello: world") self.patcher1 = patch( - "moban.engine.verify_the_existence_of_directories" - ) - self.patcher2 = patch( - "moban.engine_factory.verify_the_existence_of_directories" + "moban.plugins.verify_the_existence_of_directories" ) self.patcher1.start() - self.patcher2.start() - @patch("moban.engine.Engine.render_to_file") + @patch("moban.jinja2.engine.Engine.render_to_file") def test_custom_options(self, fake_template_doer): test_args = [ "moban", @@ -35,15 +30,19 @@ def test_custom_options(self, fake_template_doer): "a.jj2", ] with patch.object(sys, "argv", test_args): + from moban.main import main + main() fake_template_doer.assert_called_with( "a.jj2", "config.yaml", "moban.output" ) - @patch("moban.engine.Engine.render_to_file") + @patch("moban.jinja2.engine.Engine.render_to_file") def test_minimal_options(self, fake_template_doer): test_args = ["moban", "-c", self.config_file, "-t", "a.jj2"] with patch.object(sys, "argv", test_args): + from moban.main import main + main() fake_template_doer.assert_called_with( "a.jj2", "config.yaml", "moban.output" @@ -53,11 +52,12 @@ def test_minimal_options(self, fake_template_doer): def test_missing_template(self): test_args = ["moban", "-c", self.config_file] with patch.object(sys, "argv", test_args): + from moban.main import main + main() def tearDown(self): self.patcher1.stop() - self.patcher2.stop() os.unlink(self.config_file) @@ -67,18 +67,16 @@ def setUp(self): with open(self.config_file, "w") as f: f.write("hello: world") self.patcher1 = patch( - "moban.engine.verify_the_existence_of_directories" - ) - self.patcher2 = patch( - "moban.engine_factory.verify_the_existence_of_directories" + "moban.plugins.verify_the_existence_of_directories" ) self.patcher1.start() - self.patcher2.start() - @patch("moban.engine.Engine.render_to_file") + @patch("moban.jinja2.engine.Engine.render_to_file") def test_default_options(self, fake_template_doer): test_args = ["moban", "-t", "a.jj2"] with patch.object(sys, "argv", test_args): + from moban.main import main + main() fake_template_doer.assert_called_with( "a.jj2", "data.yml", "moban.output" @@ -88,11 +86,12 @@ def test_default_options(self, fake_template_doer): def test_no_argments(self): test_args = ["moban"] with patch.object(sys, "argv", test_args): + from moban.main import main + main() def tearDown(self): self.patcher1.stop() - self.patcher2.stop() os.unlink(self.config_file) @@ -100,6 +99,8 @@ def tearDown(self): def test_missing_configuration(): test_args = ["moban", "-t", "a.jj2"] with patch.object(sys, "argv", test_args): + from moban.main import main + main() @@ -114,18 +115,16 @@ def setUp(self): with open(self.data_file, "w") as f: f.write("hello: world") self.patcher1 = patch( - "moban.engine.verify_the_existence_of_directories" - ) - self.patcher2 = patch( - "moban.engine_factory.verify_the_existence_of_directories" + "moban.plugins.verify_the_existence_of_directories" ) self.patcher1.start() - self.patcher2.start() - @patch("moban.engine.Engine.render_to_files") + @patch("moban.jinja2.engine.Engine.render_to_files") def test_single_command(self, fake_template_doer): test_args = ["moban"] with patch.object(sys, "argv", test_args): + from moban.main import main + main() call_args = list(fake_template_doer.call_args[0][0]) eq_( @@ -136,10 +135,12 @@ def test_single_command(self, fake_template_doer): ], ) - @patch("moban.engine.Engine.render_to_files") + @patch("moban.jinja2.engine.Engine.render_to_files") def test_single_command_with_a_few_options(self, fake_template_doer): test_args = ["moban", "-t", "abc.jj2", "-o", "xyz.output"] with patch.object(sys, "argv", test_args): + from moban.main import main + main() call_args = list(fake_template_doer.call_args[0][0]) eq_( @@ -151,7 +152,7 @@ def test_single_command_with_a_few_options(self, fake_template_doer): ], ) - @patch("moban.engine.Engine.render_to_files") + @patch("moban.jinja2.engine.Engine.render_to_files") def test_single_command_with_options(self, fake_template_doer): test_args = [ "moban", @@ -163,6 +164,8 @@ def test_single_command_with_options(self, fake_template_doer): "xyz.output", ] with patch.object(sys, "argv", test_args): + from moban.main import main + main() call_args = list(fake_template_doer.call_args[0][0]) eq_( @@ -175,17 +178,18 @@ def test_single_command_with_options(self, fake_template_doer): ) @raises(Exception) - @patch("moban.engine.Engine.render_to_files") + @patch("moban.jinja2.engine.Engine.render_to_files") def test_single_command_without_output_option(self, fake_template_doer): test_args = ["moban", "-t", "abc.jj2"] with patch.object(sys, "argv", test_args): + from moban.main import main + main() def tearDown(self): os.unlink(self.config_file) os.unlink(self.data_file) self.patcher1.stop() - self.patcher2.stop() class TestNoOptions2: @@ -199,18 +203,16 @@ def setUp(self): with open(self.data_file, "w") as f: f.write("hello: world") self.patcher1 = patch( - "moban.engine.verify_the_existence_of_directories" - ) - self.patcher2 = patch( - "moban.engine_factory.verify_the_existence_of_directories" + "moban.plugins.verify_the_existence_of_directories" ) self.patcher1.start() - self.patcher2.start() - @patch("moban.engine.Engine.render_to_files") + @patch("moban.jinja2.engine.Engine.render_to_files") def test_single_command(self, fake_template_doer): test_args = ["moban"] with patch.object(sys, "argv", test_args): + from moban.main import main + main() call_args = list(fake_template_doer.call_args[0][0]) eq_( @@ -223,7 +225,6 @@ def test_single_command(self, fake_template_doer): def tearDown(self): self.patcher1.stop() - self.patcher2.stop() os.unlink(self.config_file) os.unlink(self.data_file) @@ -238,18 +239,16 @@ def setUp(self): with open(self.data_file, "w") as f: f.write("hello: world") self.patcher1 = patch( - "moban.engine.verify_the_existence_of_directories" - ) - self.patcher2 = patch( - "moban.engine_factory.verify_the_existence_of_directories" + "moban.plugins.verify_the_existence_of_directories" ) self.patcher1.start() - self.patcher2.start() - @patch("moban.engine.Engine.render_to_files") + @patch("moban.jinja2.engine.Engine.render_to_files") def test_single_command(self, fake_template_doer): test_args = ["moban", "-m", self.config_file] with patch.object(sys, "argv", test_args): + from moban.main import main + main() call_args = list(fake_template_doer.call_args[0][0]) eq_( @@ -262,17 +261,18 @@ def test_single_command(self, fake_template_doer): def tearDown(self): self.patcher1.stop() - self.patcher2.stop() os.unlink(self.config_file) os.unlink(self.data_file) -@patch("moban.engine.verify_the_existence_of_directories") +@patch("moban.plugins.verify_the_existence_of_directories") def test_duplicated_targets_in_moban_file(fake_verify): config_file = "duplicated.moban.yml" copyfile(os.path.join("tests", "fixtures", config_file), ".moban.yml") test_args = ["moban"] with patch.object(sys, "argv", test_args): + from moban.main import main + assert_raises(SystemExit, main) os.unlink(".moban.yml") @@ -282,30 +282,36 @@ def setUp(self): self.config_file = ".moban.yml" @raises(SystemExit) - @patch("moban.engine.Engine.render_to_files") + @patch("moban.jinja2.engine.Engine.render_to_files") def test_no_configuration(self, fake_template_doer): with open(self.config_file, "w") as f: f.write("") test_args = ["moban"] with patch.object(sys, "argv", test_args): + from moban.main import main + main() @raises(SystemExit) - @patch("moban.engine.Engine.render_to_files") + @patch("moban.jinja2.engine.Engine.render_to_files") def test_no_configuration_2(self, fake_template_doer): with open(self.config_file, "w") as f: f.write("not: related") test_args = ["moban"] with patch.object(sys, "argv", test_args): + from moban.main import main + main() @raises(SystemExit) - @patch("moban.engine.Engine.render_to_files") + @patch("moban.jinja2.engine.Engine.render_to_files") def test_no_targets(self, fake_template_doer): with open(self.config_file, "w") as f: f.write("configuration: test") test_args = ["moban"] with patch.object(sys, "argv", test_args): + from moban.main import main + main() def tearDown(self): @@ -322,33 +328,30 @@ def setUp(self): with open(self.data_file, "w") as f: f.write("hello: world") self.patcher1 = patch( - "moban.engine.verify_the_existence_of_directories" - ) - self.patcher2 = patch( - "moban.engine_factory.verify_the_existence_of_directories" + "moban.plugins.verify_the_existence_of_directories" ) self.patcher1.start() - self.patcher2.start() - @patch("moban.engine.Engine.render_to_files") - def test_single_command(self, fake_template_doer): + def test_single_command(self): test_args = ["moban"] with patch.object(sys, "argv", test_args): - main() - call_args = list(fake_template_doer.call_args[0][0]) - eq_( - call_args, - [ - ("README.rst.jj2", "custom-data.yaml", "README.rst"), - ("setup.py.jj2", "data.yml", "setup.py"), - ], - ) + from moban.main import main + + with patch("moban.jinja2.engine.Engine.render_to_files") as fake: + main() + call_args = list(fake.call_args[0][0]) + eq_( + call_args, + [ + ("README.rst.jj2", "custom-data.yaml", "README.rst"), + ("setup.py.jj2", "data.yml", "setup.py"), + ], + ) def tearDown(self): os.unlink(self.config_file) os.unlink(self.data_file) self.patcher1.stop() - self.patcher2.stop() class TestTemplateTypeOption: @@ -357,10 +360,12 @@ def setUp(self): with open(self.config_file, "w") as f: f.write("hello: world") - @patch("moban.engine.Engine.render_to_file") + @patch("moban.jinja2.engine.Engine.render_to_file") def test_mako_option(self, fake_template_doer): test_args = ["moban", "-t", "a.mako"] with patch.object(sys, "argv", test_args): + from moban.main import main + main() fake_template_doer.assert_called_with( "a.mako", "data.yml", "moban.output" diff --git a/tests/test_context.py b/tests/test_context.py index afe9419a..7c06b048 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -1,7 +1,7 @@ import os from nose.tools import eq_ -from moban.engine import Context +from moban.plugins import Context def test_context(): diff --git a/tests/test_engine.py b/tests/test_engine.py index f8a173e4..b08042b0 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -5,11 +5,9 @@ import moban.exceptions as exceptions from mock import patch from nose.tools import eq_, raises -from moban.engine import Engine +from moban.plugins import ENGINES, Context, expand_template_directories from moban.extensions import jinja_global -from moban.engine_factory import Context -from moban.plugins import expand_template_directories -from moban.plugins import ENGINES +from moban.jinja2.engine import Engine from moban.engine_handlebars import EngineHandlebars diff --git a/tests/test_filter_github.py b/tests/test_filter_github.py index 5cbc8e1e..2452328f 100644 --- a/tests/test_filter_github.py +++ b/tests/test_filter_github.py @@ -1,5 +1,5 @@ from nose.tools import eq_ -from moban.filters.github import github_expand +from moban.jinja2.filters.github import github_expand def test_github_expand(): diff --git a/tests/test_filter_repr.py b/tests/test_filter_repr.py index 62116dba..c18d458b 100644 --- a/tests/test_filter_repr.py +++ b/tests/test_filter_repr.py @@ -1,5 +1,5 @@ from nose.tools import eq_ -from moban.filters.repr import repr as repr_function +from moban.jinja2.filters.repr import repr as repr_function def test_string(): diff --git a/tests/test_template.py b/tests/test_template.py index d4c0389c..7f55cdb0 100644 --- a/tests/test_template.py +++ b/tests/test_template.py @@ -1,11 +1,11 @@ import os from mock import patch -from moban.engine import Engine +from moban.jinja2.engine import Engine from moban.engine_handlebars import EngineHandlebars -@patch("moban.engine.Engine._render_with_finding_data_first") +@patch("moban.jinja2.engine.Engine._render_with_finding_data_first") def test_do_templates_1(_do_templates_with_more_shared_data): jobs = [ ("1.template", "data.yml", "1.output"), @@ -30,7 +30,7 @@ def test_do_templates_1(_do_templates_with_more_shared_data): os.unlink(".moban.hashes") -@patch("moban.engine.Engine._render_with_finding_template_first") +@patch("moban.jinja2.engine.Engine._render_with_finding_template_first") def test_do_templates_2(_do_templates_with_more_shared_templates): jobs = [ ("1.template", "data1.yml", "1.output"), @@ -83,8 +83,10 @@ def test_do_templates_with_more_shared_data(): os.unlink(".moban.hashes") -@patch("moban.engine_handlebars.EngineHandlebars." - "_render_with_finding_data_first") +@patch( + "moban.engine_handlebars.EngineHandlebars." + "_render_with_finding_data_first" +) def test_do_templates_1_handlebars(_do_templates_with_more_shared_data): jobs = [ ("1.template", "data.yml", "1.output"), @@ -109,8 +111,10 @@ def test_do_templates_1_handlebars(_do_templates_with_more_shared_data): os.unlink(".moban.hashes") -@patch("moban.engine_handlebars.EngineHandlebars." - "_render_with_finding_template_first") +@patch( + "moban.engine_handlebars.EngineHandlebars." + "_render_with_finding_template_first" +) def test_do_templates_2_handlebars(_do_templates_with_more_shared_templates): jobs = [ ("1.template", "data1.yml", "1.output"), diff --git a/tests/test_text_filter.py b/tests/test_text_filter.py index e49cd646..177c0c5c 100644 --- a/tests/test_text_filter.py +++ b/tests/test_text_filter.py @@ -1,5 +1,5 @@ from nose.tools import eq_ -from moban.filters.text import split_length +from moban.jinja2.filters.text import split_length def test_split_length(): diff --git a/tests/test_utils.py b/tests/test_utils.py index 30eb212f..631a3b7b 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -2,7 +2,7 @@ import stat from shutil import rmtree -from mock import Mock, patch +from mock import patch from nose.tools import eq_, raises from moban.utils import ( mkdir_p, @@ -98,8 +98,7 @@ def test_expand_dir(): def test_get_template_path(): temp_dirs = ["tests/fixtures/template-tests", "tests/abc", "tests/abc"] - template = Mock() - template.filename = "a.jj2" + template = "a.jj2" template_path = get_template_path(temp_dirs, template) expected = os.path.join(os.getcwd(), "tests/fixtures/template-tests/a.jj2") eq_(template_path, expected) From 93d7fedbe015caa10b9d84ccf5e85229ae009c5f Mon Sep 17 00:00:00 2001 From: jaska Date: Sat, 17 Nov 2018 15:39:48 +0000 Subject: [PATCH 4/8] Simpler interface for template engine: jinja2, handle bars and for many to come (#131) * :hammer: clarify engine interface * :hammer: minor tuning * :fire: strip the extra responsibility from engine: 1) is there a change 2) where is the template on file system. * :lipstick: format code * :fire: remove if py2 do something else do others for open. because codecs is python 2 and 3 compactible * :sparles: test the handle bar engine separately. it will be easy for up-coming separation * :fire: remove useless proxy function * :lipstick: code formatting * :micrscope: test newly create class interfaces * :fire: label output is not used for handlebar engine * :books: document engine interface * :green_heart: make flake8 happy on the coding style --- docs/conf.py | 27 +--- docs/extension.rst | 24 +++- moban/engine_handlebars.py | 61 ++------ moban/jinja2/engine.py | 101 +++++--------- moban/main.py | 9 +- moban/mobanfile.py | 4 +- moban/plugins.py | 130 +++++++++++++----- setup.py | 2 +- .../test_command_line_options.py | 28 ++-- tests/moban-mako/moban_mako/__init__.py | 3 +- tests/test_engine.py | 42 +++--- tests/test_handlebar_engine.py | 14 ++ tests/test_jinja2_engine.py | 14 ++ tests/test_template.py | 98 +------------ 14 files changed, 254 insertions(+), 303 deletions(-) create mode 100644 tests/test_handlebar_engine.py create mode 100644 tests/test_jinja2_engine.py diff --git a/docs/conf.py b/docs/conf.py index b4ffa141..d4a30bcb 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -42,7 +42,12 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode',] +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.doctest', + 'sphinx.ext.intersphinx', + 'sphinx.ext.viewcode', +] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -69,7 +74,7 @@ exclude_patterns = [] # The name of the Pygments (syntax highlighting) style to use. -pygments_style = None +pygments_style = 'sphinx' # -- Options for HTML output ------------------------------------------------- @@ -157,24 +162,6 @@ 'Miscellaneous'), ] - -# -- Options for Epub output ------------------------------------------------- - -# Bibliographic Dublin Core info. -epub_title = project - -# The unique identifier of the text. This can be a ISBN number -# or the project homepage. -# -# epub_identifier = '' - -# A unique identification for the text. -# -# epub_uid = '' - -# A list of files that should not be packed into the epub file. -epub_exclude_files = ['search.html'] - # -- Extension configuration ------------------------------------------------- # -- Options for intersphinx extension --------------------------------------- diff --git a/docs/extension.rst b/docs/extension.rst index 6f1f3449..aa4d6a98 100644 --- a/docs/extension.rst +++ b/docs/extension.rst @@ -42,7 +42,29 @@ template engines, such as marko, haml can be plugged into moban seamless. In order plugin other template engines, it is to write a lml plugin. The following is an example starting point for any template engine. -.. literalinclude:: ../tests/moban-mako/moban_mako/__init__.py +.. code:: + + @PluginInfo( + constants.TEMPLATE_ENGINE_EXTENSION, tags=["file", "extensions", "for", "your", "template"] + ) + class Engine(object): + def __init__(self, template_dirs): + """ + A list template directories will be given to your engine class + """ + + def get_template(self, template_file): + """ + Given a relative path to your template file, please return a templatable thing that does + the templating function in next function below + """ + + def apply_template(self, template, data, output): + """ + Given the template object from `get_template` function, and data as python dictionary, + and output as intended output file, please return "utf-8" encoded string. + """ + After you will have finished the engine plugin, you can either place it in `plugin_dir` in order to get it loaded, or make an installable python package. In the latter case, diff --git a/moban/engine_handlebars.py b/moban/engine_handlebars.py index e7486d47..26b3777e 100644 --- a/moban/engine_handlebars.py +++ b/moban/engine_handlebars.py @@ -1,62 +1,27 @@ -import sys +import codecs from lml.plugin import PluginInfo import moban.utils as utils -import moban.reporter as reporter import moban.constants as constants -from moban import plugins from pybars import Compiler @PluginInfo(constants.TEMPLATE_ENGINE_EXTENSION, tags=["handlebars", "hbs"]) -class EngineHandlebars(plugins.BaseEngine): +class EngineHandlebars(object): + def __init__(self, template_dirs): + self.template_dirs = template_dirs - def render_to_file(self, template_file, data_file, output_file): - data = self.context.get_data(data_file) - template_file, template = self._get_hbr_template(template_file) - self._apply_template(template_file, template, data, output_file) - reporter.report_templating(template_file, output_file) - - def _render_with_finding_template_first(self, template_file_index): - for (template_file, data_output_pairs) in template_file_index.items(): - template_file, template = self._get_hbr_template(template_file) - for (data_file, output) in data_output_pairs: - data = self.context.get_data(data_file) - self._apply_template(template_file, template, data, output) - reporter.report_templating(template_file, output) - self.templated_count += 1 - self.file_count += 1 - - def _render_with_finding_data_first(self, data_file_index): - for (data_file, template_output_pairs) in data_file_index.items(): - data = self.context.get_data(data_file) - for (template_file, output) in template_output_pairs: - template_file, template = self._get_hbr_template(template_file) - self._apply_template(template_file, template, data, output) - reporter.report_templating(template_file, output) - self.templated_count += 1 - self.file_count += 1 + def get_template(self, template_file): + actual_file = utils.get_template_path( + self.template_dirs, template_file + ) + with codecs.open(actual_file, "r", encoding="utf-8") as source: + hbr_template = Compiler().compile(source.read()) + return hbr_template - def _apply_template(self, template_file, template, data, output): + def apply_template(self, template, data, _): rendered_content = "".join(template(data)) rendered_content = utils.strip_off_trailing_new_lines(rendered_content) rendered_content = rendered_content.encode("utf-8") - utils.write_file_out( - output, rendered_content, strip=False, encode=False - ) - utils.file_permissions_copy(template_file, output) - - def _get_hbr_template(self, template_file): - actual_template_file = self.find_template_file(template_file) - with open(actual_template_file, "r") as source: - if sys.version_info[0] < 3: - hbr_template = Compiler().compile( - unicode(source.read()) # noqa: F821 - ) - else: - hbr_template = Compiler().compile(source.read()) - return actual_template_file, hbr_template - - def find_template_file(self, template_file): - return utils.get_template_path(self.template_dirs, template_file) + return rendered_content diff --git a/moban/jinja2/engine.py b/moban/jinja2/engine.py index 5e1c1bdb..c6e56651 100644 --- a/moban/jinja2/engine.py +++ b/moban/jinja2/engine.py @@ -1,23 +1,25 @@ -import os - from jinja2 import Environment, FileSystemLoader from lml.plugin import PluginInfo import moban.utils as utils -import moban.reporter as reporter import moban.constants as constants from moban import plugins -from moban.utils import get_template_path -from moban.hashstore import HASH_STORE @PluginInfo( constants.TEMPLATE_ENGINE_EXTENSION, tags=["jinja2", "jinja", "jj2", "j2"] ) -class Engine(plugins.BaseEngine): - def __init__(self, template_dirs, context_dirs): - super(Engine, self).__init__(template_dirs, context_dirs) - template_loader = FileSystemLoader(self.template_dirs) +class Engine(object): + def __init__(self, template_dirs): + """ + Contruct a jinja2 template engine + + A list template directories will be given to your engine class + + :param list temp_dirs: a list of template directories + """ + self.template_dirs = template_dirs + template_loader = FileSystemLoader(template_dirs) self.jj2_environment = Environment( loader=template_loader, keep_trailing_newline=True, @@ -33,67 +35,38 @@ def __init__(self, template_dirs, context_dirs): for global_name, dict_obj in plugins.GLOBALS.get_all(): self.jj2_environment.globals[global_name] = dict_obj - def render_to_file(self, template_file, data_file, output_file): - data = self.context.get_data(data_file) - template_file, template = self._get_jinja2_template(template_file) - self._apply_template(template_file, template, data, output_file) - reporter.report_templating(template_file, output_file) + def get_template(self, template_file): + """ + :param str template_file: the template file name that appeared in moban + file. It could be a file name, or a relative + file path with reference to template + directories. + :return: a jinja2 template - def _render_with_finding_template_first(self, template_file_index): - for (template_file, data_output_pairs) in template_file_index.items(): - template_file, template = self._get_jinja2_template(template_file) - for (data_file, output) in data_output_pairs: - data = self.context.get_data(data_file) - flag = self._apply_template( - template_file, template, data, output - ) - if flag: - reporter.report_templating(template_file, output) - self.templated_count += 1 - self.file_count += 1 + For example: - def _render_with_finding_data_first(self, data_file_index): - for (data_file, template_output_pairs) in data_file_index.items(): - data = self.context.get_data(data_file) - for (template_file, output) in template_output_pairs: - template_file, template = self._get_jinja2_template( - template_file - ) - flag = self._apply_template( - template_file, template, data, output - ) - if flag: - reporter.report_templating(template_file, output) - self.templated_count += 1 - self.file_count += 1 + suppose your current working directory is: /User/moban-pro/ and your + template folder list is: ['./my-template'], and the given template + file equals to: 'templates/myfile.jj2', they as a group tells the + template file exists at: + '/User/moban-pro/my-template/templates/myfile.jj2' + """ + template = self.jj2_environment.get_template(template_file) + return template - def _file_permissions_copy(self, template_file, output_file): - true_template_file = template_file - for a_template_dir in self.template_dirs: - true_template_file = os.path.join(a_template_dir, template_file) - if os.path.exists(true_template_file): - break - utils.file_permissions_copy(true_template_file, output_file) + def apply_template(self, template, data, output): + """ + It is not expected this function to write content to file system. + Please just apply data inside the template and return utf-8 encoded + content. - def _apply_template(self, template_file, template, data, output): + :param template: a jinja2 template from :class:`.get_template` + :param dict data: python data dictionary + :param str output: output file name + """ template.globals["__target__"] = output template.globals["__template__"] = template.name rendered_content = template.render(**data) rendered_content = utils.strip_off_trailing_new_lines(rendered_content) rendered_content = rendered_content.encode("utf-8") - flag = HASH_STORE.is_file_changed( - output, rendered_content, template_file - ) - if flag: - utils.write_file_out( - output, rendered_content, strip=False, encode=False - ) - utils.file_permissions_copy(template_file, output) - return flag - - def _get_jinja2_template(self, template_file): - actual_template_file = get_template_path( - self.template_dirs, template_file - ) - template = self.jj2_environment.get_template(template_file) - return actual_template_file, template + return rendered_content diff --git a/moban/main.py b/moban/main.py index 8a6d9dda..d49313bb 100644 --- a/moban/main.py +++ b/moban/main.py @@ -132,11 +132,10 @@ def handle_command_line(options): options = merge(options, constants.DEFAULT_OPTIONS) if options[constants.LABEL_TEMPLATE] is None: raise exceptions.NoTemplate(constants.ERROR_NO_TEMPLATE) - engine_class = plugins.ENGINES.get_engine( - options[constants.LABEL_TEMPLATE_TYPE] - ) - engine = engine_class( - options[constants.LABEL_TMPL_DIRS], options[constants.LABEL_CONFIG_DIR] + engine = plugins.ENGINES.get_engine( + options[constants.LABEL_TEMPLATE_TYPE], + options[constants.LABEL_TMPL_DIRS], + options[constants.LABEL_CONFIG_DIR], ) engine.render_to_file( options[constants.LABEL_TEMPLATE], diff --git a/moban/mobanfile.py b/moban/mobanfile.py index b6919007..ffff7277 100644 --- a/moban/mobanfile.py +++ b/moban/mobanfile.py @@ -106,8 +106,8 @@ def handle_targets(merged_options, targets): count = 0 for template_type in jobs_for_each_engine.keys(): - engine_class = plugins.ENGINES.get_engine(template_type) - engine = engine_class( + engine = plugins.ENGINES.get_engine( + template_type, merged_options[constants.LABEL_TMPL_DIRS], merged_options[constants.LABEL_CONFIG_DIR], ) diff --git a/moban/plugins.py b/moban/plugins.py index b5a18458..b1893200 100644 --- a/moban/plugins.py +++ b/moban/plugins.py @@ -6,6 +6,7 @@ import moban.reporter as reporter from moban import utils, constants, exceptions from moban.strategy import Strategy +from moban.hashstore import HASH_STORE class PluginMixin: @@ -58,14 +59,107 @@ def resource_path_of(self, library_name): return library.resources_path +class BaseEngine(object): + def __init__(self, template_dirs, context_dirs, engine_cls): + refresh_plugins() + template_dirs = list(expand_template_directories(template_dirs)) + verify_the_existence_of_directories(template_dirs) + context_dirs = expand_template_directory(context_dirs) + self.context = Context(context_dirs) + self.template_dirs = template_dirs + self.engine = engine_cls(self.template_dirs) + self.engine_cls = engine_cls + self.templated_count = 0 + self.file_count = 0 + + def report(self): + if self.templated_count == 0: + reporter.report_no_action() + elif self.templated_count == self.file_count: + reporter.report_full_run(self.file_count) + else: + reporter.report_partial_run(self.templated_count, self.file_count) + + def number_of_templated_files(self): + return self.templated_count + + def render_to_file(self, template_file, data_file, output_file): + data = self.context.get_data(data_file) + template = self.engine.get_template(template_file) + template_abs_path = utils.get_template_path( + self.template_dirs, template_file + ) + flag = self.apply_template( + template_abs_path, template, data, output_file + ) + if flag: + reporter.report_templating(template_file, output_file) + + def apply_template(self, template_abs_path, template, data, output_file): + rendered_content = self.engine.apply_template( + template, data, output_file + ) + flag = HASH_STORE.is_file_changed( + output_file, rendered_content, template_abs_path + ) + if flag: + utils.write_file_out( + output_file, rendered_content, strip=False, encode=False + ) + utils.file_permissions_copy(template_abs_path, output_file) + return flag + + def render_to_files(self, array_of_param_tuple): + sta = Strategy(array_of_param_tuple) + sta.process() + choice = sta.what_to_do() + if choice == Strategy.DATA_FIRST: + self._render_with_finding_data_first(sta.data_file_index) + else: + self._render_with_finding_template_first(sta.template_file_index) + + def _render_with_finding_template_first(self, template_file_index): + for (template_file, data_output_pairs) in template_file_index.items(): + template = self.engine.get_template(template_file) + template_abs_path = utils.get_template_path( + self.template_dirs, template_file + ) + for (data_file, output) in data_output_pairs: + data = self.context.get_data(data_file) + flag = self.apply_template( + template_abs_path, template, data, output + ) + if flag: + reporter.report_templating(template_file, output) + self.templated_count += 1 + self.file_count += 1 + + def _render_with_finding_data_first(self, data_file_index): + for (data_file, template_output_pairs) in data_file_index.items(): + data = self.context.get_data(data_file) + for (template_file, output) in template_output_pairs: + template = self.engine.get_template(template_file) + template_abs_path = utils.get_template_path( + self.template_dirs, template_file + ) + flag = self.apply_template( + template_abs_path, template, data, output + ) + if flag: + reporter.report_templating(template_file, output) + self.templated_count += 1 + self.file_count += 1 + + class EngineFactory(PluginManager): def __init__(self): super(EngineFactory, self).__init__( constants.TEMPLATE_ENGINE_EXTENSION ) - def get_engine(self, template_type): - return self.load_me_now(template_type) + def get_engine(self, template_type, template_dirs, context_dirs): + engine_cls = self.load_me_now(template_type) + return BaseEngine(template_dirs, context_dirs, engine_cls) def all_types(self): return list(self.registry.keys()) @@ -136,38 +230,6 @@ def get_data(self, file_name): return data -class BaseEngine(object): - def __init__(self, template_dirs, context_dirs): - refresh_plugins() - template_dirs = list(expand_template_directories(template_dirs)) - verify_the_existence_of_directories(template_dirs) - context_dirs = expand_template_directory(context_dirs) - self.context = Context(context_dirs) - self.template_dirs = template_dirs - self.templated_count = 0 - self.file_count = 0 - - def report(self): - if self.templated_count == 0: - reporter.report_no_action() - elif self.templated_count == self.file_count: - reporter.report_full_run(self.file_count) - else: - reporter.report_partial_run(self.templated_count, self.file_count) - - def number_of_templated_files(self): - return self.templated_count - - def render_to_files(self, array_of_param_tuple): - sta = Strategy(array_of_param_tuple) - sta.process() - choice = sta.what_to_do() - if choice == Strategy.DATA_FIRST: - self._render_with_finding_data_first(sta.data_file_index) - else: - self._render_with_finding_template_first(sta.template_file_index) - - def refresh_plugins(): scan_plugins_regex(constants.MOBAN_ALL, "moban", None, BUILTIN_EXENSIONS) diff --git a/setup.py b/setup.py index fb593dff..0d5b00dc 100644 --- a/setup.py +++ b/setup.py @@ -28,9 +28,9 @@ DOWNLOAD_URL = '%s/archive/0.3.3.tar.gz' % URL FILES = ['README.rst', 'CONTRIBUTORS.rst', 'CHANGELOG.rst'] KEYWORDS = [ - 'python', 'jinja2', 'moban', + 'python' ] CLASSIFIERS = [ diff --git a/tests/integration_tests/test_command_line_options.py b/tests/integration_tests/test_command_line_options.py index fd8184f2..e0ee5e9c 100644 --- a/tests/integration_tests/test_command_line_options.py +++ b/tests/integration_tests/test_command_line_options.py @@ -16,7 +16,7 @@ def setUp(self): ) self.patcher1.start() - @patch("moban.jinja2.engine.Engine.render_to_file") + @patch("moban.plugins.BaseEngine.render_to_file") def test_custom_options(self, fake_template_doer): test_args = [ "moban", @@ -37,7 +37,7 @@ def test_custom_options(self, fake_template_doer): "a.jj2", "config.yaml", "moban.output" ) - @patch("moban.jinja2.engine.Engine.render_to_file") + @patch("moban.plugins.BaseEngine.render_to_file") def test_minimal_options(self, fake_template_doer): test_args = ["moban", "-c", self.config_file, "-t", "a.jj2"] with patch.object(sys, "argv", test_args): @@ -71,7 +71,7 @@ def setUp(self): ) self.patcher1.start() - @patch("moban.jinja2.engine.Engine.render_to_file") + @patch("moban.plugins.BaseEngine.render_to_file") def test_default_options(self, fake_template_doer): test_args = ["moban", "-t", "a.jj2"] with patch.object(sys, "argv", test_args): @@ -119,7 +119,7 @@ def setUp(self): ) self.patcher1.start() - @patch("moban.jinja2.engine.Engine.render_to_files") + @patch("moban.plugins.BaseEngine.render_to_files") def test_single_command(self, fake_template_doer): test_args = ["moban"] with patch.object(sys, "argv", test_args): @@ -135,7 +135,7 @@ def test_single_command(self, fake_template_doer): ], ) - @patch("moban.jinja2.engine.Engine.render_to_files") + @patch("moban.plugins.BaseEngine.render_to_files") def test_single_command_with_a_few_options(self, fake_template_doer): test_args = ["moban", "-t", "abc.jj2", "-o", "xyz.output"] with patch.object(sys, "argv", test_args): @@ -152,7 +152,7 @@ def test_single_command_with_a_few_options(self, fake_template_doer): ], ) - @patch("moban.jinja2.engine.Engine.render_to_files") + @patch("moban.plugins.BaseEngine.render_to_files") def test_single_command_with_options(self, fake_template_doer): test_args = [ "moban", @@ -178,7 +178,7 @@ def test_single_command_with_options(self, fake_template_doer): ) @raises(Exception) - @patch("moban.jinja2.engine.Engine.render_to_files") + @patch("moban.plugins.BaseEngine.render_to_files") def test_single_command_without_output_option(self, fake_template_doer): test_args = ["moban", "-t", "abc.jj2"] with patch.object(sys, "argv", test_args): @@ -207,7 +207,7 @@ def setUp(self): ) self.patcher1.start() - @patch("moban.jinja2.engine.Engine.render_to_files") + @patch("moban.plugins.BaseEngine.render_to_files") def test_single_command(self, fake_template_doer): test_args = ["moban"] with patch.object(sys, "argv", test_args): @@ -243,7 +243,7 @@ def setUp(self): ) self.patcher1.start() - @patch("moban.jinja2.engine.Engine.render_to_files") + @patch("moban.plugins.BaseEngine.render_to_files") def test_single_command(self, fake_template_doer): test_args = ["moban", "-m", self.config_file] with patch.object(sys, "argv", test_args): @@ -282,7 +282,7 @@ def setUp(self): self.config_file = ".moban.yml" @raises(SystemExit) - @patch("moban.jinja2.engine.Engine.render_to_files") + @patch("moban.plugins.BaseEngine.render_to_files") def test_no_configuration(self, fake_template_doer): with open(self.config_file, "w") as f: f.write("") @@ -293,7 +293,7 @@ def test_no_configuration(self, fake_template_doer): main() @raises(SystemExit) - @patch("moban.jinja2.engine.Engine.render_to_files") + @patch("moban.plugins.BaseEngine.render_to_files") def test_no_configuration_2(self, fake_template_doer): with open(self.config_file, "w") as f: f.write("not: related") @@ -304,7 +304,7 @@ def test_no_configuration_2(self, fake_template_doer): main() @raises(SystemExit) - @patch("moban.jinja2.engine.Engine.render_to_files") + @patch("moban.plugins.BaseEngine.render_to_files") def test_no_targets(self, fake_template_doer): with open(self.config_file, "w") as f: f.write("configuration: test") @@ -337,7 +337,7 @@ def test_single_command(self): with patch.object(sys, "argv", test_args): from moban.main import main - with patch("moban.jinja2.engine.Engine.render_to_files") as fake: + with patch("moban.plugins.BaseEngine.render_to_files") as fake: main() call_args = list(fake.call_args[0][0]) eq_( @@ -360,7 +360,7 @@ def setUp(self): with open(self.config_file, "w") as f: f.write("hello: world") - @patch("moban.jinja2.engine.Engine.render_to_file") + @patch("moban.plugins.BaseEngine.render_to_file") def test_mako_option(self, fake_template_doer): test_args = ["moban", "-t", "a.mako"] with patch.object(sys, "argv", test_args): diff --git a/tests/moban-mako/moban_mako/__init__.py b/tests/moban-mako/moban_mako/__init__.py index cfd47681..5e8d2458 100644 --- a/tests/moban-mako/moban_mako/__init__.py +++ b/tests/moban-mako/moban_mako/__init__.py @@ -5,4 +5,5 @@ @PluginInfo(TEMPLATE_ENGINE_EXTENSION, tags=["mako"]) class MakoEngine: - pass + def __init__(self, template_dirs): + pass diff --git a/tests/test_engine.py b/tests/test_engine.py index b08042b0..f67435e9 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -5,7 +5,12 @@ import moban.exceptions as exceptions from mock import patch from nose.tools import eq_, raises -from moban.plugins import ENGINES, Context, expand_template_directories +from moban.plugins import ( + ENGINES, + Context, + BaseEngine, + expand_template_directories, +) from moban.extensions import jinja_global from moban.jinja2.engine import Engine from moban.engine_handlebars import EngineHandlebars @@ -34,33 +39,33 @@ def test_expand_repo_dir(_, __): def test_default_template_type(): - engine_class = ENGINES.get_engine("jj2") - assert engine_class == Engine + engine = ENGINES.get_engine("jj2", [], "") + assert engine.engine_cls == Engine def test_handlebars_template_type(): - engine_class = ENGINES.get_engine("hbs") - assert engine_class == EngineHandlebars + engine = ENGINES.get_engine("hbs", [], "") + assert engine.engine_cls == EngineHandlebars def test_default_mako_type(): # fake mako - engine_class = ENGINES.get_engine("mako") - assert engine_class.__name__ == "MakoEngine" + engine = ENGINES.get_engine("mako", [], "") + assert engine.engine_cls.__name__ == "MakoEngine" @raises(exceptions.NoThirdPartyEngine) def test_unknown_template_type(): - ENGINES.get_engine("unknown_template_type") + ENGINES.get_engine("unknown_template_type", [], "") @raises(exceptions.DirectoryNotFound) def test_non_existent_tmpl_directries(): - Engine("abc", "tests") + BaseEngine("abc", "tests", Engine) @raises(exceptions.DirectoryNotFound) def test_non_existent_config_directries(): - Engine("tests", "abc") + BaseEngine("tests", "abc", Engine) @raises(exceptions.DirectoryNotFound) @@ -71,7 +76,7 @@ def test_non_existent_ctx_directries(): def test_file_tests(): output = "test.txt" path = os.path.join("tests", "fixtures", "jinja_tests") - engine = Engine([path], path) + engine = BaseEngine([path], path, Engine) engine.render_to_file("file_tests.template", "file_tests.yml", output) with open(output, "r") as output_file: content = output_file.read() @@ -82,7 +87,7 @@ def test_file_tests(): def test_handlebars_file_tests(): output = "test.txt" path = os.path.join("tests", "fixtures", "handlebars_tests") - engine = EngineHandlebars([path], path) + engine = BaseEngine([path], path, EngineHandlebars) engine.render_to_file("file_tests.template", "file_tests.json", output) with open(output, "r") as output_file: content = output_file.read() @@ -90,19 +95,12 @@ def test_handlebars_file_tests(): os.unlink(output) -@raises(exceptions.FileNotFound) -def test_handlebars_template_not_found(): - path = os.path.join("tests", "fixtures", "handlebars_tests") - engine = EngineHandlebars([path], path) - engine.find_template_file("thisisnotafile.template") - - def test_globals(): output = "globals.txt" test_dict = dict(hello="world") jinja_global("test", test_dict) path = os.path.join("tests", "fixtures", "globals") - engine = Engine([path], path) + engine = BaseEngine([path], path, Engine) engine.render_to_file("basic.template", "basic.yml", output) with open(output, "r") as output_file: content = output_file.read() @@ -113,7 +111,7 @@ def test_globals(): def test_global_template_variables(): output = "test.txt" path = os.path.join("tests", "fixtures", "globals") - engine = Engine([path], path) + engine = BaseEngine([path], path, Engine) engine.render_to_file("variables.template", "variables.yml", output) with open(output, "r") as output_file: content = output_file.read() @@ -124,7 +122,7 @@ def test_global_template_variables(): def test_nested_global_template_variables(): output = "test.txt" path = os.path.join("tests", "fixtures", "globals") - engine = Engine([path], path) + engine = BaseEngine([path], path, Engine) engine.render_to_file("nested.template", "variables.yml", output) with open(output, "r") as output_file: content = output_file.read() diff --git a/tests/test_handlebar_engine.py b/tests/test_handlebar_engine.py new file mode 100644 index 00000000..4b08bd63 --- /dev/null +++ b/tests/test_handlebar_engine.py @@ -0,0 +1,14 @@ +import os + +from nose.tools import eq_ +from moban.jinja2.engine import Engine + + +def test_handlebars_template_not_found(): + path = os.path.join("tests", "fixtures", "jinja_tests") + engine = Engine([path]) + template = engine.get_template("file_tests.template") + data = dict(test="here") + result = engine.apply_template(template, data, None) + expected = "yes\nhere".encode("utf-8") + eq_(expected, result) diff --git a/tests/test_jinja2_engine.py b/tests/test_jinja2_engine.py new file mode 100644 index 00000000..91c20a89 --- /dev/null +++ b/tests/test_jinja2_engine.py @@ -0,0 +1,14 @@ +import os + +from nose.tools import eq_ +from moban.engine_handlebars import EngineHandlebars + + +def test_handlebars_template_not_found(): + path = os.path.join("tests", "fixtures", "handlebars_tests") + engine = EngineHandlebars([path]) + template = engine.get_template("file_tests.template") + data = dict(test="here") + result = engine.apply_template(template, data, None) + expected = "here".encode("utf-8") + eq_(expected, result) diff --git a/tests/test_template.py b/tests/test_template.py index 7f55cdb0..806af465 100644 --- a/tests/test_template.py +++ b/tests/test_template.py @@ -1,11 +1,11 @@ import os from mock import patch +from moban.plugins import BaseEngine from moban.jinja2.engine import Engine -from moban.engine_handlebars import EngineHandlebars -@patch("moban.jinja2.engine.Engine._render_with_finding_data_first") +@patch("moban.plugins.BaseEngine._render_with_finding_data_first") def test_do_templates_1(_do_templates_with_more_shared_data): jobs = [ ("1.template", "data.yml", "1.output"), @@ -23,14 +23,14 @@ def test_do_templates_1(_do_templates_with_more_shared_data): ("5.template", "6.output"), ] } - engine = Engine(".", ".") + engine = BaseEngine(".", ".", Engine) engine.render_to_files(jobs) _do_templates_with_more_shared_data.assert_called_with(expected) if os.path.exists(".moban.hashes"): os.unlink(".moban.hashes") -@patch("moban.jinja2.engine.Engine._render_with_finding_template_first") +@patch("moban.plugins.BaseEngine._render_with_finding_template_first") def test_do_templates_2(_do_templates_with_more_shared_templates): jobs = [ ("1.template", "data1.yml", "1.output"), @@ -48,7 +48,7 @@ def test_do_templates_2(_do_templates_with_more_shared_templates): ("data5.yml", "6.output"), ] } - engine = Engine(".", ".") + engine = BaseEngine(".", ".", Engine) engine.render_to_files(jobs) _do_templates_with_more_shared_templates.assert_called_with(expected) if os.path.exists(".moban.hashes"): @@ -57,7 +57,7 @@ def test_do_templates_2(_do_templates_with_more_shared_templates): def test_do_templates_with_more_shared_templates(): base_dir = os.path.join("tests", "fixtures") - engine = Engine(base_dir, os.path.join(base_dir, "config")) + engine = BaseEngine(base_dir, os.path.join(base_dir, "config"), Engine) engine._render_with_finding_template_first( {"a.jj2": [(os.path.join(base_dir, "child.yaml"), "test")]} ) @@ -71,7 +71,7 @@ def test_do_templates_with_more_shared_templates(): def test_do_templates_with_more_shared_data(): base_dir = os.path.join("tests", "fixtures") - engine = Engine(base_dir, os.path.join(base_dir, "config")) + engine = BaseEngine(base_dir, os.path.join(base_dir, "config"), Engine) engine._render_with_finding_data_first( {os.path.join(base_dir, "child.yaml"): [("a.jj2", "test")]} ) @@ -81,87 +81,3 @@ def test_do_templates_with_more_shared_data(): os.unlink("test") if os.path.exists(".moban.hashes"): os.unlink(".moban.hashes") - - -@patch( - "moban.engine_handlebars.EngineHandlebars." - "_render_with_finding_data_first" -) -def test_do_templates_1_handlebars(_do_templates_with_more_shared_data): - jobs = [ - ("1.template", "data.yml", "1.output"), - ("2.template", "data.yml", "2.output"), - ("3.template", "data.yml", "3.output"), - ("4.template", "data.yml", "4.output"), - ("5.template", "data.yml", "6.output"), - ] - expected = { - "data.yml": [ - ("1.template", "1.output"), - ("2.template", "2.output"), - ("3.template", "3.output"), - ("4.template", "4.output"), - ("5.template", "6.output"), - ] - } - engine = EngineHandlebars(".", ".") - engine.render_to_files(jobs) - _do_templates_with_more_shared_data.assert_called_with(expected) - if os.path.exists(".moban.hashes"): - os.unlink(".moban.hashes") - - -@patch( - "moban.engine_handlebars.EngineHandlebars." - "_render_with_finding_template_first" -) -def test_do_templates_2_handlebars(_do_templates_with_more_shared_templates): - jobs = [ - ("1.template", "data1.yml", "1.output"), - ("1.template", "data2.yml", "2.output"), - ("1.template", "data3.yml", "3.output"), - ("1.template", "data4.yml", "4.output"), - ("1.template", "data5.yml", "6.output"), - ] - expected = { - "1.template": [ - ("data1.yml", "1.output"), - ("data2.yml", "2.output"), - ("data3.yml", "3.output"), - ("data4.yml", "4.output"), - ("data5.yml", "6.output"), - ] - } - engine = EngineHandlebars(".", ".") - engine.render_to_files(jobs) - _do_templates_with_more_shared_templates.assert_called_with(expected) - if os.path.exists(".moban.hashes"): - os.unlink(".moban.hashes") - - -def test_do_templates_with_more_shared_templates_handlebars(): - base_dir = os.path.join("tests", "fixtures") - engine = EngineHandlebars(base_dir, os.path.join(base_dir, "config")) - engine._render_with_finding_template_first( - {"a.handlebars": [(os.path.join(base_dir, "child.json"), "test")]} - ) - with open("test", "r") as f: - content = f.read() - assert content == "hello world ox" - os.unlink("test") - if os.path.exists(".moban.hashes"): - os.unlink(".moban.hashes") - - -def test_do_templates_with_more_shared_data_handlebars(): - base_dir = os.path.join("tests", "fixtures") - engine = EngineHandlebars(base_dir, os.path.join(base_dir, "config")) - engine._render_with_finding_data_first( - {os.path.join(base_dir, "child.json"): [("a.handlebars", "test")]} - ) - with open("test", "r") as f: - content = f.read() - assert content == "hello world ox" - os.unlink("test") - if os.path.exists(".moban.hashes"): - os.unlink(".moban.hashes") From 72ae965907925f7d5a0274a4988872f63b1fc371 Mon Sep 17 00:00:00 2001 From: jaska Date: Sat, 17 Nov 2018 18:10:41 +0000 Subject: [PATCH 5/8] Jinja2 optimization (#132) * :hammer: further optimization. all jinja2 related code should go in jinja2 folder * :tractor: all jinja2 extensions shall live in jinja2 folder. * :micrscope: add jinja2 extensions test * :green_heart: keep two stage lml loading. Althrough it is not yet understood why, it works. * :bug: fix loading bug. otherwise, Error: jinja2 * :books: make notes on critical code lines --- .../custom-jj2-plugin/filter.py | 2 +- .../custom-jj2-plugin/global.py | 2 +- .../custom-jj2-plugin/test.py | 2 +- moban/engine_handlebars.py | 3 +- moban/jinja2/engine.py | 61 ++++++++++++++++--- moban/{ => jinja2}/extensions.py | 0 moban/jinja2/filters/github.py | 2 +- moban/jinja2/filters/repr.py | 2 +- moban/jinja2/filters/text.py | 6 +- moban/jinja2/tests/files.py | 2 +- moban/main.py | 6 +- moban/plugins.py | 53 +++------------- tests/test_engine.py | 14 ----- tests/test_handlebar_engine.py | 2 +- tests/test_jinja2_engine.py | 2 +- tests/test_jinja2_extensions.py | 19 ++++++ 16 files changed, 95 insertions(+), 83 deletions(-) rename moban/{ => jinja2}/extensions.py (100%) create mode 100644 tests/test_jinja2_extensions.py diff --git a/docs/level-7-use-custom-jinja2-filter-test-n-global/custom-jj2-plugin/filter.py b/docs/level-7-use-custom-jinja2-filter-test-n-global/custom-jj2-plugin/filter.py index 7888a1bc..ac71c4c3 100644 --- a/docs/level-7-use-custom-jinja2-filter-test-n-global/custom-jj2-plugin/filter.py +++ b/docs/level-7-use-custom-jinja2-filter-test-n-global/custom-jj2-plugin/filter.py @@ -1,7 +1,7 @@ import sys import base64 -from moban.extensions import JinjaFilter +from moban.jinja2.extensions import JinjaFilter @JinjaFilter() diff --git a/docs/level-7-use-custom-jinja2-filter-test-n-global/custom-jj2-plugin/global.py b/docs/level-7-use-custom-jinja2-filter-test-n-global/custom-jj2-plugin/global.py index 007bc4a5..b4396280 100644 --- a/docs/level-7-use-custom-jinja2-filter-test-n-global/custom-jj2-plugin/global.py +++ b/docs/level-7-use-custom-jinja2-filter-test-n-global/custom-jj2-plugin/global.py @@ -1,3 +1,3 @@ -from moban.extensions import jinja_global +from moban.jinja2.extensions import jinja_global jinja_global('global', dict(hello='world')) diff --git a/docs/level-7-use-custom-jinja2-filter-test-n-global/custom-jj2-plugin/test.py b/docs/level-7-use-custom-jinja2-filter-test-n-global/custom-jj2-plugin/test.py index 635a5d9d..903b0b17 100644 --- a/docs/level-7-use-custom-jinja2-filter-test-n-global/custom-jj2-plugin/test.py +++ b/docs/level-7-use-custom-jinja2-filter-test-n-global/custom-jj2-plugin/test.py @@ -1,4 +1,4 @@ -from moban.extensions import JinjaTest +from moban.jinja2.extensions import JinjaTest @JinjaTest() diff --git a/moban/engine_handlebars.py b/moban/engine_handlebars.py index 26b3777e..8a190ae2 100644 --- a/moban/engine_handlebars.py +++ b/moban/engine_handlebars.py @@ -22,6 +22,5 @@ def get_template(self, template_file): def apply_template(self, template, data, _): rendered_content = "".join(template(data)) - rendered_content = utils.strip_off_trailing_new_lines(rendered_content) - rendered_content = rendered_content.encode("utf-8") + rendered_content = rendered_content return rendered_content diff --git a/moban/jinja2/engine.py b/moban/jinja2/engine.py index c6e56651..eef41c02 100644 --- a/moban/jinja2/engine.py +++ b/moban/jinja2/engine.py @@ -1,9 +1,48 @@ from jinja2 import Environment, FileSystemLoader -from lml.plugin import PluginInfo +from lml.loader import scan_plugins_regex +from lml.plugin import PluginInfo, PluginManager -import moban.utils as utils import moban.constants as constants -from moban import plugins + +JINJA2_LIBRARIES = "^moban_jinja2_.+$" +JINJA2_EXENSIONS = [ + "moban.jinja2.filters.repr", + "moban.jinja2.filters.github", + "moban.jinja2.filters.text", + "moban.jinja2.tests.files", +] + + +class PluginMixin: + def get_all(self): + for name in self.registry.keys(): + # only the first matching one is returned + the_filter = self.load_me_now(name) + yield (name, the_filter) + + +class JinjaFilterManager(PluginManager, PluginMixin): + def __init__(self): + super(JinjaFilterManager, self).__init__( + constants.JINJA_FILTER_EXTENSION + ) + + +class JinjaTestManager(PluginManager, PluginMixin): + def __init__(self): + super(JinjaTestManager, self).__init__(constants.JINJA_TEST_EXTENSION) + + +class JinjaGlobalsManager(PluginManager, PluginMixin): + def __init__(self): + super(JinjaGlobalsManager, self).__init__( + constants.JINJA_GLOBALS_EXTENSION + ) + + +FILTERS = JinjaFilterManager() +TESTS = JinjaTestManager() +GLOBALS = JinjaGlobalsManager() @PluginInfo( @@ -18,6 +57,7 @@ def __init__(self, template_dirs): :param list temp_dirs: a list of template directories """ + load_jinja2_extensions() self.template_dirs = template_dirs template_loader = FileSystemLoader(template_dirs) self.jj2_environment = Environment( @@ -26,13 +66,13 @@ def __init__(self, template_dirs): trim_blocks=True, lstrip_blocks=True, ) - for filter_name, filter_function in plugins.FILTERS.get_all(): + for filter_name, filter_function in FILTERS.get_all(): self.jj2_environment.filters[filter_name] = filter_function - for test_name, test_function in plugins.TESTS.get_all(): + for test_name, test_function in TESTS.get_all(): self.jj2_environment.tests[test_name] = test_function - for global_name, dict_obj in plugins.GLOBALS.get_all(): + for global_name, dict_obj in GLOBALS.get_all(): self.jj2_environment.globals[global_name] = dict_obj def get_template(self, template_file): @@ -57,7 +97,7 @@ def get_template(self, template_file): def apply_template(self, template, data, output): """ It is not expected this function to write content to file system. - Please just apply data inside the template and return utf-8 encoded + Please just apply data inside the template and return utf-8 content. :param template: a jinja2 template from :class:`.get_template` @@ -67,6 +107,9 @@ def apply_template(self, template, data, output): template.globals["__target__"] = output template.globals["__template__"] = template.name rendered_content = template.render(**data) - rendered_content = utils.strip_off_trailing_new_lines(rendered_content) - rendered_content = rendered_content.encode("utf-8") + rendered_content = rendered_content return rendered_content + + +def load_jinja2_extensions(): + scan_plugins_regex(JINJA2_LIBRARIES, "moban", None, JINJA2_EXENSIONS) diff --git a/moban/extensions.py b/moban/jinja2/extensions.py similarity index 100% rename from moban/extensions.py rename to moban/jinja2/extensions.py diff --git a/moban/jinja2/filters/github.py b/moban/jinja2/filters/github.py index efea56ad..5a12972a 100644 --- a/moban/jinja2/filters/github.py +++ b/moban/jinja2/filters/github.py @@ -1,6 +1,6 @@ import re -from moban.extensions import JinjaFilter +from moban.jinja2.extensions import JinjaFilter GITHUB_REF_PATTERN = "`([^`]*?#[0-9]+)`" ISSUE = "^.*?" + GITHUB_REF_PATTERN + ".*?$" diff --git a/moban/jinja2/filters/repr.py b/moban/jinja2/filters/repr.py index 26ea0340..3f278915 100644 --- a/moban/jinja2/filters/repr.py +++ b/moban/jinja2/filters/repr.py @@ -1,4 +1,4 @@ -from moban.extensions import JinjaFilter +from moban.jinja2.extensions import JinjaFilter @JinjaFilter() diff --git a/moban/jinja2/filters/text.py b/moban/jinja2/filters/text.py index 95306992..19c529e1 100644 --- a/moban/jinja2/filters/text.py +++ b/moban/jinja2/filters/text.py @@ -1,6 +1,6 @@ import re -from moban.extensions import JinjaFilter +from moban.jinja2.extensions import JinjaFilter @JinjaFilter() @@ -13,7 +13,7 @@ def split_length(input_line, length): yield line else: while True: - if " " in line[start : start + limit]: # flake8: noqa + if " " in line[start : start + limit]: # noqa # go back and find a space while limit > 0 and line[start + limit] != " ": limit -= 1 @@ -25,7 +25,7 @@ def split_length(input_line, length): ] != " ": limit += 1 - yield line[start : start + limit] # flake8: noqa + yield line[start : start + limit] # noqa start = start + limit + 1 limit = length if len(line[start:]) < length or start + limit >= len(line): diff --git a/moban/jinja2/tests/files.py b/moban/jinja2/tests/files.py index 07170dee..66357dcd 100644 --- a/moban/jinja2/tests/files.py +++ b/moban/jinja2/tests/files.py @@ -9,7 +9,7 @@ samefile, ) -from moban.extensions import jinja_tests +from moban.jinja2.extensions import jinja_tests jinja_tests( is_dir=isdir, diff --git a/moban/main.py b/moban/main.py index d49313bb..991d4b93 100644 --- a/moban/main.py +++ b/moban/main.py @@ -24,11 +24,11 @@ def main(): """ program entry point """ - plugins.refresh_plugins() parser = create_parser() options = vars(parser.parse_args()) HASH_STORE.IGNORE_CACHE_FILE = options[constants.LABEL_FORCE] moban_file = options[constants.LABEL_MOBANFILE] + load_engine_factory_and_engines() # Error: jinja2 if removed if moban_file is None: moban_file = mobanfile.find_default_moban_file() if moban_file: @@ -147,3 +147,7 @@ def handle_command_line(options): engine.number_of_templated_files() ) return exit_code + + +def load_engine_factory_and_engines(): + plugins.make_sure_all_pkg_are_loaded() diff --git a/moban/plugins.py b/moban/plugins.py index b1893200..f8a230d0 100644 --- a/moban/plugins.py +++ b/moban/plugins.py @@ -8,46 +8,7 @@ from moban.strategy import Strategy from moban.hashstore import HASH_STORE - -class PluginMixin: - def get_all(self): - for name in self.registry.keys(): - # only the first matching one is returned - the_filter = self.load_me_now(name) - yield (name, the_filter) - - -class JinjaFilterManager(PluginManager, PluginMixin): - def __init__(self): - super(JinjaFilterManager, self).__init__( - constants.JINJA_FILTER_EXTENSION - ) - - -class JinjaTestManager(PluginManager, PluginMixin): - def __init__(self): - super(JinjaTestManager, self).__init__(constants.JINJA_TEST_EXTENSION) - - -class JinjaGlobalsManager(PluginManager, PluginMixin): - def __init__(self): - super(JinjaGlobalsManager, self).__init__( - constants.JINJA_GLOBALS_EXTENSION - ) - - -FILTERS = JinjaFilterManager() -TESTS = JinjaTestManager() -GLOBALS = JinjaGlobalsManager() - -BUILTIN_EXENSIONS = [ - "moban.jinja2.filters.repr", - "moban.jinja2.filters.github", - "moban.jinja2.filters.text", - "moban.jinja2.tests.files", - "moban.jinja2.engine", - "moban.engine_handlebars", -] +BUILTIN_EXENSIONS = ["moban.jinja2.engine", "moban.engine_handlebars"] class LibraryManager(PluginManager): @@ -61,7 +22,9 @@ def resource_path_of(self, library_name): class BaseEngine(object): def __init__(self, template_dirs, context_dirs, engine_cls): - refresh_plugins() + # pypi-moban-pkg cannot be found if removed + make_sure_all_pkg_are_loaded() + template_dirs = list(expand_template_directories(template_dirs)) verify_the_existence_of_directories(template_dirs) context_dirs = expand_template_directory(context_dirs) @@ -100,12 +63,10 @@ def apply_template(self, template_abs_path, template, data, output_file): template, data, output_file ) flag = HASH_STORE.is_file_changed( - output_file, rendered_content, template_abs_path + output_file, rendered_content.encode("utf-8"), template_abs_path ) if flag: - utils.write_file_out( - output_file, rendered_content, strip=False, encode=False - ) + utils.write_file_out(output_file, rendered_content) utils.file_permissions_copy(template_abs_path, output_file) return flag @@ -230,7 +191,7 @@ def get_data(self, file_name): return data -def refresh_plugins(): +def make_sure_all_pkg_are_loaded(): scan_plugins_regex(constants.MOBAN_ALL, "moban", None, BUILTIN_EXENSIONS) diff --git a/tests/test_engine.py b/tests/test_engine.py index f67435e9..03590c95 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -11,7 +11,6 @@ BaseEngine, expand_template_directories, ) -from moban.extensions import jinja_global from moban.jinja2.engine import Engine from moban.engine_handlebars import EngineHandlebars @@ -95,19 +94,6 @@ def test_handlebars_file_tests(): os.unlink(output) -def test_globals(): - output = "globals.txt" - test_dict = dict(hello="world") - jinja_global("test", test_dict) - path = os.path.join("tests", "fixtures", "globals") - engine = BaseEngine([path], path, Engine) - engine.render_to_file("basic.template", "basic.yml", output) - with open(output, "r") as output_file: - content = output_file.read() - eq_(content, "world\n\ntest") - os.unlink(output) - - def test_global_template_variables(): output = "test.txt" path = os.path.join("tests", "fixtures", "globals") diff --git a/tests/test_handlebar_engine.py b/tests/test_handlebar_engine.py index 4b08bd63..57249fd0 100644 --- a/tests/test_handlebar_engine.py +++ b/tests/test_handlebar_engine.py @@ -10,5 +10,5 @@ def test_handlebars_template_not_found(): template = engine.get_template("file_tests.template") data = dict(test="here") result = engine.apply_template(template, data, None) - expected = "yes\nhere".encode("utf-8") + expected = "yes\nhere" eq_(expected, result) diff --git a/tests/test_jinja2_engine.py b/tests/test_jinja2_engine.py index 91c20a89..fa72dbe7 100644 --- a/tests/test_jinja2_engine.py +++ b/tests/test_jinja2_engine.py @@ -10,5 +10,5 @@ def test_handlebars_template_not_found(): template = engine.get_template("file_tests.template") data = dict(test="here") result = engine.apply_template(template, data, None) - expected = "here".encode("utf-8") + expected = "here" eq_(expected, result) diff --git a/tests/test_jinja2_extensions.py b/tests/test_jinja2_extensions.py new file mode 100644 index 00000000..43b79b54 --- /dev/null +++ b/tests/test_jinja2_extensions.py @@ -0,0 +1,19 @@ +import os + +from nose.tools import eq_ +from moban.plugins import BaseEngine +from moban.jinja2.engine import Engine +from moban.jinja2.extensions import jinja_global + + +def test_globals(): + output = "globals.txt" + test_dict = dict(hello="world") + jinja_global("test", test_dict) + path = os.path.join("tests", "fixtures", "globals") + engine = BaseEngine([path], path, Engine) + engine.render_to_file("basic.template", "basic.yml", output) + with open(output, "r") as output_file: + content = output_file.read() + eq_(content, "world\n\ntest") + os.unlink(output) From 7573f8e9702c2b168606b6306efe41cd401f3716 Mon Sep 17 00:00:00 2001 From: jaska Date: Sun, 18 Nov 2018 10:24:11 +0000 Subject: [PATCH 6/8] Separate handlebars (#134) * :fire: separate handlebars to moban-handlebars * :bug: fix get_primary_key will fail when a module is loaded later. fix #8. and remove pybars3 * :books: update handlebars usage * :bug: moban hash does not work because trailing new line was not removed when the hash is calculated but is removed when saving the files. hence moban think the file is always updated. :fire: remove engine handlebars loading * :books: update conf.py from pypi-mobans-pkg * :green_heart: use moban-handlebars git repo for testing * :books: update change log, docs and correct typepos --- .moban.cd/changelog.yml | 4 +++ .moban.cd/moban.yml | 3 +- CHANGELOG.rst | 6 ++++ docs/README.rst | 2 ++ docs/conf.py | 27 +++++++++++----- docs/index.rst | 1 + .../.moban.cd/data.base.yaml | 2 ++ .../.moban.td/base.hbs | 5 +++ docs/level-11-use-handlebars/.moban.yml | 6 ++++ docs/level-11-use-handlebars/README.rst | 31 +++++++++++++++++++ .../a.template.handlebars | 1 + docs/level-11-use-handlebars/b.output | 4 +++ docs/level-11-use-handlebars/data.yml | 3 ++ docs/level-4-single-command/README.rst | 2 +- moban/engine_handlebars.py | 26 ---------------- moban/plugins.py | 11 ++++--- requirements.txt | 3 +- rnd_requirements.txt | 1 + setup.py | 5 ++- .../fixtures/handlebars_tests/file_tests.json | 1 - .../handlebars_tests/file_tests.template | 1 - tests/test_docs.py | 5 +++ tests/test_engine.py | 17 ---------- tests/test_handlebar_engine.py | 14 --------- tests/test_jinja2_engine.py | 8 ++--- 25 files changed, 107 insertions(+), 82 deletions(-) create mode 100644 docs/level-11-use-handlebars/.moban.cd/data.base.yaml create mode 100644 docs/level-11-use-handlebars/.moban.td/base.hbs create mode 100644 docs/level-11-use-handlebars/.moban.yml create mode 100644 docs/level-11-use-handlebars/README.rst create mode 100644 docs/level-11-use-handlebars/a.template.handlebars create mode 100644 docs/level-11-use-handlebars/b.output create mode 100644 docs/level-11-use-handlebars/data.yml delete mode 100644 moban/engine_handlebars.py create mode 100644 rnd_requirements.txt delete mode 100644 tests/fixtures/handlebars_tests/file_tests.json delete mode 100644 tests/fixtures/handlebars_tests/file_tests.template delete mode 100644 tests/test_handlebar_engine.py diff --git a/.moban.cd/changelog.yml b/.moban.cd/changelog.yml index 392eed67..aca0eb33 100644 --- a/.moban.cd/changelog.yml +++ b/.moban.cd/changelog.yml @@ -5,6 +5,10 @@ releases: - action: Added details: - "global variables to store the target and template file names in the jinja2 engine" + - "moban-handlebars is tested to work well with this version and above" + - action: Updated + details: + - Template engine interface has been clarified and documented date: 14-11-2018 version: 0.3.4 - changes: diff --git a/.moban.cd/moban.yml b/.moban.cd/moban.yml index 061a144a..e2e1d7f3 100644 --- a/.moban.cd/moban.yml +++ b/.moban.cd/moban.yml @@ -17,8 +17,7 @@ keywords: dependencies: - pyyaml>=3.11 - jinja2>=2.7.1 - - lml==0.0.4 + - lml>=0.0.7 - crayons - - pybars3 description: Yet another jinja2 cli command for static text generation scm_host: github.com diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f189ae90..d6977c04 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,6 +9,12 @@ Added #. global variables to store the target and template file names in the jinja2 engine +#. moban-handlebars is tested to work well with this version and above + +Updated +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +#. Template engine interface has been clarified and documented 0.3.3 - 05-11-2018 -------------------------------------------------------------------------------- diff --git a/docs/README.rst b/docs/README.rst index 37ccfaf4..d3cafab8 100644 --- a/docs/README.rst +++ b/docs/README.rst @@ -13,6 +13,7 @@ This section covers the use cases for moban. Please check them out individually. #. `Pass a folder full of templates`_ #. `Use pypi package as a moban dependency`_ #. `Use git repository as a moban dependency`_ +#. `Use handlebars template with moban`_ .. _Jinja2 command line: level-1-jinja2-cli .. _Template inheritance: level-2-template-inheritance @@ -24,3 +25,4 @@ This section covers the use cases for moban. Please check them out individually. .. _Pass a folder full of templates: level-8-pass-a-folder-full-of-templates .. _Use pypi package as a moban dependency: level-9-moban-dependency-as-pypi-package .. _Use git repository as a moban dependency: level-10-moban-dependency-as-git-repo +.. _Use handlebars template with moban: level-11-use-handlebars diff --git a/docs/conf.py b/docs/conf.py index d4a30bcb..b4ffa141 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -42,12 +42,7 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.doctest', - 'sphinx.ext.intersphinx', - 'sphinx.ext.viewcode', -] +extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode',] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -74,7 +69,7 @@ exclude_patterns = [] # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = None # -- Options for HTML output ------------------------------------------------- @@ -162,6 +157,24 @@ 'Miscellaneous'), ] + +# -- Options for Epub output ------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = project + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +# +# epub_identifier = '' + +# A unique identification for the text. +# +# epub_uid = '' + +# A list of files that should not be packed into the epub file. +epub_exclude_files = ['search.html'] + # -- Extension configuration ------------------------------------------------- # -- Options for intersphinx extension --------------------------------------- diff --git a/docs/index.rst b/docs/index.rst index a2dc5109..6668ac83 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -25,6 +25,7 @@ examples folder. level-8-pass-a-folder-full-of-templates/README.rst level-9-moban-dependency-as-pypi-package/README.rst level-10-moban-dependency-as-git-repo/README.rst + level-11-use-handlebars/README.rst In pratice, the following use cases were found interesting to go along with. diff --git a/docs/level-11-use-handlebars/.moban.cd/data.base.yaml b/docs/level-11-use-handlebars/.moban.cd/data.base.yaml new file mode 100644 index 00000000..2c16d47a --- /dev/null +++ b/docs/level-11-use-handlebars/.moban.cd/data.base.yaml @@ -0,0 +1,2 @@ +nihao: shijie +hello: shijie \ No newline at end of file diff --git a/docs/level-11-use-handlebars/.moban.td/base.hbs b/docs/level-11-use-handlebars/.moban.td/base.hbs new file mode 100644 index 00000000..ded3b3f7 --- /dev/null +++ b/docs/level-11-use-handlebars/.moban.td/base.hbs @@ -0,0 +1,5 @@ + +{{hello}} + +{{nihao}} + diff --git a/docs/level-11-use-handlebars/.moban.yml b/docs/level-11-use-handlebars/.moban.yml new file mode 100644 index 00000000..62af65fe --- /dev/null +++ b/docs/level-11-use-handlebars/.moban.yml @@ -0,0 +1,6 @@ +requires: + - moban-handlebars + +targets: + - a.output: a.template.handlebars + - b.output: base.hbs diff --git a/docs/level-11-use-handlebars/README.rst b/docs/level-11-use-handlebars/README.rst new file mode 100644 index 00000000..80c1f6b7 --- /dev/null +++ b/docs/level-11-use-handlebars/README.rst @@ -0,0 +1,31 @@ +Level 11: use handlebars +================================================================================ + +moban is extensible via lml. Charlie Liu through Google Code-in 2018 has +kindly contributed moban-handlebars plugin. + + +Evaluation +-------------------------------------------------------------------------------- + +Please go to `docs/level-11-use-handlebars` directory. + + +Here is the `.moban.yml`, which replaces `jj2` with handlebars files in level 4:: + + requires: + - moban-handlebars + + targets: + - a.output: a.template.handlebars + - b.output: base.hbs + + +where `targets` should lead an array of dictionaries, `requires` installs +moban-handlebars extension. You can provide file suffixes: ".handlebars" +or ".hbs" to your handlebars template. + +Here is how to launch it +.. code-block:: bash + + moban diff --git a/docs/level-11-use-handlebars/a.template.handlebars b/docs/level-11-use-handlebars/a.template.handlebars new file mode 100644 index 00000000..2cc9b416 --- /dev/null +++ b/docs/level-11-use-handlebars/a.template.handlebars @@ -0,0 +1 @@ +{{no-inheritance}} diff --git a/docs/level-11-use-handlebars/b.output b/docs/level-11-use-handlebars/b.output new file mode 100644 index 00000000..e91bcf2b --- /dev/null +++ b/docs/level-11-use-handlebars/b.output @@ -0,0 +1,4 @@ + +world + +shijie diff --git a/docs/level-11-use-handlebars/data.yml b/docs/level-11-use-handlebars/data.yml new file mode 100644 index 00000000..dc8c957d --- /dev/null +++ b/docs/level-11-use-handlebars/data.yml @@ -0,0 +1,3 @@ +overrides: data.base.yaml +hello: world +no-inheritance: handlebars does not support inheritance \ No newline at end of file diff --git a/docs/level-4-single-command/README.rst b/docs/level-4-single-command/README.rst index 718290a0..5ad698f6 100644 --- a/docs/level-4-single-command/README.rst +++ b/docs/level-4-single-command/README.rst @@ -12,7 +12,7 @@ Evaluation Please go to `docs/level-4-single-command` directory. -Here is the `.moban.yml`, whihc replaces the command in level 3:: +Here is the `.moban.yml`, which replaces the command in level 3:: targets: - a.output: a.template diff --git a/moban/engine_handlebars.py b/moban/engine_handlebars.py deleted file mode 100644 index 8a190ae2..00000000 --- a/moban/engine_handlebars.py +++ /dev/null @@ -1,26 +0,0 @@ -import codecs - -from lml.plugin import PluginInfo - -import moban.utils as utils -import moban.constants as constants -from pybars import Compiler - - -@PluginInfo(constants.TEMPLATE_ENGINE_EXTENSION, tags=["handlebars", "hbs"]) -class EngineHandlebars(object): - def __init__(self, template_dirs): - self.template_dirs = template_dirs - - def get_template(self, template_file): - actual_file = utils.get_template_path( - self.template_dirs, template_file - ) - with codecs.open(actual_file, "r", encoding="utf-8") as source: - hbr_template = Compiler().compile(source.read()) - return hbr_template - - def apply_template(self, template, data, _): - rendered_content = "".join(template(data)) - rendered_content = rendered_content - return rendered_content diff --git a/moban/plugins.py b/moban/plugins.py index f8a230d0..544dc2a4 100644 --- a/moban/plugins.py +++ b/moban/plugins.py @@ -8,7 +8,7 @@ from moban.strategy import Strategy from moban.hashstore import HASH_STORE -BUILTIN_EXENSIONS = ["moban.jinja2.engine", "moban.engine_handlebars"] +BUILTIN_EXENSIONS = ["moban.jinja2.engine"] class LibraryManager(PluginManager): @@ -24,7 +24,6 @@ class BaseEngine(object): def __init__(self, template_dirs, context_dirs, engine_cls): # pypi-moban-pkg cannot be found if removed make_sure_all_pkg_are_loaded() - template_dirs = list(expand_template_directories(template_dirs)) verify_the_existence_of_directories(template_dirs) context_dirs = expand_template_directory(context_dirs) @@ -62,11 +61,15 @@ def apply_template(self, template_abs_path, template, data, output_file): rendered_content = self.engine.apply_template( template, data, output_file ) + rendered_content = utils.strip_off_trailing_new_lines(rendered_content) + rendered_content = rendered_content.encode("utf-8") flag = HASH_STORE.is_file_changed( - output_file, rendered_content.encode("utf-8"), template_abs_path + output_file, rendered_content, template_abs_path ) if flag: - utils.write_file_out(output_file, rendered_content) + utils.write_file_out( + output_file, rendered_content, strip=False, encode=False + ) utils.file_permissions_copy(template_abs_path, output_file) return flag diff --git a/requirements.txt b/requirements.txt index 0a7e7b8d..cc7ce754 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ pyyaml>=3.11 jinja2>=2.7.1 -lml==0.0.4 +lml>=0.0.7 crayons -pybars3 diff --git a/rnd_requirements.txt b/rnd_requirements.txt new file mode 100644 index 00000000..8ead802a --- /dev/null +++ b/rnd_requirements.txt @@ -0,0 +1 @@ +https://github.com/moremoban/moban-handlebars/archive/master.zip diff --git a/setup.py b/setup.py index 0d5b00dc..8aad5959 100644 --- a/setup.py +++ b/setup.py @@ -28,9 +28,9 @@ DOWNLOAD_URL = '%s/archive/0.3.3.tar.gz' % URL FILES = ['README.rst', 'CONTRIBUTORS.rst', 'CHANGELOG.rst'] KEYWORDS = [ + 'python', 'jinja2', 'moban', - 'python' ] CLASSIFIERS = [ @@ -48,9 +48,8 @@ INSTALL_REQUIRES = [ 'pyyaml>=3.11', 'jinja2>=2.7.1', - 'lml==0.0.4', + 'lml>=0.0.7', 'crayons', - 'pybars3', ] SETUP_COMMANDS = {} diff --git a/tests/fixtures/handlebars_tests/file_tests.json b/tests/fixtures/handlebars_tests/file_tests.json deleted file mode 100644 index 39e8d602..00000000 --- a/tests/fixtures/handlebars_tests/file_tests.json +++ /dev/null @@ -1 +0,0 @@ -{"test": "here"} \ No newline at end of file diff --git a/tests/fixtures/handlebars_tests/file_tests.template b/tests/fixtures/handlebars_tests/file_tests.template deleted file mode 100644 index c905f153..00000000 --- a/tests/fixtures/handlebars_tests/file_tests.template +++ /dev/null @@ -1 +0,0 @@ -{{test}} \ No newline at end of file diff --git a/tests/test_docs.py b/tests/test_docs.py index 795b3793..08fd7aeb 100644 --- a/tests/test_docs.py +++ b/tests/test_docs.py @@ -100,6 +100,11 @@ def test_level_10(self): folder = "level-10-moban-dependency-as-git-repo" self._raw_moban(["moban"], folder, expected, "test.txt") + def test_level_11(self): + expected = "handlebars does not support inheritance\n" + folder = "level-11-use-handlebars" + self._raw_moban(["moban"], folder, expected, "a.output") + def test_misc_1(self): expected = "test file\n" diff --git a/tests/test_engine.py b/tests/test_engine.py index 03590c95..dc47d4f5 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -12,7 +12,6 @@ expand_template_directories, ) from moban.jinja2.engine import Engine -from moban.engine_handlebars import EngineHandlebars @PluginInfo("library", tags=["testmobans"]) @@ -42,11 +41,6 @@ def test_default_template_type(): assert engine.engine_cls == Engine -def test_handlebars_template_type(): - engine = ENGINES.get_engine("hbs", [], "") - assert engine.engine_cls == EngineHandlebars - - def test_default_mako_type(): # fake mako engine = ENGINES.get_engine("mako", [], "") assert engine.engine_cls.__name__ == "MakoEngine" @@ -83,17 +77,6 @@ def test_file_tests(): os.unlink(output) -def test_handlebars_file_tests(): - output = "test.txt" - path = os.path.join("tests", "fixtures", "handlebars_tests") - engine = BaseEngine([path], path, EngineHandlebars) - engine.render_to_file("file_tests.template", "file_tests.json", output) - with open(output, "r") as output_file: - content = output_file.read() - eq_(content, "here") - os.unlink(output) - - def test_global_template_variables(): output = "test.txt" path = os.path.join("tests", "fixtures", "globals") diff --git a/tests/test_handlebar_engine.py b/tests/test_handlebar_engine.py deleted file mode 100644 index 57249fd0..00000000 --- a/tests/test_handlebar_engine.py +++ /dev/null @@ -1,14 +0,0 @@ -import os - -from nose.tools import eq_ -from moban.jinja2.engine import Engine - - -def test_handlebars_template_not_found(): - path = os.path.join("tests", "fixtures", "jinja_tests") - engine = Engine([path]) - template = engine.get_template("file_tests.template") - data = dict(test="here") - result = engine.apply_template(template, data, None) - expected = "yes\nhere" - eq_(expected, result) diff --git a/tests/test_jinja2_engine.py b/tests/test_jinja2_engine.py index fa72dbe7..57249fd0 100644 --- a/tests/test_jinja2_engine.py +++ b/tests/test_jinja2_engine.py @@ -1,14 +1,14 @@ import os from nose.tools import eq_ -from moban.engine_handlebars import EngineHandlebars +from moban.jinja2.engine import Engine def test_handlebars_template_not_found(): - path = os.path.join("tests", "fixtures", "handlebars_tests") - engine = EngineHandlebars([path]) + path = os.path.join("tests", "fixtures", "jinja_tests") + engine = Engine([path]) template = engine.get_template("file_tests.template") data = dict(test="here") result = engine.apply_template(template, data, None) - expected = "here" + expected = "yes\nhere" eq_(expected, result) From c2fd28e01cbbf19ee9284ecfcbb802ac980c9026 Mon Sep 17 00:00:00 2001 From: chfw Date: Sun, 18 Nov 2018 13:23:29 +0000 Subject: [PATCH 7/8] :fire: moban-handlebars has been released --- rnd_requirements.txt | 1 - 1 file changed, 1 deletion(-) delete mode 100644 rnd_requirements.txt diff --git a/rnd_requirements.txt b/rnd_requirements.txt deleted file mode 100644 index 8ead802a..00000000 --- a/rnd_requirements.txt +++ /dev/null @@ -1 +0,0 @@ -https://github.com/moremoban/moban-handlebars/archive/master.zip From b9ebbd0eb1f0c337b8736bbf16142f76b4aea326 Mon Sep 17 00:00:00 2001 From: chfw Date: Sun, 18 Nov 2018 13:53:20 +0000 Subject: [PATCH 8/8] :green_heart: update the usage for moban-handlebars --- CONTRIBUTORS.rst | 7 ++++--- docs/level-11-use-handlebars/.moban.yml | 3 --- docs/level-11-use-handlebars/README.rst | 7 +++---- tests/requirements.txt | 1 + 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/CONTRIBUTORS.rst b/CONTRIBUTORS.rst index edce5f66..b2a00887 100644 --- a/CONTRIBUTORS.rst +++ b/CONTRIBUTORS.rst @@ -3,6 +3,7 @@ Contributors In alphabetical order: -`John Vandenberg `_ -`PRAJWAL M `_ -`SerekKiri `_ +* `Charlie Liu `_ +* `John Vandenberg `_ +* `PRAJWAL M `_ +* `SerekKiri `_ diff --git a/docs/level-11-use-handlebars/.moban.yml b/docs/level-11-use-handlebars/.moban.yml index 62af65fe..150e891a 100644 --- a/docs/level-11-use-handlebars/.moban.yml +++ b/docs/level-11-use-handlebars/.moban.yml @@ -1,6 +1,3 @@ -requires: - - moban-handlebars - targets: - a.output: a.template.handlebars - b.output: base.hbs diff --git a/docs/level-11-use-handlebars/README.rst b/docs/level-11-use-handlebars/README.rst index 80c1f6b7..7de5dfdd 100644 --- a/docs/level-11-use-handlebars/README.rst +++ b/docs/level-11-use-handlebars/README.rst @@ -8,14 +8,13 @@ kindly contributed moban-handlebars plugin. Evaluation -------------------------------------------------------------------------------- -Please go to `docs/level-11-use-handlebars` directory. +Please go to `docs/level-11-use-handlebars` directory. You will have to:: + + $ pip install moban-handlebars Here is the `.moban.yml`, which replaces `jj2` with handlebars files in level 4:: - requires: - - moban-handlebars - targets: - a.output: a.template.handlebars - b.output: base.hbs diff --git a/tests/requirements.txt b/tests/requirements.txt index 7730caf8..b894a288 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -6,3 +6,4 @@ flake8 moban black;python_version>="3.6" isort;python_version>="3.6" +moban-handlebars