├── MANIFEST.in
├── requirements
└── development.txt
├── .gitignore
├── examples
├── pre_processed_template.py
├── strings_with_markers_as_template.py
├── named_markers.py
├── change_min_block_size.py
├── simple_example.py
├── parsing_files.py
└── save_open_dump_load.py
├── Makefile
├── run-tests.sh
├── setup.py
├── tests
├── test_parser.py
├── test_create_template.py
└── test_Templater.py
├── CHANGELOG.rst
├── templater.c
├── templater.py
├── README.rst
└── LICENSE
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include README.rst
2 | include LICENSE
3 | include CHANGELOG.rst
4 |
--------------------------------------------------------------------------------
/requirements/development.txt:
--------------------------------------------------------------------------------
1 | virtualenv
2 | virtualenvwrapper
3 | nose
4 | yanc
5 | coverage
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.sw[a-z]
3 | *.pyc
4 | build/
5 | reg_settings*
6 | _templater.so
7 | .coverage
8 |
--------------------------------------------------------------------------------
/examples/pre_processed_template.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # coding: utf-8
3 |
4 | from templater import Templater
5 |
6 | template = Templater(template=[None, '', None, '', None])
7 | print template.join(['', 'Python rules', '']) # prints 'Python rules'
8 |
--------------------------------------------------------------------------------
/examples/strings_with_markers_as_template.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # coding: utf-8
3 |
4 | from templater import Templater
5 |
6 |
7 | template = Templater(template='||| and |||', marker='|||')
8 | print template.join(['', 'red', 'blue', '']) # prints 'red and blue'
9 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | clean:
2 | rm -rf *.pyc **/*.pyc reg_settings* .coverage MANIFEST build/ dist/ \
3 | _templater.so readme.html
4 |
5 | build:
6 | python setup.py build
7 |
8 | install:
9 | python setup.py install
10 |
11 | test: clean
12 | ./run-tests.sh
13 | make clean
14 |
15 | sdist: clean test
16 | python setup.py sdist
17 |
18 | upload: clean test
19 | python setup.py sdist upload
20 | make clean
21 |
22 | readme:
23 | rst2html README.rst > readme.html
24 |
25 | .PHONY: clean build install test sdist upload readme
26 |
--------------------------------------------------------------------------------
/run-tests.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | if [ -z "$VIRTUAL_ENV" ]; then
4 | echo 'Error: you should be in a virtualenv. Create it using:'
5 | echo ' mkvirtualenv --no-site-packages templater-env'
6 | echo 'You need to have virtualenv and virtualenvwrapper installed:'
7 | echo ' pip install virtualenv virtualenvwrapper'
8 | exit 1
9 | fi
10 |
11 | clear
12 | make clean build
13 | cp $(find build/ -name _templater.so) .
14 | $VIRTUAL_ENV/bin/python `which nosetests` -dv \
15 | --with-coverage --cover-package templater --with-yanc
16 |
--------------------------------------------------------------------------------
/examples/named_markers.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # coding: utf-8
3 |
4 | from re import compile as re_compile
5 | from templater import Templater
6 |
7 |
8 | regexp_marker = re_compile(r'{{([a-zA-Z0-9_-]*)}}') # match ''{{var}}''
9 | template = Templater('{{first-var}}{{second-var}}{{third-var}}',
10 | marker=regexp_marker)
11 | # regexp marker also works for Templater.open to specify named markers
12 | result = template.parse('This is a test.') # returns a dict
13 | print result
14 |
15 | template.save('template-with-named-markers.html', marker='{{{{{}}}}}')
16 |
--------------------------------------------------------------------------------
/examples/change_min_block_size.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # coding: utf-8
3 |
4 | from templater import Templater
5 |
6 |
7 | str_1 = 'my favorite color is blue'
8 | str_2 = 'my favorite color is violet'
9 | print 'Learning from:'
10 | print ' ', str_1
11 | print ' ', str_2
12 |
13 | t = Templater() # default min_block_size = 1
14 | t.learn(str_1)
15 | t.learn(str_2)
16 | print 'Template for min_block_size=1 (default):'
17 | print ' ', t._template
18 |
19 | t = Templater(min_block_size=2)
20 | t.learn(str_1)
21 | t.learn(str_2)
22 | print 'Template for min_block_size=2:'
23 | print ' ', t._template
24 |
--------------------------------------------------------------------------------
/examples/simple_example.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # coding: utf-8
3 |
4 | from templater import Templater
5 |
6 |
7 | texts_to_learn = [' spam and eggs ', ' ham and spam ',
8 | ' white and black ']
9 | text_to_parse = texts_to_learn[-1]
10 | template = Templater()
11 | for text in texts_to_learn:
12 | print 'Learning "%s"...' % text
13 | template.learn(text)
14 | print 'Template created:', template._template
15 | print 'Parsing text "%s"...' % text_to_parse
16 | print ' Result:', template.parse(text_to_parse)
17 | print 'Filling the blanks:', template.join(['', 'yellow', 'blue', ''])
18 |
--------------------------------------------------------------------------------
/examples/parsing_files.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # coding: utf-8
3 |
4 | from time import time
5 | from glob import glob
6 | from templater import Templater
7 |
8 |
9 | files = glob('html/*.html') # You must have some .html files in html/
10 | template = Templater()
11 | print 'Time to learn'
12 | start = time()
13 | for filename in files:
14 | print ' Learning "%s"...' % filename,
15 | fp = open(filename)
16 | template.learn(fp.read())
17 | fp.close()
18 | print 'OK'
19 | end = time()
20 | print ' Time:', end - start
21 |
22 | print 'Template created:'
23 | print template._template
24 |
25 | print 'Now, work!'
26 | start = time()
27 | for filename in files:
28 | print ' Parsing "%s"...' % filename
29 | fp = open(filename)
30 | print ' Result:', template.parse(fp.read())
31 | fp.close()
32 | end = time()
33 | print ' Time: ', end - start
34 |
--------------------------------------------------------------------------------
/examples/save_open_dump_load.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # coding: utf-8
3 |
4 | from os import unlink
5 | from templater import Templater
6 |
7 |
8 | t = Templater()
9 | t.learn('spam')
10 | t.learn('eggs')
11 | t.learn('ham')
12 | t.save('my-little-template.html', marker='|||') # will put a `\n` in the EOF
13 | t.dump('my-template.tpl')
14 | print t.parse('parsing using first template object')
15 |
16 | t2 = Templater.open('my-little-template.html', marker='|||')
17 | # it removes `\n`/`\r\n` in the end of file before creating template definition
18 | print t2.parse('parsing using second template object')
19 |
20 | t3 = Templater.load('my-template.tpl')
21 | print t3.parse('parsing using third template object')
22 |
23 | # 'my-little-template.html' will have the template string with blanks filled by
24 | # '|||'
25 | # 'my-template.tpl' will have the pickle of Templater object
26 |
27 | # Removing files:
28 | unlink('my-little-template.html')
29 | unlink('my-template.tpl')
30 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # coding: utf-8
3 |
4 | from distutils.core import setup, Extension
5 |
6 |
7 | setup(name='templater',
8 | version='0.4.0',
9 | description=('Extract template (a pattern) from strings and parse other'
10 | 'strings with this pattern.'),
11 | long_description=open('README.rst').read(),
12 | author=u'Álvaro Justen',
13 | author_email='alvarojusten@gmail.com',
14 | url='https://github.com/turicas/templater/',
15 | py_modules=['templater'],
16 | ext_modules=[Extension('_templater', ['templater.c'])],
17 | keywords=['template', 'reversed template', 'template making',
18 | 'wrapper induction'],
19 | classifiers = [
20 | 'Development Status :: 3 - Alpha',
21 | 'Intended Audience :: Developers',
22 | 'Intended Audience :: Science/Research',
23 | 'License :: OSI Approved :: GNU General Public License (GPL)',
24 | 'Natural Language :: English',
25 | 'Operating System :: OS Independent',
26 | 'Programming Language :: Python :: 2.7',
27 | 'Topic :: Software Development :: Libraries :: Python Modules'
28 | ],
29 | )
30 |
--------------------------------------------------------------------------------
/tests/test_parser.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # coding: utf-8
3 |
4 | from templater import _parser
5 |
6 |
7 | def test_parsing_template_with_one_blank():
8 | text = ' testing '
9 | template = [None]
10 | result = _parser(template, text)
11 | expected = [text]
12 | assert result == expected
13 |
14 | def test_parsing_template_with_three_blanks():
15 | text = ' testing '
16 | template = [None, ' ', None, ' ', None]
17 | result = _parser(template, text)
18 | expected = ['', 'testing', '']
19 | assert result == expected
20 |
21 | def test_parsing_four_blanks():
22 | text = ' testing and programming '
23 | template = [None, ' ', None, ' and ', None, ' ', None]
24 | result = _parser(template, text)
25 | expected = ['', 'testing', 'programming', '']
26 | assert result == expected
27 |
28 | def test_parsing_non_parseable_text_should_raise_ValueError():
29 | text = ' testing programming '
30 | template = [' ', None, ' and ', None, ' ']
31 | try:
32 | result = _parser(template, text)
33 | except ValueError:
34 | pass
35 | else:
36 | assert 'ValueError not raised!' == False
37 |
38 | def test_parsing_last_blank():
39 | text = ' testing and programming blah'
40 | template = [None, ' ', None, ' and ', None, ' ', None]
41 | result = _parser(template, text)
42 | expected = ['', 'testing', 'programming', 'blah']
43 | assert result == expected
44 |
45 | def test_parsing_first_blank():
46 | text = 'blah testing and programming '
47 | template = [None, ' ', None, ' and ', None, ' ', None]
48 | result = _parser(template, text)
49 | expected = ['blah', 'testing', 'programming', '']
50 | assert result == expected
51 |
--------------------------------------------------------------------------------
/CHANGELOG.rst:
--------------------------------------------------------------------------------
1 | Changelog
2 | =========
3 |
4 | Version 0.4.0
5 | -------------
6 |
7 | - Released at: 2012-04-24 15:07:58.
8 | - `New feature `_: named markers
9 | (new method ``add_headers``).
10 | - `Bug fixed `_: last blank
11 | wasn't being filled.
12 | - `Bug fixed `_: ignoring
13 | ``\r\n``/``\n`` in the end of files in ``save`` and ``open`` methods.
14 | - `New Feature `_: added method
15 | ``parse_file``.
16 | - `Enhancement `_: renamed
17 | ``tolerance`` to ``min_block_size``.
18 | - Enhancement: refactored tests.
19 | - Enhancement: refactored ``README.rst``.
20 | - Enhancement: created ``CHANGELOG.rst``.
21 |
22 |
23 | Version 0.3.0
24 | -------------
25 |
26 | - Released at: 2012-04-20 20:09:52.
27 | - Enhancement: method ``save`` renamed to ``dump``.
28 | - New feature: new methods ``save`` and ``open`` - saves and open the template
29 | to/from a document (file) - do not pickle the entire object as ``dump`` and
30 | ``load``.
31 | - Enhancement: using nose coverage plugin.
32 |
33 |
34 | Version 0.2.1
35 | -------------
36 |
37 | - Released at: 2012-04-30 18:34:35.
38 | - Bug fixed: replaced ``README.markdown`` with ``README.rst`` in
39 | ``MANIFEST.in``.
40 |
41 |
42 | Version 0.2.0
43 | -------------
44 |
45 | - Released at: 2012-04-20 18:30:23.
46 | - `New feature `_: ``tolerance``
47 | (min block size).
48 | - Bug fixed: ``save`` and ``load`` now pickle the entire object (instead of
49 | only template definition).
50 |
51 |
52 | Version 0.1.1
53 | -------------
54 |
55 | - Released at: 2012-04-20 15:08:05.
56 | - Enhancement: ``README.markdown`` is now ``README.rst`` (reStructuredText).
57 |
58 |
59 | Version 0.1.0
60 | -------------
61 |
62 | - Released at: 2012-04-20 14:57:44.
63 | - First working version (``learn`` and ``parse`` methods created).
64 | - New feature: ``save`` and ``load`` methods created.
65 | - New feature: ``join`` method created.
66 |
--------------------------------------------------------------------------------
/tests/test_create_template.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # coding: utf-8
3 |
4 | from templater import _create_template
5 |
6 |
7 | def test_different_strings_should_return_one_blank():
8 | str_1 = '1'
9 | str_2 = '2'
10 | result = _create_template(str_1, str_2, (0, len(str_1)), (0, len(str_2)))
11 | expected = [None]
12 | assert result == expected
13 |
14 | def test_equal_strings_should_return_one_block():
15 | str_1 = ' asd '
16 | str_2 = ' asd '
17 | result = _create_template(str_1, str_2, (0, len(str_1)), (0, len(str_2)))
18 | expected = [None, ' asd ', None]
19 | assert result == expected
20 |
21 | def test_different_strings_with_same_size():
22 | str_1 = 'a1'
23 | str_2 = 'a2'
24 | result_1 = _create_template(str_1, str_2, (0, len(str_1)), (0, len(str_2)))
25 | expected_1 = [None, 'a', None]
26 | assert result_1 == expected_1
27 |
28 | str_1 = '1a'
29 | str_2 = '2a'
30 | result_2 = _create_template(str_1, str_2, (0, len(str_1)), (0, len(str_2)))
31 | expected_2 = [None, 'a', None]
32 | assert result_2 == expected_2
33 |
34 | str_1 = ' asd '
35 | str_2 = ' qwe '
36 | result_3 = _create_template(str_1, str_2, (0, len(str_1)), (0, len(str_2)))
37 | expected_3 = [None, ' ', None, ' ', None]
38 | assert result_3 == expected_3
39 |
40 | def test_different_strings_with_different_size():
41 | str_1 = ' asd '
42 | str_2 = ' qwe123 '
43 | result = _create_template(str_1, str_2, (0, len(str_1)), (0, len(str_2)))
44 | expected = [None, ' ', None, ' ', None]
45 | assert result == expected
46 |
47 | def test_different_strings_with_one_of_size_zero():
48 | str_1 = ' asd '
49 | str_2 = ' '
50 | result = _create_template(str_1, str_2, (0, len(str_1)), (0, len(str_2)))
51 | expected = [None, ' ', None, ' ', None]
52 | assert result == expected
53 |
54 | def test_more_than_one_variable_with_same_size():
55 | str_1 = ' asd 123 '
56 | str_2 = ' qwe 456 '
57 | result = _create_template(str_1, str_2, (0, len(str_1)), (0, len(str_2)))
58 | expected = [None, ' ', None, ' ', None, ' ', None]
59 | assert result == expected
60 |
61 | def test_more_than_one_variable_with_different_sizes():
62 | str_1 = ' asdfgh 123 '
63 | str_2 = ' qwe 456qwe '
64 | result = _create_template(str_1, str_2, (0, len(str_1)), (0, len(str_2)))
65 | expected = [None, ' ', None, ' ', None, ' ', None]
66 | assert result == expected
67 |
68 | def test_min_block_size():
69 | str_1 = 'my favorite color is blue'
70 | str_2 = 'my favorite color is violet'
71 | result = _create_template(str_1, str_2, (0, len(str_1)), (0, len(str_2)),
72 | min_block_size=1)
73 | expected = [None, 'my favorite color is ', None, 'l', None, 'e', None]
74 | assert result == expected
75 |
76 | result = _create_template(str_1, str_2, (0, len(str_1)), (0, len(str_2)),
77 | min_block_size=2)
78 | expected = [None, 'my favorite color is ', None]
79 | assert result == expected
80 |
--------------------------------------------------------------------------------
/templater.c:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | /*
4 | Original code from:
5 | author='Adrian Holovaty',
6 | author_email='adrian@holovaty.com',
7 | http://templatemaker.googlecode.com/
8 |
9 | longest_match() and longest_match_shifter()
10 |
11 | Returns the length of the longest common substring (LCS) in the strings
12 | a and b.
13 |
14 | Sets a_offset to the index of a where the LCS begins.
15 | Sets b_offset to the index of b where the LCS begins.
16 |
17 | If there is NO common substring, it returns 0 and sets both
18 | a_offset and b_offset to -1.
19 |
20 | The strings do not have to be equal length.
21 |
22 | The algorithm works by comparing one character at a time and "shifting" the
23 | strings so that different characters are compared. For example, given
24 | a="ABC" and b="DEF", picture these alignments:
25 |
26 | (Shift a to the right)
27 | -------------------------------------------------------
28 | a | ABC ABC ABC
29 | b | DEF DEF DEF
30 | shift index | 0 1 2
31 | possible LCS | 3 2 1
32 | comparisons | AD, BE, CF AE, BF AF
33 |
34 | (Shift b to the right)
35 | -------------------------------------------------------
36 | | ABC ABC ABC
37 | | DEF DEF DEF
38 | shift index | 0 1 2
39 | possible LCS | 3 2 1
40 | comparisons | AD, BE, CF BD, CE CD
41 |
42 | The algorithm short circuits based on the best_size found so far. For example,
43 | given a="ABC" and b="ABC", the first cycle of comparisons (AA, BB, CC) would
44 | result in a best_size=3. Because the algorithm starts with zero shift (i.e.,
45 | it starts with the highest possible LCS) and adds 1 to the shift index each
46 | time through, it can safely exit without doing any more comparisons.
47 |
48 | This algorithm is O^(m + m-1 + m-2 + ... + 1 + n + n-1 + n-2 + ... + 1), where
49 | m and n are the length of the two strings. Due to short circuiting, the
50 | algorithm could potentially finish after the very
51 | first set of comparisons. The algorithm is slowest when the LCS is smallest,
52 | and the algorithm is fastest when the LCS is biggest.
53 |
54 | longest_match_shifter() performs "one side" of the shift -- e.g., "Shift a to
55 | the right" in the above illustration. longest_match() simply calls
56 | longest_match_shifter() twice, flipping the strings.
57 | */
58 |
59 | int longest_match_shifter(char* a, char* b, int a_start, int a_end, int b_start, int b_end, int best_size, int* a_offset, int* b_offset) {
60 | int i, j, k;
61 | unsigned int current_size;
62 |
63 | for (i = b_start, current_size = 0; i < b_end; i++, current_size = 0) { // i is b starting index.
64 | if (best_size >= b_end - i) break; // Short-circuit. See comment above.
65 | for (j = i, k = a_start; k < a_end && j < b_end; j++, k++) { // k is index of a, j is index of b.
66 | if (a[k] == b[j]) {
67 | if (++current_size > best_size) {
68 | best_size = current_size;
69 | *a_offset = k - current_size + 1;
70 | *b_offset = j - current_size + 1;
71 | }
72 | }
73 | else {
74 | current_size = 0;
75 | }
76 | }
77 | }
78 | return best_size;
79 | }
80 |
81 | // a_offset and b_offset are relative to the *whole* string, not the substring
82 | // (as defined by a_start and a_end).
83 | // a_end and b_end are (the last index + 1).
84 | int longest_match(char* a, char* b, int a_start, int a_end, int b_start, int b_end, int* a_offset, int* b_offset) {
85 | unsigned int best_size;
86 | *a_offset = -1;
87 | *b_offset = -1;
88 | best_size = longest_match_shifter(a, b, a_start, a_end, b_start, b_end, 0, a_offset, b_offset);
89 | best_size = longest_match_shifter(b, a, b_start, b_end, a_start, a_end, best_size, b_offset, a_offset);
90 | return best_size;
91 | }
92 |
93 | static PyObject * function_longest_match(PyObject *self, PyObject *args) {
94 | char* a;
95 | char* b;
96 | int a_offset, b_offset, lena, lenb;
97 | unsigned int best_size;
98 |
99 | if (!PyArg_ParseTuple(args, "s#s#", &a, &lena, &b, &lenb))
100 | return NULL;
101 |
102 | best_size = longest_match(a, b, 0, lena, 0, lenb, &a_offset, &b_offset);
103 | return Py_BuildValue("(iii)", best_size, a_offset, b_offset);
104 | }
105 |
106 | static PyMethodDef ModuleMethods[] = {
107 | {"longest_match", function_longest_match, METH_VARARGS, "Given two strings, determines the longest common substring and returns a tuple of (best_size, a_offset, b_offset)."},
108 | {NULL, NULL, 0, NULL} // sentinel
109 | };
110 |
111 | PyMODINIT_FUNC init_templater(void) {
112 | (void) Py_InitModule("_templater", ModuleMethods);
113 | }
114 |
--------------------------------------------------------------------------------
/templater.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # coding: utf-8
3 |
4 | from cPickle import dump as pickle_dump, load as pickle_load
5 | from re import compile as re_compile
6 | from _templater import longest_match as lcs
7 |
8 |
9 | type_regexp = type(re_compile(''))
10 | MARKER = '|||'
11 | NAMED_MARKER = '{{{{{}}}}}' # '{{sample-var}}'
12 |
13 | class Templater(object):
14 | def __init__(self, template=None, marker='|||', min_block_size=1):
15 | self._template = template
16 | self._min_block_size = min_block_size
17 | self._marker = marker
18 | self._headers = None
19 | self._named_markers = False
20 | if type(template) in (str, unicode):
21 | self._template, self._named_markers, self._headers = \
22 | _create_template_from_string(template, marker)
23 |
24 | def learn(self, new_text):
25 | if self._named_markers:
26 | raise NotImplementedError("Actually you can't learn in a template "
27 | "with named markers")
28 | if self._template is None:
29 | text = new_text
30 | else:
31 | text = '\0\0\0'.join(filter(lambda x: x is not None, self._template))
32 | self._template = _create_template(new_text, text, (0, len(new_text)),
33 | (0, len(text)), self._min_block_size)
34 |
35 | def parse(self, text):
36 | result = _parser(self._template, text)
37 | if self._named_markers:
38 | return dict(zip(self._headers, result))
39 | else:
40 | return result
41 |
42 | def join(self, elements):
43 | elements_length = len(elements)
44 | variables_length = self._template.count(None)
45 | if elements_length != variables_length:
46 | error_msg = ("Wrong number of variables (passed: {}, expected: "
47 | "{})".format(elements_length, variables_length))
48 | raise AttributeError(error_msg)
49 | text = self._template[:]
50 | variable_index = 0
51 | for index, element in enumerate(text):
52 | if element is None:
53 | text[index] = elements[variable_index]
54 | variable_index += 1
55 | return ''.join(text)
56 |
57 | def dump(self, filename):
58 | """Dump the template object to ``filename`` so you can re-use it later.
59 |
60 | This method uses cPickle to serialize internal template model, so you
61 | don't need to pass through the learn process everytime you need to
62 | parse data. It's worth using this method since learning process
63 | generally cost a lot of time compared to parsing.
64 | """
65 | fp = open(filename, 'w')
66 | pickle_dump(self, fp)
67 | fp.close()
68 |
69 | @staticmethod
70 | def load(filename):
71 | """Load a template from ``filename``, return ``Templater`` object.
72 |
73 | This method must be used in pair with ``Templater.dump`` - it loads
74 | the template definition from a file using cPickle, creates a
75 | ``Templater`` object with the definition and returns it.
76 | """
77 | fp = open(filename)
78 | processed_template = pickle_load(fp)
79 | fp.close()
80 | return processed_template
81 |
82 | def save(self, filename, marker=None, headers=None):
83 | """Save the template to ``filename`` using ``marker`` as marker.
84 |
85 | This method looks like ``Templater.dump``, the difference is that it
86 | does not save/pickle the entire ``Templater`` object - it uses
87 | ``Templater.join`` to fill the blanks with ``marker`` and then save the
88 | resulting string to ``filename``.
89 | It should be used in pair with ``Templater.open``.
90 | """
91 | if not self._named_markers:
92 | if marker is None:
93 | marker = self._marker
94 | blanks = [marker] * self._template.count(None)
95 | else:
96 | if headers is not None and len(headers) != len(self._headers):
97 | raise AttributeError('Incorrect number of headers (passed:'
98 | ' {}, expected: {})'.format(
99 | len(headers), len(self._headers)))
100 | if marker is None:
101 | marker = NAMED_MARKER
102 | if headers is None:
103 | headers = self._headers
104 | blanks = [marker.format(header) for header in headers]
105 | fp = open(filename, 'w')
106 | fp.write(self.join(blanks) + '\n')
107 | fp.close()
108 |
109 | @staticmethod
110 | def open(filename, marker=MARKER):
111 | """Open ``filename``, split in ``marker``, return ``Templater`` object.
112 |
113 | You should use this method in pair with ``Templater.save`` or if you
114 | want to write the templates with your own hands. It works similar to
115 | ``Templater.load``, except by the fact that ``load`` saves the entire
116 | ``Templater`` object and ``open`` saves only the template string,
117 | filling the blanks with ``marker``.
118 | """
119 | fp = open(filename)
120 | contents = fp.read()
121 | fp.close()
122 | if contents[-2:] == '\r\n':
123 | contents = contents[:-2]
124 | elif contents[-1] == '\n':
125 | contents = contents[:-1]
126 | template = Templater(template=contents, marker=marker)
127 | return template
128 |
129 | def add_headers(self, headers):
130 | """Add/modifiy headers (names of markers) to a template."""
131 | if len(headers) != self._template.count(None):
132 | raise ValueError("Wrong number of headers (passed: {}, expected: "
133 | "{})".format(len(headers), self._template.count(None)))
134 | self._named_markers = True
135 | self._headers = headers
136 |
137 | def parse_file(self, filename):
138 | """Open, read a file and call ``Templater.parse`` with its contents.
139 |
140 | If the file ends with ``\n`` or ``\r\n``, it'll be removed.
141 | """
142 | fp = open(filename)
143 | contents = fp.read()
144 | fp.close()
145 | if contents[-2:] == '\r\n':
146 | contents = contents[:-2]
147 | elif contents[-1] == '\n':
148 | contents = contents[:-1]
149 | return self.parse(contents)
150 |
151 |
152 | def _parser(template, text):
153 | result = []
154 | text_index = 0
155 | last_element_index = len(template) - 1
156 | for index, element in enumerate(template):
157 | if element is None:
158 | if index != last_element_index:
159 | new_index = text.index(template[index + 1], text_index)
160 | else:
161 | new_index = None
162 | result.append(text[text_index:new_index])
163 | text_index = new_index
164 | else:
165 | element_length = len(element)
166 | assert text[text_index:text_index + element_length] == element
167 | text_index += element_length
168 | return result
169 |
170 | def _create_template(str_1, str_2, (start_1, end_1), (start_2, end_2),
171 | min_block_size=1):
172 | lcs_size, lcs_1_start, lcs_2_start = lcs(str_1[start_1:end_1],
173 | str_2[start_2:end_2])
174 | if lcs_size < min_block_size:
175 | return [None]
176 | else:
177 | common = str_1[start_1 + lcs_1_start:start_1 + lcs_1_start + lcs_size]
178 | return _create_template(str_1, str_2,
179 | (start_1, start_1 + lcs_1_start),
180 | (start_2, start_2 + lcs_2_start),
181 | min_block_size) + \
182 | [str_1[start_1 + lcs_1_start:start_1 + lcs_1_start + lcs_size]] + \
183 | _create_template(str_1, str_2,
184 | (start_1 + lcs_1_start + lcs_size, end_1),
185 | (start_2 + lcs_2_start + lcs_size, end_2),
186 | min_block_size)
187 |
188 | def _create_template_from_string(text, marker):
189 | named_markers = type(marker) == type_regexp
190 | if named_markers:
191 | results = marker.split(text)
192 | tokens, headers = [x for x in results[::2] if x], results[1::2]
193 | else:
194 | tokens = [x for x in text.split(marker) if x != '']
195 | template = list(sum(zip([None] * len(tokens), tokens), ())) + [None]
196 | if named_markers:
197 | if template.count(None) != len(headers):
198 | raise ValueError("Template error! Verify if markers are separated"
199 | " at least by one character")
200 | return template, True, headers
201 | return template, False, None
202 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | Templater
2 | =========
3 |
4 | Introduction
5 | ------------
6 |
7 | Given some strings (or files), this library extracts a common template between
8 | them (method ``learn``) -- some people call it "reverse templating". Having
9 | your template created, you can parse other strings/files using it - the
10 | ``parse`` method will return only what changes in this file (the "blanks"). It
11 | does something like the opposite of what template libraries (such as
12 | `Jinja `_) do. But for now, it only can identify
13 | fixed variables (it can't create ``for`` and ``if`` blocks, for example).
14 |
15 | If you have the template and the "blanks" you can also fill the blanks with
16 | the method ``join`` - it'll return a string with the template filled. There are
17 | some other features:
18 |
19 | - If you don't want/need to ``Templater`` (the main class) create the template
20 | for you, you can pass a pre-processed template (created manually or created
21 | before using ``learn`` and saved somewhere).
22 | - You can split the learning and parsing process, since the learning process
23 | generally is executed one time and takes a lot of time compared to parsing
24 | process. To turn this process handy, ``Templater`` has the methods ``dump``,
25 | ``save``, ``load`` and ``open``, so you can learn and save a template
26 | definition for later loading and parsing how many times you want (you can
27 | also load, learn more and save).
28 |
29 | `templater `_ is simple to use, easy to
30 | learn and does the hard work for you (for example: part of the learning
31 | algorithm is implemented in C for performance). Do you have 5 minutes? So learn
32 | with the `Examples`_.
33 |
34 |
35 | Installation
36 | ------------
37 |
38 | `templater is available at PyPI `_, so
39 | installing it is as simple as executing::
40 |
41 | pip install templater
42 |
43 | Or you can download the latest version and install it using ``setup.py``::
44 |
45 | git clone https://turicas@github.com/turicas/templater.git
46 | cd templater
47 | python setup.py build install
48 |
49 |
50 | Terminology
51 | -----------
52 |
53 | There are some definitions/concepts we should explicit here:
54 |
55 | - **Template**: the whole object (instance of ``Templater``).
56 | - **Document**: a string or file that have some kind of pattern. You'll use
57 | documents to make a template object learn and recognize these patterns, so
58 | later you can use the template object to parse a document and get only the
59 | information that is not "static".
60 | - **Blocks**: the fixed parts of a template. Can change (in number and size)
61 | when ``learn`` is run.
62 | - **Blanks**: also called holes or variables, blanks are the parts in a
63 | template that changes between documents with the same template.
64 | - **Template definition**: the information stored in a template that defines it
65 | (it is a Python list with a very simple grammar that describes how the
66 | template is composed).
67 | - **Markers**: when you want to save a template, something should be put
68 | between blocks to "mark" the blanks (so the template definition can be
69 | reconstructed later).
70 | - **Named marker**: a marker plus a header is called a named marker. They are
71 | handy and more legible since you can access the "blanks" by names instead of
72 | indexes.
73 |
74 | Doubts? Don't worry, see the `Examples`_ and you'll get it.
75 |
76 |
77 | Examples
78 | --------
79 |
80 | All you need to know is below (and in the ``examples`` directory)::
81 |
82 | >>> from templater import Templater
83 | >>> documents_to_learn = [' spam and eggs ', ' ham and spam ',
84 | ' white and black '] # list of documents
85 | >>> template = Templater()
86 | >>> for document in documents_to_learn:
87 | ... template.learn(document)
88 | ...
89 |
90 | >>> print 'Template created:', template._template # template definition
91 | Template created: [None, ' ', None, ' and ', None, ' ', None]
92 |
93 | >>> document_to_parse = ' yellow and blue '
94 | >>> print 'Parsing other document:', template.parse(document_to_parse)
95 | Parsing other document: ['', 'yellow', 'blue', '']
96 |
97 | >>> print 'Filling the blanks:', template.join(['', 'red', 'orange', ''])
98 | Filling the blanks: red and orange
99 |
100 | You can pass pre-processed templates as a list (blanks are ``None``, blocks are
101 | strings)::
102 |
103 | >>> t2 = Templater(template=[None, 'Music: ', None, ', Band: ', None])
104 | >>> print t2.join(['', 'Welcome to the Jungle', 'Guns and Roses'])
105 | Music: Welcome to the Jungle, Band: Guns and Roses
106 |
107 | ...or you can pass a string with markers, then ``Templater`` will create the
108 | list for you::
109 |
110 | >>> t3 = Templater(template='language=#,cool=#', marker='#')
111 | >>> print t3.join(['', 'Python', 'YES', ''])
112 | language=Python,cool=YES
113 |
114 | Saving and opening templates is easy::
115 |
116 | >>> template.save('my-first-template.html', marker='|||')
117 | >>> # and some time later...
118 | >>> loaded_template = Templater.open('my-first-template.html', marker='|||')
119 | >>> print loaded_template.parse(' Romeo and Juliet ')
120 | ['', 'Romeo', 'Juliet', '']
121 |
122 | The difference between ``save`` and ``dump`` is that ``save`` stores the
123 | template string, filling the blanks with a marker and ``dump`` saves the whole
124 | ``Templater`` object with ``cPickle``. The pairs are:
125 |
126 | - ``save`` and ``open`` (raw template string filled with marker)
127 | - ``load`` and ``dump`` (whole object)
128 |
129 | **Note**: ``save`` always add a ``\n`` to the end of file; ``load``
130 | deletes trailing ``\r\n`` or ``\n`` in the end of file (if any).
131 |
132 | **Note-2**: when passing a pre-processed template (using ``Templater``
133 | initializer or ``Templater.open``) make sure it **starts and ends** with a
134 | marker.
135 |
136 | If you are getting a lot of blanks you can configure the learning process: just
137 | adjust ``min_block_size`` - it's the minimum number of characters permitted to
138 | create a new block in template::
139 |
140 | >>> str_1 = 'my favorite color is blue'
141 | >>> str_2 = 'my favorite color is violet'
142 | >>> t = Templater() # default min_block_size = 1
143 | >>> t.learn(str_1)
144 | >>> t.learn(str_2)
145 | >>> print t._template
146 | [None, 'my favorite color is ', None, 'l', None, 'e', None]
147 |
148 | We don't want that ``'l'`` and ``'e'`` there, right? So::
149 |
150 | >>> t = Templater(min_block_size=2)
151 | >>> t.learn(str_1)
152 | >>> t.learn(str_2)
153 | >>> print t._template
154 | [None, 'my favorite color is ', None]
155 |
156 |
157 | You can also add "headers" to your template - the headers will be the name of
158 | your markers, so you'll have a template with named markers and ``parse`` will
159 | return a ``dict`` instead of ``list``. It's more legible than using list
160 | indices, let's see::
161 |
162 | >>> import re
163 | >>> # Let's create a regexp that cases with '{{var}}' (it'll be our marker)
164 | >>> regexp_marker = re.compile(r'{{([a-zA-Z0-9_-]*)}}')
165 | >>> template = Templater('{{first-var}}{{second-var}}{{third-var}}',
166 | marker=regexp_marker)
167 | >>> # The template knows the name of each marker just using the regexp provided
168 | >>> # Passing marker as regexp to specify named markers also work for Templater.open
169 |
170 | >>> print template.parse('This is a test.')
171 | {'second-var': ' is ', 'third-var': ' a test.', 'first-var': 'This '}
172 |
173 | >>> # To save the template with named markers we need to provide a Python string.
174 | >>> # Templater will call .format() of this string for each marker with its name
175 | >>> template.save('template-with-named-markers.html', marker='--{}--')
176 | >>> # Will save '--first-var----second-var----third-var--\n'
177 |
178 | And if you have a template without headers, just add to it with ``add_headers``
179 | method::
180 |
181 | >>> t = Templater('+| + | + |
+', marker='+')
182 | >>> t.parse('| hello | world |
')
183 | ['', 'hello', 'world', '']
184 |
185 | >>> t.add_headers(['before', 'first-column', 'second-column', 'after'])
186 | >>> t.parse('| hello | world |
')
187 | {'after': '', 'before': '', 'first-column': 'hello', 'second-column': 'world'}
188 |
189 | **Note**: named markers have a problem: you can't run ``learn`` if you use them.
190 |
191 |
192 | Notes
193 | -----
194 |
195 | I really want to know if you are using this project and what is your impression
196 | about it. If you have new ideas of features, discovered bugs or just want to
197 | say "thank you, I'm using it!", please contact me at
198 | `alvarojusten at gmail `_.
199 |
200 | If you want to code some stuff,
201 | just `fork it on GitHub `_ and create a
202 | pull request. Some technical notes for you:
203 |
204 | - This project uses `Test-Driven Development
205 | `_.
206 |
207 | - The tests are run using Python 2.7.2 on Ubuntu 11.10 amd64.
208 | - You can see the changes between versions in
209 | `CHANGELOG.rst `_.
210 | - This project uses `semantic versioning `_ (thanks,
211 | `Tom Preston-Werner `_).
212 |
213 |
214 |
215 | Author
216 | ------
217 |
218 | This software is developed by
219 | `Álvaro Justen aka Turicas `_.
220 |
221 | Many thanks to `Adrian Holovaty `_ - he created
222 | `templatemaker `_, the project which
223 | ``templater`` was inspired in/forked from - and to
224 | `Escola de Matemática Aplicada (Fundação Getúlio Vargas) `_
225 | which gives me interesting problems to solve. :-)
226 |
227 |
228 | License
229 | -------
230 |
231 | `GPL version 2 `_
232 |
--------------------------------------------------------------------------------
/tests/test_Templater.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # coding: utf-8
3 |
4 | from os import unlink
5 | from re import compile as re_compile
6 | from templater import Templater, MARKER, NAMED_MARKER
7 |
8 |
9 | regexp_marker = re_compile(r'{{([a-zA-Z0-9_-]*)}}')
10 |
11 | def read_file_and_delete(filename):
12 | fp = open(filename)
13 | contents = fp.read()
14 | fp.close()
15 | unlink(filename)
16 | return contents
17 |
18 | def write_file(filename, contents):
19 | fp = open(filename, 'w')
20 | fp.write(contents)
21 | fp.close()
22 |
23 | def test_new_learn_text_trying_to_delete_some_variable():
24 | template = Templater()
25 | template.learn(' a and b ')
26 | template.learn(' c and d ')
27 | template.learn(' e and ')
28 | result = template._template
29 | expected = [None, ' ', None, ' and ', None, ' ', None]
30 | assert result == expected
31 |
32 | def test_parse_should_return_a_list_with_the_blanks_contents():
33 | template = Templater()
34 | template.learn('a b d')
35 | template.learn('a e d')
36 | result = template.parse('a b c d')
37 | expected = ['', 'b c', '']
38 | assert result == expected
39 |
40 | def test_Templater_parse_file_should_open_and_parse_a_file_from_filename():
41 | template = Templater('+++', marker='+')
42 | fp = open('test.html', 'w')
43 | fp.write('testing parsing files\n')
44 | fp.close()
45 | result_1 = template.parse_file('test.html')
46 | expected = ['testing ', ' parsing ', ' files']
47 | unlink('test.html')
48 | assert expected == result_1
49 |
50 | fp = open('test.html', 'w')
51 | fp.write('testing parsing files\r\n')
52 | fp.close()
53 | result_2 = template.parse_file('test.html')
54 | unlink('test.html')
55 | assert expected == result_2
56 |
57 | def test_join_should_fill_the_blanks_with_elements_received():
58 | template = Templater()
59 | template.learn('a b d')
60 | template.learn('a e d')
61 | parsed = template.parse('a b c d')
62 | result = template.join(parsed)
63 | expected = 'a b c d'
64 | assert result == expected
65 |
66 | def test_join_with_less_parameters_than_variables_should_raise_AttributeError():
67 | template = Templater()
68 | template.learn('a b d')
69 | template.learn('a e d')
70 | try:
71 | result = template.join([''])
72 | except AttributeError:
73 | pass
74 | else:
75 | assert 'AttributeError not raised!' == False
76 |
77 | def test_Templater_should_optionally_import_pre_processed_template():
78 | pre_processed = [None, '', None, '', None]
79 | template = Templater(template=pre_processed)
80 | assert template._template == pre_processed
81 | assert template.join(['', 'python', '']) == 'python'
82 |
83 | def test_Templater_should_optionally_import_template_as_string_with_marks():
84 | template = Templater(template='|||', marker='|||')
85 | result_template = template._template
86 | assert result_template == [None, '', None, '', None]
87 | assert template.join(['', 'spam eggs', '']) == 'spam eggs'
88 |
89 | def test_Templater_dump_and_load_should_pickle_and_unpickle():
90 | processed_template = [None, '', None, '', None, '', None]
91 | template = Templater(template=processed_template, min_block_size=6)
92 | template.dump('my-template.tpl')
93 | t2 = Templater.load('my-template.tpl')
94 | unlink('my-template.tpl')
95 | result_1 = t2._template
96 | expected_1 = processed_template
97 | result_2 = t2._min_block_size
98 | expected_2 = 6
99 | assert expected_1 == result_1
100 | assert expected_2 == result_2
101 |
102 | def test_should_be_able_to_adjust_minimum_size_of_a_block():
103 | t = Templater(min_block_size=2)
104 | t.learn('git and pyth')
105 | t.learn('eggs and spam')
106 | expected = [None, ' and ', None]
107 | result = t._template
108 | assert expected == result
109 |
110 | def test_Templater_save_should_save_template_as_a_raw_file_with_markers():
111 | processed_template = [None, '', None, '', None, '', None]
112 | t = Templater(template=processed_template)
113 | t.save('test.html', marker='|||')
114 | result = read_file_and_delete('test.html')
115 | expected = '||||||||||||\n'
116 | assert expected == result
117 |
118 | def test_Templater_should_accept_named_markers_in_init():
119 | template = '{{start}}{{middle}}{{end}}'
120 | t = Templater(template=template, marker=regexp_marker)
121 | result_1 = t._template
122 | expected_1 = [None, '', None, '', None]
123 | assert expected_1 == result_1
124 |
125 | result_2 = t._headers
126 | expected_2 = ['start', 'middle', 'end']
127 | assert expected_2 == result_2
128 |
129 | def test_Templater_open_should_load_template_from_a_raw_file_with_markers():
130 | write_file('test.html', '||||||||||||')
131 | t = Templater.open('test.html', marker='|||')
132 | unlink('test.html')
133 | result = t._template
134 | expected = [None, '', None, '', None, '', None]
135 | assert expected == result
136 |
137 | def test_Templater_open_should_remove_leading_linefeed_if_there_is_some():
138 | fp = open('test.html', 'w')
139 | fp.write('||||||||||||\n')
140 | fp.close()
141 | t = Templater.open('test.html', marker='|||')
142 | unlink('test.html')
143 | result_1 = t._template
144 | expected = [None, '', None, '', None, '', None]
145 | assert expected == result_1
146 |
147 | fp = open('test.html', 'w')
148 | fp.write('||||||||||||\r\n')
149 | fp.close()
150 | t = Templater.open('test.html', marker='|||')
151 | unlink('test.html')
152 | result_2 = t._template
153 | assert expected == result_2
154 |
155 | def test_named_markers_should_work():
156 | write_file('test.html',
157 | '|||[first]|||[second]|||[third]|||[fourth]')
158 | t = Templater.open('test.html', marker=re_compile(r'\|\|\|\[([^\]]+)\]'))
159 | unlink('test.html')
160 | result_1 = t._template
161 | expected_1 = [None, '', None, '', None, '', None]
162 | assert result_1 == expected_1
163 |
164 | result_2 = t.parse('helloworld')
165 | expected_2 = {'first': '', 'second': 'hello', 'third': 'world',
166 | 'fourth': ''}
167 | assert expected_2 == result_2
168 |
169 | def test_should_not_have_named_marks_without_nothing_in_the_middle():
170 | write_file('test.html', '{{first}}{{second}}{{text}}{{last}}')
171 | try:
172 | t = Templater.open('test.html', marker=regexp_marker)
173 | except ValueError:
174 | pass
175 | else:
176 | assert "ValueError not raised!" == False
177 |
178 | def test_if_there_are_no_named_marker_in_the_start_of_template():
179 | write_file('test.html', '{{text}}{{end}}')
180 | try:
181 | t = Templater.open('test.html', marker=regexp_marker)
182 | except ValueError:
183 | unlink('test.html')
184 | else:
185 | unlink('test.html')
186 | assert "ValueError not raised!" == False
187 |
188 | def test_raise_ValueError_if_there_is_no_named_marker_in_the_end_of_template():
189 | write_file('test.html', '{{start}}{{text}}')
190 | try:
191 | t = Templater.open('test.html', marker=regexp_marker)
192 | except ValueError:
193 | unlink('test.html')
194 | else:
195 | unlink('test.html')
196 | assert "ValueError not raised!" == False
197 |
198 | # SAVE: marker + headers cases:
199 | # marker | self._named_markers | result
200 | # NO | NO | self._marker / ignore headers
201 | # NO | YES | NAMED_MARKER / headers or self._headers
202 | # YES | NO | marker / ignore headers
203 | # YES | YES | marker.format() / headers or self._headers
204 |
205 | def test_save_should_use_self_marker_if_no_marker_supplied():
206 | t = Templater(template='+++', marker='+')
207 | t.save('test.html')
208 | result = read_file_and_delete('test.html')
209 | expected = '+++\n'
210 | assert expected == result
211 |
212 | def test_save_should_use_NAMED_MARKER_if_template_has_named_markers_and_no_marker_supplied():
213 | t = Templater(template='{{one}}{{two}}{{three}}',
214 | marker=regexp_marker)
215 | t.save('test.html')
216 | result = read_file_and_delete('test.html')
217 | named_markers = [NAMED_MARKER.format(header) for header in t._headers]
218 | expected = t.join(named_markers) + '\n'
219 | assert expected == result
220 |
221 | def test_save_should_use_marker_if_supplied_and_template_hasnt_named_markers():
222 | t = Templater(template='+++', marker='+')
223 | t.save('test.html', marker='%%')
224 | result = read_file_and_delete('test.html')
225 | expected = '%%%%%%\n'
226 | assert expected == result
227 |
228 | def test_save_should_use_python_format_if_marker_is_supplied_and_template_has_named_markers():
229 | t = Templater(template='{{start}}{{text}}{{end}}',
230 | marker=regexp_marker)
231 | t.save('test.html', marker='[--{}--]')
232 | result = read_file_and_delete('test.html')
233 | expected = '[--start--][--text--][--end--]\n'
234 | assert expected == result
235 |
236 | def test_save_should_use_headers_instead_of_self_headers_if_supplied():
237 | t = Templater(template='{{one}}{{two}}{{three}}',
238 | marker=regexp_marker)
239 | t.save('test.html', headers=list('abc'))
240 | result_1 = read_file_and_delete('test.html')
241 | named_markers = [NAMED_MARKER.format(header) for header in list('abc')]
242 | expected_1 = t.join(named_markers) + '\n'
243 | assert expected_1 == result_1
244 |
245 | t.save('test.html', marker='[--{}--]', headers=list('abc'))
246 | result_2 = read_file_and_delete('test.html')
247 | expected_2 = '[--a--][--b--][--c--]' + '\n'
248 | assert expected_2 == result_2
249 |
250 | def test_passing_headers_with_different_size_from_self_headers_should_raise_AttributeError():
251 | t = Templater(template='{{one}}{{two}}{{three}}',
252 | marker=regexp_marker)
253 | try:
254 | t.save('test.html', headers=list('abcde'))
255 | except AttributeError:
256 | pass
257 | else:
258 | unlink('test.html')
259 | raise 'AttributeError not raised!'
260 |
261 | def test_template_with_named_markers_should_not_be_able_to_learn():
262 | t = Templater(template='{{one}}{{two}}{{three}}',
263 | marker=regexp_marker)
264 | try:
265 | t.learn('abc')
266 | except NotImplementedError:
267 | pass
268 | else:
269 | print t._template
270 | assert 'NotImplementedError not raised' == False
271 |
272 | def test_should_be_able_to_add_headers_to_a_template_without_named_markers():
273 | t = Templater(template='|||||||||', marker='|||')
274 | t.add_headers(['one', 'two', 'three'])
275 | result = t.parse('abc')
276 | expected = {'one': 'a', 'two': 'b', 'three': 'c'}
277 | assert result == expected
278 |
279 | def test_add_headers_should_raise_ValueError_if_number_of_blanks_differ_from_number_of_headers():
280 | t = Templater(template='|||||||||', marker='|||')
281 | try:
282 | t.add_headers(['one', 'two', 'three', 'four'])
283 | except ValueError:
284 | pass
285 | else:
286 | assert 'ValueError not raised!' == False
287 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 2, June 1991
3 |
4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
6 | Everyone is permitted to copy and distribute verbatim copies
7 | of this license document, but changing it is not allowed.
8 |
9 | Preamble
10 |
11 | The licenses for most software are designed to take away your
12 | freedom to share and change it. By contrast, the GNU General Public
13 | License is intended to guarantee your freedom to share and change free
14 | software--to make sure the software is free for all its users. This
15 | General Public License applies to most of the Free Software
16 | Foundation's software and to any other program whose authors commit to
17 | using it. (Some other Free Software Foundation software is covered by
18 | the GNU Lesser General Public License instead.) You can apply it to
19 | your programs, too.
20 |
21 | When we speak of free software, we are referring to freedom, not
22 | price. Our General Public Licenses are designed to make sure that you
23 | have the freedom to distribute copies of free software (and charge for
24 | this service if you wish), that you receive source code or can get it
25 | if you want it, that you can change the software or use pieces of it
26 | in new free programs; and that you know you can do these things.
27 |
28 | To protect your rights, we need to make restrictions that forbid
29 | anyone to deny you these rights or to ask you to surrender the rights.
30 | These restrictions translate to certain responsibilities for you if you
31 | distribute copies of the software, or if you modify it.
32 |
33 | For example, if you distribute copies of such a program, whether
34 | gratis or for a fee, you must give the recipients all the rights that
35 | you have. You must make sure that they, too, receive or can get the
36 | source code. And you must show them these terms so they know their
37 | rights.
38 |
39 | We protect your rights with two steps: (1) copyright the software, and
40 | (2) offer you this license which gives you legal permission to copy,
41 | distribute and/or modify the software.
42 |
43 | Also, for each author's protection and ours, we want to make certain
44 | that everyone understands that there is no warranty for this free
45 | software. If the software is modified by someone else and passed on, we
46 | want its recipients to know that what they have is not the original, so
47 | that any problems introduced by others will not reflect on the original
48 | authors' reputations.
49 |
50 | Finally, any free program is threatened constantly by software
51 | patents. We wish to avoid the danger that redistributors of a free
52 | program will individually obtain patent licenses, in effect making the
53 | program proprietary. To prevent this, we have made it clear that any
54 | patent must be licensed for everyone's free use or not licensed at all.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | GNU GENERAL PUBLIC LICENSE
60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
61 |
62 | 0. This License applies to any program or other work which contains
63 | a notice placed by the copyright holder saying it may be distributed
64 | under the terms of this General Public License. The "Program", below,
65 | refers to any such program or work, and a "work based on the Program"
66 | means either the Program or any derivative work under copyright law:
67 | that is to say, a work containing the Program or a portion of it,
68 | either verbatim or with modifications and/or translated into another
69 | language. (Hereinafter, translation is included without limitation in
70 | the term "modification".) Each licensee is addressed as "you".
71 |
72 | Activities other than copying, distribution and modification are not
73 | covered by this License; they are outside its scope. The act of
74 | running the Program is not restricted, and the output from the Program
75 | is covered only if its contents constitute a work based on the
76 | Program (independent of having been made by running the Program).
77 | Whether that is true depends on what the Program does.
78 |
79 | 1. You may copy and distribute verbatim copies of the Program's
80 | source code as you receive it, in any medium, provided that you
81 | conspicuously and appropriately publish on each copy an appropriate
82 | copyright notice and disclaimer of warranty; keep intact all the
83 | notices that refer to this License and to the absence of any warranty;
84 | and give any other recipients of the Program a copy of this License
85 | along with the Program.
86 |
87 | You may charge a fee for the physical act of transferring a copy, and
88 | you may at your option offer warranty protection in exchange for a fee.
89 |
90 | 2. You may modify your copy or copies of the Program or any portion
91 | of it, thus forming a work based on the Program, and copy and
92 | distribute such modifications or work under the terms of Section 1
93 | above, provided that you also meet all of these conditions:
94 |
95 | a) You must cause the modified files to carry prominent notices
96 | stating that you changed the files and the date of any change.
97 |
98 | b) You must cause any work that you distribute or publish, that in
99 | whole or in part contains or is derived from the Program or any
100 | part thereof, to be licensed as a whole at no charge to all third
101 | parties under the terms of this License.
102 |
103 | c) If the modified program normally reads commands interactively
104 | when run, you must cause it, when started running for such
105 | interactive use in the most ordinary way, to print or display an
106 | announcement including an appropriate copyright notice and a
107 | notice that there is no warranty (or else, saying that you provide
108 | a warranty) and that users may redistribute the program under
109 | these conditions, and telling the user how to view a copy of this
110 | License. (Exception: if the Program itself is interactive but
111 | does not normally print such an announcement, your work based on
112 | the Program is not required to print an announcement.)
113 |
114 | These requirements apply to the modified work as a whole. If
115 | identifiable sections of that work are not derived from the Program,
116 | and can be reasonably considered independent and separate works in
117 | themselves, then this License, and its terms, do not apply to those
118 | sections when you distribute them as separate works. But when you
119 | distribute the same sections as part of a whole which is a work based
120 | on the Program, the distribution of the whole must be on the terms of
121 | this License, whose permissions for other licensees extend to the
122 | entire whole, and thus to each and every part regardless of who wrote it.
123 |
124 | Thus, it is not the intent of this section to claim rights or contest
125 | your rights to work written entirely by you; rather, the intent is to
126 | exercise the right to control the distribution of derivative or
127 | collective works based on the Program.
128 |
129 | In addition, mere aggregation of another work not based on the Program
130 | with the Program (or with a work based on the Program) on a volume of
131 | a storage or distribution medium does not bring the other work under
132 | the scope of this License.
133 |
134 | 3. You may copy and distribute the Program (or a work based on it,
135 | under Section 2) in object code or executable form under the terms of
136 | Sections 1 and 2 above provided that you also do one of the following:
137 |
138 | a) Accompany it with the complete corresponding machine-readable
139 | source code, which must be distributed under the terms of Sections
140 | 1 and 2 above on a medium customarily used for software interchange; or,
141 |
142 | b) Accompany it with a written offer, valid for at least three
143 | years, to give any third party, for a charge no more than your
144 | cost of physically performing source distribution, a complete
145 | machine-readable copy of the corresponding source code, to be
146 | distributed under the terms of Sections 1 and 2 above on a medium
147 | customarily used for software interchange; or,
148 |
149 | c) Accompany it with the information you received as to the offer
150 | to distribute corresponding source code. (This alternative is
151 | allowed only for noncommercial distribution and only if you
152 | received the program in object code or executable form with such
153 | an offer, in accord with Subsection b above.)
154 |
155 | The source code for a work means the preferred form of the work for
156 | making modifications to it. For an executable work, complete source
157 | code means all the source code for all modules it contains, plus any
158 | associated interface definition files, plus the scripts used to
159 | control compilation and installation of the executable. However, as a
160 | special exception, the source code distributed need not include
161 | anything that is normally distributed (in either source or binary
162 | form) with the major components (compiler, kernel, and so on) of the
163 | operating system on which the executable runs, unless that component
164 | itself accompanies the executable.
165 |
166 | If distribution of executable or object code is made by offering
167 | access to copy from a designated place, then offering equivalent
168 | access to copy the source code from the same place counts as
169 | distribution of the source code, even though third parties are not
170 | compelled to copy the source along with the object code.
171 |
172 | 4. You may not copy, modify, sublicense, or distribute the Program
173 | except as expressly provided under this License. Any attempt
174 | otherwise to copy, modify, sublicense or distribute the Program is
175 | void, and will automatically terminate your rights under this License.
176 | However, parties who have received copies, or rights, from you under
177 | this License will not have their licenses terminated so long as such
178 | parties remain in full compliance.
179 |
180 | 5. You are not required to accept this License, since you have not
181 | signed it. However, nothing else grants you permission to modify or
182 | distribute the Program or its derivative works. These actions are
183 | prohibited by law if you do not accept this License. Therefore, by
184 | modifying or distributing the Program (or any work based on the
185 | Program), you indicate your acceptance of this License to do so, and
186 | all its terms and conditions for copying, distributing or modifying
187 | the Program or works based on it.
188 |
189 | 6. Each time you redistribute the Program (or any work based on the
190 | Program), the recipient automatically receives a license from the
191 | original licensor to copy, distribute or modify the Program subject to
192 | these terms and conditions. You may not impose any further
193 | restrictions on the recipients' exercise of the rights granted herein.
194 | You are not responsible for enforcing compliance by third parties to
195 | this License.
196 |
197 | 7. If, as a consequence of a court judgment or allegation of patent
198 | infringement or for any other reason (not limited to patent issues),
199 | conditions are imposed on you (whether by court order, agreement or
200 | otherwise) that contradict the conditions of this License, they do not
201 | excuse you from the conditions of this License. If you cannot
202 | distribute so as to satisfy simultaneously your obligations under this
203 | License and any other pertinent obligations, then as a consequence you
204 | may not distribute the Program at all. For example, if a patent
205 | license would not permit royalty-free redistribution of the Program by
206 | all those who receive copies directly or indirectly through you, then
207 | the only way you could satisfy both it and this License would be to
208 | refrain entirely from distribution of the Program.
209 |
210 | If any portion of this section is held invalid or unenforceable under
211 | any particular circumstance, the balance of the section is intended to
212 | apply and the section as a whole is intended to apply in other
213 | circumstances.
214 |
215 | It is not the purpose of this section to induce you to infringe any
216 | patents or other property right claims or to contest validity of any
217 | such claims; this section has the sole purpose of protecting the
218 | integrity of the free software distribution system, which is
219 | implemented by public license practices. Many people have made
220 | generous contributions to the wide range of software distributed
221 | through that system in reliance on consistent application of that
222 | system; it is up to the author/donor to decide if he or she is willing
223 | to distribute software through any other system and a licensee cannot
224 | impose that choice.
225 |
226 | This section is intended to make thoroughly clear what is believed to
227 | be a consequence of the rest of this License.
228 |
229 | 8. If the distribution and/or use of the Program is restricted in
230 | certain countries either by patents or by copyrighted interfaces, the
231 | original copyright holder who places the Program under this License
232 | may add an explicit geographical distribution limitation excluding
233 | those countries, so that distribution is permitted only in or among
234 | countries not thus excluded. In such case, this License incorporates
235 | the limitation as if written in the body of this License.
236 |
237 | 9. The Free Software Foundation may publish revised and/or new versions
238 | of the General Public License from time to time. Such new versions will
239 | be similar in spirit to the present version, but may differ in detail to
240 | address new problems or concerns.
241 |
242 | Each version is given a distinguishing version number. If the Program
243 | specifies a version number of this License which applies to it and "any
244 | later version", you have the option of following the terms and conditions
245 | either of that version or of any later version published by the Free
246 | Software Foundation. If the Program does not specify a version number of
247 | this License, you may choose any version ever published by the Free Software
248 | Foundation.
249 |
250 | 10. If you wish to incorporate parts of the Program into other free
251 | programs whose distribution conditions are different, write to the author
252 | to ask for permission. For software which is copyrighted by the Free
253 | Software Foundation, write to the Free Software Foundation; we sometimes
254 | make exceptions for this. Our decision will be guided by the two goals
255 | of preserving the free status of all derivatives of our free software and
256 | of promoting the sharing and reuse of software generally.
257 |
258 | NO WARRANTY
259 |
260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
268 | REPAIR OR CORRECTION.
269 |
270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
278 | POSSIBILITY OF SUCH DAMAGES.
279 |
280 | END OF TERMS AND CONDITIONS
281 |
282 | How to Apply These Terms to Your New Programs
283 |
284 | If you develop a new program, and you want it to be of the greatest
285 | possible use to the public, the best way to achieve this is to make it
286 | free software which everyone can redistribute and change under these terms.
287 |
288 | To do so, attach the following notices to the program. It is safest
289 | to attach them to the start of each source file to most effectively
290 | convey the exclusion of warranty; and each file should have at least
291 | the "copyright" line and a pointer to where the full notice is found.
292 |
293 |
294 | Copyright (C)
295 |
296 | This program is free software; you can redistribute it and/or modify
297 | it under the terms of the GNU General Public License as published by
298 | the Free Software Foundation; either version 2 of the License, or
299 | (at your option) any later version.
300 |
301 | This program is distributed in the hope that it will be useful,
302 | but WITHOUT ANY WARRANTY; without even the implied warranty of
303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
304 | GNU General Public License for more details.
305 |
306 | You should have received a copy of the GNU General Public License along
307 | with this program; if not, write to the Free Software Foundation, Inc.,
308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
309 |
310 | Also add information on how to contact you by electronic and paper mail.
311 |
312 | If the program is interactive, make it output a short notice like this
313 | when it starts in an interactive mode:
314 |
315 | Gnomovision version 69, Copyright (C) year name of author
316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
317 | This is free software, and you are welcome to redistribute it
318 | under certain conditions; type `show c' for details.
319 |
320 | The hypothetical commands `show w' and `show c' should show the appropriate
321 | parts of the General Public License. Of course, the commands you use may
322 | be called something other than `show w' and `show c'; they could even be
323 | mouse-clicks or menu items--whatever suits your program.
324 |
325 | You should also get your employer (if you work as a programmer) or your
326 | school, if any, to sign a "copyright disclaimer" for the program, if
327 | necessary. Here is a sample; alter the names:
328 |
329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program
330 | `Gnomovision' (which makes passes at compilers) written by James Hacker.
331 |
332 | , 1 April 1989
333 | Ty Coon, President of Vice
334 |
335 | This General Public License does not permit incorporating your program into
336 | proprietary programs. If your program is a subroutine library, you may
337 | consider it more useful to permit linking proprietary applications with the
338 | library. If this is what you want to do, use the GNU Lesser General
339 | Public License instead of this License.
340 |
--------------------------------------------------------------------------------