Skip to content

Commit

Permalink
improved undefined behavior
Browse files Browse the repository at this point in the history
--HG--
branch : trunk
  • Loading branch information
mitsuhiko committed Apr 17, 2008
1 parent 814f6c2 commit 9a82205
Show file tree
Hide file tree
Showing 10 changed files with 126 additions and 76 deletions.
3 changes: 2 additions & 1 deletion jinja2/__init__.py
Expand Up @@ -57,6 +57,7 @@
:license: BSD, see LICENSE for more details. :license: BSD, see LICENSE for more details.
""" """
from jinja2.environment import Environment 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.runtime import Undefined, DebugUndefined, StrictUndefined
from jinja2.utils import Markup, escape from jinja2.utils import Markup, escape
7 changes: 1 addition & 6 deletions jinja2/compiler.py
Expand Up @@ -938,7 +938,7 @@ def visitor(self, node, frame):
def uaop(operator): def uaop(operator):
def visitor(self, node, frame): def visitor(self, node, frame):
self.write('(' + operator) self.write('(' + operator)
self.visit(node.node) self.visit(node.node, frame)
self.write(')') self.write(')')
return visitor return visitor


Expand Down Expand Up @@ -977,11 +977,6 @@ def visit_Subscript(self, node, frame):
have_const = True have_const = True
except nodes.Impossible: except nodes.Impossible:
have_const = False 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.write('environment.subscribe(')
self.visit(node.node, frame) self.visit(node.node, frame)
self.write(', ') self.write(', ')
Expand Down
2 changes: 1 addition & 1 deletion jinja2/defaults.py
Expand Up @@ -9,7 +9,7 @@
:license: BSD, see LICENSE for more details. :license: BSD, see LICENSE for more details.
""" """
from jinja2.filters import FILTERS as DEFAULT_FILTERS 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 = { DEFAULT_NAMESPACE = {
Expand Down
9 changes: 6 additions & 3 deletions jinja2/environment.py
Expand Up @@ -110,15 +110,14 @@ def subscribe(self, obj, argument):
try: try:
return obj[argument] return obj[argument]
except (TypeError, LookupError): except (TypeError, LookupError):
return self.undefined(obj, argument) return self.undefined(obj=obj, name=argument)


def parse(self, source, name=None): def parse(self, source, name=None):
"""Parse the sourcecode and return the abstract syntax tree. This tree """Parse the sourcecode and return the abstract syntax tree. This tree
of nodes is used by the compiler to convert the template into of nodes is used by the compiler to convert the template into
executable source- or bytecode. executable source- or bytecode.
""" """
parser = Parser(self, source, name) return Parser(self, source, name).parse()
return parser.parse()


def lex(self, source, name=None): def lex(self, source, name=None):
"""Lex the given sourcecode and return a generator that yields tokens. """Lex the given sourcecode and return a generator that yields tokens.
Expand Down Expand Up @@ -198,12 +197,15 @@ def __init__(self, environment, code, globals, uptodate=None):
namespace['__jinja_template__'] = self namespace['__jinja_template__'] = self


def render(self, *args, **kwargs): def render(self, *args, **kwargs):
"""Render the template into a string."""
return u''.join(self.generate(*args, **kwargs)) return u''.join(self.generate(*args, **kwargs))


def stream(self, *args, **kwargs): def stream(self, *args, **kwargs):
"""Return a `TemplateStream` that generates the template."""
return TemplateStream(self.generate(*args, **kwargs)) return TemplateStream(self.generate(*args, **kwargs))


def generate(self, *args, **kwargs): def generate(self, *args, **kwargs):
"""Return a generator that generates the template."""
# assemble the context # assemble the context
context = dict(*args, **kwargs) context = dict(*args, **kwargs)


Expand Down Expand Up @@ -240,6 +242,7 @@ def get_corresponding_lineno(self, lineno):
return template_line return template_line
return 1 return 1


@property
def is_up_to_date(self): def is_up_to_date(self):
"""Check if the template is still up to date.""" """Check if the template is still up to date."""
if self._uptodate is None: if self._uptodate is None:
Expand Down
21 changes: 12 additions & 9 deletions jinja2/exceptions.py
Expand Up @@ -11,23 +11,23 @@




class TemplateError(Exception): 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): class TemplateNotFound(IOError, LookupError, TemplateError):
""" """Raised if a template does not exist."""
Raised if a template does not exist.
"""


def __init__(self, name): def __init__(self, name):
IOError.__init__(self, name) IOError.__init__(self, name)
self.name = name self.name = name




class TemplateSyntaxError(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): def __init__(self, message, lineno, name):
TEmplateError.__init__(self, '%s (line %s)' % (message, lineno)) TEmplateError.__init__(self, '%s (line %s)' % (message, lineno))
Expand All @@ -37,14 +37,17 @@ def __init__(self, message, lineno, name):




class TemplateAssertionError(AssertionError, TemplateSyntaxError): 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): def __init__(self, message, lineno, name):
AssertionError.__init__(self, message) AssertionError.__init__(self, message)
TemplateSyntaxError.__init__(self, message, lineno, name) TemplateSyntaxError.__init__(self, message, lineno, name)




class TemplateRuntimeError(TemplateError): 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. rendering.
""" """
9 changes: 3 additions & 6 deletions jinja2/filters.py
Expand Up @@ -237,8 +237,7 @@ def do_first(environment, seq):
try: try:
return iter(seq).next() return iter(seq).next()
except StopIteration: except StopIteration:
return environment.undefined('seq|first', return environment.undefined('No first item, sequence was empty.')
extra='the sequence was empty')




@environmentfilter @environmentfilter
Expand All @@ -247,8 +246,7 @@ def do_last(environment, seq):
try: try:
return iter(reversed(seq)).next() return iter(reversed(seq)).next()
except StopIteration: except StopIteration:
return environment.undefined('seq|last', return environment.undefined('No last item, sequence was empty.')
extra='the sequence was empty')




@environmentfilter @environmentfilter
Expand All @@ -257,8 +255,7 @@ def do_random(environment, seq):
try: try:
return choice(seq) return choice(seq)
except IndexError: except IndexError:
return environment.undefined('seq|random', return environment.undefined('No random item, sequence was empty.')
extra='the sequence was empty')




def do_filesizeformat(value): def do_filesizeformat(value):
Expand Down
62 changes: 44 additions & 18 deletions jinja2/loaders.py
Expand Up @@ -9,12 +9,26 @@
:license: BSD, see LICENSE for more details. :license: BSD, see LICENSE for more details.
""" """
from os import path from os import path
from time import time
from jinja2.exceptions import TemplateNotFound from jinja2.exceptions import TemplateNotFound
from jinja2.environment import Template from jinja2.environment import Template
from jinja2.utils import LRUCache 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): class BaseLoader(object):
""" """
Baseclass for all loaders. Subclass this and override `get_source` to Baseclass for all loaders. Subclass this and override `get_source` to
Expand Down Expand Up @@ -61,7 +75,7 @@ def load(self, environment, name, globals=None):
if self.cache is not None: if self.cache is not None:
template = self.cache.get(name) template = self.cache.get(name)
if template is not None and (not self.auto_reload or \ if template is not None and (not self.auto_reload or \
template.is_up_to_date()): template.is_up_to_date):
return template return template


source, filename, uptodate = self.get_source(environment, name) source, filename, uptodate = self.get_source(environment, name)
Expand All @@ -84,27 +98,39 @@ def __init__(self, searchpath, encoding='utf-8', cache_size=50,
self.encoding = encoding self.encoding = encoding


def get_source(self, environment, template): def get_source(self, environment, template):
pieces = [] pieces = split_template_path(template)
for piece in template.split('/'):
if piece == '..':
raise TemplateNotFound(template)
elif piece != '.':
pieces.append(piece)
for searchpath in self.searchpath: for searchpath in self.searchpath:
filename = path.join(searchpath, *pieces) filename = path.join(searchpath, *pieces)
if path.isfile(filename): if not path.isfile(filename):
f = file(filename) continue
try: f = file(filename)
contents = f.read().decode(self.encoding) try:
finally: contents = f.read().decode(self.encoding)
f.close() finally:
mtime = path.getmtime(filename) f.close()
def uptodate(): old = path.getmtime(filename)
return path.getmtime(filename) != mtime return contents, filename, lambda: path.getmtime(filename) != old
return contents, filename, uptodate
raise TemplateNotFound(template) 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): class DictLoader(BaseLoader):
"""Loads a template from a python dict. Used for unittests mostly.""" """Loads a template from a python dict. Used for unittests mostly."""


Expand Down
2 changes: 1 addition & 1 deletion jinja2/parser.py
Expand Up @@ -639,7 +639,7 @@ def parse_test(self, node):
node = nodes.Test(node, name, args, kwargs, dyn_args, node = nodes.Test(node, name, args, kwargs, dyn_args,
dyn_kwargs, lineno=token.lineno) dyn_kwargs, lineno=token.lineno)
if negated: if negated:
node = nodes.NotExpression(node, lineno=token.lineno) node = nodes.Not(node, lineno=token.lineno)
return node return node


def subparse(self, end_tokens=None): def subparse(self, end_tokens=None):
Expand Down
59 changes: 37 additions & 22 deletions jinja2/runtime.py
Expand Up @@ -13,6 +13,7 @@
except ImportError: except ImportError:
defaultdict = None defaultdict = None
from jinja2.utils import Markup from jinja2.utils import Markup
from jinja2.exceptions import UndefinedError




__all__ = ['LoopContext', 'StaticLoopContext', 'TemplateContext', __all__ = ['LoopContext', 'StaticLoopContext', 'TemplateContext',
Expand Down Expand Up @@ -46,8 +47,8 @@ def super(self, block):
try: try:
func = self.blocks[block][-2] func = self.blocks[block][-2]
except LookupError: except LookupError:
return self.environment.undefined('super', return self.environment.undefined('there is no parent block '
extra='there is probably no parent block with this name') 'called %r.' % block)
return SuperBlock(block, self, func) return SuperBlock(block, self, func)


def __setitem__(self, key, value): def __setitem__(self, key, value):
Expand All @@ -65,10 +66,10 @@ def get_exported(self):
def __getitem__(self, name): def __getitem__(self, name):
if name in self: if name in self:
return self[name] return self[name]
return self.environment.undefined(name) return self.environment.undefined(name=name)
else: else:
def __missing__(self, key): def __missing__(self, name):
return self.environment.undefined(key) return self.environment.undefined(name=name)


def __repr__(self): def __repr__(self):
return '<%s %s of %r>' % ( return '<%s %s of %r>' % (
Expand Down Expand Up @@ -241,15 +242,13 @@ def __call__(self, *args, **kwargs):
try: try:
value = self.defaults[idx - arg_count] value = self.defaults[idx - arg_count]
except IndexError: except IndexError:
value = self._environment.undefined(name, value = self._environment.undefined(
extra='parameter not provided') 'parameter %r was not provided' % name)
arguments['l_' + name] = value arguments['l_' + name] = value
if self.caller: if self.caller:
caller = kwargs.pop('caller', None) caller = kwargs.pop('caller', None)
if caller is None: if caller is None:
caller = self._environment.undefined('caller', caller = self._environment.undefined('No caller defined')
extra='The macro was called from an expression and not '
'a call block.')
arguments['l_caller'] = caller arguments['l_caller'] = caller
if self.catch_all: if self.catch_all:
arguments['l_arguments'] = kwargs arguments['l_arguments'] = kwargs
Expand All @@ -268,19 +267,28 @@ class Undefined(object):
`NameError`. Custom undefined classes must subclass this. `NameError`. Custom undefined classes must subclass this.
""" """


def __init__(self, name=None, attr=None, extra=None): def __init__(self, hint=None, obj=None, name=None):
if attr is None: self._undefined_hint = hint
self._undefined_hint = '%r is undefined' % name self._undefined_obj = obj
self._error_class = NameError self._undefined_name = name
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 _fail_with_error(self, *args, **kwargs): 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__ = \ __add__ = __radd__ = __mul__ = __rmul__ = __div__ = __rdiv__ = \
__realdiv__ = __rrealdiv__ = __floordiv__ = __rfloordiv__ = \ __realdiv__ = __rrealdiv__ = __floordiv__ = __rfloordiv__ = \
__mod__ = __rmod__ = __pos__ = __neg__ = __call__ = \ __mod__ = __rmod__ = __pos__ = __neg__ = __call__ = \
Expand Down Expand Up @@ -310,7 +318,14 @@ class DebugUndefined(Undefined):
"""An undefined that returns the debug info when printed.""" """An undefined that returns the debug info when printed."""


def __unicode__(self): 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): class StrictUndefined(Undefined):
Expand Down

0 comments on commit 9a82205

Please sign in to comment.