diff --git a/.moban.cd/changelog.yml b/.moban.cd/changelog.yml index d899c1e7..aca0eb33 100644 --- a/.moban.cd/changelog.yml +++ b/.moban.cd/changelog.yml @@ -1,6 +1,16 @@ name: moban organisation: moremoban releases: +- changes: + - 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: - action: Added details: diff --git a/.moban.cd/moban.yml b/.moban.cd/moban.yml index a36be3eb..e2e1d7f3 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" @@ -17,7 +17,7 @@ keywords: dependencies: - pyyaml>=3.11 - jinja2>=2.7.1 - - lml==0.0.4 + - lml>=0.0.7 - crayons description: Yet another jinja2 cli command for static text generation scm_host: github.com diff --git a/CHANGELOG.rst b/CHANGELOG.rst index bfa01b0c..d6977c04 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,21 @@ Change log ================================================================================ +0.3.4 - 14-11-2018 +-------------------------------------------------------------------------------- + +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/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/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 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/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/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..150e891a --- /dev/null +++ b/docs/level-11-use-handlebars/.moban.yml @@ -0,0 +1,3 @@ +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..7de5dfdd --- /dev/null +++ b/docs/level-11-use-handlebars/README.rst @@ -0,0 +1,30 @@ +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. You will have to:: + + $ pip install moban-handlebars + + +Here is the `.moban.yml`, which replaces `jj2` with handlebars files in level 4:: + + 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/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/_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.py b/moban/engine.py deleted file mode 100644 index 571b9e10..00000000 --- a/moban/engine.py +++ /dev/null @@ -1,111 +0,0 @@ -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.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): - 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) - self.jj2_environment = Environment( - loader=template_loader, - keep_trailing_newline=True, - trim_blocks=True, - lstrip_blocks=True, - ) - for filter_name, filter_function in plugins.FILTERS.get_all(): - self.jj2_environment.filters[filter_name] = filter_function - - for test_name, test_function in plugins.TESTS.get_all(): - self.jj2_environment.tests[test_name] = test_function - - 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) - 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) - for (data_file, output) in data_output_pairs: - data = self.context.get_data(data_file) - flag = self._apply_template(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.jj2_environment.get_template(template_file) - flag = self._apply_template(template, data, output) - if flag: - reporter.report_templating(template_file, output) - self.templated_count += 1 - self.file_count += 1 - - 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): - 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) - rendered_content = rendered_content.encode("utf-8") - flag = HASH_STORE.is_file_changed( - output, rendered_content, temp_file_path - ) - if flag: - utils.write_file_out( - output, rendered_content, strip=False, encode=False - ) - utils.file_permissions_copy(temp_file_path, output) - return flag 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 deleted file mode 100644 index f7c32fd8..00000000 --- a/moban/engine_handlebars.py +++ /dev/null @@ -1,103 +0,0 @@ -import os -import sys - -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) - - 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) - 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) - for (data_file, output) in data_output_pairs: - data = self.context.get_data(data_file) - self._apply_template(template_file, 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 = self.find_template_file(template_file) - self._apply_template(template_file, 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)) - 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) diff --git a/moban/extensions.py b/moban/extensions.py deleted file mode 100644 index 61e174c7..00000000 --- a/moban/extensions.py +++ /dev/null @@ -1,69 +0,0 @@ -from lml.plugin import PluginInfo, PluginManager - -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 - ) - - -class JinjaFilter(PluginInfo): - def __init__(self): - super(JinjaFilter, self).__init__(constants.JINJA_FILTER_EXTENSION) - - 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) - self.test_name = test_name - - def tags(self): - if self.test_name: - yield self.test_name - else: - yield self.cls.__name__ - - -def jinja_tests(**keywords): - for key, value in keywords.items(): - 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/jinja2/engine.py b/moban/jinja2/engine.py new file mode 100644 index 00000000..eef41c02 --- /dev/null +++ b/moban/jinja2/engine.py @@ -0,0 +1,115 @@ +from jinja2 import Environment, FileSystemLoader +from lml.loader import scan_plugins_regex +from lml.plugin import PluginInfo, PluginManager + +import moban.constants as constants + +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( + constants.TEMPLATE_ENGINE_EXTENSION, tags=["jinja2", "jinja", "jj2", "j2"] +) +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 + """ + load_jinja2_extensions() + self.template_dirs = template_dirs + template_loader = FileSystemLoader(template_dirs) + self.jj2_environment = Environment( + loader=template_loader, + keep_trailing_newline=True, + trim_blocks=True, + lstrip_blocks=True, + ) + for filter_name, filter_function in FILTERS.get_all(): + self.jj2_environment.filters[filter_name] = filter_function + + for test_name, test_function in TESTS.get_all(): + self.jj2_environment.tests[test_name] = test_function + + for global_name, dict_obj in GLOBALS.get_all(): + self.jj2_environment.globals[global_name] = dict_obj + + 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 + + For example: + + 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 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 + content. + + :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 = rendered_content + return rendered_content + + +def load_jinja2_extensions(): + scan_plugins_regex(JINJA2_LIBRARIES, "moban", None, JINJA2_EXENSIONS) diff --git a/moban/jinja2/extensions.py b/moban/jinja2/extensions.py new file mode 100644 index 00000000..7b21dbfd --- /dev/null +++ b/moban/jinja2/extensions.py @@ -0,0 +1,33 @@ +from lml.plugin import PluginInfo + +from moban import constants + + +class JinjaFilter(PluginInfo): + def __init__(self): + super(JinjaFilter, self).__init__(constants.JINJA_FILTER_EXTENSION) + + def tags(self): + yield self.cls.__name__ + + +class JinjaTest(PluginInfo): + def __init__(self, test_name=None): + super(JinjaTest, self).__init__(constants.JINJA_TEST_EXTENSION) + self.test_name = test_name + + def tags(self): + if self.test_name: + yield self.test_name + else: + yield self.cls.__name__ + + +def jinja_tests(**keywords): + for key, value in keywords.items(): + JinjaTest(key)(value) + + +def jinja_global(identifier, dict_obj): + plugin = PluginInfo(constants.JINJA_GLOBALS_EXTENSION, tags=[identifier]) + plugin(dict_obj) 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 96% rename from moban/filters/github.py rename to moban/jinja2/filters/github.py index efea56ad..5a12972a 100644 --- a/moban/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/filters/repr.py b/moban/jinja2/filters/repr.py similarity index 79% rename from moban/filters/repr.py rename to moban/jinja2/filters/repr.py index 26ea0340..3f278915 100644 --- a/moban/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/filters/text.py b/moban/jinja2/filters/text.py similarity index 83% rename from moban/filters/text.py rename to moban/jinja2/filters/text.py index 95306992..19c529e1 100644 --- a/moban/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/__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 90% rename from moban/tests/files.py rename to moban/jinja2/tests/files.py index 07170dee..66357dcd 100644 --- a/moban/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 6d5b6dd6..991d4b93 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,20 +15,20 @@ 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(): """ 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: @@ -133,10 +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], @@ -148,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/mobanfile.py b/moban/mobanfile.py index b52c040b..ffff7277 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 @@ -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 9de12bd4..544dc2a4 100644 --- a/moban/plugins.py +++ b/moban/plugins.py @@ -1,32 +1,139 @@ import os -from moban import utils + from lml.loader import scan_plugins_regex +from lml.plugin import PluginManager -from moban.extensions import ( - JinjaTestManager, - JinjaFilterManager, - JinjaGlobalsManager -) -from moban.extensions import LibraryManager -from moban.engine_factory import EngineFactory -from moban.constants import MOBAN_ALL +import moban.reporter as reporter +from moban import utils, constants, exceptions +from moban.strategy import Strategy +from moban.hashstore import HASH_STORE -LIBRARIES = LibraryManager() -FILTERS = JinjaFilterManager() -TESTS = JinjaTestManager() -GLOBALS = JinjaGlobalsManager() -ENGINES = EngineFactory() +BUILTIN_EXENSIONS = ["moban.jinja2.engine"] + + +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 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) + 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 + ) + 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, 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, 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()) -BUILTIN_EXENSIONS = [ - "moban.filters.repr", - "moban.filters.github", - "moban.filters.text", - "moban.tests.files", -] + def raise_exception(self, key): + raise exceptions.NoThirdPartyEngine(key) -def refresh_plugins(): - scan_plugins_regex(MOBAN_ALL, "moban", None, BUILTIN_EXENSIONS) +LIBRARIES = LibraryManager() +ENGINES = EngineFactory() def expand_template_directories(dirs): @@ -65,3 +172,47 @@ 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 + + +def make_sure_all_pkg_are_loaded(): + 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/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/setup.py b/setup.py index f79b17a5..8aad5959 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ NAME = 'moban' AUTHOR = 'C. W.' -VERSION = '0.3.3' +VERSION = '0.3.4' EMAIL = 'wangc_2011@hotmail.com' LICENSE = 'MIT' ENTRY_POINTS = { @@ -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,7 +48,7 @@ INSTALL_REQUIRES = [ 'pyyaml>=3.11', 'jinja2>=2.7.1', - 'lml==0.0.4', + 'lml>=0.0.7', 'crayons', ] 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/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/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/integration_tests/test_command_line_options.py b/tests/integration_tests/test_command_line_options.py index 0bab4889..e0ee5e9c 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.plugins.BaseEngine.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.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): + 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.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): + 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.plugins.BaseEngine.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.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): + 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.plugins.BaseEngine.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.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): + 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.plugins.BaseEngine.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.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): + 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.plugins.BaseEngine.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.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") 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.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") 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.plugins.BaseEngine.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.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): + from moban.main import main + main() fake_template_doer.assert_called_with( "a.mako", "data.yml", "moban.output" 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/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 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_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 9d885945..dc47d4f5 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -5,12 +5,13 @@ import moban.exceptions as exceptions from mock import patch from nose.tools import eq_, raises -from moban.engine import Engine -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.engine_handlebars import EngineHandlebars +from moban.plugins import ( + ENGINES, + Context, + BaseEngine, + expand_template_directories, +) +from moban.jinja2.engine import Engine @PluginInfo("library", tags=["testmobans"]) @@ -36,33 +37,28 @@ def test_expand_repo_dir(_, __): def test_default_template_type(): - engine_class = ENGINES.get_engine("jj2") - assert engine_class == Engine - - -def test_handlebars_template_type(): - engine_class = ENGINES.get_engine("hbs") - assert engine_class == EngineHandlebars + engine = ENGINES.get_engine("jj2", [], "") + assert engine.engine_cls == Engine 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) @@ -73,7 +69,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() @@ -81,32 +77,23 @@ def test_file_tests(): os.unlink(output) -def test_handlebars_file_tests(): +def test_global_template_variables(): output = "test.txt" - path = os.path.join("tests", "fixtures", "handlebars_tests") - engine = EngineHandlebars([path], path) - engine.render_to_file("file_tests.template", "file_tests.json", output) + path = os.path.join("tests", "fixtures", "globals") + 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() - eq_(content, "here") + eq_(content, "template: variables.template\ntarget: test.txt\nhere") 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) +def test_nested_global_template_variables(): + output = "test.txt" path = os.path.join("tests", "fixtures", "globals") - engine = Engine([path], path) - engine.render_to_file("basic.template", "basic.yml", output) + 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() - eq_(content, "world\n\ntest") + eq_(content, "template: nested.template\ntarget: test.txt\nhere") os.unlink(output) 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_jinja2_engine.py b/tests/test_jinja2_engine.py new file mode 100644 index 00000000..57249fd0 --- /dev/null +++ b/tests/test_jinja2_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" + 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) diff --git a/tests/test_template.py b/tests/test_template.py index d4c0389c..806af465 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.engine_handlebars import EngineHandlebars +from moban.plugins import BaseEngine +from moban.jinja2.engine import Engine -@patch("moban.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.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,83 +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") 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)