Created
April 2, 2013 06:25
-
-
Save shiweifu/5290302 to your computer and use it in GitHub Desktop.
bottle template
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import functools | |
from urllib2 import * | |
TEMPLATE_PATH = ['./', './views/'] | |
TEMPLATES = {} | |
#------------------------------------------utils------------------------------------------ | |
def tob(s, enc='utf8'): | |
return s.encode(enc) if isinstance(s, unicode) else bytes(s) | |
def touni(s, enc='utf8', err='strict'): | |
return s.decode(enc, err) if isinstance(s, bytes) else unicode(s) | |
def depr(message): | |
warnings.warn(message, DeprecationWarning, stacklevel=3) | |
def makelist(data): # This is just to handy | |
if isinstance(data, (tuple, list, set, dict)): return list(data) | |
elif data: return [data] | |
else: return [] | |
class DictProperty(object): | |
''' Property that maps to a key in a local dict-like attribute. ''' | |
def __init__(self, attr, key=None, read_only=False): | |
self.attr, self.key, self.read_only = attr, key, read_only | |
def __call__(self, func): | |
functools.update_wrapper(self, func, updated=[]) | |
self.getter, self.key = func, self.key or func.__name__ | |
return self | |
def __get__(self, obj, cls): | |
if obj is None: return self | |
key, storage = self.key, getattr(obj, self.attr) | |
if key not in storage: storage[key] = self.getter(obj) | |
return storage[key] | |
def __set__(self, obj, value): | |
if self.read_only: raise AttributeError("Read-Only property.") | |
getattr(obj, self.attr)[self.key] = value | |
def __delete__(self, obj): | |
if self.read_only: raise AttributeError("Read-Only property.") | |
del getattr(obj, self.attr)[self.key] | |
class cached_property(object): | |
''' A property that is only computed once per instance and then replaces | |
itself with an ordinary attribute. Deleting the attribute resets the | |
property. ''' | |
def __init__(self, func): | |
self.func = func | |
def __get__(self, obj, cls): | |
if obj is None: return self | |
value = obj.__dict__[self.func.__name__] = self.func(obj) | |
return value | |
def html_escape(string): | |
''' Escape HTML special characters ``&<>`` and quotes ``'"``. ''' | |
return string.replace('&','&').replace('<','<').replace('>','>')\ | |
.replace('"','"').replace("'",''') | |
def html_quote(string): | |
''' Escape and quote a string to be used as an HTTP attribute.''' | |
return '"%s"' % html_escape(string).replace('\n','%#10;')\ | |
.replace('\r',' ').replace('\t','	') | |
class lazy_attribute(object): | |
''' A property that caches itself to the class object. ''' | |
def __init__(self, func): | |
functools.update_wrapper(self, func, updated=[]) | |
self.getter = func | |
def __get__(self, obj, cls): | |
value = self.getter(cls) | |
setattr(cls, self.__name__, value) | |
return value | |
class TemplateError(HTTPError): | |
def __init__(self, message): | |
HTTPError.__init__(self, 500, message) | |
class BaseTemplate(object): | |
""" Base class and minimal API for template adapters """ | |
extensions = ['tpl','html','thtml','stpl'] | |
settings = {} #used in prepare() | |
defaults = {} #used in render() | |
def __init__(self, source=None, name=None, lookup=[], encoding='utf8', **settings): | |
""" Create a new template. | |
If the source parameter (str or buffer) is missing, the name argument | |
is used to guess a template filename. Subclasses can assume that | |
self.source and/or self.filename are set. Both are strings. | |
The lookup, encoding and settings parameters are stored as instance | |
variables. | |
The lookup parameter stores a list containing directory paths. | |
The encoding parameter should be used to decode byte strings or files. | |
The settings parameter contains a dict for engine-specific settings. | |
""" | |
self.name = name | |
self.source = source.read() if hasattr(source, 'read') else source | |
self.filename = source.filename if hasattr(source, 'filename') else None | |
self.lookup = [os.path.abspath(x) for x in lookup] | |
self.encoding = encoding | |
self.settings = self.settings.copy() # Copy from class variable | |
self.settings.update(settings) # Apply | |
if not self.source and self.name: | |
self.filename = self.search(self.name, self.lookup) | |
if not self.filename: | |
raise TemplateError('Template %s not found.' % repr(name)) | |
if not self.source and not self.filename: | |
raise TemplateError('No template specified.') | |
self.prepare(**self.settings) | |
@classmethod | |
def search(cls, name, lookup=[]): | |
""" Search name in all directories specified in lookup. | |
First without, then with common extensions. Return first hit. """ | |
if not lookup: | |
depr('The template lookup path list should not be empty.') | |
lookup = ['.'] | |
if os.path.isabs(name) and os.path.isfile(name): | |
depr('Absolute template path names are deprecated.') | |
return os.path.abspath(name) | |
for spath in lookup: | |
spath = os.path.abspath(spath) + os.sep | |
fname = os.path.abspath(os.path.join(spath, name)) | |
if not fname.startswith(spath): continue | |
if os.path.isfile(fname): return fname | |
for ext in cls.extensions: | |
if os.path.isfile('%s.%s' % (fname, ext)): | |
return '%s.%s' % (fname, ext) | |
@classmethod | |
def global_config(cls, key, *args): | |
''' This reads or sets the global settings stored in class.settings. ''' | |
if args: | |
cls.settings = cls.settings.copy() # Make settings local to class | |
cls.settings[key] = args[0] | |
else: | |
return cls.settings[key] | |
def prepare(self, **options): | |
""" Run preparations (parsing, caching, ...). | |
It should be possible to call this again to refresh a template or to | |
update settings. | |
""" | |
raise NotImplementedError | |
def render(self, *args, **kwargs): | |
""" Render the template with the specified local variables and return | |
a single byte or unicode string. If it is a byte string, the encoding | |
must match self.encoding. This method must be thread-safe! | |
Local variables may be provided in dictionaries (*args) | |
or directly, as keywords (**kwargs). | |
""" | |
raise NotImplementedError | |
class SimpleTemplate(BaseTemplate): | |
blocks = ('if', 'elif', 'else', 'try', 'except', 'finally', 'for', 'while', | |
'with', 'def', 'class') | |
dedent_blocks = ('elif', 'else', 'except', 'finally') | |
@lazy_attribute | |
def re_pytokens(cls): | |
''' This matches comments and all kinds of quoted strings but does | |
NOT match comments (#...) within quoted strings. (trust me) ''' | |
return re.compile(r''' | |
(''(?!')|""(?!")|'{6}|"{6} # Empty strings (all 4 types) | |
|'(?:[^\\']|\\.)+?' # Single quotes (') | |
|"(?:[^\\"]|\\.)+?" # Double quotes (") | |
|'{3}(?:[^\\]|\\.|\n)+?'{3} # Triple-quoted strings (') | |
|"{3}(?:[^\\]|\\.|\n)+?"{3} # Triple-quoted strings (") | |
|\#.* # Comments | |
)''', re.VERBOSE) | |
def prepare(self, escape_func=html_escape, noescape=False, **kwargs): | |
self.cache = {} | |
enc = self.encoding | |
self._str = lambda x: touni(x, enc) | |
self._escape = lambda x: escape_func(touni(x, enc)) | |
if noescape: | |
self._str, self._escape = self._escape, self._str | |
@classmethod | |
def split_comment(cls, code): | |
""" Removes comments (#...) from python code. """ | |
if '#' not in code: return code | |
#: Remove comments only (leave quoted strings as they are) | |
subf = lambda m: '' if m.group(0)[0]=='#' else m.group(0) | |
return re.sub(cls.re_pytokens, subf, code) | |
@cached_property | |
def co(self): | |
return compile(self.code, self.filename or '<string>', 'exec') | |
@cached_property | |
def code(self): | |
stack = [] # Current Code indentation | |
lineno = 0 # Current line of code | |
ptrbuffer = [] # Buffer for printable strings and token tuple instances | |
codebuffer = [] # Buffer for generated python code | |
multiline = dedent = oneline = False | |
template = self.source or open(self.filename, 'rb').read() | |
def yield_tokens(line): | |
for i, part in enumerate(re.split(r'\{\{(.*?)\}\}', line)): | |
if i % 2: | |
if part.startswith('!'): yield 'RAW', part[1:] | |
else: yield 'CMD', part | |
else: yield 'TXT', part | |
def flush(): # Flush the ptrbuffer | |
if not ptrbuffer: return | |
cline = '' | |
for line in ptrbuffer: | |
for token, value in line: | |
if token == 'TXT': cline += repr(value) | |
elif token == 'RAW': cline += '_str(%s)' % value | |
elif token == 'CMD': cline += '_escape(%s)' % value | |
cline += ', ' | |
cline = cline[:-2] + '\\\n' | |
cline = cline[:-2] | |
if cline[:-1].endswith('\\\\\\\\\\n'): | |
cline = cline[:-7] + cline[-1] # 'nobr\\\\\n' --> 'nobr' | |
cline = '_printlist([' + cline + '])' | |
del ptrbuffer[:] # Do this before calling code() again | |
code(cline) | |
def code(stmt): | |
for line in stmt.splitlines(): | |
codebuffer.append(' ' * len(stack) + line.strip()) | |
for line in template.splitlines(True): | |
lineno += 1 | |
line = touni(line, self.encoding) | |
sline = line.lstrip() | |
if lineno <= 2: | |
m = re.match(r"%\s*#.*coding[:=]\s*([-\w.]+)", sline) | |
if m: self.encoding = m.group(1) | |
if m: line = line.replace('coding','coding (removed)') | |
if sline and sline[0] == '%' and sline[:2] != '%%': | |
line = line.split('%',1)[1].lstrip() # Full line following the % | |
cline = self.split_comment(line).strip() | |
cmd = re.split(r'[^a-zA-Z0-9_]', cline)[0] | |
flush() # You are actually reading this? Good luck, it's a mess :) | |
if cmd in self.blocks or multiline: | |
cmd = multiline or cmd | |
dedent = cmd in self.dedent_blocks # "else:" | |
if dedent and not oneline and not multiline: | |
cmd = stack.pop() | |
code(line) | |
oneline = not cline.endswith(':') # "if 1: pass" | |
multiline = cmd if cline.endswith('\\') else False | |
if not oneline and not multiline: | |
stack.append(cmd) | |
elif cmd == 'end' and stack: | |
code('#end(%s) %s' % (stack.pop(), line.strip()[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: | |
code(line) | |
else: # Line starting with text (not '%') or '%%' (escaped) | |
if line.strip().startswith('%%'): | |
line = line.replace('%%', '%', 1) | |
ptrbuffer.append(yield_tokens(line)) | |
flush() | |
return '\n'.join(codebuffer) + '\n' | |
def subtemplate(self, _name, _stdout, *args, **kwargs): | |
for dictarg in args: kwargs.update(dictarg) | |
if _name not in self.cache: | |
self.cache[_name] = self.__class__(name=_name, lookup=self.lookup) | |
return self.cache[_name].execute(_stdout, kwargs) | |
def execute(self, _stdout, *args, **kwargs): | |
for dictarg in args: kwargs.update(dictarg) | |
env = self.defaults.copy() | |
env.update({'_stdout': _stdout, '_printlist': _stdout.extend, | |
'_include': self.subtemplate, '_str': self._str, | |
'_escape': self._escape, 'get': env.get, | |
'setdefault': env.setdefault, 'defined': env.__contains__}) | |
env.update(kwargs) | |
eval(self.co, env) | |
if '_rebase' in env: | |
subtpl, rargs = env['_rebase'] | |
rargs['_base'] = _stdout[:] #copy stdout | |
del _stdout[:] # clear stdout | |
return self.subtemplate(subtpl,_stdout,rargs) | |
return env | |
def render(self, *args, **kwargs): | |
""" Render the template using keyword arguments as local variables. """ | |
for dictarg in args: kwargs.update(dictarg) | |
stdout = [] | |
self.execute(stdout, kwargs) | |
return ''.join(stdout) | |
def template(*args, **kwargs): | |
''' | |
Get a rendered template as a string iterator. | |
You can use a name, a filename or a template string as first parameter. | |
Template rendering arguments can be passed as dictionaries | |
or directly (as keyword arguments). | |
''' | |
tpl = args[0] if args else None | |
adapter = kwargs.pop('template_adapter', SimpleTemplate) | |
lookup = kwargs.pop('template_lookup', TEMPLATE_PATH) | |
tplid = (id(lookup), tpl) | |
if tplid not in TEMPLATES or DEBUG: | |
settings = kwargs.pop('template_settings', {}) | |
if isinstance(tpl, adapter): | |
TEMPLATES[tplid] = tpl | |
if settings: TEMPLATES[tplid].prepare(**settings) | |
elif "\n" in tpl or "{" in tpl or "%" in tpl or '$' in tpl: | |
TEMPLATES[tplid] = adapter(source=tpl, lookup=lookup, **settings) | |
else: | |
TEMPLATES[tplid] = adapter(name=tpl, lookup=lookup, **settings) | |
if not TEMPLATES[tplid]: | |
abort(500, 'Template (%s) not found' % tpl) | |
for dictarg in args[1:]: kwargs.update(dictarg) | |
return TEMPLATES[tplid].render(kwargs) | |
def main(): | |
print template("hello {{name}}", name="world") | |
if __name__ == '__main__': | |
main() | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment