From fed44b5f36e9da972dd4f792eacedafe80dd5e7f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 13 Apr 2008 19:42:53 +0200 Subject: [PATCH] added globals --HG-- branch : trunk --- jinja2/compiler.py | 107 +++++++++++++++++++++++++++++++++++------- jinja2/environment.py | 52 ++++++++++++++++---- jinja2/loaders.py | 6 +-- jinja2/optimizer.py | 3 +- 4 files changed, 137 insertions(+), 31 deletions(-) diff --git a/jinja2/compiler.py b/jinja2/compiler.py index 3c2347d..bf72963 100644 --- a/jinja2/compiler.py +++ b/jinja2/compiler.py @@ -39,8 +39,7 @@ else: def generate(node, environment, filename, stream=None): """Generate the python source for a node tree.""" - is_child = node.find(nodes.Extends) is not None - generator = CodeGenerator(environment, is_child, filename, stream) + generator = CodeGenerator(environment, filename, stream) generator.visit(node) if stream is None: return generator.stream.getvalue() @@ -114,16 +113,31 @@ class Frame(object): def __init__(self, parent=None): self.identifiers = Identifiers() + # a toplevel frame is the root + soft frames such as if conditions. self.toplevel = False + # the root frame is basically just the outermost frame, so no if # conditions. This information is used to optimize inheritance # situations. self.rootlevel = False - self.parent = parent + + # inside some tags we are using a buffer rather than yield statements. + # this for example affects {% filter %} or {% macro %}. If a frame + # is buffered this variable points to the name of the list used as + # buffer. self.buffer = None + + # if a frame has name_overrides, all read access to a name in this + # dict is redirected to a string expression. self.name_overrides = {} + + # the name of the block we're in, otherwise None. self.block = parent and parent.block or None + + # the parent of this frame + self.parent = parent + if parent is not None: self.identifiers.declared.update( parent.identifiers.declared | @@ -214,33 +228,61 @@ class CompilerExit(Exception): class CodeGenerator(NodeVisitor): - def __init__(self, environment, is_child, filename, stream=None): + def __init__(self, environment, filename, stream=None): if stream is None: stream = StringIO() self.environment = environment - self.is_child = is_child self.filename = filename self.stream = stream + + # a registry for all blocks. Because blocks are moved out + # into the global python scope they are registered here self.blocks = {} - self.indentation = 0 - self.new_lines = 0 - self.last_identifier = 0 + + # the number of extends statements so far self.extends_so_far = 0 + + # some templates have a rootlevel extends. In this case we + # can safely assume that we're a child template and do some + # more optimizations. self.has_known_extends = False + + # the number of new lines before the next write() + self._new_lines = 0 + + # the line number of the last written statement self._last_line = 0 + + # true if nothing was written so far. self._first_write = True + # used by the `temporary_identifier` method to get new + # unique, temporary identifier + self._last_identifier = 0 + + # the current indentation + self._indentation = 0 + def temporary_identifier(self): - self.last_identifier += 1 - return 't%d' % self.last_identifier + """Get a new unique identifier.""" + self._last_identifier += 1 + return 't%d' % self._last_identifier def indent(self): - self.indentation += 1 + """Indent by one.""" + self._indentation += 1 def outdent(self, step=1): - self.indentation -= step + """Outdent by step.""" + self._indentation -= step def blockvisit(self, nodes, frame, indent=True, force_generator=True): + """Visit a list of nodes as block in a frame. Per default the + code is indented, but this can be disabled by setting the indent + parameter to False. If the current frame is no buffer a dummy + ``if 0: yield None`` is written automatically unless the + force_generator parameter is set to False. + """ if indent: self.indent() if frame.buffer is None and force_generator: @@ -254,26 +296,36 @@ class CodeGenerator(NodeVisitor): self.outdent() def write(self, x): - if self.new_lines: + """Write a string into the output stream.""" + if self._new_lines: if not self._first_write: - self.stream.write('\n' * self.new_lines) + self.stream.write('\n' * self._new_lines) self._first_write = False - self.stream.write(' ' * self.indentation) - self.new_lines = 0 + self.stream.write(' ' * self._indentation) + self._new_lines = 0 self.stream.write(x) def writeline(self, x, node=None, extra=0): + """Combination of newline and write.""" self.newline(node, extra) self.write(x) def newline(self, node=None, extra=0): - self.new_lines = max(self.new_lines, 1 + extra) + """Add one or more newlines before the next write.""" + self._new_lines = max(self._new_lines, 1 + extra) if node is not None and node.lineno != self._last_line: self.write('# line: %s' % node.lineno) - self.new_lines = 1 + self._new_lines = 1 self._last_line = node.lineno def signature(self, node, frame, have_comma=True, extra_kwargs=None): + """Writes a function call to the stream for the current node. + Per default it will write a leading comma but this can be + disabled by setting have_comma to False. If extra_kwargs is + given it must be a string that represents a single keyword + argument call that is inserted at the end of the regular + keyword argument calls. + """ have_comma = have_comma and [True] or [] def touch_comma(): if have_comma: @@ -300,6 +352,10 @@ class CodeGenerator(NodeVisitor): self.visit(node.dyn_kwargs, frame) def pull_locals(self, frame, indent=True): + """Pull all the references identifiers into the local scope. + This affects regular names, filters and tests. If indent is + set to False, no automatic indentation will take place. + """ if indent: self.indent() for name in frame.identifiers.undeclared: @@ -312,6 +368,10 @@ class CodeGenerator(NodeVisitor): self.outdent() def collect_shadowed(self, frame): + """This function returns all the shadowed variables in a dict + in the form name: alias and will write the required assignments + into the current scope. No indentation takes place. + """ # make sure we "backup" overridden, local identifiers # TODO: we should probably optimize this and check if the # identifier is in use afterwards. @@ -322,6 +382,17 @@ class CodeGenerator(NodeVisitor): return aliases def function_scoping(self, node, frame): + """In Jinja a few statements require the help of anonymous + functions. Those are currently macros and call blocks and in + the future also recursive loops. As there is currently + technical limitation that doesn't allow reading and writing a + variable in a scope where the initial value is coming from an + outer scope, this function tries to fall back with a common + error message. Additionally the frame passed is modified so + that the argumetns are collected and callers are looked up. + + This will return the modified frame. + """ func_frame = frame.inner() func_frame.inspect(node.iter_child_nodes(), hard_scope=True) diff --git a/jinja2/environment.py b/jinja2/environment.py index 8458a23..7c1962c 100644 --- a/jinja2/environment.py +++ b/jinja2/environment.py @@ -32,6 +32,7 @@ class Environment(object): comment_end_string='#}', line_statement_prefix=None, trim_blocks=False, + optimized=True, loader=None): """Here the possible initialization parameters: @@ -52,6 +53,8 @@ class Environment(object): `trim_blocks` If this is set to ``True`` the first newline after a block is removed (block, not variable tag!). Defaults to ``False``. + `optimized` should the optimizer be enabled? Default is + ``True``. `loader` the loader which should be used. ========================= ============================================ """ @@ -65,6 +68,7 @@ class Environment(object): self.comment_end_string = comment_end_string self.line_statement_prefix = line_statement_prefix self.trim_blocks = trim_blocks + self.optimized = optimized # defaults self.filters = DEFAULT_FILTERS.copy() @@ -101,11 +105,12 @@ class Environment(object): """ return self.lexer.tokeniter(source, filename) - def compile(self, source, filename=None, raw=False): + def compile(self, source, filename=None, raw=False, globals=None): """Compile a node or source.""" if isinstance(source, basestring): source = self.parse(source, filename) - node = optimize(source, self) + if self.optimized: + node = optimize(source, self, globals or {}) source = generate(node, self, filename) if raw: return source @@ -119,35 +124,64 @@ class Environment(object): function can be used to calculate the real filename.""" return template - def get_template(self, name, parent=None): + def get_template(self, name, parent=None, globals=None): """Load a template.""" if self.loader is None: raise TypeError('no loader for this environment specified') if parent is not None: name = self.join_path(name, parent) - return self.loader.load(self, name) + globals = self.make_globals(globals) + return self.loader.load(self, name, globals) - def from_string(self, source, filename=''): + def from_string(self, source, filename='', globals=None): """Load a template from a string.""" - return Template(self, self.compile(source, filename)) + globals = self.make_globals(globals) + return Template(self, self.compile(source, filename), globals) + + def make_globals(self, d): + """Return a dict for the globals.""" + if d is None: + return self.globals + return dict(self.globals, **d) class Template(object): """Represents a template.""" - def __init__(self, environment, code): + def __init__(self, environment, code, globals): namespace = {'environment': environment} exec code in namespace self.environment = environment self.name = namespace['filename'] self.root_render_func = namespace['root'] self.blocks = namespace['blocks'] + self.globals = globals def render(self, *args, **kwargs): - return u''.join(self.stream(*args, **kwargs)) + return u''.join(self.generate(*args, **kwargs)) def stream(self, *args, **kwargs): - gen = self.root_render_func(dict(*args, **kwargs)) + return TemplateStream(self.generate(*args, **kwargs)) + + def generate(self, *args, **kwargs): + # assemble the context + context = self.globals.copy() + context.update(*args, **kwargs) + + # if the environment is using the optimizer locals may never + # override globals as optimizations might have happened + # depending on values of certain globals. This assertion goes + # away if the python interpreter is started with -O + if __debug__ and self.environment.optimized: + overrides = set(context) & set(self.globals) + if overrides: + plural = len(overrides) != 1 and 's' or '' + raise AssertionError('the per template variable%s %s ' + 'override%s global variable%s. ' + 'With an enabled optimizer this ' + 'will lead to unexpected results.' % + (plural, ', '.join(overrides), plural or ' a', plural)) + gen = self.root_render_func(context) # skip the first item which is a reference to the stream gen.next() return gen diff --git a/jinja2/loaders.py b/jinja2/loaders.py index 97e51d6..4d1c335 100644 --- a/jinja2/loaders.py +++ b/jinja2/loaders.py @@ -19,10 +19,10 @@ class BaseLoader(object): def get_source(self, environment, template): raise TemplateNotFound() - def load(self, environment, template): + def load(self, environment, template, globals=None): source, filename = self.get_source(environment, template) - code = environment.compile(source, filename) - return Template(environment, code) + code = environment.compile(source, filename, globals=globals) + return Template(environment, code, globals or {}) class FileSystemLoader(BaseLoader): diff --git a/jinja2/optimizer.py b/jinja2/optimizer.py index 4dbd5d9..48155c5 100644 --- a/jinja2/optimizer.py +++ b/jinja2/optimizer.py @@ -249,8 +249,9 @@ class Optimizer(NodeTransformer): environment=self.environment) except nodes.Impossible: return node + visit_Add = visit_Sub = visit_Mul = visit_Div = visit_FloorDiv = \ visit_Pow = visit_Mod = visit_And = visit_Or = visit_Pos = visit_Neg = \ visit_Not = visit_Compare = visit_Subscript = visit_Call = \ - visit_Filter = visit_Test = fold + visit_Filter = visit_Test = visit_CondExpr = fold del fold -- 2.26.2