1
\$\begingroup\$

For a specific usage I made a very simple template engine in Python, to display value. The code:

import re

class Template(object):
    """A simple template engine support only the substitute of variable"""
    def __init__(
            self, delimiter_start='[', delimiter_end=']',
            identifier=r'^[^\d\W]\w*$'):
        """To initialize the template class.

        @param delimiter_start: To specify the delimiter start (default [)
        @type param: str

        @param delimiter_end: To specify the delimiter stop (default ])
        @type param: str

        @param identifier: Identifier for variable name (default ^[^\d\W]\w*$)
        @type param: str
        """
        self.delimiter_start = delimiter_start
        self.delimiter_end = delimiter_end
        self.identifier = re.compile(identifier, re.UNICODE)

    def substitute(self, string, template={}):
        """To substitute the string.

        @param string: The string to substitute
        @type string: str

        @param template: The template
        @type string: dict

        @return:  The string substitute
        @rtype :  str

        @raise ValueError:  Identifier is not valid
        """
        words = string.split(" ")

        for index, word in enumerate(words):
            if word.startswith(self.delimiter_start) \
                    and word.endswith(self.delimiter_end):
                        word_without_delimiter = word[1:-1]
                        result = re.match(
                            self.identifier, word_without_delimiter)

                        if result is None:
                            raise ValueError(
                                'The identifier is not a valid identifier')

                        if word_without_delimiter not in template:
                            continue

                        words[index] = '{value}'.format(
                            value=template[word_without_delimiter])

        return ' '.join(words)

And the test:

import unittest

from passport.passport import Template


class TemplateTestCase(unittest.TestCase):
    """To test the template"""
    def setUp(self):
        self.template = Template()

    def test_simple(self):
        string = "To try only [var] it's display correctly"
        result = self.template.substitute(string, {'var': 'if'})

        self.assertEqual("To try only if it's display correctly", result)

    def test_multiple_variable(self):
        string = "To try only [var] it's [other_var] correctly"
        result = self.template.substitute(string, {
            'var': 'if', 'other_var': 'display'})

        self.assertEqual("To try only if it's display correctly", result)

    def test_one_word(self):
        string = '[var]'
        result = self.template.substitute(string, {'var': 'Try'})

        self.assertEqual('Try', result)

    def test_without_template(self):
        string = 'Try without a template in string no substitute'
        result = self.template.substitute(string, {'var': 'Try'})

        self.assertEqual(string, result)

    def test_with_other_delimiter(self):
        template = Template(delimiter_start='{', delimiter_end='}')
        string = "To try only {var} it's display correctly"
        result = template.substitute(string, {'var': 'if'})

        self.assertEqual("To try only if it's display correctly", result)

    def test_key_is_not_present(self):
        string = "To try only [var] it's display correctly"
        result = self.template.substitute(string)

        self.assertEqual("To try only [var] it's display correctly", result)

    def test_with_invalid_identifier(self):
        string = '[42var]'

        with self.assertRaises(ValueError):
            self.template.substitute(string, {'42var': 'Try'})

New methods will be added soon, for example add some take a file and not a string.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ What can't you just use the builtin string.format()? \$\endgroup\$ Commented Sep 21, 2016 at 2:52

1 Answer 1

1
\$\begingroup\$

1. Introduction

This code looks mostly fine to me. I make a bunch of points below, but it's a case of tightening things up rather than fixing mistakes.

2. Review

  1. The Template class is misleadlingly named. Normally the term template refers to the string into which the parameters are substituted. But an instance of the Template class actually represents the settings controlling the substitution. A better name for the class would be TemplateProcessor.

  2. Since the only thing you can do with a Template object is to call its substitute method, it would simplify things to use __call__ instead.

  3. The parameter name template to the substitute function is misleadingly named: this isn't the template, but actually the parameters to be substituted into the template. A better name would be params.

  4. It would be simpler to take the substitution parameters as keyword arguments rather than a dictionary. This would slightly simplify the callers because instead of writing:

    processor(string, {'var': 'Try'})
    

    you could write:

    processor(string, var='Try')
    
  5. The value of result is a match object and so match or m would be a better name.

  6. The code takes four steps to match a word against the syntax rules:

    if word.startswith(self.delimiter_start) \
            and word.endswith(self.delimiter_end):
                word_without_delimiter = word[1:-1]
                result = re.match(
                    self.identifier, word_without_delimiter)
    

    But this could all be done in a single step using a regular expression that matches the initial delimiter, the identifer or nonidentifier, and the final delimiter: that is,

    \[(?:([^\d\W]\w*)|(.*))\]$
    

    See the revised code below for how this regular expression can be built and used.

  7. The error message ought to include the incorrect identifier, for assistance with debugging.

  8. Instead of:

    '{value}'.format(value=template[word_without_delimiter])
    

    use the format function and write:

    format(template[word_without_delimiter])
    

3. Revised code

(Docstrings omitted—they were fine in the original.)

class TemplateProcessor(object):
    def __init__(self, delimiter_start='[', delimiter_end=']',
                 identifier=r'[^\d\W]\w*'):
        self.identifier_re = re.compile(
            '{}(?:({})|(.*)){}$'.format(re.escape(delimiter_start),
                                        identifier,
                                        re.escape(delimiter_end)),
            re.UNICODE)

    def __call__(self, string, **params):
        words = string.split(' ')
        for i, word in enumerate(words):
            m = self.identifier_re.match(word)
            if not m:
                continue
            identifier, nonidentifier = m.groups()
            if identifier is None:
                raise ValueError("{!r} is not a valid identifier"
                                 .format(nonidentifier))
            elif identifier in params:
                words[i] = format(params[identifier])
        return ' '.join(words)
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.