├── .coveragerc
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.rst
├── multigen
├── __init__.py
├── formatter.py
├── generator.py
└── jinja.py
├── setup.py
└── tests
├── conftest.py
├── input
└── test.py.tpl
├── test_formatter.py
├── test_generator.py
└── test_jinja.py
/.coveragerc:
--------------------------------------------------------------------------------
1 | [run]
2 | branch = True
3 | source = multigen
4 |
5 | [report]
6 | exclude_lines =
7 | raise NotImplementedError
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | .eggs/
3 | .cache/
4 | **/__pycache__/
5 | *.egg-info/
6 | build/
7 | dist/
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | python:
3 | - "3.3"
4 | - "3.4"
5 | - "3.5"
6 | - "3.6"
7 | install:
8 | - pip install --upgrade setuptools
9 | - pip install --upgrade pytest pytest-cov coveralls
10 | - pip install -e .
11 | script:
12 | - python setup.py test -a -v -a --cov=multigen
13 | after_success:
14 | - coveralls
15 | deploy:
16 | provider: pypi
17 | user: $PYPI_USER
18 | password: $PYPI_PASSWORD
19 | distributions: "bdist_wheel"
20 | on:
21 | tags: true
22 | python: 3.6
23 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Mike Pagel
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | pymultigen - Multi-file frontend for single-file code generators
2 | ================================================================
3 |
4 | .. image:: https://travis-ci.org/moltob/pymultigen.svg?branch=master
5 | :target: https://travis-ci.org/moltob/pymultigen
6 |
7 | .. image:: https://badge.fury.io/py/pymultigen.svg
8 | :target: https://badge.fury.io/py/pymultigen
9 |
10 | .. image:: https://coveralls.io/repos/github/moltob/pymultigen/badge.svg?branch=master
11 | :target: https://coveralls.io/github/moltob/pymultigen?branch=master
12 |
13 | .. image:: https://img.shields.io/badge/License-MIT-yellow.svg
14 | :target: https://opensource.org/licenses/MIT
15 |
16 | .. image:: https://img.shields.io/badge/contributions-welcome-brightgreen.svg
17 |
18 | This small library adds multi-file management on top of one or more existing single-file code
19 | generators.
20 |
21 | .. contents:: :depth: 2
22 |
23 | Why would I need pymultigen?
24 | ----------------------------
25 |
26 | Code generators like `Mako `_ or `Jinja `_
27 | are great and can be used to generate just about any kind of textual output from templates with a
28 | nice template language. They are very mature and battle-proven. However, most of those generators
29 | have their origin in the web application domain. The typical usecase is to dynamically render a
30 | single HTTP response (most of the time an HTML page) from one or more templates. *One* HTML page.
31 |
32 | If you want to use these generators in other scenarious, e.g. to generate code or reports, but not
33 | to *one* but to *multiple* files in different folders, pymultigen can help. It simply adds an easy
34 | to configure file and folder management layer on top of one or more existing code generators.
35 |
36 | Installation
37 | ------------
38 |
39 | pymultigen comes in form or a regular Python distribution and can be installed from Github or PyPI
40 | with a simple:
41 |
42 | .. code-block:: shell
43 |
44 | $ pip install pymultigen
45 |
46 | The library works with any version of Python >= 3.3.
47 |
48 | Usage
49 | -----
50 |
51 | The overall concept of pymultigen is simple:
52 |
53 | * A ``Generator`` class controls the overall generation workflow. The most important method it
54 | implements is ``generate(model, folder)``. This is the single method called by *users* of the
55 | created multi-file generator.
56 | * The ``Generator`` has a static list of ``Task`` objects. Each ``Task`` describes a step executed
57 | at generation time.
58 | * One ``Task`` is responsible for translating a specific set of elements in the input model to one
59 | output file in the output folder. The input set can be chosen arbitrarily, often this is the list
60 | of a certain model element type (e.g. instance of a ``Table`` class in a relational model from
61 | which SQL statements should be generated).
62 |
63 | Using pymultigen means therefore to create one ``Generator`` class for your new generator and one or
64 | more ``Task`` classes, one for each type of output artifact. If you are using a template-based code
65 | generator under the hood, you usually will have one ``Task`` per output template.
66 |
67 | Before you start, you need to check, whether pymultigen already has an integration for your
68 | single-file code generator built-in. Currently, the following integrations are available:
69 |
70 | * Jinja2
71 |
72 | If you want to use another generation engine, you can easily add support yourself (the current
73 | Jinja2 integration consists of less than 20 lines of code). If you've done so, please consider
74 | giving back to the community. Your contribution is welcome! Please see below for instructions how to
75 | extend pymultigen with a new integration.
76 |
77 | Using the Jinja2 integration
78 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
79 |
80 | You may want to check out `pyecoregen `_, a code generator
81 | from `pyecore `_-based models to Python classes. It is a
82 | concrete Jinja2-based code generator built with pymultigen.
83 |
84 | Jinja2 is a template-based text generator. Writing a file-generator with Jinja therefore involves
85 | writing a template for each type of output file. In pymultigen you will then implement a ``Task``
86 | class per output file type, i.e. per Jinja template.
87 |
88 | The general form of such a ``Task`` looks like this:
89 |
90 | .. code-block:: python
91 |
92 | class MyTask(multigen.jinja.JinjaTask):
93 |
94 | # Name of template file used by this task.
95 | template_name = 'name-of-template.tpl'
96 |
97 | def filtered_elements(self, model):
98 | """Return iterator over elements in model that are passed to the above template."""
99 |
100 | def relative_path_for_element(self, element):
101 | """Return relative file path receiving the generator output for given element."""
102 |
103 | The workflow engine will initially call ``filtered_elements``. This method is expected to return an
104 | interator over model elements for which a single file needs to be generated. *Model* is meant here
105 | in an abstract way: It may be an instance of a formal metamodel, but it could be any Python object,
106 | like a dictionaries or lists. The contained elements being iterated over are accessible from within
107 | a template as ``element``.
108 |
109 | Once Jinja has produced a textual result it must be written to file. This is where
110 | ``relative_path_for_element`` comes into play. For a given element that was filtered from the model
111 | before, it returns the corresponding filepath. Note that this path is interpreted to be relative to
112 | the top-level output path of the overall generation (see below). If subfolders are mentioned here,
113 | they are created on demand.
114 |
115 | One or more task classes like this must then be registered with a top-level generator. Just like
116 | before, a new ``Generator`` class is derived from the appropriate base class:
117 |
118 | .. code-block:: python
119 |
120 | class MyGenerator(multigen.jinja.JinjaGenerator):
121 |
122 | # List of task objects to be processed by this generator.
123 | tasks = [
124 | MyTask(),
125 | ]
126 |
127 | # Root path where Jinja templates are found.
128 | templates_path = os.path.join(
129 | os.path.abspath(os.path.dirname(__file__)),
130 | 'templates'
131 | )
132 |
133 | def create_environment(self, **kwargs):
134 | """Create Jinja2 environment."""
135 | environment = super().create_environment(**kwargs)
136 | # Do any customization of environment here, or delete this method.
137 | return environment
138 |
139 | The base class implementation of {{create_environment}} passes {{templates_path}} to the created
140 | environment object to allow Jinja to find the template names specified in a ``Tasks``'s
141 | ``template_name``. By overriding this method you can extend the environment, e.g. to add filters and
142 | tests. Of course you can also completely replace the implementation, e.g. to change the way how
143 | templates are looked up.
144 |
145 | The example above simply instantiates the new ``Task`` class. Here you can optionally pass a
146 | formatter function, that is then applied to the output of Jinja. Formatters are simple string
147 | transformations, some of which are built into the ``formatters.py`` module. If you actually are
148 | writing a Python code generator you may want to clean up the generated code according to pep8,
149 | so simply pass the appropriate formatter during task instantiation:
150 |
151 | .. code-block:: python
152 |
153 | class MyGeneratorWithPep8(multigen.jinja.JinjaGenerator):
154 |
155 | # List of task objects to be processed by this generator.
156 | tasks = [
157 | MyTask(formatter=multigen.formatter.format_autopep8),
158 | ]
159 |
160 | ...
161 |
162 | Extending pymultigen
163 | --------------------
164 |
165 | Contributions welcome!
166 |
167 | Below the most typical extension scenarios are described. Note that in theory pymultigen can be used
168 | with *any* code that produces text, not just a templating engine. Take a look at the class hierarchy
169 | in ``generator.py`` to get more insights or drop me a note if this is something you plan to do.
170 |
171 | Formatters
172 | ~~~~~~~~~~
173 |
174 | Writing a new formatter is trivial: Simply create a function that transforms an input string into
175 | the nicely formatted output string. If you want to get your formatter added to pymultigen, please
176 | make sure that:
177 |
178 | * New dependencies (like autopep8 in the existing pep8 formatter) are only imported in the
179 | formatting function. This way user only pay for what they use.
180 | * Please write unittests and add your possible dependencies to the ``tests_require`` argument in
181 | ``setup.py``.
182 |
183 | There is not much more to it.
184 |
185 | Templating engine
186 | ~~~~~~~~~~~~~~~~~
187 |
188 | For a live sample, look at the Jinja2 integration in ``jinja.py``. For your templating engine ``X``,
189 | you probably have to write small ``Generator`` and ``Task`` base classes like this:
190 |
191 | .. code-block:: python
192 |
193 | class XGenerator(TemplateGenerator):
194 |
195 | def __init__(self, environment=None, **kwargs):
196 | super().__init__(**kwargs)
197 | # Add any attributes to the generator that are static with respect to a full generation
198 | # run (over all files), like a Jinja2 environment.
199 | ...
200 |
201 |
202 | class XTask(TemplateFileTask):
203 |
204 | def generate_file(self, element, filepath):
205 | """Actual generation of element."""
206 |
207 | Each element that is iterated over from the input model is eventually passed to the tasks's
208 | ``generate_file`` method. Here simply call you template engine to produce the output string. You
209 | also want to apply the optional formatter before writing the string to disk. This is how the Jinja
210 | task does it:
211 |
212 | .. code-block:: python
213 |
214 | def generate_file(self, element, filepath):
215 | template = self.environment.get_template(self.template_name)
216 | context = self.create_template_context(element=element)
217 |
218 | with open(filepath, 'wt') as file:
219 | file.write(self.formatter(template.render(**context)))
220 |
221 | The implementation shows two more things:
222 |
223 | * The template to be used is retrieved from an ``environment`` that is specific to the template
224 | engine. Such an environment is usually passed down from the ``Generator`` class to the ``Task``.
225 | * ``create_template_context`` is a function implemented in base class ``TemplateTask``. It
226 | implements the very common case of dictionaries being used as template context objects. Of course
227 | you can override this if it doesn't match your engine.
228 |
--------------------------------------------------------------------------------
/multigen/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moltob/pymultigen/3095c7579ab29199cb421b2e70d3088067a450bd/multigen/__init__.py
--------------------------------------------------------------------------------
/multigen/formatter.py:
--------------------------------------------------------------------------------
1 | """
2 | Post-generation code formatters.
3 |
4 | The module provides simple functions taking the raw input string and returning the result string
5 | after application of the corresponding format. The function can be given to the `Generator` during
6 | construction.
7 | """
8 |
9 |
10 | def format_raw(raw: str) -> str:
11 | return raw
12 |
13 |
14 | def format_autopep8(raw: str) -> str:
15 | import autopep8
16 | return autopep8.fix_code(raw, options={'max_line_length': 100})
17 |
--------------------------------------------------------------------------------
/multigen/generator.py:
--------------------------------------------------------------------------------
1 | """Small framework for multifile generation on top of another template code generator."""
2 | import logging
3 | import os
4 |
5 | from .formatter import format_raw
6 |
7 | _logger = logging.getLogger(__name__)
8 |
9 |
10 | class Generator:
11 | """
12 | Code generator from PyEcore models.
13 |
14 | Attributes:
15 | tasks:
16 | List of generator tasks to be processed as part of this generator.
17 | """
18 |
19 | tasks = []
20 |
21 | def __init__(self, **kwargs):
22 | if kwargs:
23 | raise AttributeError('Unexpected arguments: {!r}'.format(kwargs))
24 |
25 | def generate(self, model, outfolder):
26 | """
27 | Generate artifacts for given model.
28 |
29 | Attributes:
30 | model:
31 | Model for which to generate code.
32 | outfolder:
33 | Folder where code files are created.
34 | """
35 | _logger.info('Generating code to {!r}.'.format(outfolder))
36 |
37 | for task in self.tasks:
38 | for element in task.filtered_elements(model):
39 | task.run(element, outfolder)
40 |
41 |
42 | class Task:
43 | """
44 | File generation task applied to a set of model elements.
45 |
46 | Attributes:
47 | formatter: Callable converting this generator tasks raw output into a nicely formatted
48 | string.
49 | """
50 |
51 | def __init__(self, formatter=None, **kwargs):
52 | if kwargs:
53 | raise AttributeError('Unexpected arguments: {!r}'.format(kwargs))
54 | self.formatter = formatter or format_raw
55 |
56 | def run(self, element, outfolder):
57 | """Apply this task to model element."""
58 | filepath = self.relative_path_for_element(element)
59 | if outfolder and not os.path.isabs(filepath):
60 | filepath = os.path.join(outfolder, filepath)
61 |
62 | _logger.debug('{!r} --> {!r}'.format(element, filepath))
63 |
64 | self.ensure_folder(filepath)
65 | self.generate_file(element, filepath)
66 |
67 | @staticmethod
68 | def ensure_folder(filepath):
69 | dirname = os.path.dirname(filepath)
70 | if not os.path.isdir(dirname):
71 | os.makedirs(dirname)
72 |
73 | def filtered_elements(self, model):
74 | """Iterator over model elements to execute this task for."""
75 | raise NotImplementedError()
76 |
77 | def relative_path_for_element(self, element):
78 | """Returns relative file path receiving the generator output for given element."""
79 | raise NotImplementedError()
80 |
81 | def generate_file(self, element, filepath):
82 | """Actual file generation from model element."""
83 | raise NotImplementedError()
84 |
85 |
86 | class TemplateGenerator(Generator):
87 | templates_path = 'templates'
88 |
89 | def __init__(self, global_context=None, **kwargs):
90 | super().__init__(**kwargs)
91 | global_context = global_context or self.create_global_context()
92 |
93 | # pass optional global context to tasks:
94 | for task in self.tasks:
95 | task.global_context = global_context
96 |
97 | def create_global_context(self, **kwargs):
98 | """Model-wide code generation context, passed to all templates."""
99 | context = dict(**kwargs)
100 | return context
101 |
102 |
103 | class TemplateFileTask(Task):
104 | """Task to generate code via a code-generator template.
105 |
106 | Attributes:
107 | template_name: Name of the template to use for this task.
108 | global_context: Template-independent context data, propagated by generator class.
109 | """
110 | template_name = None
111 | global_context = None
112 |
113 | def create_template_context(self, element, **kwargs):
114 | """Code generation context, specific to template and current element."""
115 | context = dict(element=element, **kwargs)
116 | if self.global_context:
117 | context.update(**self.global_context)
118 | return context
119 |
--------------------------------------------------------------------------------
/multigen/jinja.py:
--------------------------------------------------------------------------------
1 | """Jinja2 support for multifile generation."""
2 | import jinja2
3 |
4 | from .generator import TemplateGenerator, TemplateFileTask
5 |
6 |
7 | class JinjaGenerator(TemplateGenerator):
8 | """Jinja2 based code generator."""
9 |
10 | def __init__(self, environment=None, **kwargs):
11 | super().__init__(**kwargs)
12 | environment_ = environment or self.create_environment()
13 |
14 | # pass Jinja environment to tasks:
15 | for task in self.tasks:
16 | task.environment = environment_
17 |
18 | def create_environment(self, **kwargs):
19 | """
20 | Return a new Jinja environment.
21 |
22 | Derived classes may override method to pass additional parameters or to change the template
23 | loader type.
24 | """
25 | return jinja2.Environment(
26 | loader=jinja2.FileSystemLoader(self.templates_path),
27 | **kwargs
28 | )
29 |
30 |
31 | class JinjaTask(TemplateFileTask):
32 | """
33 | Base class for Jinja2 based code generator tasks.
34 |
35 | Attributes:
36 | environment: Jinja2 environment, to be set by generator.
37 | """
38 |
39 | environment = None
40 |
41 | def generate_file(self, element, filepath):
42 | template = self.environment.get_template(self.template_name)
43 | context = self.create_template_context(element=element)
44 |
45 | with open(filepath, 'wt') as file:
46 | file.write(self.formatter(template.render(**context)))
47 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import sys
4 | from setuptools import setup, find_packages
5 | from setuptools.command.test import test as TestCommand
6 |
7 | if sys.version_info < (3, 3):
8 | sys.exit('Sorry, Python < 3.3 is not supported')
9 |
10 |
11 | class PyTest(TestCommand):
12 | user_options = [('pytest-args=', 'a', "Arguments to pass to py.test")]
13 |
14 | def initialize_options(self):
15 | TestCommand.initialize_options(self)
16 | self.pytest_args = []
17 |
18 | def finalize_options(self):
19 | TestCommand.finalize_options(self)
20 | self.test_args = []
21 | self.test_suite = True
22 |
23 | def run_tests(self):
24 | import pytest
25 | args = self.pytest_args if isinstance(self.pytest_args, list) else [self.pytest_args]
26 | errno = pytest.main(args)
27 | sys.exit(errno)
28 |
29 |
30 | setup(
31 | name='pymultigen',
32 | version='0.2.0',
33 | description='Multi-file frontend for single-file code generators.',
34 | long_description=open('README.rst').read(),
35 | keywords='code generator jinja multi-file',
36 | url='https://github.com/moltob/pymultigen',
37 | author='Mike Pagel',
38 | author_email='mike@mpagel.de',
39 |
40 | packages=find_packages(exclude=['tests']),
41 | install_requires=[],
42 | tests_require=['pytest', 'jinja2', 'autopep8'], # optional packages tested
43 | cmdclass={'test': PyTest},
44 |
45 | license='MIT License',
46 | classifiers=[
47 | 'Development Status :: 4 - Beta',
48 | 'Programming Language :: Python',
49 | 'Programming Language :: Python :: 3 :: Only',
50 | 'Operating System :: OS Independent',
51 | 'Intended Audience :: Developers',
52 | 'Topic :: Software Development',
53 | 'Topic :: Software Development :: Libraries',
54 | 'License :: OSI Approved :: MIT License',
55 | ]
56 | )
57 |
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | """Common fixtures and configurations for this test directory."""
2 | import os
3 | import shutil
4 | import sys
5 |
6 | import pytest
7 |
8 |
9 | @pytest.fixture('module')
10 | def cwd_module_dir():
11 | """Change current directory to this module's folder to access inputs and write outputs."""
12 | cwd = os.getcwd()
13 | os.chdir(os.path.dirname(__file__))
14 | yield
15 | os.chdir(cwd)
16 |
17 |
18 | @pytest.fixture(scope='module')
19 | def pygen_output_dir(cwd_module_dir):
20 | """Return an empty output directory, part of syspath to allow importing generated code."""
21 | path = 'output'
22 | shutil.rmtree(path, ignore_errors=True)
23 | original_sys_path = sys.path
24 | sys.path.append(path)
25 | yield path
26 | sys.path.remove(path)
27 | shutil.rmtree(path, ignore_errors=False)
28 |
--------------------------------------------------------------------------------
/tests/input/test.py.tpl:
--------------------------------------------------------------------------------
1 | print('This is a test template for element {{ element.name }}.')
--------------------------------------------------------------------------------
/tests/test_formatter.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from multigen.formatter import format_raw, format_autopep8
4 |
5 |
6 | @pytest.fixture
7 | def ugly_code():
8 | return "import a , b"
9 |
10 |
11 | def test__format_raw(ugly_code):
12 | assert format_raw(ugly_code) is ugly_code
13 |
14 |
15 | def test__format_pep8(ugly_code):
16 | assert format_autopep8(ugly_code) == "import a\nimport b\n"
17 |
--------------------------------------------------------------------------------
/tests/test_generator.py:
--------------------------------------------------------------------------------
1 | import os
2 | from unittest import mock
3 |
4 | import pytest
5 |
6 | from multigen.generator import Generator, Task, TemplateFileTask, TemplateGenerator
7 |
8 |
9 | def test__generator__generate__no_tasks():
10 | # calling empty generator not raising anything:
11 | Generator().generate(mock.sentinel.MODEL, mock.sentinel.OUTFOLDER)
12 |
13 |
14 | def test__generator__generate__tasks():
15 | mock_task1 = mock.MagicMock()
16 | mock_task2 = mock.MagicMock()
17 |
18 | elements = mock.sentinel.ELEM1, mock.sentinel.ELEM2
19 | mock_task1.filtered_elements = mock.Mock(return_value=iter(elements))
20 |
21 | # no matching elements for this task:
22 | mock_task2.filtered_elements = mock.Mock(return_value=iter(tuple()))
23 |
24 | mock_manager = mock.MagicMock()
25 | mock_manager.attach_mock(mock_task1, 'task1')
26 | mock_manager.attach_mock(mock_task2, 'task2')
27 |
28 | generator = Generator()
29 | generator.tasks = (mock_task1, mock_task2)
30 | generator.generate(mock.sentinel.MODEL, mock.sentinel.FOLDERPATH)
31 |
32 | assert mock_manager.mock_calls == [
33 | mock.call.task1.filtered_elements(mock.sentinel.MODEL),
34 | mock.call.task1.run(mock.sentinel.ELEM1, mock.sentinel.FOLDERPATH),
35 | mock.call.task1.run(mock.sentinel.ELEM2, mock.sentinel.FOLDERPATH),
36 | mock.call.task2.filtered_elements(mock.sentinel.MODEL),
37 | ]
38 |
39 |
40 | class MyTemplateGenerator(TemplateGenerator):
41 | tasks = [
42 | mock.MagicMock()
43 | ]
44 |
45 |
46 | def test__template_generator__global_context_passed_to_tasks():
47 | generator = MyTemplateGenerator(global_context=mock.sentinel.GLOBAL_CONTEXT)
48 | assert generator.tasks[0].global_context is mock.sentinel.GLOBAL_CONTEXT
49 |
50 |
51 | @mock.patch.object(TemplateGenerator, 'create_global_context',
52 | side_effect=lambda **kwargs: kwargs)
53 | def test__template_generator__global_context_constructed(mock_create_global_context):
54 | MyTemplateGenerator()
55 | assert mock_create_global_context.call_count == 1
56 |
57 |
58 | @mock.patch.object(Task, 'ensure_folder')
59 | @mock.patch.object(Task, 'relative_path_for_element', return_value='file.ext')
60 | @mock.patch.object(Task, 'generate_file')
61 | def test__task__run(mock_generate_file, mock_relative_path_for_element, mock_ensure_folder):
62 | task = Task()
63 | task.run(mock.sentinel.ELEMENT, 'somefolder')
64 |
65 | outfile = os.path.join('somefolder', 'file.ext')
66 | mock_relative_path_for_element.assert_called_once_with(mock.sentinel.ELEMENT)
67 | mock_ensure_folder.assert_called_once_with(outfile)
68 | mock_generate_file.assert_called_once_with(mock.sentinel.ELEMENT, outfile)
69 |
70 |
71 | @mock.patch.object(Task, 'ensure_folder')
72 | @mock.patch.object(Task, 'relative_path_for_element', return_value=os.path.abspath('file.ext'))
73 | @mock.patch.object(Task, 'generate_file')
74 | def test__task__run_abspath(mock_generate_file, mock_relative_path_for_element, mock_ensure_folder):
75 | task = Task()
76 | task.run(mock.sentinel.ELEMENT, 'somefolder')
77 |
78 | # no concat with with outfolder:
79 | outfile = os.path.abspath('file.ext')
80 | mock_ensure_folder.assert_called_once_with(outfile)
81 |
82 |
83 | def test__template_task__create_context():
84 | task = TemplateFileTask()
85 | task.global_context = dict(global_key=mock.sentinel.GLOBAL_VALUE)
86 | context = task.create_template_context(
87 | element=mock.sentinel.ELEMENT,
88 | test_key=mock.sentinel.TEST_VALUE
89 | )
90 |
91 | assert context['element'] is mock.sentinel.ELEMENT
92 | assert context['test_key'] is mock.sentinel.TEST_VALUE
93 | assert context['global_key'] is mock.sentinel.GLOBAL_VALUE
94 |
95 |
96 | @pytest.mark.parametrize("factory", [Generator, Task])
97 | def test__unexpected_argument(factory):
98 | with pytest.raises(AttributeError) as ex:
99 | factory(unexpected=42)
100 | assert 'unexpected' in ex.message
101 |
--------------------------------------------------------------------------------
/tests/test_jinja.py:
--------------------------------------------------------------------------------
1 | import os
2 | from unittest import mock
3 |
4 | from multigen.jinja import JinjaTask, JinjaGenerator
5 |
6 |
7 | class MyTemplateTask(JinjaTask):
8 | template_name = 'test.py.tpl'
9 |
10 | def filtered_elements(self, model):
11 | # apply to all elements in "model":
12 | return model.elements
13 |
14 | def relative_path_for_element(self, element):
15 | return '{}.py'.format(element.name)
16 |
17 |
18 | class MyGenerator(JinjaGenerator):
19 | templates_path = 'input'
20 |
21 | tasks = [
22 | MyTemplateTask()
23 | ]
24 |
25 |
26 | class MyModel:
27 | def __init__(self, elements):
28 | self.elements = elements
29 |
30 |
31 | class MyElement:
32 | def __init__(self, name):
33 | self.name = name
34 |
35 |
36 | def test__jinja_generator__integration(pygen_output_dir):
37 | model = MyModel([
38 | MyElement('A'),
39 | MyElement('B'),
40 | ])
41 |
42 | generator = MyGenerator()
43 | generator.generate(model, pygen_output_dir)
44 |
45 | with open(os.path.join(pygen_output_dir, 'A.py')) as file:
46 | generated_text = file.read()
47 | assert generated_text == 'print(\'This is a test template for element A.\')'
48 |
49 | with open(os.path.join(pygen_output_dir, 'B.py')) as file:
50 | generated_text = file.read()
51 | assert generated_text == 'print(\'This is a test template for element B.\')'
52 |
53 |
54 | @mock.patch.object(JinjaTask, 'create_template_context', side_effect=lambda **kwargs: kwargs)
55 | def test__jinja_task__generate_file(mock_create_template_context):
56 | mock_template = mock.MagicMock()
57 | mock_template.render = mock.MagicMock(return_value='rendered text')
58 | mock_environment = mock.MagicMock()
59 | mock_environment.get_template = mock.MagicMock(return_value=mock_template)
60 |
61 | task = JinjaTask()
62 | task.environment = mock_environment
63 | task.template_name = mock.sentinel.TEMPLATE_NAME
64 |
65 | mock_open = mock.mock_open()
66 | with mock.patch('multigen.jinja.open', mock_open, create=True):
67 | task.generate_file(mock.sentinel.ELEMENT, 'filepath.ext')
68 |
69 | mock_template.render.assert_called_once_with(element=mock.sentinel.ELEMENT)
70 | mock_open.assert_called_once_with('filepath.ext', 'wt')
71 | mock_open().write.assert_called_once_with('rendered text')
72 |
--------------------------------------------------------------------------------