improved undefined behavior

--HG--
branch : trunk
diff --git a/jinja2/__init__.py b/jinja2/__init__.py
index a59d42c..9099a21 100644
--- a/jinja2/__init__.py
+++ b/jinja2/__init__.py
@@ -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
diff --git a/jinja2/compiler.py b/jinja2/compiler.py
index 596091e..955403d 100644
--- a/jinja2/compiler.py
+++ b/jinja2/compiler.py
@@ -938,7 +938,7 @@
     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 @@
             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(', ')
diff --git a/jinja2/defaults.py b/jinja2/defaults.py
index 4deb8e5..ee698a0 100644
--- a/jinja2/defaults.py
+++ b/jinja2/defaults.py
@@ -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 = {
diff --git a/jinja2/environment.py b/jinja2/environment.py
index b650ac7..1ed0464 100644
--- a/jinja2/environment.py
+++ b/jinja2/environment.py
@@ -110,15 +110,14 @@
             try:
                 return obj[argument]
             except (TypeError, LookupError):
-                return self.undefined(obj, argument)
+                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 @@
         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 @@
                 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:
diff --git a/jinja2/exceptions.py b/jinja2/exceptions.py
index 024ff58..efa9e89 100644
--- a/jinja2/exceptions.py
+++ b/jinja2/exceptions.py
@@ -11,13 +11,15 @@
 
 
 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 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 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 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.
     """
diff --git a/jinja2/filters.py b/jinja2/filters.py
index 176685d..df1d898 100644
--- a/jinja2/filters.py
+++ b/jinja2/filters.py
@@ -237,8 +237,7 @@
     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 @@
     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 @@
     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):
diff --git a/jinja2/loaders.py b/jinja2/loaders.py
index a78116c..b621bbf 100644
--- a/jinja2/loaders.py
+++ b/jinja2/loaders.py
@@ -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 @@
         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 @@
         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."""
 
diff --git a/jinja2/parser.py b/jinja2/parser.py
index fd43af7..a8969a8 100644
--- a/jinja2/parser.py
+++ b/jinja2/parser.py
@@ -639,7 +639,7 @@
         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):
diff --git a/jinja2/runtime.py b/jinja2/runtime.py
index a18b7bc..31c17f0 100644
--- a/jinja2/runtime.py
+++ b/jinja2/runtime.py
@@ -13,6 +13,7 @@
 except ImportError:
     defaultdict = None
 from jinja2.utils import Markup
+from jinja2.exceptions import UndefinedError
 
 
 __all__ = ['LoopContext', 'StaticLoopContext', 'TemplateContext',
@@ -46,8 +47,8 @@
         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 @@
         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 @@
                     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 @@
     `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 @@
     """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):
diff --git a/jinja2/sandbox.py b/jinja2/sandbox.py
index 0c8b940..71f0239 100644
--- a/jinja2/sandbox.py
+++ b/jinja2/sandbox.py
@@ -44,7 +44,7 @@
         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 @@
         """
         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."""