First extension interface documentation and updates in that interface
--HG--
branch : trunk
diff --git a/jinja2/nodes.py b/jinja2/nodes.py
index 83d7573..0d921a7 100644
--- a/jinja2/nodes.py
+++ b/jinja2/nodes.py
@@ -48,6 +48,10 @@
}
+# if this is `True` no new Node classes can be created.
+_node_setup_finished = False
+
+
class Impossible(Exception):
"""Raised if the node could not perform a requested action."""
@@ -58,6 +62,8 @@
automatically forwarded to the child."""
def __new__(cls, name, bases, d):
+ if __debug__ and _node_setup_finished:
+ raise TypeError('Can\'t create custom node types.')
for attr in 'fields', 'attributes':
storage = []
storage.extend(getattr(bases[0], attr, ()))
@@ -65,18 +71,34 @@
assert len(bases) == 1, 'multiple inheritance not allowed'
assert len(storage) == len(set(storage)), 'layout conflict'
d[attr] = tuple(storage)
+ d.setdefault('abstract', False)
return type.__new__(cls, name, bases, d)
class Node(object):
- """Baseclass for all Jinja nodes."""
+ """Baseclass for all Jinja2 nodes. There are a number of nodes available
+ of different types. There are three major types:
+
+ - :class:`Stmt`: statements
+ - :class:`Expr`: expressions
+ - :class:`Helper`: helper nodes
+ - :class:`Template`: the outermost wrapper node
+
+ All nodes have fields and attributes. Fields may be other nodes, lists,
+ or arbitrary values. Fields are passed to the constructor as regular
+ positional arguments, attributes as keyword arguments. Each node has
+ two attributes: `lineno` (the line number of the node) and `environment`.
+ The `environment` attribute is set at the end of the parsing process for
+ all nodes automatically.
+ """
__metaclass__ = NodeType
fields = ()
attributes = ('lineno', 'environment')
+ abstract = True
- def __init__(self, *args, **kw):
- if args:
- if len(args) != len(self.fields):
+ def __init__(self, *fields, **attributes):
+ if fields:
+ if len(fields) != len(self.fields):
if not self.fields:
raise TypeError('%r takes 0 arguments' %
self.__class__.__name__)
@@ -85,16 +107,19 @@
len(self.fields),
len(self.fields) != 1 and 's' or ''
))
- for name, arg in izip(self.fields, args):
+ for name, arg in izip(self.fields, fields):
setattr(self, name, arg)
for attr in self.attributes:
- setattr(self, attr, kw.pop(attr, None))
- if kw:
- raise TypeError('unknown keyword argument %r' %
- iter(kw).next())
+ setattr(self, attr, attributes.pop(attr, None))
+ if attributes:
+ raise TypeError('unknown attribute %r' %
+ iter(attributes).next())
def iter_fields(self, exclude=()):
- """Iterate over all fields."""
+ """This method iterates over all fields that are defined and yields
+ ``(key, value)`` tuples. Optionally a parameter of ignored fields
+ can be provided.
+ """
for name in self.fields:
if name not in exclude:
try:
@@ -103,7 +128,10 @@
pass
def iter_child_nodes(self, exclude=()):
- """Iterate over all child nodes."""
+ """Iterates over all direct child nodes of the node. This iterates
+ over all fields and yields the values of they are nodes. If the value
+ of a field is a list all the nodes in that list are returned.
+ """
for field, item in self.iter_fields(exclude):
if isinstance(item, list):
for n in item:
@@ -113,7 +141,9 @@
yield item
def find(self, node_type):
- """Find the first node of a given type."""
+ """Find the first node of a given type. If no such node exists the
+ return value is `None`.
+ """
for result in self.find_all(node_type):
return result
@@ -161,6 +191,7 @@
if 'ctx' in node.fields:
node.ctx = ctx
todo.extend(node.iter_child_nodes())
+ return self
def set_lineno(self, lineno, override=False):
"""Set the line numbers of the node and children."""
@@ -171,6 +202,7 @@
if node.lineno is None or override:
node.lineno = lineno
todo.extend(node.iter_child_nodes())
+ return self
def set_environment(self, environment):
"""Set the environment for all nodes."""
@@ -179,6 +211,7 @@
node = todo.popleft()
node.environment = environment
todo.extend(node.iter_child_nodes())
+ return self
def __repr__(self):
return '%s(%s)' % (
@@ -190,14 +223,18 @@
class Stmt(Node):
"""Base node for all statements."""
+ abstract = True
class Helper(Node):
"""Nodes that exist in a specific context only."""
+ abstract = True
class Template(Node):
- """Node that represents a template."""
+ """Node that represents a template. This must be the outermost node that
+ is passed to the compiler.
+ """
fields = ('body',)
@@ -229,22 +266,33 @@
class For(Stmt):
- """A node that represents a for loop"""
+ """The for loop. `target` is the target for the iteration (usually a
+ :class:`Name` or :class:`Tuple`), `iter` the iterable. `body` is a list
+ of nodes that are used as loop-body, and `else_` a list of nodes for the
+ `else` block. If no else node exists it has to be an empty list.
+
+ For filtered nodes an expression can be stored as `test`, otherwise `None`.
+ """
fields = ('target', 'iter', 'body', 'else_', 'test')
class If(Stmt):
- """A node that represents an if condition."""
+ """If `test` is true, `body` is rendered, else `else_`."""
fields = ('test', 'body', 'else_')
class Macro(Stmt):
- """A node that represents a macro."""
+ """A macro definition. `name` is the name of the macro, `args` a list of
+ arguments and `defaults` a list of defaults if there are any. `body` is
+ a list of nodes for the macro body.
+ """
fields = ('name', 'args', 'defaults', 'body')
class CallBlock(Stmt):
- """A node that represents am extended macro call."""
+ """Like a macro without a name but a call instead. `call` is called with
+ the unnamed macro as `caller` argument this node holds.
+ """
fields = ('call', 'args', 'defaults', 'body')
@@ -287,13 +335,8 @@
fields = ('template', 'names', 'with_context')
-class Trans(Stmt):
- """A node for translatable sections."""
- fields = ('singular', 'plural', 'indicator', 'replacements')
-
-
class ExprStmt(Stmt):
- """A statement that evaluates an expression to None."""
+ """A statement that evaluates an expression and discards the result."""
fields = ('node',)
@@ -304,10 +347,21 @@
class Expr(Node):
"""Baseclass for all expressions."""
+ abstract = True
def as_const(self):
"""Return the value of the expression as constant or raise
- `Impossible` if this was not possible.
+ :exc:`Impossible` if this was not possible:
+
+ >>> Add(Const(23), Const(42)).as_const()
+ 65
+ >>> Add(Const(23), Name('var', 'load')).as_const()
+ Traceback (most recent call last):
+ ...
+ Impossible
+
+ This requires the `environment` attribute of all nodes to be
+ set to the environment that created the nodes.
"""
raise Impossible()
@@ -343,27 +397,29 @@
class Name(Expr):
- """any name such as {{ foo }}"""
+ """Looks up a name or stores a value in a name.
+ The `ctx` of the node can be one of the following values:
+
+ - `store`: store a value in the name
+ - `load`: load that name
+ - `param`: like `store` but if the name was defined as function parameter.
+ """
fields = ('name', 'ctx')
def can_assign(self):
return self.name not in ('true', 'false', 'none')
-class MarkSafe(Expr):
- """Mark the wrapped expression as safe (Markup)"""
- fields = ('expr',)
-
- def as_const(self):
- return Markup(self.expr.as_const())
-
-
class Literal(Expr):
"""Baseclass for literals."""
class Const(Literal):
- """any constat such as {{ "foo" }}"""
+ """All constant values. The parser will return this node for simple
+ constants such as ``42`` or ``"foo"`` but it can be used to store more
+ complex values such as lists too. Only constants with a safe
+ representation (objects where ``eval(repr(x)) == x`` is true).
+ """
fields = ('value',)
def as_const(self):
@@ -383,7 +439,8 @@
class Tuple(Literal):
"""For loop unpacking and some other things like multiple arguments
- for subscripts.
+ for subscripts. Like for :class:`Name` `ctx` specifies if the tuple
+ is used for loading the names or storing.
"""
fields = ('items', 'ctx')
@@ -398,7 +455,7 @@
class List(Literal):
- """any list literal such as {{ [1, 2, 3] }}"""
+ """Any list literal such as ``[1, 2, 3]``"""
fields = ('items',)
def as_const(self):
@@ -406,7 +463,9 @@
class Dict(Literal):
- """any dict literal such as {{ {1: 2, 3: 4} }}"""
+ """Any dict literal such as ``{1: 2, 3: 4}``. The items must be a list of
+ :class:`Pair` nodes.
+ """
fields = ('items',)
def as_const(self):
@@ -422,12 +481,14 @@
class Keyword(Helper):
- """A key, value pair for keyword arguments."""
+ """A key, value pair for keyword arguments where key is a string."""
fields = ('key', 'value')
class CondExpr(Expr):
- """{{ foo if bar else baz }}"""
+ """A conditional expression (inline if expression). (``{{
+ foo if bar else baz }}``)
+ """
fields = ('test', 'expr1', 'expr2')
def as_const(self):
@@ -437,7 +498,9 @@
class Filter(Expr):
- """{{ foo|bar|baz }}"""
+ """This node applies a filter on an expression. `name` is the name of
+ the filter, the rest of the fields are the same as for :class:`Call`.
+ """
fields = ('node', 'name', 'args', 'kwargs', 'dyn_args', 'dyn_kwargs')
def as_const(self, obj=None):
@@ -469,12 +532,19 @@
class Test(Expr):
- """{{ foo is lower }}"""
+ """Applies a test on an expression. `name` is the name of the test, the
+ rest of the fields are the same as for :class:`Call`.
+ """
fields = ('node', 'name', 'args', 'kwargs', 'dyn_args', 'dyn_kwargs')
class Call(Expr):
- """{{ foo(bar) }}"""
+ """Calls an expression. `args` is a list of arguments, `kwargs` a list
+ of keyword arguments (list of :class:`Keyword` nodes), and `dyn_args`
+ and `dyn_kwargs` has to be either `None` or a node that is used as
+ node for dynamic positional (``*args``) or keyword (``**kwargs``)
+ arguments.
+ """
fields = ('node', 'args', 'kwargs', 'dyn_args', 'dyn_kwargs')
def as_const(self):
@@ -506,7 +576,9 @@
class Subscript(Expr):
- """{{ foo.bar }} and {{ foo['bar'] }} etc."""
+ """Subscribe an expression by an argument. This node performs a dict
+ and an attribute lookup on the object whatever succeeds.
+ """
fields = ('node', 'arg', 'ctx')
def as_const(self):
@@ -523,7 +595,9 @@
class Slice(Expr):
- """1:2:3 etc."""
+ """Represents a slice object. This must only be used as argument for
+ :class:`Subscript`.
+ """
fields = ('start', 'stop', 'step')
def as_const(self):
@@ -535,7 +609,9 @@
class Concat(Expr):
- """For {{ foo ~ bar }}. Concatenates strings."""
+ """Concatenates the list of expressions provided after converting them to
+ unicode.
+ """
fields = ('nodes',)
def as_const(self):
@@ -543,7 +619,9 @@
class Compare(Expr):
- """{{ foo == bar }}, {{ foo >= bar }} etc."""
+ """Compares an expression with some other expressions. `ops` must be a
+ list of :class:`Operand`\s.
+ """
fields = ('expr', 'ops')
def as_const(self):
@@ -559,47 +637,54 @@
class Operand(Helper):
- """Operator + expression."""
+ """Holds an operator and an expression."""
fields = ('op', 'expr')
+if __debug__:
+ Operand.__doc__ += '\nThe following operators are available: ' + \
+ ', '.join(sorted('``%s``' % x for x in set(_binop_to_func) |
+ set(_uaop_to_func) | set(_cmpop_to_func)))
+
class Mul(BinExpr):
- """{{ foo * bar }}"""
+ """Multiplies the left with the right node."""
operator = '*'
class Div(BinExpr):
- """{{ foo / bar }}"""
+ """Divides the left by the right node."""
operator = '/'
class FloorDiv(BinExpr):
- """{{ foo // bar }}"""
+ """Divides the left by the right node and truncates conver the
+ result into an integer by truncating.
+ """
operator = '//'
class Add(BinExpr):
- """{{ foo + bar }}"""
+ """Add the left to the right node."""
operator = '+'
class Sub(BinExpr):
- """{{ foo - bar }}"""
+ """Substract the right from the left node."""
operator = '-'
class Mod(BinExpr):
- """{{ foo % bar }}"""
+ """Left modulo right."""
operator = '%'
class Pow(BinExpr):
- """{{ foo ** bar }}"""
+ """Left to the power of right."""
operator = '**'
class And(BinExpr):
- """{{ foo and bar }}"""
+ """Short circuited AND."""
operator = 'and'
def as_const(self):
@@ -607,7 +692,7 @@
class Or(BinExpr):
- """{{ foo or bar }}"""
+ """Short circuited OR."""
operator = 'or'
def as_const(self):
@@ -615,15 +700,66 @@
class Not(UnaryExpr):
- """{{ not foo }}"""
+ """Negate the expression."""
operator = 'not'
class Neg(UnaryExpr):
- """{{ -foo }}"""
+ """Make the expression negative."""
operator = '-'
class Pos(UnaryExpr):
- """{{ +foo }}"""
+ """Make the expression positive (noop for most expressions)"""
operator = '+'
+
+
+# Helpers for extensions
+
+
+class EnvironmentAttribute(Expr):
+ """Loads an attribute from the environment object. This is useful for
+ extensions that want to call a callback stored on the environment.
+ """
+ fields = ('name',)
+
+
+class ExtensionAttribute(Expr):
+ """Returns the attribute of an extension bound to the environment.
+ The identifier is the identifier of the :class:`Extension`.
+ """
+ fields = ('identifier', 'attr')
+
+
+class ImportedName(Expr):
+ """If created with an import name the import name is returned on node
+ access. For example ``ImportedName('cgi.escape')`` returns the `escape`
+ function from the cgi module on evaluation. Imports are optimized by the
+ compiler so there is no need to assign them to local variables.
+ """
+ fields = ('importname',)
+
+
+class InternalName(Expr):
+ """An internal name in the compiler. You cannot create these nodes
+ yourself but the parser provides a `free_identifier` method that creates
+ a new identifier for you. This identifier is not available from the
+ template and is not threated specially by the compiler.
+ """
+ fields = ('name',)
+
+ def __init__(self):
+ raise TypeError('Can\'t create internal names. Use the '
+ '`free_identifier` method on a parser.')
+
+
+class MarkSafe(Expr):
+ """Mark the wrapped expression as safe (wrap it as `Markup`)."""
+ fields = ('expr',)
+
+ def as_const(self):
+ return Markup(self.expr.as_const())
+
+
+# and close down
+_node_setup_finished = True