diff --git a/pystache/loader.py b/pystache/loader.py index 63b7ee6..58fb0fb 100644 --- a/pystache/loader.py +++ b/pystache/loader.py @@ -1,47 +1,63 @@ +# -*- coding: utf-8 -*- + +""" +pystache.loader +~~~~~~~~~~~~~~~ + +This module provides Pystache's Loader class. +""" + import os + class Loader(object): - + template_extension = 'mustache' template_path = '.' template_encoding = None - + def load_template(self, template_name, template_dirs=None, encoding=None, extension=None): - '''Returns the template string from a file or throws IOError if it non existent''' + """Returns the template string from a file, or throws IOError + if it is non-existent. + """ + if None == template_dirs: template_dirs = self.template_path - + if encoding is not None: self.template_encoding = encoding - + if extension is not None: self.template_extension = extension - + file_name = template_name + '.' + self.template_extension - # Given a single directory we'll load from it + # Given a single directory, we'll load from it. if isinstance(template_dirs, basestring): file_path = os.path.join(template_dirs, file_name) return self._load_template_file(file_path) - - # Given a list of directories we'll check each for our file + + # Given a list of directories, we'll check each for our file. for path in template_dirs: file_path = os.path.join(path, file_name) if os.path.exists(file_path): return self._load_template_file(file_path) - + raise IOError('"%s" not found in "%s"' % (template_name, ':'.join(template_dirs),)) - + + def _load_template_file(self, file_path): - '''Loads and returns the template file from disk''' + """Loads and returns the template file from disk.""" + f = open(file_path, 'r') - + try: template = f.read() if self.template_encoding: template = unicode(template, self.template_encoding) + finally: f.close() - + return template \ No newline at end of file diff --git a/pystache/template.py b/pystache/template.py index 563d830..4fbb02c 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -1,14 +1,15 @@ +# -*- coding: utf-8 -*- + import re import cgi import collections -import os -import copy + +from .loader import Loader try: import markupsafe escape = markupsafe.escape literal = markupsafe.Markup - except ImportError: escape = lambda x: cgi.escape(unicode(x)) literal = unicode @@ -18,15 +19,15 @@ class Modifiers(dict): """Dictionary with a decorator for assigning functions to keys.""" def set(self, key): - """ - Decorator function to set the given key to the decorated function. + """Decorator function to set the given key to + the decorated function. - >>> modifiers = {} - >>> @modifiers.set('P') - ... def render_tongue(self, tag_name=None, context=None): - ... return ":P %s" % tag_name - >>> modifiers - {'P': } + >>> modifiers = {} + >>> @modifiers.set('P') + ... def render_tongue(self, tag_name=None, context=None): + ... return ":P %s" % tag_name + >>> modifiers + {'P': } """ def setter(func): @@ -35,18 +36,17 @@ class Modifiers(dict): return setter + class Template(object): tag_re = None - otag = '{{' - ctag = '}}' modifiers = Modifiers() def __init__(self, template=None, context=None, **kwargs): - from view import View + from .view import View self.template = template @@ -56,6 +56,7 @@ class Template(object): self.view = context if isinstance(context, View) else View(context=context) self._compile_regexps() + def _compile_regexps(self): tags = { 'otag': re.escape(self.otag), @@ -68,6 +69,7 @@ class Template(object): tag = r"%(otag)s(#|=|&|!|>|\{)?(.+?)\1?%(ctag)s+" self.tag_re = re.compile(tag % tags) + def _render_sections(self, template, view): while True: match = self.section_re.search(template) @@ -82,18 +84,22 @@ class Template(object): # Callable if it and isinstance(it, collections.Callable): replacer = it(inner) + # Dictionary elif it and hasattr(it, 'keys') and hasattr(it, '__getitem__'): if section[2] != '^': replacer = self._render_dictionary(inner, it) + # Lists elif it and hasattr(it, '__iter__'): if section[2] != '^': replacer = self._render_list(inner, it) + # Other objects elif it and isinstance(it, object): if section[2] != '^': replacer = self._render_dictionary(inner, it) + # Falsey and Negated or Truthy and Not Negated elif (not it and section[2] == '^') or (it and section[2] != '^'): replacer = self._render_dictionary(inner, it) @@ -102,6 +108,7 @@ class Template(object): return template + def _render_tags(self, template): while True: match = self.tag_re.search(template) @@ -116,22 +123,32 @@ class Template(object): return template + def _render_dictionary(self, template, context): + self.view.context_list.insert(0, context) + template = Template(template, self.view) out = template.render() + self.view.context_list.pop(0) + return out + def _render_list(self, template, listing): + insides = [] + for item in listing: insides.append(self._render_dictionary(template, item)) return ''.join(insides) + @modifiers.set(None) def _render_tag(self, tag_name): + raw = self.view.get(tag_name, '') # For methods with no return value @@ -143,30 +160,41 @@ class Template(object): return escape(raw) + @modifiers.set('!') def _render_comment(self, tag_name): return '' + @modifiers.set('>') def _render_partial(self, template_name): - from pystache import Loader - markup = Loader().load_template(template_name, self.view.template_path, encoding=self.view.template_encoding) + + markup = Loader().load_template( + template_name, + self.view.template_path, + encoding=self.view.template_encoding) template = Template(markup, self.view) return template.render() + @modifiers.set('=') def _change_delimiter(self, tag_name): """Changes the Mustache delimiter.""" + self.otag, self.ctag = tag_name.split(' ') self._compile_regexps() + return '' + @modifiers.set('{') @modifiers.set('&') def render_unescaped(self, tag_name): """Render a tag without escaping it.""" + return literal(self.view.get(tag_name, '')) + def render(self, encoding=None): template = self._render_sections(self.template, self.view) result = self._render_tags(template) diff --git a/pystache/view.py b/pystache/view.py index 925998e..0e4732f 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -1,9 +1,121 @@ -from pystache import Template -import os.path +# -*- coding: utf-8 -*- + +""" +pystache.loader +~~~~~~~~~~~~~~~ + +This module provides Pystache's views. +""" + import re -from types import * +from types import UnboundMethodType + +from .loader import Loader +from .template import Template + + + +class View(object): + """A Pystache view.""" + + template_name = None + template_path = None + template = None + template_encoding = None + template_extension = 'mustache' + + def __init__(self, template=None, context=None, **kwargs): + + self.template = template + + context = context or {} + context.update(kwargs) + + self.context_list = [context] + + + def get(self, attr, default=None): + + attr = get_or_attr(self.context_list, attr, getattr(self, attr, default)) + + if hasattr(attr, '__call__') and type(attr) is UnboundMethodType: + return attr() + else: + return attr + + + def get_template(self, template_name): + + if not self.template: + + template_name = self._get_template_name(template_name) + + self.template = Loader().load_template( + template_name, + self.template_path, + encoding=self.template_encoding, + extension=self.template_extension) + + return self.template + + + def _get_template_name(self, template_name=None): + """TemplatePartial => template_partial + Takes a string but defaults to using the current class' name or + the `template_name` attribute. + """ + + if template_name: + return template_name + + template_name = self.__class__.__name__ + + def repl(match): + return '_' + match.group(0).lower() + + return re.sub('[A-Z]', repl, template_name)[1:] + + + def _get_context(self): + context = {} + for item in self.context_list: + if hasattr(item, 'keys') and hasattr(item, '__getitem__'): + context.update(item) + return context + + + def render(self, encoding=None): + template = Template(self.get_template(self.template_name), self) + return template.render(encoding=encoding) + + + def __contains__(self, needle): + return needle in self.context or hasattr(self, needle) + + + def __getitem__(self, attr): + val = self.get(attr, None) + + if not val and val is not 0: + raise KeyError("Key '%s' does not exist in View" % attr) + return val + + + def __getattr__(self, attr): + if attr == 'context': + return self._get_context() + + raise AttributeError("Attribute '%s' does not exist in View" % attr) + + + def __str__(self): + return self.render() + + def get_or_attr(context_list, name, default=None): + """Returns an attribute from given context.""" + if not context_list: return default @@ -17,78 +129,7 @@ def get_or_attr(context_list, name, default=None): return getattr(obj, name) except AttributeError: pass + return default -class View(object): - - template_name = None - template_path = None - template = None - template_encoding = None - template_extension = 'mustache' - - def __init__(self, template=None, context=None, **kwargs): - self.template = template - context = context or {} - context.update(**kwargs) - self.context_list = [context] - - def get(self, attr, default=None): - attr = get_or_attr(self.context_list, attr, getattr(self, attr, default)) - if hasattr(attr, '__call__') and type(attr) is UnboundMethodType: - return attr() - else: - return attr - - def get_template(self, template_name): - if not self.template: - from pystache import Loader - template_name = self._get_template_name(template_name) - self.template = Loader().load_template(template_name, self.template_path, encoding=self.template_encoding, extension=self.template_extension) - - return self.template - - def _get_template_name(self, template_name=None): - """TemplatePartial => template_partial - Takes a string but defaults to using the current class' name or - the `template_name` attribute - """ - if template_name: - return template_name - - template_name = self.__class__.__name__ - - def repl(match): - return '_' + match.group(0).lower() - - return re.sub('[A-Z]', repl, template_name)[1:] - - def _get_context(self): - context = {} - for item in self.context_list: - if hasattr(item, 'keys') and hasattr(item, '__getitem__'): - context.update(item) - return context - - def render(self, encoding=None): - return Template(self.get_template(self.template_name), self).render(encoding=encoding) - - def __contains__(self, needle): - return needle in self.context or hasattr(self, needle) - - def __getitem__(self, attr): - val = self.get(attr, None) - - if not val and val is not 0: - raise KeyError("Key '%s' does not exist in View" % attr) - return val - - def __getattr__(self, attr): - if attr == 'context': - return self._get_context() - - raise AttributeError("Attribute '%s' does not exist in View" % attr) - - def __str__(self): - return self.render() \ No newline at end of file