From 9a82205d536ca9708768a439373b91651556512f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 17 Apr 2008 18:44:07 +0200 Subject: [PATCH] improved undefined behavior --HG-- branch : trunk --- jinja2/__init__.py | 3 ++- jinja2/compiler.py | 7 +---- jinja2/defaults.py | 2 +- jinja2/environment.py | 9 ++++--- jinja2/exceptions.py | 21 ++++++++------- jinja2/filters.py | 9 +++---- jinja2/loaders.py | 62 ++++++++++++++++++++++++++++++------------- jinja2/parser.py | 2 +- jinja2/runtime.py | 59 +++++++++++++++++++++++++--------------- jinja2/sandbox.py | 28 ++++++++++++------- 10 files changed, 126 insertions(+), 76 deletions(-) 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 @@ 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(', ') 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 @@ class Environment(object): 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 @@ 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: 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 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. """ 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 @@ 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): 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 @@ 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.""" 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 @@ 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): 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 @@ 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): 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 @@ 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.""" -- 2.26.2