summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarcel Hellkamp <marc@gsites.de>2010-03-08 01:19:58 +0100
committerMarcel Hellkamp <marc@gsites.de>2010-03-08 01:19:58 +0100
commitef0f5d3b4d9a0bf812e9149b392856ed09d0f663 (patch)
tree11164cf1096e16baf7a8b76812cb65782b3a743a
parent9ef4ca8545f6e967f25fbb42b2e91b4ae82f4eea (diff)
parente68aa735e54d2efecdebf961a69babb488b8ca4e (diff)
downloadbottle-ef0f5d3b4d9a0bf812e9149b392856ed09d0f663.tar.gz
Merge branch 'stplunicode'
-rw-r--r--apidoc/sphinx/conf.py2
-rwxr-xr-xbottle.py193
-rw-r--r--docs/docs.md130
-rw-r--r--test/test_stpl.py95
4 files changed, 250 insertions, 170 deletions
diff --git a/apidoc/sphinx/conf.py b/apidoc/sphinx/conf.py
index 8933047..6320061 100644
--- a/apidoc/sphinx/conf.py
+++ b/apidoc/sphinx/conf.py
@@ -17,7 +17,7 @@ import sys, os
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.append(os.path.abspath('.'))
-sys.path.append(os.path.abspath('../../'))
+sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__),'../../')))
import bottle
# -- General configuration -----------------------------------------------------
diff --git a/bottle.py b/bottle.py
index d53f7e6..12f860b 100755
--- a/bottle.py
+++ b/bottle.py
@@ -912,7 +912,9 @@ def static_file(filename, root, guessmime=True, mimetype=None, download=False):
else:
return HTTPResponse(open(filename, 'rb'), header=header)
-
+def url(routename, **kargs):
+ """ Helper generates URLs out of named routes """
+ return app().get_url(routename, **kargs)
@@ -973,6 +975,13 @@ def cookie_is_encoded(data):
return bool(data.startswith(u'!'.encode('ascii')) and u'?'.encode('ascii') in data) #2to3 hack
+def tonativefunc(enc='utf-8'):
+ ''' Returns a function that turns everything into 'native' strings using enc '''
+ if sys.version_info >= (3,0,0):
+ return lambda x: x.decode(enc) if isinstance(x, bytes) else str(x)
+ return lambda x: x.encode(enc) if isinstance(x, unicode) else str(x)
+
+
def yieldroutes(func):
""" Return a generator for routes that match the signature (name, args)
of the func parameter. This may yield more than one route if the function
@@ -993,6 +1002,9 @@ def yieldroutes(func):
+
+
+
# Decorators
#TODO: Replace default_app() with app()
@@ -1368,88 +1380,124 @@ class Jinja2Template(BaseTemplate):
class SimpleTemplate(BaseTemplate):
- re_python = re.compile(r'^\s*%\s*(?:(if|elif|else|try|except|finally|for|'
- 'while|with|def|class)|(include|rebase)|(end)|(.*))')
- re_inline = re.compile(r'\{\{(.*?)\}\}')
- dedent_keywords = ('elif', 'else', 'except', 'finally')
+ blocks = ('if','elif','else','except','finally','for','while','with','def','class')
+ dedent_blocks = ('elif', 'else', 'except', 'finally')
def prepare(self):
if self.source:
- code = self.translate(self.source)
- self.co = compile(code, '<string>', 'exec')
+ self.code = self.translate(self.source)
+ self.co = compile(self.code, '<string>', 'exec')
else:
- code = self.translate(open(self.filename).read())
- self.co = compile(code, self.filename, 'exec')
+ self.code = self.translate(open(self.filename).read())
+ self.co = compile(self.code, self.filename, 'exec')
def translate(self, template):
- indent = 0
- strbuffer = []
- code = []
- self.includes = dict()
- class PyStmt(str):
- def __repr__(self): return 'str(' + self + ')'
- def flush(allow_nobreak=False):
- if len(strbuffer):
- if allow_nobreak and strbuffer[-1].endswith("\\\\\n"):
- strbuffer[-1]=strbuffer[-1][:-3]
- code.append(' ' * indent + "_stdout.append(%s)" % repr(''.join(strbuffer)))
- code.append((' ' * indent + '\n') * len(strbuffer)) # to preserve line numbers
- del strbuffer[:]
- def cadd(line): code.append(" " * indent + line.strip() + '\n')
+ stack = [] # Current Code indentation
+ lineno = 0 # Current line of code
+ ptrbuffer = [] # Buffer for printable strings and PyStmt instances
+ codebuffer = [] # Buffer for generated python code
+ touni = functools.partial(unicode, encoding=self.encoding)
+
+ class PyStmt(object): # Python statement with filter function
+ def __init__(self, s, f='_str'): self.s, self.f = s, f
+ def __repr__(self): return '%s(%s)' % (self.f, self.s.strip())
+
+ def prt(txt): # Add a string or a PyStmt object to ptrbuffer
+ if ptrbuffer and isinstance(txt, str) \
+ and isinstance(ptrbuffer[-1], str): # Requied for line preserving
+ ptrbuffer[-1] += txt
+ else: ptrbuffer.append(txt)
+
+ def flush(): # Flush the ptrbuffer
+ if ptrbuffer:
+ # Remove escaped newline in last string
+ if isinstance(ptrbuffer[-1], unicode):
+ if ptrbuffer[-1].rstrip('\n\r').endswith('\\\\'):
+ ptrbuffer[-1] = ptrbuffer[-1].rstrip('\n\r')[:-2]
+ # Add linebreaks to output code, if strings contains newlines
+ out = []
+ for s in ptrbuffer:
+ out.append(repr(s))
+ if isinstance(s, PyStmt): s = s.s
+ if '\n' in s: out.append('\n'*s.count('\n'))
+ codeline = ', '.join(out)
+ if codeline.endswith('\n'): codeline = codeline[:-1] #Remove last newline
+ codeline = codeline.replace('\n, ','\n')
+ codeline = "_printlist([%s])" % codeline
+ del ptrbuffer[:] # Do this before calling code() again
+ code(codeline)
+
+ def code(stmt):
+ for line in stmt.splitlines():
+ codebuffer.append(' ' * len(stack) + line.strip())
+
for line in template.splitlines(True):
- m = self.re_python.match(line)
- if m:
- flush(allow_nobreak=True)
- keyword, subtpl, end, statement = m.groups()
- if keyword:
- if keyword in self.dedent_keywords:
- indent -= 1
- cadd(line[m.start(1):])
- indent += 1
- elif subtpl:
- tmp = line[m.end(2):].strip().split(None, 1)
- if not tmp:
- cadd("_stdout.extend(_base)")
- else:
- name = tmp[0]
- args = tmp[1:] and tmp[1] or ''
- if name not in self.includes:
- self.includes[name] = SimpleTemplate(name=name, lookup=self.lookup)
- if subtpl == 'include':
- cadd("_ = _includes[%s].execute(_stdout, %s)"
- % (repr(name), args))
- else:
- cadd("_tpl['_rebase'] = (_includes[%s], dict(%s))"
- % (repr(name), args))
- elif end:
- indent -= 1
- cadd('#' + line[m.start(3):])
- elif statement:
- cadd(line[m.start(4):])
- else:
- splits = self.re_inline.split(line) # text, (expr, text)*
- if len(splits) == 1:
- strbuffer.append(line)
+ lineno += 1
+ line = unicode(line, encoding=self.encoding) if not isinstance(line, unicode) else line
+ if lineno <= 2 and 'coding' in line:
+ m = re.search(r"%.*coding[:=]\s*([-\w\.]+)", line)
+ if m: self.encoding = m.group(1)
+ if m: line = line.replace('coding','coding (removed)')
+ if line.strip().startswith('%') and not line.strip().startswith('%%'):
+ line = line.strip().lstrip('%') # Full line
+ cline = line.split('#')[0]
+ cline = cline.strip()
+ cmd = line.split()[0] # Command word
+ flush() ##encodig
+ if cmd in self.blocks:
+ if cmd in self.dedent_blocks: cmd = stack.pop() #last block ended
+ code(line)
+ if cline.endswith(':'): stack.append(cmd) # false: one line blocks
+ elif cmd == 'end' and stack:
+ code('#end(%s) %s' % (stack.pop(), line[3:]))
+ elif cmd == 'include':
+ p = cline.split(None, 2)[1:]
+ if len(p) == 2:
+ code("_=_include(%s, _stdout, %s)" % (repr(p[0]), p[1]))
+ elif p:
+ code("_=_include(%s, _stdout)" % repr(p[0]))
+ else: # Empty %include -> reverse of %rebase
+ code("_printlist(_base)")
+ elif cmd == 'rebase':
+ p = cline.split(None, 2)[1:]
+ if len(p) == 2:
+ code("globals()['_rebase']=(%s, dict(%s))" % (repr(p[0]), p[1]))
+ elif p:
+ code("globals()['_rebase']=(%s, {})" % repr(p[0]))
else:
- flush()
- for i in range(1, len(splits), 2):
- splits[i] = PyStmt(splits[i])
- splits = [x for x in splits if bool(x)]
- cadd("_stdout.extend(%s)" % repr(splits))
+ code(line)
+ else: # Line starting with text (not '%') or '%%' (escaped)
+ if line.strip().startswith('%%'):
+ line = line.replace('%%', '%', 1)
+ for i, part in enumerate(re.split(r'\{\{(.*?)\}\}', line)):
+ if part and i%2:
+ if part.startswith('!'):
+ prt(PyStmt(part[1:], f='_escape'))
+ else:
+ prt(PyStmt(part))
+ elif part:
+ prt(part)
flush()
- return ''.join(code)
+ return '\n'.join(codebuffer) + '\n'
+
+ def subtemplate(self, name, stdout, **args):
+ return self.__class__(name=name, lookup=self.lookup).execute(stdout, **args)
def execute(self, stdout, **args):
- args['_stdout'] = stdout
- args['_includes'] = self.includes
- args['_tpl'] = args
- eval(self.co, args)
- if '_rebase' in args:
- subtpl, args = args['_rebase']
- args['_base'] = stdout[:] #copy stdout
+ enc = self.encoding
+ def _str(x): return touni(x, enc)
+ def _escape(x): return cgi.escape(touni(x, enc))
+ env = {'_stdout': stdout, '_printlist': stdout.extend,
+ '_include': self.subtemplate, '_str': _str, '_escape': _escape}
+ env.update(args)
+ eval(self.co, env)
+ if '_rebase' in env:
+ subtpl, rargs = env['_rebase']
+ subtpl = self.__class__(name=subtpl, lookup=self.lookup)
+ rargs['_base'] = stdout[:] #copy stdout
del stdout[:] # clear stdout
- return subtpl.execute(stdout, **args)
- return args
+ return subtpl.execute(stdout, **rargs)
+ return env
def render(self, **args):
""" Render the template using keyword arguments as local variables. """
@@ -1581,6 +1629,9 @@ ERROR_PAGE_TEMPLATE = SimpleTemplate("""
""") #TODO: use {{!bla}} instead of cgi.escape as soon as strlunicode is merged
""" The HTML template used for error messages """
+TRACEBACK_TEMPLATE = '<h2>Error:</h2>\n<pre>%s</pre>\n' \
+ '<h2>Traceback:</h2>\n<pre>%s</pre>\n'
+
request = Request()
""" Whenever a page is requested, the :class:`Bottle` WSGI handler stores
metadata about the current request into this instance of :class:`Request`.
diff --git a/docs/docs.md b/docs/docs.md
index e176858..283d091 100644
--- a/docs/docs.md
+++ b/docs/docs.md
@@ -351,9 +351,17 @@ the result.
def hello(name):
return template('hello_template', username=name)
-This will load the template `hello_template.tpl` with the `username` variable set to the URL `:name` part and return the result as a string.
+The `@view` decorator does is another option:
-The `hello_template.tpl` file could look like this:
+ #!Python
+ @route('/hello/:name')
+ @view('hello_template')
+ def hello(name):
+ return dict(username=name)
+
+Both examples would load the template `hello_template.tpl` with the `username` variable set to the URL `:name` part and return the result as a string.
+
+The `hello_template.tpl` file looks this:
#!html
<h1>Hello {{username}}</h1>
@@ -365,7 +373,7 @@ The `hello_template.tpl` file could look like this:
## Template search path
The list `bottle.TEMPLATE_PATH` is used to map template names to actual
-file names. By default, this list contains `['./%s.tpl', './views/%s.tpl']`.
+file names. By default, this list contains `['./', './views/']`.
@@ -381,101 +389,71 @@ cache. Call `bottle.TEMPLATES.clear()` to do so.
## Template Syntax
-The template syntax is a very thin layer around the Python language.
-It's main purpose is to ensure correct indention of blocks, so you
-can format your template without worrying about indentions. Here is the
-complete syntax description:
+The template syntax is a very thin layer around the Python language. It's main purpose is to ensure correct indention of blocks, so you can format your template without worrying about indentions. It does not prevent your template code from doing bad stuff, so **never ever** execute template code from untrusted sources.
+
+Here is how it works:
- * `%...` starts a line of python code. You don't have to worry about indentions. Bottle handles that for you.
- * `%end` closes a Python block opened by `%if ...`, `%for ...` or other block statements. Explicitly closing of blocks is required.
- * `{{...}}` prints the result of the included python statement.
- * `%include template_name optional_arguments` allows you to include other templates.
- * Every other line is returned as text.
+ * Lines starting with `%` are interpreted as python code. You can intend these lines but you don't have to. The template engine handles the correct indention of python blocks.
+ * A line starting with `%end` closes a python block opened by `%if ...`, `%for ...` or other block statements. Explicitly closing of blocks is required.
+ * Every other line is just returned as text.
+ * `{{...}}` within a text line is replaced by the result of the included python statement. This is useful to include template variables.
+ * The two statements `%include` and `%rebase` have special meanings
-Example:
+Here is a simple example, printing a HTML-list of names.
#!html
- %header = 'Test Template'
- %items = [1,2,3,'fly']
- %include http_header title=header, use_js=['jquery.js', 'default.js']
- <h1>{{header.title()}}</h1>
<ul>
- %for item in items:
- <li>
- %if isinstance(item, int):
- Zahl: {{item}}
- %else:
- %try:
- Other type: ({{type(item).__name__}}) {{repr(item)}}
- %except:
- Error: Item has no string representation.
- %end try-block (yes, you may add comments here)
- %end
- </li>
- %end
+ % for name in names:
+ <li>{{name}}</li>
+ % end
</ul>
- %include http_footer
-
-
-
-
-# Key/Value Databases
-
-<div style="color:darkred">Warning: The included key/value database is depreciated.</div> Please switch to a [real](http://code.google.com/p/redis/) [key](http://couchdb.apache.org/) [value](http://www.mongodb.org/) [database](http://docs.python.org/library/anydbm.html).
-
-Bottle (>0.4.6) offers a persistent key/value database accessible through the
-`bottle.db` module variable. You can use key or attribute syntax to store or
-fetch any pickle-able object to the database. Both
-`bottle.db.bucket_name.key_name` and `bottle.db[bucket_name][key_name]`
-will work.
-
-Missing buckets are created on demand. You don't have to check for
-their existence before using them. Just be sure to use alphanumeric
-bucket-names.
-
-The bucket objects behave like mappings (dictionaries), except that
-only strings are allowed for keys and values must be pickle-able.
-Printing a bucket object doesn't print the keys and values, and the
-`items()` and `values()` methods are not supported. Missing keys will raise
-`KeyError` as expected.
+ #!python
+ import template
+ print template('mylist', names=['Marc','Susan','Alice','Bob'])
+You can include other template using the `%include` statement followed by a template name and an optional parameter list. The include-line is replaced by the rendered result of the named sub-template.
+ #!html
+ <h1>{{title}}</h1>
-## Persistence
-During a request live-cycle, all changes are cached in thread-local memory. At
-the end of the request, the changes are saved automatically so the next request
-will have access to the updated values. Each bucket is stored in a separate file
-in `bottle.DB_PATH`. Be sure to allow write-access to this path and use bucket
-names that are allowed in filenames.
+ #!html
+ %include header_template title='Hello World'
+ <p>
+ Hello World!
+ </p>
+The `%rebase` statement is the inverse of `%include` and is used to render a template into a surrounding base-template. This is similar to the 'Inheritance' feature found in most other template engines. The base-template can access all the parameters specified by the `%rebase` statement and use an empty `%include` statement to include the text returned by the rebased template.
+ #!html
+ <h1>{{title}}</h1>
+ <p>
+ %include
+ </p>
+ #!html
+ %rebase paragraph_template title='Hello World'
+ hello world!
-## Race conditions
-You don't have do worry about file corruption but race conditions are still a
-problem in multi-threaded or forked environments. You can call
-`bottle.db.save()` or `botle.db.bucket_name.save()` to flush the thread-local
-memory cache to disk, but there is no way to detect database changes made in
-other threads until these threads call `bottle.db.save()` or leave the current
-request cycle.
+Ad a last thing: You can add `\\` to the end of a text line preceding a line of python code to suppress the line break.
+ #!html
+ List: \\
+ %for i in range(5):
+ {{i}}
+ <br />
+ #!html
+ List: 1 2 3 4 5 <br />
+Thats all.
-## Example
- #!Python
- from bottle import route, db
- @route('/db/counter')
- def db_counter():
- if 'hits' not in db.counter:
- db.counter.hits = 0
- db['counter']['hits'] += 1
- return "Total hits: %d!" % db.counter.hits
+# Key/Value Databases
+<div style="color:darkred">Warning: The included key/value database is depreciated since 0.6.4.</div> Please switch to a [real](http://code.google.com/p/redis/) [key](http://couchdb.apache.org/) [value](http://www.mongodb.org/) [database](http://docs.python.org/library/anydbm.html).
# Using WSGI and Middleware
diff --git a/test/test_stpl.py b/test/test_stpl.py
index c5fafe4..6c4cd2d 100644
--- a/test/test_stpl.py
+++ b/test/test_stpl.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
import unittest
from bottle import SimpleTemplate, TemplateError
@@ -5,58 +6,80 @@ class TestSimpleTemplate(unittest.TestCase):
def test_string(self):
""" Templates: Parse string"""
t = SimpleTemplate('start {{var}} end').render(var='var')
- self.assertEqual('start var end', ''.join(t))
+ self.assertEqual(u'start var end', ''.join(t))
def test_file(self):
""" Templates: Parse file"""
t = SimpleTemplate(name='./views/stpl_simple.tpl').render(var='var')
- self.assertEqual('start var end\n', ''.join(t))
+ self.assertEqual(u'start var end\n', ''.join(t))
def test_name(self):
""" Templates: Lookup by name """
t = SimpleTemplate(name='stpl_simple', lookup=['./views/']).render(var='var')
- self.assertEqual('start var end\n', ''.join(t))
+ self.assertEqual(u'start var end\n', ''.join(t))
+
+ def test_unicode(self):
+ """ Templates: Unicode variables """
+ t = SimpleTemplate('start {{var}} end').render(var=u'äöü')
+ self.assertEqual(u'start äöü end', ''.join(t))
+
+ def test_import(self):
+ """ Templates: import statement"""
+ t = '%from base64 import b64encode\nstart {{b64encode(var.encode("ascii") if hasattr(var, "encode") else var)}} end'
+ t = SimpleTemplate(t).render(var='var')
+ self.assertEqual(u'start dmFy end', ''.join(t))
def test_data(self):
""" Templates: Data representation """
t = SimpleTemplate('<{{var}}>')
- self.assertEqual('<True>', ''.join(t.render(var=True)))
- self.assertEqual('<False>', ''.join(t.render(var=False)))
- self.assertEqual('<None>', ''.join(t.render(var=None)))
- self.assertEqual('<0>', ''.join(t.render(var=0)))
- self.assertEqual('<5>', ''.join(t.render(var=5)))
- self.assertEqual('<b>', ''.join(t.render(var='b')))
- self.assertEqual('<1.0>', ''.join(t.render(var=1.0)))
- self.assertEqual('<[1, 2]>', ''.join(t.render(var=[1,2])))
+ self.assertEqual(u'<True>', ''.join(t.render(var=True)))
+ self.assertEqual(u'<False>', ''.join(t.render(var=False)))
+ self.assertEqual(u'<None>', ''.join(t.render(var=None)))
+ self.assertEqual(u'<0>', ''.join(t.render(var=0)))
+ self.assertEqual(u'<5>', ''.join(t.render(var=5)))
+ self.assertEqual(u'<b>', ''.join(t.render(var='b')))
+ self.assertEqual(u'<1.0>', ''.join(t.render(var=1.0)))
+ self.assertEqual(u'<[1, 2]>', ''.join(t.render(var=[1,2])))
+
+ def test_escape(self):
+ t = SimpleTemplate('<{{!var}}>')
+ self.assertEqual(u'<b>', ''.join(t.render(var='b')))
+ self.assertEqual(u'<&lt;&amp;&gt;>', ''.join(t.render(var='<&>')))
def test_blocks(self):
""" Templates: Code blocks and loops """
t = SimpleTemplate("start\n%for i in l:\n{{i}} \n%end\nend")
- self.assertEqual('start\n1 \n2 \n3 \nend', ''.join(t.render(l=[1,2,3])))
- self.assertEqual('start\nend', ''.join(t.render(l=[])))
+ self.assertEqual(u'start\n1 \n2 \n3 \nend', ''.join(t.render(l=[1,2,3])))
+ self.assertEqual(u'start\nend', ''.join(t.render(l=[])))
t = SimpleTemplate("start\n%if i:\n{{i}} \n%end\nend")
- self.assertEqual('start\nTrue \nend', ''.join(t.render(i=True)))
- self.assertEqual('start\nend', ''.join(t.render(i=False)))
+ self.assertEqual(u'start\nTrue \nend', ''.join(t.render(i=True)))
+ self.assertEqual(u'start\nend', ''.join(t.render(i=False)))
+
+ def test_onelineblocks(self):
+ """ Templates: one line code blocks """
+ t = SimpleTemplate("start\n%a=''\n%for i in l: a += str(i)\n{{a}}\nend")
+ self.assertEqual(u'start\n123\nend', ''.join(t.render(l=[1,2,3])))
+ self.assertEqual(u'start\n\nend', ''.join(t.render(l=[])))
def test_nobreak(self):
""" Templates: Nobreak statements"""
t = SimpleTemplate("start\\\\\n%pass\nend")
- self.assertEqual('startend', ''.join(t.render()))
+ self.assertEqual(u'startend', ''.join(t.render()))
def test_nonobreak(self):
""" Templates: Escaped nobreak statements"""
t = SimpleTemplate("start\\\\\n\\\\\n%pass\nend")
- self.assertEqual('start\\\\\nend', ''.join(t.render()))
+ self.assertEqual(u'start\\\\\nend', ''.join(t.render()))
def test_include(self):
""" Templates: Include statements"""
t = SimpleTemplate(name='stpl_include', lookup=['./views/'])
- self.assertEqual('before\nstart var end\nafter\n', ''.join(t.render(var='var')))
+ self.assertEqual(u'before\nstart var end\nafter\n', ''.join(t.render(var='var')))
def test_rebase(self):
""" Templates: %rebase and method passing """
t = SimpleTemplate(name='stpl_t2main', lookup=['./views/'])
- result='+base+\n+main+\n!1234!\n+include+\n-main-\n+include+\n-base-\n'
+ result=u'+base+\n+main+\n!1234!\n+include+\n-main-\n+include+\n-base-\n'
self.assertEqual(result, ''.join(t.render(content='1234')))
def test_notfound(self):
@@ -70,11 +93,39 @@ class TestSimpleTemplate(unittest.TestCase):
def test_winbreaks(self):
""" Templates: Test windows line breaks """
- t = SimpleTemplate('%var+=1\r\n{{var}}\r\n').render(var=5)
- self.assertEqual('6\r\n', ''.join(t))
+ t = SimpleTemplate('%var+=1\r\n{{var}}\r\n')
+ t = t.render(var=5)
+ self.assertEqual(u'6\r\n', ''.join(t))
-
+ def test_commentonly(self):
+ """ Templates: Commentd should behave like code-lines (e.g. flush text-lines) """
+ t = SimpleTemplate('...\n%#test\n...')
+ self.failIfEqual('#test', t.code.splitlines()[0])
+ def test_detect_pep263(self):
+ ''' PEP263 strings in code-lines change the template encoding on the fly '''
+ t = SimpleTemplate(u'%#coding: iso8859_15\nöäü?@€'.encode('utf8'))
+ self.failIfEqual(u'öäü?@€', ''.join(t.render()))
+ self.assertEqual(t.encoding, 'iso8859_15')
+ t = SimpleTemplate(u'%#coding: iso8859_15\nöäü?@€'.encode('iso8859_15'))
+ self.assertEqual(u'öäü?@€', ''.join(t.render()))
+ self.assertEqual(t.encoding, 'iso8859_15')
+ self.assertEqual(2, len(t.code.splitlines()))
+
+ def test_ignore_pep263_in_textline(self):
+ ''' PEP263 strings in text-lines have no effect '''
+ self.assertRaises(UnicodeError, SimpleTemplate, u'#coding: iso8859_15\nöäü?@€'.encode('iso8859_15'))
+ t = SimpleTemplate(u'#coding: iso8859_15\nöäü?@€'.encode('utf8'))
+ self.assertEqual(u'#coding: iso8859_15\nöäü?@€', ''.join(t.render()))
+ self.assertEqual(t.encoding, 'utf8')
+
+ def test_ignore_late_pep263(self):
+ ''' PEP263 strings must appear within the first two lines '''
+ self.assertRaises(UnicodeError, SimpleTemplate, u'\n\n%#coding: iso8859_15\nöäü?@€'.encode('iso8859_15'))
+ t = SimpleTemplate(u'\n\n%#coding: iso8859_15\nöäü?@€'.encode('utf8'))
+ self.assertEqual(u'\n\nöäü?@€', ''.join(t.render()))
+ self.assertEqual(t.encoding, 'utf8')
+
if __name__ == '__main__':
unittest.main()