Hello community, here is the log from the commit of package python-Chameleon for openSUSE:Factory checked in at 2020-07-08 19:20:00 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-Chameleon (Old) and /work/SRC/openSUSE:Factory/.python-Chameleon.new.3060 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-Chameleon" Wed Jul 8 19:20:00 2020 rev:11 rq:819491 version:3.8.1 Changes: -------- --- /work/SRC/openSUSE:Factory/python-Chameleon/python-Chameleon.changes 2020-06-09 00:07:30.509896107 +0200 +++ /work/SRC/openSUSE:Factory/.python-Chameleon.new.3060/python-Chameleon.changes 2020-07-08 19:20:33.976510513 +0200 @@ -1,0 +2,12 @@ +Wed Jul 8 14:03:55 UTC 2020 - Ondřej Súkup <mimi...@gmail.com> + +- Update to 3.8.1 + * Added code optimization to reduce sequential appends of static text. + * The default symbol in dynamic attributes is now symbolic. + * The built-in attrs dictionary of static element attributes now correctly + works with tal:define, etc. + * Fix slice code generation compatibility issue on Python 3.9. + * Expose default marker as importable symbol chameleon.tales.DEFAULT_MARKER. + * Removed legacy flag literal_false. To get a similar behavior, use boolean_attributes. + +------------------------------------------------------------------- Old: ---- Chameleon-3.7.2.tar.gz New: ---- Chameleon-3.8.1.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-Chameleon.spec ++++++ --- /var/tmp/diff_new_pack.Jeeu4X/_old 2020-07-08 19:20:35.264514788 +0200 +++ /var/tmp/diff_new_pack.Jeeu4X/_new 2020-07-08 19:20:35.264514788 +0200 @@ -18,7 +18,7 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} Name: python-Chameleon -Version: 3.7.2 +Version: 3.8.1 Release: 0 Summary: Fast HTML/XML Template Compiler License: BSD-3-Clause AND BSD-4-Clause AND Python-2.0 AND ZPL-2.1 ++++++ Chameleon-3.7.2.tar.gz -> Chameleon-3.8.1.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/chameleon-3.7.2/CHANGES.rst new/chameleon-3.8.1/CHANGES.rst --- old/chameleon-3.7.2/CHANGES.rst 2020-05-31 07:51:18.000000000 +0200 +++ new/chameleon-3.8.1/CHANGES.rst 2020-07-06 21:23:38.000000000 +0200 @@ -1,6 +1,43 @@ Changes ======= +3.8.1 (2020-07-06) +------------------ + +- Added code optimization to reduce sequential appends of static text. + +- The `default` symbol in dynamic attributes is now + symbolic. Previously, it was assigned the string value of the + default attribute text. A similar change has been made for + switch/case expressions. + +- The built-in `attrs` dictionary of static element attributes now + correctly works with `tal:define`, etc. + +- Fix slice code generation compatibility issue on Python 3.9. + +3.8.0 (2020-06-25) +------------------ + +- Expose default marker as importable symbol + `chameleon.tales.DEFAULT_MARKER`. + +- Removed legacy flag `literal_false`. To get a similar behavior, use + `boolean_attributes`. + +3.7.4 (2020-06-17) +------------------ + +- Fix brown-bag release. + +3.7.3 (2020-06-17) +------------------ + +- Fix regression introduced in 3.6.2 where the default marker would + incorrectly change its value between templates, causing issues in + software which depends on the value being treated as a global + object. + 3.7.2 (2020-05-31) ------------------ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/chameleon-3.7.2/setup.py new/chameleon-3.8.1/setup.py --- old/chameleon-3.7.2/setup.py 2020-05-31 07:51:18.000000000 +0200 +++ new/chameleon-3.8.1/setup.py 2020-07-06 21:23:38.000000000 +0200 @@ -1,4 +1,4 @@ -__version__ = '3.7.2' +__version__ = '3.8.1' import os diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/chameleon-3.7.2/src/chameleon/astutil.py new/chameleon-3.8.1/src/chameleon/astutil.py --- old/chameleon-3.7.2/src/chameleon/astutil.py 2020-05-31 07:51:18.000000000 +0200 +++ new/chameleon-3.8.1/src/chameleon/astutil.py 2020-07-06 21:23:38.000000000 +0200 @@ -23,6 +23,9 @@ import weakref import collections + +AST_NONE = ast.Name(id='None', ctx=ast.Load()) + node_annotations = weakref.WeakKeyDictionary() try: @@ -925,20 +928,31 @@ for dim in node.slice.elts[1:]: self._write(', ') self.visit(dim) + elif isinstance(node.slice, ast.Slice): + self.visit_Slice(node.slice, True) else: self.visit(node.slice) self._write(']') # Slice(expr? lower, expr? upper, expr? step) - def visit_Slice(self, node): - if getattr(node, 'lower', None) is not None: - self.visit(node.lower) - self._write(':') - if getattr(node, 'upper', None) is not None: - self.visit(node.upper) - if getattr(node, 'step', None) is not None: + def visit_Slice(self, node, subscription=False): + if subscription: + if getattr(node, 'lower', None) is not None: + self.visit(node.lower) self._write(':') - self.visit(node.step) + if getattr(node, 'upper', None) is not None: + self.visit(node.upper) + if getattr(node, 'step', None) is not None: + self._write(':') + self.visit(node.step) + else: + self._write('slice(') + self.visit(getattr(node, "lower", None) or AST_NONE) + self._write(', ') + self.visit(getattr(node, "upper", None) or AST_NONE) + self._write(', ') + self.visit(getattr(node, "step", None) or AST_NONE) + self._write(')') # Index(expr value) def visit_Index(self, node): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/chameleon-3.7.2/src/chameleon/compiler.py new/chameleon-3.8.1/src/chameleon/compiler.py --- old/chameleon-3.7.2/src/chameleon/compiler.py 2020-05-31 07:51:18.000000000 +0200 +++ new/chameleon-3.8.1/src/chameleon/compiler.py 2020-07-06 21:23:38.000000000 +0200 @@ -21,6 +21,7 @@ from .astutil import Builtin from .astutil import Static from .astutil import TokenRef +from .astutil import Node from .codegen import TemplateCodeGenerator from .codegen import template @@ -35,6 +36,11 @@ from .nodes import Assignment from .nodes import Module from .nodes import Context +from .nodes import Is +from .nodes import IsNot +from .nodes import Equals +from .nodes import Logical +from .nodes import And from .tokenize import Token from .config import DEBUG_MODE @@ -54,6 +60,7 @@ from .utils import safe_native from .utils import builtins from .utils import decode_htmlentities +from .utils import join if version >= (3, 0, 0): long = int @@ -133,10 +140,6 @@ ) -emit_node = template(is_func=True, func_args=('node',), source=r""" - __append(node)""") - - emit_node_if_non_trivial = template(is_func=True, func_args=('node',), source=r""" if node is not None: @@ -270,6 +273,22 @@ return target""") +class EmitText(Node): + """Append text to output.""" + + _fields = "s", + + +class Scope(Node): + """"Set a local output scope.""" + + _fields = "body", "append", "stream" + + body = None + append = None + stream = None + + class Interpolator(object): braces_required_regex = re.compile( r'(\$)?\$({(?P<expression>.*)})', @@ -805,19 +824,21 @@ return stmts def visit_Value(self, node, target): - engine = self.engine_factory() + engine = self.engine_factory( + default=node.default, + default_marker=node.default_marker + ) compiler = engine.parse(node.value) return compiler.assign_value(target) def visit_Copy(self, node, target): return self.translate(node.expression, target) - def visit_Default(self, node, target): - value = annotated(node.marker) - return [ast.Assign(targets=[target], value=value)] - def visit_Substitution(self, node, target): - engine = self.engine_factory(default=node.default) + engine = self.engine_factory( + default=node.default, + default_marker=node.default_marker + ) compiler = engine.parse(node.value, char_escape=node.char_escape) return compiler.assign_text(target) @@ -825,22 +846,23 @@ return self.translate(node.value, target) + \ template("TARGET = not TARGET", TARGET=target) - def visit_Identity(self, node, target): - expression = self.translate(node.expression, "__expression") - value = self.translate(node.value, "__value") - - return expression + value + \ - template("TARGET = __expression is __value", TARGET=target) - - def visit_Equality(self, node, target): - expression = self.translate(node.expression, "__expression") - value = self.translate(node.value, "__value") - + def visit_BinOp(self, node, target): + expression = self.translate(node.left, "__expression") + value = self.translate(node.right, "__value") + + op = { + Is: "is", + IsNot: "is not", + Equals: "==", + }[node.op] return expression + value + \ - template("TARGET = __expression == __value", TARGET=target) + template("TARGET = __expression %s __value" % op, TARGET=target) def visit_Boolean(self, node, target): - engine = self.engine_factory() + engine = self.engine_factory( + default=node.default, + default_marker=node.default_marker, + ) compiler = engine.parse(node.value) return compiler.assign_bool(target, node.s) @@ -850,9 +872,13 @@ engine = self.engine_factory( char_escape=expr.char_escape, default=expr.default, - ) + default_marker=expr.default_marker + ) elif isinstance(expr, Value): - engine = self.engine_factory() + engine = self.engine_factory( + default=expr.default, + default_marker=expr.default_marker + ) else: raise RuntimeError("Bad value: %r." % node.value) @@ -886,6 +912,10 @@ value = annotated(node) return [ast.Assign(targets=[target], value=value)] + def visit_Symbol(self, node, target): + value = annotated(node) + return template("TARGET = SYMBOL", TARGET=target, SYMBOL=node) + class Compiler(object): """Generic compiler class. @@ -949,8 +979,26 @@ module = ast.Module([]) module.body += self.visit(node) ast.fix_missing_locations(module) - prelude = "__filename = %r\n__default = object()" % filename - generator = TemplateCodeGenerator(module, source) + + class Generator(TemplateCodeGenerator): + scopes = [Scope()] + + def visit_EmitText(self, node): + append = load(self.scopes[-1].append or "__append") + for node in template("append(s)", append=append, s=ast.Str(s=node.s)): + self.visit(node) + + def visit_Scope(self, node): + self.scopes.append(node) + body = list(node.body) + swap(body, load(node.append), "__append") + if node.stream: + swap(body, load(node.stream), "__stream") + for node in body: + self.visit(node) + self.scopes.pop() + + generator = Generator(module, source) tokens = [ Token(source[pos:pos + length], pos, source) for pos, length in generator.tokens @@ -966,7 +1014,7 @@ self.lock.release() self.code = "\n".join(( - prelude, + "__filename = %r\n" % filename, token_map_def, generator.code )) @@ -977,7 +1025,15 @@ kind = type(node).__name__ visitor = getattr(self, "visit_%s" % kind) iterator = visitor(node) - return list(iterator) + result = [] + for key, group in itertools.groupby(iterator, lambda node: node.__class__): + nodes = list(group) + if key is EmitText: + text = join(node.s for node in nodes) + nodes = [EmitText(text)] + result.extend(nodes) + return result + def visit_Sequence(self, node): for item in node.items: @@ -1003,6 +1059,9 @@ body += template("from itertools import chain as __chain") if version < (3, 0, 0): body += template("from sys import exc_clear as __exc_clear") + else: + body += template("from sys import intern") + body += template("__default = intern('__default__')") body += template("__marker = object()") body += template( r"g_re_amp = re.compile(r'&(?!([A-Za-z]+|#[0-9]+);)')" @@ -1125,7 +1184,7 @@ yield function def visit_Text(self, node): - return emit_node(ast.Str(s=node.value)) + yield EmitText(node.value) def visit_Domain(self, node): backup = "__previous_i18n_domain_%s" % mangle(id(node)) @@ -1267,22 +1326,44 @@ def visit_Condition(self, node): target = "__condition" - assignment = self._engine(node.expression, target) - assert assignment + def step(expressions, body, condition): + for i, expression in enumerate(reversed(expressions)): + stmts = evaluate(expression, body) + if i > 0: + stmts.append( + ast.If( + ast.Compare( + left=load(target), + ops=[ast.Is()], + comparators=[load(str(condition))] + ), + body, + None + ) + ) + body = stmts + return body - for stmt in assignment: - yield stmt + def evaluate(node, body=None): + if isinstance(node, Logical): + condition = isinstance(node, And) + return step(node.expressions, body, condition) - body = self.visit(node.node) or [ast.Pass()] + return self._engine(node, target) + body = evaluate(node.expression) orelse = getattr(node, "orelse", None) - if orelse is not None: - orelse = self.visit(orelse) - test = load(target) + body.append( + ast.If( + test=load(target), + body=self.visit(node.node) or [ast.Pass()], + orelse=self.visit(orelse) if orelse else None, + ) + ) - yield ast.If(test=test, body=body, orelse=orelse) + return body def visit_Translate(self, node): """Translation. @@ -1306,9 +1387,7 @@ # Visit body to generate the message body code = self.visit(node.node) - swap(code, load(append), "__append") - swap(code, load(stream), "__stream") - body += code + body.append(Scope(code, append, stream)) # Reduce white space and assign as message id msgid = identifier("msgid", id(node)) @@ -1369,23 +1448,16 @@ node.prefix, node.name, line, column)) if node.attributes: - for stmt in emit_node(ast.Str(s=node.prefix + node.name)): - yield stmt - + yield EmitText(node.prefix + node.name) for stmt in self.visit(node.attributes): yield stmt - for stmt in emit_node(ast.Str(s=node.suffix)): - yield stmt + yield EmitText(node.suffix) else: - for stmt in emit_node( - ast.Str(s=node.prefix + node.name + node.suffix)): - yield stmt + yield EmitText(node.prefix + node.name + node.suffix) def visit_End(self, node): - for stmt in emit_node(ast.Str( - s=node.prefix + node.name + node.space + node.suffix)): - yield stmt + yield EmitText(node.prefix + node.name + node.space + node.suffix) def visit_Attribute(self, node): attr_format = (node.space + node.name + node.eq + @@ -1414,7 +1486,7 @@ "if C: __append(S)", C=filter_condition, S=ast.Str(s=s) ) else: - return template("__append(S)", S=ast.Str(s=s)) + return [EmitText(s)] target = identifier("attr", node.name) body = self._engine(node.expression, store(target)) @@ -1464,13 +1536,13 @@ body = [] for expression in node.expressions: - name = identifier("cache", id(expression)) - target = store(name) - # Skip re-evaluation if self._expression_cache.get(expression): continue + name = identifier("cache", id(expression)) + target = store(name) + body += self._engine(expression, target) self._expression_cache[expression] = target @@ -1482,13 +1554,10 @@ body = [] for expression in node.expressions: + assert self._expression_cache.get(expression) is not None name = identifier("cache", id(expression)) target = store(name) - - if not self._expression_cache.get(expression): - continue - - body.append(ast.Assign([target], load("None"))) + body += self._engine(node.value, target) body += self.visit(node.node) @@ -1545,8 +1614,7 @@ # generate code code = self.visit(node.node) - swap(code, load(append), "__append") - body += code + body.append(Scope(code, append)) # output msgid text = Text('${%s}' % node.name) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/chameleon-3.7.2/src/chameleon/nodes.py new/chameleon-3.8.1/src/chameleon/nodes.py --- old/chameleon-3.7.2/src/chameleon/nodes.py 2020-05-31 07:51:18.000000000 +0200 +++ new/chameleon-3.8.1/src/chameleon/nodes.py 2020-07-06 21:23:38.000000000 +0200 @@ -22,12 +22,6 @@ _fields = "expression", "char_escape", "translate" -class Default(Node): - """Represents a default value.""" - - _fields = "marker", - - class CodeBlock(Node): _fields = "source", @@ -35,7 +29,11 @@ class Value(Node): """Expression object value.""" - _fields = "value", + _fields = "value", "default", "default_marker" + + default = None + + default_marker = None def __repr__(self): try: @@ -51,13 +49,13 @@ class Substitution(Value): """Expression value for text substitution.""" - _fields = "value", "char_escape", "default" + _fields = "value", "char_escape", "default", "default_marker" default = None class Boolean(Value): - _fields = "value", "s" + _fields = "value", "s", "default", "default_marker" class Negate(Node): @@ -81,7 +79,7 @@ class Attribute(Node): """Element attribute.""" - _fields = "name", "expression", "quote", "eq", "space", "filters" + _fields = "name", "expression", "quote", "eq", "space", "default", "filters" class Start(Node): @@ -97,21 +95,45 @@ class Condition(Node): - """Node visited only if some condition holds.""" + """Node visited only if one of the condition holds.""" _fields = "expression", "node", "orelse" -class Identity(Node): - """Condition expression that is true on identity.""" +class Op(Node): + """An operator node.""" + + +class Is(Op): + """Object identity.""" + + +class IsNot(Op): + """Object identity.""" - _fields = "expression", "value" +class Equals(Op): + """Object equality.""" -class Equality(Node): - """Condition expression that is true on equality.""" - _fields = "expression", "value" +class Logical(Node): + """Logical operator.""" + + _fields = "expressions", + + +class And(Logical): + """All terms must be met.""" + + +class Or(Logical): + """At least one term must be met.""" + + +class BinOp(Node): + """Binary comparison.""" + + _fields = "left", "op", "right" class Cache(Node): @@ -123,7 +145,7 @@ class Cancel(Cache): - pass + _fields = "expressions", "node", "value" class Copy(Node): @@ -184,7 +206,9 @@ class Interpolation(Node): """String interpolation output.""" - _fields = "value", "braces_required", "translation" + _fields = "value", "braces_required", "translation", "default", "default_marker" + + class Translate(Node): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/chameleon-3.7.2/src/chameleon/tales.py new/chameleon-3.8.1/src/chameleon/tales.py --- old/chameleon-3.7.2/src/chameleon/tales.py 2020-05-31 07:51:18.000000000 +0200 +++ new/chameleon-3.8.1/src/chameleon/tales.py 2020-07-06 21:23:38.000000000 +0200 @@ -1,5 +1,7 @@ import re import sys +import types +import importlib from .astutil import parse from .astutil import store @@ -11,13 +13,16 @@ from .astutil import Builtin from .astutil import Symbol from .exc import ExpressionError +from .utils import ast from .utils import resolve_dotted +from .utils import ImportableMarker from .utils import Markup -from .utils import ast from .tokenize import Token from .parser import substitute from .compiler import Interpolator +DEFAULT_MARKER = ImportableMarker(__name__, "DEFAULT") + try: from .py26 import lookup_attr except SyntaxError: @@ -390,7 +395,7 @@ In previous versions, it was possible to escape using a regular backslash coding, but this is no longer supported. - >>> test(StringExpr(r'\${name}'), name='Hello world!') + >>> test(StringExpr(r'\\${name}'), name='Hello world!') '\\\\Hello world!' Multiple interpolations in one: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/chameleon-3.7.2/src/chameleon/tests/inputs/005-default.pt new/chameleon-3.8.1/src/chameleon/tests/inputs/005-default.pt --- old/chameleon-3.7.2/src/chameleon/tests/inputs/005-default.pt 2020-05-31 07:51:18.000000000 +0200 +++ new/chameleon-3.8.1/src/chameleon/tests/inputs/005-default.pt 2020-07-06 21:23:38.000000000 +0200 @@ -2,7 +2,7 @@ <body> <img class="default" tal:attributes="class default" /> <img tal:attributes="class default" /> - <span tal:content="default">Default</span> + <span tal:define="foo string:" tal:content="foo or default">Default</span> <span tal:content="True">Default</span> <span tal:content="False">Default</span> <span tal:content="default"> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/chameleon-3.7.2/src/chameleon/tests/inputs/008-builtins.pt new/chameleon-3.8.1/src/chameleon/tests/inputs/008-builtins.pt --- old/chameleon-3.7.2/src/chameleon/tests/inputs/008-builtins.pt 2020-05-31 07:51:18.000000000 +0200 +++ new/chameleon-3.8.1/src/chameleon/tests/inputs/008-builtins.pt 2020-07-06 21:23:38.000000000 +0200 @@ -5,6 +5,9 @@ <div tal:attributes="class string:dynamic" class="static"> ${attrs['class']} </div> + <div class="static" tal:content="attrs['class']" /> + <div class="static" tal:replace="attrs['class']" /> + <div class="static" tal:define="x attrs">${x['class']}</div> <div tal:define="nothing string:nothing"> ${nothing} </div> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/chameleon-3.7.2/src/chameleon/tests/inputs/017-omit-tag.pt new/chameleon-3.8.1/src/chameleon/tests/inputs/017-omit-tag.pt --- old/chameleon-3.7.2/src/chameleon/tests/inputs/017-omit-tag.pt 2020-05-31 07:51:18.000000000 +0200 +++ new/chameleon-3.8.1/src/chameleon/tests/inputs/017-omit-tag.pt 2020-07-06 21:23:38.000000000 +0200 @@ -9,4 +9,4 @@ <div class="omitted" tal:omit-tag="True">Hello world!</div> <div class="${'omitted'}" tal:omit-tag="True">Hello world!</div> </body> -</html> \ No newline at end of file +</html> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/chameleon-3.7.2/src/chameleon/tests/inputs/027-attribute-replacement.pt new/chameleon-3.8.1/src/chameleon/tests/inputs/027-attribute-replacement.pt --- old/chameleon-3.7.2/src/chameleon/tests/inputs/027-attribute-replacement.pt 2020-05-31 07:51:18.000000000 +0200 +++ new/chameleon-3.8.1/src/chameleon/tests/inputs/027-attribute-replacement.pt 2020-07-06 21:23:38.000000000 +0200 @@ -4,7 +4,7 @@ class="dummy" onClick="" tal:define="a 'abc'" - tal:attributes="class 'def' + a + default; style 'hij'; onClick 'alert();;'" + tal:attributes="class default; style 'hij'; onClick 'alert();;'" tal:content="a + 'ghi'" /> <span tal:replace="'Hello World!'">Hello <b>Universe</b>!</span> <span tal:replace="'Hello World!'"><b>Hello Universe!</b></span> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/chameleon-3.7.2/src/chameleon/tests/inputs/072-repeat-interpolation.pt new/chameleon-3.8.1/src/chameleon/tests/inputs/072-repeat-interpolation.pt --- old/chameleon-3.7.2/src/chameleon/tests/inputs/072-repeat-interpolation.pt 2020-05-31 07:51:18.000000000 +0200 +++ new/chameleon-3.8.1/src/chameleon/tests/inputs/072-repeat-interpolation.pt 2020-07-06 21:23:38.000000000 +0200 @@ -6,8 +6,5 @@ <ul> <li tal:repeat="i range(1, 4)" class="${'odd' if i % 2 != 0 else None}">${i}</li> </ul> - <ul> - <li tal:repeat="i range(1, 4)" class="${'odd' if i % 2 != 0 else False}">${i}</li> - </ul> </body> </html> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/chameleon-3.7.2/src/chameleon/tests/outputs/008.pt new/chameleon-3.8.1/src/chameleon/tests/outputs/008.pt --- old/chameleon-3.7.2/src/chameleon/tests/outputs/008.pt 2020-05-31 07:51:18.000000000 +0200 +++ new/chameleon-3.8.1/src/chameleon/tests/outputs/008.pt 2020-07-06 21:23:38.000000000 +0200 @@ -5,6 +5,9 @@ <div class="dynamic"> static </div> + <div class="static">static</div> + static + <div class="static">static</div> <div> nothing </div> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/chameleon-3.7.2/src/chameleon/tests/outputs/027.pt new/chameleon-3.8.1/src/chameleon/tests/outputs/027.pt --- old/chameleon-3.7.2/src/chameleon/tests/outputs/027.pt 2020-05-31 07:51:18.000000000 +0200 +++ new/chameleon-3.8.1/src/chameleon/tests/outputs/027.pt 2020-07-06 21:23:38.000000000 +0200 @@ -1,6 +1,6 @@ <div xmlns="http://www.w3.org/1999/xhtml"> <span id="test" - class="defabcdummy" + class="dummy" onClick="alert();" style="hij">abcghi</span> Hello World! Hello World! diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/chameleon-3.7.2/src/chameleon/tests/outputs/028.pt new/chameleon-3.8.1/src/chameleon/tests/outputs/028.pt --- old/chameleon-3.7.2/src/chameleon/tests/outputs/028.pt 2020-05-31 07:51:18.000000000 +0200 +++ new/chameleon-3.8.1/src/chameleon/tests/outputs/028.pt 2020-07-06 21:23:38.000000000 +0200 @@ -1,5 +1,5 @@ <div xmlns="http://www.w3.org/1999/xhtml"> <option selected="True"></option> - <option></option> + <option selected="False"></option> <option></option> </div> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/chameleon-3.7.2/src/chameleon/tests/outputs/071.pt new/chameleon-3.8.1/src/chameleon/tests/outputs/071.pt --- old/chameleon-3.7.2/src/chameleon/tests/outputs/071.pt 2020-05-31 07:51:18.000000000 +0200 +++ new/chameleon-3.8.1/src/chameleon/tests/outputs/071.pt 2020-07-06 21:23:38.000000000 +0200 @@ -1,7 +1,7 @@ <html> <body> <input type="input" checked="True" /> - <input type="input" /> + <input type="input" checked="False" /> <input type="input" /> <input type="input" checked="checked" /> <input type="input" checked /> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/chameleon-3.7.2/src/chameleon/tests/outputs/072.pt new/chameleon-3.8.1/src/chameleon/tests/outputs/072.pt --- old/chameleon-3.7.2/src/chameleon/tests/outputs/072.pt 2020-05-31 07:51:18.000000000 +0200 +++ new/chameleon-3.8.1/src/chameleon/tests/outputs/072.pt 2020-07-06 21:23:38.000000000 +0200 @@ -10,10 +10,5 @@ <li>2</li> <li class="odd">3</li> </ul> - <ul> - <li class="odd">1</li> - <li>2</li> - <li class="odd">3</li> - </ul> </body> </html> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/chameleon-3.7.2/src/chameleon/tests/test_astutil.py new/chameleon-3.8.1/src/chameleon/tests/test_astutil.py --- old/chameleon-3.7.2/src/chameleon/tests/test_astutil.py 1970-01-01 01:00:00.000000000 +0100 +++ new/chameleon-3.8.1/src/chameleon/tests/test_astutil.py 2020-07-06 21:23:38.000000000 +0200 @@ -0,0 +1,31 @@ +import ast +import sys +import unittest + + +class ASTCodeGeneratorTestCase(unittest.TestCase): + def _eval(self, tree, env): + from chameleon.astutil import ASTCodeGenerator + source = ASTCodeGenerator(tree).code + code = compile(source, '<string>', 'exec') + exec(code, env) + + if sys.version_info >= (3, 7): + def test_slice(self): + tree = ast.Module( + body=[ + ast.Assign( + targets=[ + ast.Name(id='x', ctx=ast.Store())], + value=ast.Call( + func=ast.Name(id='f', ctx=ast.Load()), + args=[ + ast.Slice( + upper=ast.Constant(value=0))], + keywords=[]))], + type_ignores=[] + ) + def f(x): return x + d = {"f": f} + self._eval(tree, d) + assert d['x'] == slice(None, 0, None) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/chameleon-3.7.2/src/chameleon/tests/test_templates.py new/chameleon-3.8.1/src/chameleon/tests/test_templates.py --- old/chameleon-3.7.2/src/chameleon/tests/test_templates.py 2020-05-31 07:51:18.000000000 +0200 +++ new/chameleon-3.8.1/src/chameleon/tests/test_templates.py 2020-07-06 21:23:38.000000000 +0200 @@ -23,6 +23,7 @@ from chameleon.utils import byte_string from chameleon.exc import RenderError +from chameleon.tales import DEFAULT_MARKER class Message(object): @@ -111,6 +112,7 @@ class RenderTestCase(TestCase): root = os.path.dirname(__file__) + maxDiff = 4096 def find_files(self, ext): inputs = os.path.join(self.root, "inputs") @@ -573,23 +575,13 @@ self.assertTrue('foo' in result) self.assertTrue('foo' in result) - def test_literal_false(self): - template = self.from_string( - '<input type="input" tal:attributes="checked False" />' - '<input type="input" tal:attributes="checked True" />' - '<input type="input" tal:attributes="checked None" />' - '<input type="input" tal:attributes="checked default" />', - literal_false=True, - ) - + def test_default_marker(self): + template = self.from_string('<span tal:replace="id(default)" />') self.assertEqual( template(), - '<input type="input" checked="False" />' - '<input type="input" checked="True" />' - '<input type="input" />' - '<input type="input" />', + str(id(DEFAULT_MARKER)), template.source - ) + ) def test_boolean_attributes(self): template = self.from_string( @@ -805,6 +797,9 @@ example = Example(input_path, want) diff = checker.output_difference( example, got, 0) + source = template.source + if sys.version_info < (3, ): + source = source.encode("utf-8") self.fail("(%s) - \n%s\n\nCode:\n%s" % ( input_path, diff.rstrip('\n'), - template.source.encode('utf-8'))) + source)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/chameleon-3.7.2/src/chameleon/utils.py new/chameleon-3.8.1/src/chameleon/utils.py --- old/chameleon-3.7.2/src/chameleon/utils.py 2020-05-31 07:51:18.000000000 +0200 +++ new/chameleon-3.8.1/src/chameleon/utils.py 2020-07-06 21:23:38.000000000 +0200 @@ -474,3 +474,17 @@ def __repr__(self): return "s'%s'" % self + + +class ImportableMarker(object): + def __init__(self, module, name): + self.__module__ = module + self.name = name + + @property + def __name__(self): + return "%s_MARKER" % self.name + + def __repr__(self): + return '<%s>' % self.name + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/chameleon-3.7.2/src/chameleon/zpt/program.py new/chameleon-3.8.1/src/chameleon/zpt/program.py --- old/chameleon-3.7.2/src/chameleon/zpt/program.py 2020-05-31 07:51:18.000000000 +0200 +++ new/chameleon-3.8.1/src/chameleon/zpt/program.py 2020-07-06 21:23:38.000000000 +0200 @@ -23,10 +23,12 @@ from ..namespaces import META_NS as META from ..astutil import Static +from ..astutil import Symbol from ..astutil import parse from ..astutil import marker from .. import tal +from .. import tales from .. import metal from .. import i18n from .. import nodes @@ -36,6 +38,7 @@ from ..exc import CompilationError from ..utils import decode_htmlentities +from ..utils import ImportableMarker try: str = unicode @@ -48,6 +51,7 @@ re_trim = re.compile(r'($\s+|\s+^)', re.MULTILINE) EMPTY_DICT = Static(ast.Dict(keys=[], values=[])) +CANCEL_MARKER = ImportableMarker(__name__, "CANCEL") def skip(node): @@ -102,6 +106,7 @@ _interpolation_enabled = True _whitespace = "\n" _last = "" + _cancel_marker = Symbol(CANCEL_MARKER) # Macro name (always trivial for a macro program) name = None @@ -168,7 +173,7 @@ 'enable_data_attributes', 'enable_comment_interpolation', 'restricted_namespace', - ) + ) super(MacroProgram, self).__init__(*args, **kwargs) @@ -219,7 +224,7 @@ switch = None else: value = nodes.Value(clause) - switch = value, nodes.Copy(value) + switch = value self._switches.append(switch) @@ -252,6 +257,7 @@ [nodes.Assignment(["macroname"], Static(ast.Str(macro_name)), True)], inner, ) + STATIC_ATTRIBUTES = None # -or- include tag else: content = nodes.Sequence(body) @@ -363,12 +369,6 @@ content, ) - # Assign static attributes dictionary to "attrs" value - inner = nodes.Define( - [nodes.Alias(["attrs"], STATIC_ATTRIBUTES or EMPTY_DICT)], - inner, - ) - if omit is not False: inner = nodes.Cache([omit], inner) @@ -419,15 +419,15 @@ ('local', ("target_language", ), target_language) ) - if defines: - DEFINE = partial( - nodes.Define, - [nodes.Assignment( - names, nodes.Value(expr), context == "local") - for (context, names, expr) in defines], - ) - else: - DEFINE = skip + assignments = [ + nodes.Assignment( + names, nodes.Value(expr), context == "local") + for (context, names, expr) in defines + ] + + # Assign static attributes dictionary to "attrs" value + assignments.insert(0, nodes.Alias(["attrs"], STATIC_ATTRIBUTES or EMPTY_DICT)) + DEFINE = partial(nodes.Define, assignments) # tal:case try: @@ -445,10 +445,16 @@ ) CASE = lambda node: nodes.Define( - [nodes.Alias(["default"], switch[1], False)], + [nodes.Alias(["default"], self.default_marker)], nodes.Condition( - nodes.Equality(switch[0], value), - nodes.Cancel([switch[0]], node), + nodes.And([ + nodes.BinOp(switch, nodes.IsNot, self._cancel_marker), + nodes.Or([ + nodes.BinOp(value, nodes.Equals, switch), + nodes.BinOp(value, nodes.Equals, self.default_marker) + ]) + ]), + nodes.Cancel([switch], node, self._cancel_marker), )) # tal:repeat @@ -489,7 +495,7 @@ if switch is None: SWITCH = skip else: - SWITCH = partial(nodes.Cache, list(switch)) + SWITCH = partial(nodes.Cache, [switch]) # i18n:domain try: @@ -712,9 +718,9 @@ def _pop_defaults(self, kwargs, *attributes): for attribute in attributes: - default = getattr(self, attribute) - value = kwargs.pop(attribute, default) - setattr(self, attribute, value) + value = kwargs.pop(attribute, None) + if value is not None: + setattr(self, attribute, value) def _check_attributes(self, namespace, ns): if namespace in self.DROP_NS and ns.get((TAL, 'attributes')): @@ -749,7 +755,7 @@ if default is not None: content = nodes.Condition( - nodes.Identity(value, marker("default")), + nodes.BinOp(value, nodes.Is, self.default_marker), default, content, ) @@ -759,7 +765,7 @@ # Define local marker "default" content = nodes.Define( - [nodes.Alias(["default"], marker("default"))], + [nodes.Alias(["default"], self.default_marker)], content ) @@ -778,59 +784,52 @@ ) char_escape = ('&', '<', '>', quote) - - # Use a provided default text as the default marker - # (aliased to the name ``default``), otherwise use the - # program's default marker value. - if text is not None: - default_marker = ast.Str(s=text) - else: - default_marker = self.default_marker - msgid = I18N_ATTRIBUTES.get(name, missing) # If (by heuristic) ``text`` contains one or more # interpolation expressions, apply interpolation - # substitution to the text + # substitution to the text. if expr is None and text is not None and '${' in text: - expr = nodes.Substitution(text, char_escape) + default = None + expr = nodes.Substitution(text, char_escape, default, self.default_marker) translation = implicit_i18n and msgid is missing value = nodes.Interpolation(expr, True, translation) - default_marker = self.default_marker + else: + default = ast.Str(s=text) if text is not None else None - # If the expression is non-trivial, the attribute is - # dynamic (computed). - elif expr is not None: - if name is None: - expression = nodes.Value(decode_htmlentities(expr)) - value = nodes.DictAttributes( - expression, ('&', '<', '>', '"'), '"', - set(filter(None, names[i:])) - ) - for fs in filtering: - fs.append(expression) - filtering.append([]) - elif name in self.boolean_attributes: - value = nodes.Boolean(expr, name) - else: - if text is not None: - default = default_marker + # If the expression is non-trivial, the attribute is + # dynamic (computed). + if expr is not None: + if name is None: + expression = nodes.Value( + decode_htmlentities(expr), + default, + self.default_marker + ) + value = nodes.DictAttributes( + expression, ('&', '<', '>', '"'), '"', + set(filter(None, names[i:])) + ) + for fs in filtering: + fs.append(expression) + filtering.append([]) + elif name in self.boolean_attributes: + value = nodes.Boolean(expr, name, default, self.default_marker) else: - default = None - - value = nodes.Substitution( - decode_htmlentities(expr), - char_escape, - default - ) + value = nodes.Substitution( + decode_htmlentities(expr), + char_escape, + default, + self.default_marker, + ) - # Otherwise, it's a static attribute. We don't include it - # here if there's one or more "computed" attributes - # (dynamic, from one or more dict values). - else: - value = ast.Str(s=text) - if msgid is missing and implicit_i18n: - msgid = text + # Otherwise, it's a static attribute. We don't include it + # here if there's one or more "computed" attributes + # (dynamic, from one or more dict values). + else: + value = ast.Str(s=text) + if msgid is missing and implicit_i18n: + msgid = text if name is not None: # If translation is required, wrap in a translation @@ -839,16 +838,24 @@ value = nodes.Translate(msgid, value) space = self._maybe_trim(space) - fs = filtering[-1] - attribute = nodes.Attribute(name, value, quote, eq, space, fs) + + attribute = nodes.Attribute( + name, + value, + quote, + eq, + space, + default, + filtering[-1], + ) if not isinstance(value, ast.Str): # Always define a ``default`` alias for non-static # expressions. attribute = nodes.Define( - [nodes.Alias(["default"], default_marker)], + [nodes.Alias(["default"], self.default_marker)], attribute, - ) + ) value = attribute @@ -856,9 +863,12 @@ result = nodes.Sequence(attributes) - fs = filtering[0] - if fs: - return nodes.Cache(fs, result) + # We're caching all expressions found during attribute processing to + # enable the filtering functionality which exists to allow later + # definitions to override previous ones. + expressions = filtering[0] + if expressions: + return nodes.Cache(expressions, result) return result diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/chameleon-3.7.2/src/chameleon/zpt/template.py new/chameleon-3.8.1/src/chameleon/zpt/template.py --- old/chameleon-3.7.2/src/chameleon/zpt/template.py 2020-05-31 07:51:18.000000000 +0200 +++ new/chameleon-3.8.1/src/chameleon/zpt/template.py 2020-07-06 21:23:38.000000000 +0200 @@ -16,17 +16,17 @@ from ..tales import ProxyExpr from ..tales import StructureExpr from ..tales import ExpressionParser - +from ..tales import DEFAULT_MARKER from ..tal import RepeatDict from ..template import BaseTemplate from ..template import BaseTemplateFile from ..compiler import ExpressionEngine from ..loader import TemplateLoader -from ..astutil import Builtin from ..utils import decode_string from ..utils import string_type from ..utils import unicode_string +from ..astutil import Symbol from .program import MacroProgram @@ -64,14 +64,6 @@ Pass an encoding to allow encoded byte string input (e.g. UTF-8). - ``literal_false`` - - Attributes are not dropped for a value of ``False``. Instead, - the value is coerced to a string. - - This setting exists to provide compatibility with the - reference implementation. - ``boolean_attributes`` Attributes included in this set are treated as booleans: if a @@ -163,6 +155,15 @@ exception output. The function must not raise an exception (it should be safe to call with any value). + ``default_marker`` + + This default marker is used as the marker object bound to the `default` + name available to any expression. The semantics is such that if an + expression evaluates to the marker object, the default action is used; + for an attribute expression, this is the static attribute text; for an + element this is the static element text. If there is no static text + then the default action is similar to an expression result of `None`. + Output is unicode on Python 2 and string on Python 3. """ @@ -184,8 +185,6 @@ boolean_attributes = set() - literal_false = False - mode = "xml" implicit_i18n_translate = False @@ -204,6 +203,8 @@ tokenizer = None + default_marker = Symbol(DEFAULT_MARKER) + def __init__(self, body, **config): self.macros = Macros(self) super(PageTemplate, self).__init__(body, **config) @@ -217,15 +218,10 @@ @property def engine(self): - if self.literal_false: - default_marker = Builtin("__default") - else: - default_marker = Builtin("False") - return partial( ExpressionEngine, self.expression_parser, - default_marker=default_marker, + default_marker=self.default_marker, ) @property @@ -233,15 +229,10 @@ return ExpressionParser(self.expression_types, self.default_expression) def parse(self, body): - if self.literal_false: - default_marker = Builtin("__default") - else: - default_marker = Builtin("False") - return MacroProgram( body, self.mode, self.filename, escape=True if self.mode == "xml" else False, - default_marker=default_marker, + default_marker=self.default_marker, boolean_attributes=self.boolean_attributes, implicit_i18n_translate=self.implicit_i18n_translate, implicit_i18n_attributes=self.implicit_i18n_attributes, @@ -329,7 +320,6 @@ for attr in ( 'trim_attribute_space', 'implicit_i18n_translate', - 'literal_false', 'strict' ): v = getattr(self, attr)