예제 #1
0
 def testBlocks_catchMissingClosingBlock(self):
     data = self.join_lines(
     '<block foo>',
     'content')
     with self.mock_open({'some/path': data}):
         with self.assertRaises(InvalidBlockName) as cm:
             link('some/path')
     self.assertEqual('Expected closing block called "foo" in some/path', str(cm.exception))
예제 #2
0
 def testIncludes_preventNonExistentFile(self):
     mock_open = self.mock_open({
         'docA.md': '<include docB.md>',  # docB.md doesn't exist.
     })
     with mock_open:
         with self.assertRaises(IncludeNonExistentBlock) as cm:
             link('docA.md')
     self.assertEqual('docA.md tried to include a non-existent file: docB.md', str(cm.exception))
예제 #3
0
 def testIncludes_preventCycle(self):
     mock_open = self.mock_open({
         'docA.md': '<include docB.md>',
         'docB.md': '<include docC.md>',
         'docC.md': '<include docB.md>',
     })
     with mock_open:
         with self.assertRaises(CyclicalIncludeError) as cm:
             link('docA.md')
     self.assertEqual('docA.md -> docB.md -> docC.md -> docB.md', str(cm.exception))
예제 #4
0
 def testIncludes_preventNonExistentBlock(self):
     mock_open = self.mock_open({
         'docA.md': '<include docB.md:foo>',  # docB.md:foo doesn't exist.
         'docB.md': 'contents',
     })
     with mock_open:
         with self.assertRaises(IncludeNonExistentBlock) as cm:
             link('docA.md')
     self.assertEqual(
         'docA.md tried to include a non-existent block: docB.md:foo',
         str(cm.exception))
예제 #5
0
 def testBlocks_preventBlockCalledAll(self):
     data = self.join_lines(
     '<block all>',
     'content',
     '</block all>')
     with self.mock_open({'some/path': data}):
         with self.assertRaises(InvalidBlockName) as cm:
             link('some/path')
     self.assertEqual(
             '"all" is a reserved block name, but found block named "all" in some/path',
             str(cm.exception))
예제 #6
0
 def testBlocks_preventMutlipleBlocksWithSameName(self):
     data = self.join_lines(
     '<block foo>',
     'content',
     '</block foo>',
     '<block foo>',
     'content',
     '</block foo>')
     with self.mock_open({'some/path': data}):
         with self.assertRaises(InvalidBlockName) as cm:
             link('some/path')
     self.assertEqual('Found multiple blocks with name "foo" in some/path', str(cm.exception))
예제 #7
0
    def testBlocks_multipleBlocks(self):
        data = self.join_lines(
        '<block one>',
        'def one():',
        '    return 1',
        '</block one>',
        'Intervening text',
        '<block two>',
        'def two():',
        '    return 2',
        '</block two>',
        '<block three>',
        'content',
        '</block three>')
        with self.mock_open({'some/path': data}):
            block, variables = link('some/path')

        self.assertEqual({}, variables)
        block_one = Block('some/path', 'one', [self.join_lines(
            'def one():',
            '    return 1')])
        block_two = Block('some/path', 'two', [self.join_lines(
            'def two():',
            '    return 2')])
        block_three = Block('some/path', 'three', ['content'])
        block_all = Block('some/path', 'all', [
            block_one,
            'Intervening text',
            block_two,
            block_three
        ])
        self.assertBlockEqual(block_all, block)
예제 #8
0
    def testNoBlocksAndIncludes(self):
        data = self.join_lines(
        'Hello world',
        'Second line')
        with self.mock_open({'some/path': data}):
            block, variables = link('some/path')

        self.assertEqual({}, variables)
        block_all = Block('some/path', 'all', [data])
        self.assertBlockEqual(block_all, block)
예제 #9
0
    def testBlocks_emptyBlock(self):
        data = self.join_lines(
        '<block foo>',
        '</block foo>')
        with self.mock_open({'some/path': data}):
            block, variables = link('some/path')

        self.assertEqual({}, variables)
        block_foo = Block('some/path', 'foo', [])  # Block should be empty.
        block_all = Block('some/path', 'all', [block_foo])
        self.assertBlockEqual(block_all, block)
예제 #10
0
    def testIncludes_leadingWhitespace(self):
        mock_open = self.mock_open({
            'docA.md': '    <include docB.md>',  # Indented four spaces
            'docB.md': 'content',
        })
        with mock_open:
            block, variables = link('docA.md')

        self.assertEqual({}, variables)
        block_all = Block('docA.md', 'all', ['    content'])
        self.assertBlockEqual(block_all, block)
예제 #11
0
    def testIncludes_relativeToSource(self):
        mock_open = self.mock_open({
            'path/to/docA.md': '<include docB.md>',
            'path/to/docB.md': 'content',
        })
        with mock_open:
            block, variables = link('path/to/docA.md')

        self.assertEqual({}, variables)
        block_all = Block('path/to/docA.md', 'all', ['content'])
        self.assertBlockEqual(block_all, block)
예제 #12
0
    def testVariables_handleWhitespace(self):
        mock_open = self.mock_open({
            'docA.md': '~     title   :     foo bar ',
        })
        with mock_open:
            block, variables = link('docA.md')

        self.assertEqual({
            'title': 'foo bar',
        }, variables)
        block_all = Block('docA.md', 'all', [])
        self.assertBlockEqual(block_all, block)
예제 #13
0
    def testVariables_noIncludes(self):
        mock_open = self.mock_open({
            'docA.md': self.join_lines(
                '~ title: foo',
                '~ author: bar')
        })
        with mock_open:
            block, variables = link('docA.md')

        self.assertEqual({
            'title': 'foo',
            'author': 'bar',
        }, variables)
        block_all = Block('docA.md', 'all', [])
        self.assertBlockEqual(block_all, block)
예제 #14
0
    def testBlocks_stripLeadingCharacters(self):
        data = self.join_lines(
        '# <block foo>',
        'def foo():',
        '    return None',
        '# </block foo>')
        with self.mock_open({'some/path': data}):
            block, variables = link('some/path')

        self.assertEqual({}, variables)
        block_foo = Block('some/path', 'foo', [self.join_lines(
            'def foo():',
            '    return None')])
        block_all = Block('some/path', 'all', [block_foo])
        self.assertBlockEqual(block_all, block)
예제 #15
0
    def testIncludes_stripWhiteSpaceInTag(self):
        mock_open = self.mock_open({
            'docA.md': '< include docB.md : foo >',
            'docB.md': self.join_lines(
                'content not in block',
                '<block foo>',
                'content in block',
                '</block foo>')
        })
        with mock_open:
            block, variables = link('docA.md')

        self.assertEqual({}, variables)
        block_all = Block('docA.md', 'all', ['content in block'])
        self.assertBlockEqual(block_all, block)
예제 #16
0
    def testVariables_includeVariablesFromOtherFiles(self):
        mock_open = self.mock_open({
            'docA.md': self.join_lines(
                '~ title: foo ',
                '<include docB.md>'),
            'docB.md': '~ author: bar '
        })
        with mock_open:
            block, variables = link('docA.md')

        self.assertEqual({
            'title': 'foo',
            'author': 'bar',
        }, variables)
        block_all = Block('docA.md', 'all', [])
        self.assertBlockEqual(block_all, block)
예제 #17
0
    def testIncludes_omitBlockName(self):
        mock_open = self.mock_open({
            'path/docA.md': self.join_lines(
                'content',
                '<include path/to/docB.md>',
                'more content'),
            'path/to/docB.md': 'docB content',
        })
        with mock_open:
            block, variables = link('path/docA.md')

        self.assertEqual({}, variables)
        block_all = Block('path/docA.md', 'all', [self.join_lines(
            'content',
            'docB content',
            'more content')])
        self.assertBlockEqual(block_all, block)
예제 #18
0
    def testVariables_variablesInAnyLocation(self):
        mock_open = self.mock_open({
            'docA.md': self.join_lines(
                '~ title: foo ',
                'Some text',
                '~ author: bar ',
                'More text')
        })
        with mock_open:
            block, variables = link('docA.md')

        self.assertEqual({
            'title': 'foo',
            'author': 'bar',
        }, variables)
        block_all = Block('docA.md', 'all', [self.join_lines(
            'Some text',
            'More text')])
        self.assertBlockEqual(block_all, block)
예제 #19
0
    def testBlocks_nested(self):
        data = self.join_lines(
        '<block outer>',
        'outer content',
        '<block inner>',
        'inner content',
        '</block inner>',
        'outer content',
        '</block outer>')
        with self.mock_open({'some/path': data}):
            block, variables = link('some/path')

        self.assertEqual({}, variables)
        block_inner = Block('some/path', 'inner', ['inner content'])
        block_outer = Block('some/path', 'outer', [
            'outer content',
            block_inner,
            'outer content'])
        block_all = Block('some/path', 'all', [block_outer])
        self.assertBlockEqual(block_all, block)
예제 #20
0
def publish(config, source=None, template=None, destination=None, jinja_env=None, no_write=False):
    """Given a config, performs an end-to-end publishing pipeline and returns the result:

        linking -> compiling -> templating -> writing

    NOTE: at most one of source and template can be None. If both are None, the publisher
    effectively has nothing to do; an exception is raised.

    PARAMETERS:
    config      -- Config; a context that includes variables, compiler options, and templater
                   information.
    source      -- str; path to a source file, relative to the current working directory. If None,
                   the publisher effectively becomes a templating engine.
    template    -- str; path to a Jinja template file. Templar treats the path as relative to the
                   list of template directories in config. If the template cannot be found relative
                   to those directories, Templar finally tries the path relative to the current
                   directory.

                   If template is None, the publisher effectively becomes a linker and compiler.
    destination -- str; path for the destination file.
    jinja_env   -- jinja2.Environment; if None, a Jinja2 Environment is created with a
                   FileSystemLoader that is configured with config.template_dirs. Otherwise, the
                   given Jinja2 Environment is used to retrieve and render the template.
    no_write    -- bool; if True, the result is not written to a file or printed. If False and
                   destination is provided, the result is written to the provided destination file.

    RETURNS:
    str; the result of the publishing pipeline.
    """
    if not isinstance(config, Config):
        raise PublishError(
                "config must be a Config object, "
                "but instead was type '{}'".format(type(config).__name__))

    if source is None and template is None:
        raise PublishError('When publishing, source and template cannot both be omitted.')

    variables = config.variables
    if source:
        # Linking stage.
        all_block, extracted_variables = linker.link(source)
        variables.update(extracted_variables)

        # Compiling stage.
        block_variables = {}
        for rule in config.rules:
            if rule.applies(source, destination):
                if isinstance(rule, VariableRule):
                    variables.update(rule.apply(str(all_block)))
                else:
                    all_block.apply_rule(rule)
        block_variables.update(linker.get_block_dict(all_block))
        variables['blocks'] = block_variables   # Blocks are namespaced with 'blocks'.

    # Templating stage.
    if template:
        if not jinja_env:
            jinja_env = jinja2.Environment(loader=jinja2.FileSystemLoader(config.template_dirs))
        jinja_template = jinja_env.get_template(template)
        result = jinja_template.render(variables)

        # Handle recursive evaluation of Jinja expressions.
        iterations = 0
        while config.recursively_evaluate_jinja_expressions \
                and iterations < _MAX_JINJA_RECURSIVE_DEPTH + 1 \
                and  _jinja_expression_re.search(result):
            if iterations == _MAX_JINJA_RECURSIVE_DEPTH:
                raise PublishError('\n'.join([
                    'Recursive Jinja expression evaluation exceeded the allowed '
                        'number of iterations. Last state of template:',
                    result]))
            jinja_env = jinja2.Environment(loader=jinja2.DictLoader({'intermediate': result}))
            jinja_template = jinja_env.get_template('intermediate')
            result = jinja_template.render(variables)
            iterations += 1
    else:
        # template is None implies source is not None, so variables['blocks'] must exist.
        result = variables['blocks']['all']

    # Writing stage.
    if not no_write and destination:
        destination_dir = os.path.dirname(destination)
        if destination_dir != '' and not os.path.isdir(destination_dir):
            os.makedirs(destination_dir)
        with open(destination, 'w') as f:
            f.write(result)
    return result
예제 #21
0
def publish(config, source=None, template=None, destination=None, jinja_env=None, no_write=False):
    """Given a config, performs an end-to-end publishing pipeline and returns the result:

        linking -> compiling -> templating -> writing

    NOTE: at most one of source and template can be None. If both are None, the publisher
    effectively has nothing to do; an exception is raised.

    PARAMETERS:
    config      -- Config; a context that includes variables, compiler options, and templater
                   information.
    source      -- str; path to a source file, relative to the current working directory. If None,
                   the publisher effectively becomes a templating engine.
    template    -- str; path to a Jinja template file. Templar treats the path as relative to the
                   list of template directories in config. If the template cannot be found relative
                   to those directories, Templar finally tries the path relative to the current
                   directory.

                   If template is None, the publisher effectively becomes a linker and compiler.
    destination -- str; path for the destination file.
    jinja_env   -- jinja2.Environment; if None, a Jinja2 Environment is created with a
                   FileSystemLoader that is configured with config.template_dirs. Otherwise, the
                   given Jinja2 Environment is used to retrieve and render the template.
    no_write    -- bool; if True, the result is not written to a file or printed. If False and
                   destination is provided, the result is written to the provided destination file.

    RETURNS:
    str; the result of the publishing pipeline.
    """
    if not isinstance(config, Config):
        raise PublishError(
                "config must be a Config object, "
                "but instead was type '{}'".format(type(config).__name__))

    if source is None and template is None:
        raise PublishError('When publishing, source and template cannot both be omitted.')

    variables = config.variables
    if source:
        # Linking stage.
        all_block, extracted_variables = linker.link(source)
        variables.update(extracted_variables)

        # Compiling stage.
        block_variables = {}
        for rule in config.rules:
            if rule.applies(source, destination):
                if isinstance(rule, VariableRule):
                    variables.update(rule.apply_with_destination(str(all_block), destination))
                elif rule.is_global():
                    result_content = rule.apply_with_destination(str(all_block), destination=destination)
                    all_block = Block(all_block.source_path, all_block.name, [result_content])
                else:
                    all_block.apply_rule(rule, destination=destination)
        block_variables.update(linker.get_block_dict(all_block))
        variables['blocks'] = block_variables   # Blocks are namespaced with 'blocks'.

    # Templating stage.
    if template:
        if not jinja_env:
            jinja_env = jinja2.Environment(loader=jinja2.FileSystemLoader(config.template_dirs))
        jinja_template = jinja_env.get_template(template)
        result = jinja_template.render(variables)

        # Handle recursive evaluation of Jinja expressions.
        iterations = 0
        while config.recursively_evaluate_jinja_expressions \
                and iterations < _MAX_JINJA_RECURSIVE_DEPTH + 1 \
                and  _jinja_expression_re.search(result):
            if iterations == _MAX_JINJA_RECURSIVE_DEPTH:
                raise PublishError('\n'.join([
                    'Recursive Jinja expression evaluation exceeded the allowed '
                        'number of iterations. Last state of template:',
                    result]))
            jinja_env = jinja2.Environment(loader=jinja2.DictLoader({'intermediate': result}))
            jinja_template = jinja_env.get_template('intermediate')
            result = jinja_template.render(variables)
            iterations += 1
    else:
        # template is None implies source is not None, so variables['blocks'] must exist.
        result = variables['blocks']['all']

    # Writing stage.
    if not no_write and destination:
        destination_dir = os.path.dirname(destination)
        if destination_dir != '' and not os.path.isdir(destination_dir):
            try:
                os.makedirs(destination_dir)
            except FileExistsError:
                pass
        with open(destination, 'w') as f:
            f.write(result)
    return result
예제 #22
0
 def testNonExistentSourceFile(self):
     self.mock_is_file.return_value = False
     with self.assertRaises(SourceNotFound) as cm:
         link('no/such/path')
     self.assertEqual('Could not find source file: no/such/path', str(cm.exception))