The proposal and motivation are essentially the same as in the last thread [1] and the ticket description [2]. I put it on the 1.2 feature list. I tried to split my patch into smaller, more readable commits here: http://github.com/emulbreh/django/tree/master I haven't converted defaulttags, loadertags and django.templatetags.* yet to show that it's (mostly) backwards compatible (see below). It would be useful to decide on some questions in the "API Design .." section below first.
I'll summarize the core ideas again as well as problems and pending design decisions. Concepts: Expressions and TokenStream ================================= Variable and FilterExpression will be deprecated as they require the caller to know the length of the expression in advance. This makes whitespace handling cumbersome and some syntactical constructs really hard to parse (e.g., the {% url ... %} parser is broken by design). Instead there would be a tokenizer (currently named TokenStream) that lets you read expressions from a stream of tokens - this would replace Token.split_contents(). >>> bits = TokenStream(p, 'url views.client_action id=client.id,action="update",num=3').tokens; bits [('name', 'url'), ('name', 'views.client_action'), ('name', 'id'), ('char', '='), ('name', 'client.id'), ('char', ','), ('name', 'action'), ('char', '='), ('string_literal', '"update"'), ('char', ','), ('name', 'num'), ('char', '='), ('numeric_literal', '3')] Now, instead of explicitly constructing a FilterExpression object you would simply call `bits.parse_expression()` and would get back an object with a `.resolve(context)` method (a subclass of django.template.expressions.Expression) that represents a Literal, Variable (called Lookup), or FilterExpression. For compatibility issues see "Variable vs FilterExpression" below. A TokenStream can be read via the following api (the current stream position is called `offset`). Low level API ------------------- ``def pop(self)`` Returns a pair (tokentype, lexeme) and offset+=1; tokentype is one of "string_literal", "numeric_literal", "char", "name". ``def pop_lexem(self, match)`` Returns True and offset+=1 if the next token's lexeme equals `match`. Returns False otherwise. ``def pop_name(self)`` Returns the next token's lexeme and offset+=1 if its tokentype is "name". Returns None otherwise. ``def pushback(self)`` offset-=1 High level API -------------------- These methods raise TokenSyntaxError and leave offset untouched if the expected result cannot be parsed. Each accepts a boolean required=False kwarg which turns TokenSyntaxErrors into TemplateSyntaxErrors if True. ``def parse_string(self, bare=False)`` Returns the value of the following string literal. If bare=True, unquoted strings will be accepted. ``def parse_int(self)`` Returns the value of the following numeric literal, if it is an int. ``def parse_value(self)`` Returns an Expression that evaluates the following literal, variable or translated value. ``def parse_expression(self)`` Returns an Expression that evaluates the following value or filterexpression. ``def parse_expression_list(self, minimum=0, maximum=None, count=None)`` Returns a list `e` of expressions; minimum <= len(e) <= maximum. count=n is a shortcut for minimum=maximum=n. Variable vs FilterExpression ====================== I could only find documentation for Variable. If the internally used Parser.compile_filter() is indeed undocumented there is no official way to use FilterExpression in custom tags. If that means that FilterExpression.resolve() behaviour doesn't have to be backwards compatible, that would help a lot .. 1.) Currently `Variable("unresolvable")` and `FilterExpression("unresolvable", p)` resolve differently: Variable raises VariableDoesNotExist and FilterExpression returns TEMPLATE_STRING_IF_INVALID. Except when `ignore_failures=True` is given. But `FilterExpression("foo|cut:unresolvable", p)` will again raise VariableDoesNotExist - regardless of `ignore_failures=True`. My Expression implementation unifies these behaviours: If any part of an expression is unresolvable a LookupError will be raised. `ignore_failures` will be deprecated but there's a resolve_safe() method on the Expression base class that reads: def resolve_safe(self, context, default=None): try: return self.resolve(context) except LookupError: return default This would be a small backwards incompatible change. I have one failing test (exception02) because the ExtendsNode implementation depends on the current FilterExpression behaviour. 2.) Currently `Variable("a._hidden")` works - but `FilterExpression("a._hidden", p)` raises a TemplateSyntaxError. This would be unified: lookup parts may not start with an underscore. If it's not acceptable to break bc here leading underscores might simply be allowed. 3.) Numeric literals ending with "." are currently treated as Variables. The following test (ifequal-numeric07) succeds because `ignore_failures=True` in IfEqualNode suppresses the inevitable VariableDoesNotExist exception and then compares None to 2: ('{% ifequal x 2. %}yes{% endifequal %}', {'x': 2}, ''). My current implementation raises a TemplateSyntaxError as soon as it encounters a numeric literal with a trailing dot. This could easily be made backwards compatible if it's considered worth it. API Design, Class Based Parsers, Combinable Mircorparsers =============================================== To be backwards compatible my original idea was to wrap TokenStream based parser functions with a decorator: @uses_token_stream def mytag(parser, bits): exp = ts.parse_expression(required=True) if not ts.pop_lexeme("as"): raise TemplateSyntaxError, "'as' expected" name = ts.pop_name() if not name: raise TemplateSyntaxError, "identifier expected" return MyNode(exp, name) But regarding Malcolm's request 'Helper methods that make it easy for the common cases to say "I expect three paramters matching the 'x y as z' pattern where x and z are strings and y must be an integer" would be very handy.' [1] class bases parsers looked promising. They would be resusable: class ExpAsNameTagParser(TagParser): def parse(self, parser, bits): exp = ts.parse_expression(required=True) if not ts.pop_lexeme("as"): raise TemplateSyntaxError, "'as' expected" name = ts.pop_name() if not name: raise TemplateSyntaxError, "identifier expected" return (exp, name) register.tag('sin', ExpAsNameTagParser(node_cls=SinNode)) register.tag('cos', ExpAsNameTagParser(node_cls=CosNode)) And with some kind of microparser infrastructure: register.tag('sin', TagParser(microparser.expression + "as" + mircoparser.name, node_cls=SinNode)) But then we should also have microparser.optional(), a | b, ... and soon a clone of pyparsing. Luckily this can be implemented later on top of the basic API - and would work as external code. If class based parsers are the preferred approach TokenStream might be renamed to TagParserState (or something similar) to make clear that it is not a stream of Token instances. Tags ===== * {% url ... %}, {% ssi ... %} currently accept unquoted literal strings. This will continue to work but the preferred syntax will use properly quoted strings. This may one day allow for viewnames and ssi paths that come from expressions. * {% if not %} would fail with my current implementation. This is fixable. But I'm not sure it's worth it: not/and/or should be keywords in the context of boolean expressions. tl;.. Johannes [1] http://groups.google.com/group/django-developers/browse_thread/thread/4f5b01719e497325 [2] http://code.djangoproject.com/ticket/7806 --~--~---------~--~----~------------~-------~--~----~ You received this message because you are subscribed to the Google Groups "Django developers" group. To post to this group, send email to django-developers@googlegroups.com To unsubscribe from this group, send email to django-developers+unsubscr...@googlegroups.com For more options, visit this group at http://groups.google.com/group/django-developers?hl=en -~----------~----~----~----~------~----~------~--~---