improved undefined behavior
authorArmin Ronacher <armin.ronacher@active-4.com>
Thu, 17 Apr 2008 16:44:07 +0000 (18:44 +0200)
committerArmin Ronacher <armin.ronacher@active-4.com>
Thu, 17 Apr 2008 16:44:07 +0000 (18:44 +0200)
--HG--
branch : trunk

jinja2/__init__.py
jinja2/compiler.py
jinja2/defaults.py
jinja2/environment.py
jinja2/exceptions.py
jinja2/filters.py
jinja2/loaders.py
jinja2/parser.py
jinja2/runtime.py
jinja2/sandbox.py

index a59d42cb0f76c845e0a9e7459a8eee199b723f51..9099a21219ca01c7d5aad66735e2c200b7f681e1 100644 (file)
@@ -57,6 +57,7 @@
     :license: BSD, see LICENSE for more details.
 """
 from jinja2.environment import Environment
-from jinja2.loaders import BaseLoader, FileSystemLoader, DictLoader
+from jinja2.loaders import BaseLoader, FileSystemLoader, PackageLoader, \
+     DictLoader
 from jinja2.runtime import Undefined, DebugUndefined, StrictUndefined
 from jinja2.utils import Markup, escape
index 596091ecf3af42c0bd975e76936c65090802f218..955403df69112c762f91c2bf031cb29b2335696d 100644 (file)
@@ -938,7 +938,7 @@ class CodeGenerator(NodeVisitor):
     def uaop(operator):
         def visitor(self, node, frame):
             self.write('(' + operator)
-            self.visit(node.node)
+            self.visit(node.node, frame)
             self.write(')')
         return visitor
 
@@ -977,11 +977,6 @@ class CodeGenerator(NodeVisitor):
             have_const = True
         except nodes.Impossible:
             have_const = False
-        if have_const:
-            if isinstance(const, (int, long, float)):
-                self.visit(node.node, frame)
-                self.write('[%s]' % const)
-                return
         self.write('environment.subscribe(')
         self.visit(node.node, frame)
         self.write(', ')
index 4deb8e53f97a6df8cae2855fe1772eb351325cae..ee698a0873edbb906026e717bc33233e3a7f0b39 100644 (file)
@@ -9,7 +9,7 @@
     :license: BSD, see LICENSE for more details.
 """
 from jinja2.filters import FILTERS as DEFAULT_FILTERS
-from jinja.tests import TESTS as DEFAULT_TESTS
+from jinja2.tests import TESTS as DEFAULT_TESTS
 
 
 DEFAULT_NAMESPACE = {
index b650ac72df5ac3e38a60ef098217fc87ab8c0329..1ed0464de496ed71ecf4d295addcc78d14c8a653 100644 (file)
@@ -110,15 +110,14 @@ class Environment(object):
             try:
                 return obj[argument]
             except (TypeError, LookupError):
-                return self.undefined(objargument)
+                return self.undefined(obj=obj, name=argument)
 
     def parse(self, source, name=None):
         """Parse the sourcecode and return the abstract syntax tree. This tree
         of nodes is used by the compiler to convert the template into
         executable source- or bytecode.
         """
-        parser = Parser(self, source, name)
-        return parser.parse()
+        return Parser(self, source, name).parse()
 
     def lex(self, source, name=None):
         """Lex the given sourcecode and return a generator that yields tokens.
@@ -198,12 +197,15 @@ class Template(object):
         namespace['__jinja_template__'] = self
 
     def render(self, *args, **kwargs):
+        """Render the template into a string."""
         return u''.join(self.generate(*args, **kwargs))
 
     def stream(self, *args, **kwargs):
+        """Return a `TemplateStream` that generates the template."""
         return TemplateStream(self.generate(*args, **kwargs))
 
     def generate(self, *args, **kwargs):
+        """Return a generator that generates the template."""
         # assemble the context
         context = dict(*args, **kwargs)
 
@@ -240,6 +242,7 @@ class Template(object):
                 return template_line
         return 1
 
+    @property
     def is_up_to_date(self):
         """Check if the template is still up to date."""
         if self._uptodate is None:
index 024ff58bf4c77ff157bb409bbc90397bc7540202..efa9e898b17ef42a4a40f17a15714cf4c820e411 100644 (file)
 
 
 class TemplateError(Exception):
-    pass
+    """Baseclass for all template errors."""
+
+
+class UndefinedError(TemplateError):
+    """Raised if a template tries to operate on `Undefined`."""
 
 
 class TemplateNotFound(IOError, LookupError, TemplateError):
-    """
-    Raised if a template does not exist.
-    """
+    """Raised if a template does not exist."""
 
     def __init__(self, name):
         IOError.__init__(self, name)
@@ -25,9 +27,7 @@ class TemplateNotFound(IOError, LookupError, TemplateError):
 
 
 class TemplateSyntaxError(TemplateError):
-    """
-    Raised to tell the user that there is a problem with the template.
-    """
+    """Raised to tell the user that there is a problem with the template."""
 
     def __init__(self, message, lineno, name):
         TEmplateError.__init__(self, '%s (line %s)' % (message, lineno))
@@ -37,6 +37,10 @@ class TemplateSyntaxError(TemplateError):
 
 
 class TemplateAssertionError(AssertionError, TemplateSyntaxError):
+    """Like a template syntax error, but covers cases where something in the
+    template caused an error at compile time that wasn't necessarily caused
+    by a syntax error.
+    """
 
     def __init__(self, message, lineno, name):
         AssertionError.__init__(self, message)
@@ -44,7 +48,6 @@ class TemplateAssertionError(AssertionError, TemplateSyntaxError):
 
 
 class TemplateRuntimeError(TemplateError):
-    """
-    Raised by the template engine if a tag encountered an error when
+    """Raised by the template engine if a tag encountered an error when
     rendering.
     """
index 176685d6837c19ec3f4da5b40fa96346856fa101..df1d898a28164471d7d8861b97073976432788a9 100644 (file)
@@ -237,8 +237,7 @@ def do_first(environment, seq):
     try:
         return iter(seq).next()
     except StopIteration:
-        return environment.undefined('seq|first',
-            extra='the sequence was empty')
+        return environment.undefined('No first item, sequence was empty.')
 
 
 @environmentfilter
@@ -247,8 +246,7 @@ def do_last(environment, seq):
     try:
         return iter(reversed(seq)).next()
     except StopIteration:
-        return environment.undefined('seq|last',
-            extra='the sequence was empty')
+        return environment.undefined('No last item, sequence was empty.')
 
 
 @environmentfilter
@@ -257,8 +255,7 @@ def do_random(environment, seq):
     try:
         return choice(seq)
     except IndexError:
-        return environment.undefined('seq|random',
-            extra='the sequence was empty')
+        return environment.undefined('No random item, sequence was empty.')
 
 
 def do_filesizeformat(value):
index a78116c4dfbc256f7e9c19378e75d540df219ab0..b621bbf27d84e2fbfb5fed355f3900c321756f7d 100644 (file)
@@ -9,12 +9,26 @@
     :license: BSD, see LICENSE for more details.
 """
 from os import path
-from time import time
 from jinja2.exceptions import TemplateNotFound
 from jinja2.environment import Template
 from jinja2.utils import LRUCache
 
 
+def split_template_path(template):
+    """Split a path into segments and perform a sanity check.  If it detects
+    '..' in the path it will raise a `TemplateNotFound` error.
+    """
+    pieces = []
+    for piece in template.split('/'):
+        if path.sep in piece \
+           or (path.altsep and path.altsep in piece) or \
+           piece == path.pardir:
+            raise TemplateNotFound(template)
+        elif piece != '.':
+            pieces.append(piece)
+    return pieces
+
+
 class BaseLoader(object):
     """
     Baseclass for all loaders.  Subclass this and override `get_source` to
@@ -61,7 +75,7 @@ class BaseLoader(object):
         if self.cache is not None:
             template = self.cache.get(name)
             if template is not None and (not self.auto_reload or \
-                                         template.is_up_to_date()):
+                                         template.is_up_to_date):
                 return template
 
         source, filename, uptodate = self.get_source(environment, name)
@@ -84,27 +98,39 @@ class FileSystemLoader(BaseLoader):
         self.encoding = encoding
 
     def get_source(self, environment, template):
-        pieces = []
-        for piece in template.split('/'):
-            if piece == '..':
-                raise TemplateNotFound(template)
-            elif piece != '.':
-                pieces.append(piece)
+        pieces = split_template_path(template)
         for searchpath in self.searchpath:
             filename = path.join(searchpath, *pieces)
-            if path.isfile(filename):
-                f = file(filename)
-                try:
-                    contents = f.read().decode(self.encoding)
-                finally:
-                    f.close()
-                mtime = path.getmtime(filename)
-                def uptodate():
-                    return path.getmtime(filename) != mtime
-                return contents, filename, uptodate
+            if not path.isfile(filename):
+                continue
+            f = file(filename)
+            try:
+                contents = f.read().decode(self.encoding)
+            finally:
+                f.close()
+            old = path.getmtime(filename)
+            return contents, filename, lambda: path.getmtime(filename) != old
         raise TemplateNotFound(template)
 
 
+class PackageLoader(BaseLoader):
+    """Load templates from python eggs."""
+
+    def __init__(self, package_name, package_path, charset='utf-8',
+                 cache_size=50, auto_reload=True):
+        BaseLoader.__init__(self, cache_size, auto_reload)
+        import pkg_resources
+        self._pkg = pkg_resources
+        self.package_name = package_name
+        self.package_path = package_path
+
+    def get_source(self, environment, template):
+        path = '/'.join(split_template_path(template))
+        if not self._pkg.resource_exists(self.package_name, path):
+            raise TemplateNotFound(template)
+        return self._pkg.resource_string(self.package_name, path), None, None
+
+
 class DictLoader(BaseLoader):
     """Loads a template from a python dict.  Used for unittests mostly."""
 
index fd43af7ae94492e834e459104ac2eac3ca93b9a1..a8969a82275b1abdf38705efe32c7cb31d2f8224 100644 (file)
@@ -639,7 +639,7 @@ class Parser(object):
         node = nodes.Test(node, name, args, kwargs, dyn_args,
                           dyn_kwargs, lineno=token.lineno)
         if negated:
-            node = nodes.NotExpression(node, lineno=token.lineno)
+            node = nodes.Not(node, lineno=token.lineno)
         return node
 
     def subparse(self, end_tokens=None):
index a18b7bceae3db00dfdbb067595dae3ffa47b8cfc..31c17f0640d989532b295dc11502ec8904a99cfc 100644 (file)
@@ -13,6 +13,7 @@ try:
 except ImportError:
     defaultdict = None
 from jinja2.utils import Markup
+from jinja2.exceptions import UndefinedError
 
 
 __all__ = ['LoopContext', 'StaticLoopContext', 'TemplateContext',
@@ -46,8 +47,8 @@ class TemplateContext(dict):
         try:
             func = self.blocks[block][-2]
         except LookupError:
-            return self.environment.undefined('super',
-                extra='there is probably no parent block with this name')
+            return self.environment.undefined('there is no parent block '
+                                              'called %r.' % block)
         return SuperBlock(block, self, func)
 
     def __setitem__(self, key, value):
@@ -65,10 +66,10 @@ class TemplateContext(dict):
         def __getitem__(self, name):
             if name in self:
                 return self[name]
-            return self.environment.undefined(name)
+            return self.environment.undefined(name=name)
     else:
-        def __missing__(self, key):
-            return self.environment.undefined(key)
+        def __missing__(self, name):
+            return self.environment.undefined(name=name)
 
     def __repr__(self):
         return '<%s %s of %r>' % (
@@ -241,15 +242,13 @@ class Macro(object):
                     try:
                         value = self.defaults[idx - arg_count]
                     except IndexError:
-                        value = self._environment.undefined(name,
-                            extra='parameter not provided')
+                        value = self._environment.undefined(
+                            'parameter %r was not provided' % name)
             arguments['l_' + name] = value
         if self.caller:
             caller = kwargs.pop('caller', None)
             if caller is None:
-                caller = self._environment.undefined('caller',
-                    extra='The macro was called from an expression and not '
-                          'a call block.')
+                caller = self._environment.undefined('No caller defined')
             arguments['l_caller'] = caller
         if self.catch_all:
             arguments['l_arguments'] = kwargs
@@ -268,19 +267,28 @@ class Undefined(object):
     `NameError`.  Custom undefined classes must subclass this.
     """
 
-    def __init__(self, name=None, attr=None, extra=None):
-        if attr is None:
-            self._undefined_hint = '%r is undefined' % name
-            self._error_class = NameError
-        else:
-            self._undefined_hint = '%r has no attribute named %r' \
-                                   % (name, attr)
-            self._error_class = AttributeError
-        if extra is not None:
-            self._undefined_hint += ' (' + extra + ')'
+    def __init__(self, hint=None, obj=None, name=None):
+        self._undefined_hint = hint
+        self._undefined_obj = obj
+        self._undefined_name = name
 
     def _fail_with_error(self, *args, **kwargs):
-        raise self._error_class(self._undefined_hint)
+        if self._undefined_hint is None:
+            if self._undefined_obj is None:
+                hint = '%r is undefined' % self._undefined_name
+            elif not isinstance(self._undefined_name, basestring):
+                hint = '%r object has no element %r' % (
+                    self._undefined_obj.__class__.__name__,
+                    self._undefined_name
+                )
+            else:
+                hint = '%r object has no attribute %s' % (
+                    self._undefined_obj.__class__.__name__,
+                    self._undefined_name
+                )
+        else:
+            hint = self._undefined_hint
+        raise UndefinedError(hint)
     __add__ = __radd__ = __mul__ = __rmul__ = __div__ = __rdiv__ = \
     __realdiv__ = __rrealdiv__ = __floordiv__ = __rfloordiv__ = \
     __mod__ = __rmod__ = __pos__ = __neg__ = __call__ = \
@@ -310,7 +318,14 @@ class DebugUndefined(Undefined):
     """An undefined that returns the debug info when printed."""
 
     def __unicode__(self):
-        return u'{{ %s }}' % self._undefined_hint
+        if self._undefined_hint is None:
+            if self._undefined_obj is None:
+                return u'{{ %s }}' % self._undefined_name
+            return '{{ no such element: %s[%r] }}' % (
+                self._undefined_obj.__class__.__name__,
+                self._undefined_name
+            )
+        return u'{{ undefined value printed: %s }}' % self._undefined_hint
 
 
 class StrictUndefined(Undefined):
index 0c8b9403169b4d23172e61d100467504a3ee9968..71f0239ca363388452bdeb3000260b876a30b34f 100644 (file)
@@ -44,7 +44,7 @@ class SandboxedEnvironment(Environment):
         Environment.__init__(self, *args, **kwargs)
         self.globals['range'] = safe_range
 
-    def is_safe_attribute(self, obj, attr):
+    def is_safe_attribute(self, obj, attr, value):
         """The sandboxed environment will call this method to check if the
         attribute of an object is safe to access.  Per default all attributes
         starting with an underscore are considered private as well as the
@@ -66,17 +66,27 @@ class SandboxedEnvironment(Environment):
         """
         return not getattr(obj, 'unsafe_callable', False)
 
-    def subscribe(self, obj, arg):
+    def subscribe(self, obj, argument):
         """Subscribe an object from sandboxed code."""
+        is_unsafe = False
         try:
-            return obj[arg]
+            value = getattr(obj, str(argument))
+        except (AttributeError, UnicodeError):
+            pass
+        else:
+            if self.is_safe_attribute(obj, argument, value):
+                return value
+            is_unsafe = True
+        try:
+            return obj[argument]
         except (TypeError, LookupError):
-            if not self.is_safe_attribute(obj, arg):
-                return Undefined(obj, arg, extra='attribute unsafe')
-            try:
-                return getattr(obj, str(arg))
-            except (AttributeError, UnicodeError):
-                return Undefined(obj, arg)
+            if is_unsafe:
+                return self.undefined('access to attribute %r of %r object is'
+                                      ' unsafe.' % (
+                    argument,
+                    obj.__class__.__name__
+                ))
+        return self.undefined(obj=obj, name=argument)
 
     def call(__self, __obj, *args, **kwargs):
         """Call an object from sandboxed code."""