Primitive Template Engine (live coding)

Du hast eine Idee für ein Projekt?
Antworten
Benutzeravatar
Defnull
User
Beiträge: 778
Registriert: Donnerstag 18. Juni 2009, 22:09
Wohnort: Göttingen
Kontaktdaten:

Python ist eine sehr schöne Sprache, eignet sich aber durch den Whitespace-sensitiven Syntax nur bedingt für Templates, wie sie z.B. zum generieren von (x)html Seiten benutzt werden. Bis zu einem gewissen Punkt kann man sich mit multiline-strings und string.Template() aushelfen, aber sobald man Blöcke oder schleifen braucht, wird der Code wirklich hässlich, besonders wenn das Ausgabeformat auch reichlich Gebrauch von Whitespaces und Newlines macht.

Also braucht man eine Zwischeninstanz, die es vereinfacht, Python-Blöcke in Fließtext einzubauen, ohne die Einrückung beachten zu müssen. Man braucht eine Template-Sprache.

Jaja, ich weis, die gibt es zu Hauf. Ich möchte aber gerne eine in mein Mirco-web-Framework Bottle einbauen. Daher hab ich mit dieser Idee 4 Ziele:

1) Unter 100 Zeilen Code für die komplette Template-Engine.
2) Volle Unterstützung aller Python-Features. Keine Verkrüppelung der Sprache. Wir vertrauen den Template-Autoren ;)
3) Es muss schnell sein. Sau schnell. So schnell, das es gar nicht schneller geht ;)

Als Syntax stelle ich mir folgendes vor:
a) '%' startet eine Code-Zeile.
b) '%end' beendet einen Block. (Brauchen wir, da Whitespaces keine Blöcke mehr markieren)
c) '{{...}}' wird durch den Wert des darin enthaltenen Ausdrucks ersetzt

Das war es schon. Mehr braucht man eigentlich nicht. Hier ist ein Test-Template:

Code: Alles auswählen

%header = 'test'
<html>
  <head>
    <title>{{header.title()}}</title>
  </head>
  <body>
    <ul>
    %for i in l:
      <li>
      %if isinstance(i, int):
        Zahl: {{str(i)}}
      %else:
        %try:
        Other: {{repr(i)}}
        %except:
        Error
        %end try-block (yes, you may add comments here)
      %end
      </li>
    %end
    </ul>
  </body>
</html>
Mal sehen, was daraus wird. Ich beginne jetzt (17:oo Uhr) mit dem coden und halte euch auf dem Laufenden :D
Bottle: Micro Web Framework + Development Blog
stuhlbein
User
Beiträge: 89
Registriert: Freitag 9. Januar 2009, 16:08

Wie auch Bottle gefällt mir diese idee ebenfalls sehr gut, wobei sma doch bereits eine ähnliche idee hatte? ist natürlich wieder reine geschmackssache, aber lass dich dadurch nicht von der idee abbringen! :D
Benutzeravatar
Defnull
User
Beiträge: 778
Registriert: Donnerstag 18. Juni 2009, 22:09
Wohnort: Göttingen
Kontaktdaten:

Ich geb zu, die Idee is geklaut, SMA verfolgt aber eine komplett anderen Ansatz: Während er eine eigene Sprache in Python schreiben will, möchte ich lediglich das Whitespace-Problem bei eingebettetem Python umgehen. Das reicht für meine Zwecke und ist deutlich einfacher :)

Ich bin übrigens fertig. Der Code ist nicht schön, funktioniert mit dem oben genannten Beispiel aber sehr gut :)

Meine Ziele:
1) Zeilen: 75/100 (erreicht)
2) Features: Templates können alles, was Python kann (erreicht)
3) Speed: Das Script kompiliert auf meiner Maschine (AMD3000) etwa 1000 Templates pro Sekunde und rendert diese etwa 10.000 mal pro Sekunde :D (erreicht!)

Hier der Code:

Code: Alles auswählen

import re
from StringIO import StringIO

class Template(object):
    ''' Allows to compile a template file into python code and execute it with different locals'''
    re_block = re.compile(r'^\s*%\s*((if|elif|else|try|except|finally|for|while|with).*:)\s*$')
    re_end   = re.compile(r'^\s*%\s*end(.*?)\s*$')
    re_code  = re.compile(r'^\s*%\s*(.*?)\s*$')
    re_inc   = re.compile(r'\{\{(.*?)\}\}')

    def __init__(self, filename):
        ''' Compiles a template file '''
        self.filename = filename
        self.code = "\n".join(list(self.compile(filename)))
        self.co   = compile(self.code, '<string>', 'exec')  

    def compile(self, filename):
        def code_str(level, line, value):
            return '    '*level + "stdout.write(r'''%s''') # Line: %d" % (value.replace("'","\'").replace('\\','\\\\'), line)
        def code_print(level, line, value):
            return '    '*level + 'stdout.write(str(%s)) # Line: %d' % (value, line)
        def code_raw(level, line, value):
            return '    '*level + value + ' # Line: %d' % line

        level = 0
        ln = 0
        fp = open(filename, 'r')
        for line in fp:
            ln += 1

            m = self.re_block.match(line)
            if m:
                if m.group(2).strip().lower() in ('elif','else','except','finally'):
                    if level > 0:
                        level -= 1
                    else:
                        yield code_raw(level, ln, '#Unexpected end of block: ' + m.group(1).strip())
                yield code_raw(level, ln, m.group(1).strip())
                level += 1
                continue
            
            m = self.re_end.match(line)
            if m:
                if level > 0:
                    yield code_raw(level, ln, '#END ' + m.group(1).strip())
                    level -= 1
                else:
                    yield code_raw(level, ln, '#Unexpected end of block: ' + m.group(1).strip())
                continue

            m = self.re_code.match(line)
            if m:
                yield code_raw(level, ln, m.group(1).strip())
                continue

            lasts = 0
            for m in self.re_inc.finditer(line):
                yield code_str(level, ln, line[lasts:m.start(0)])
                lasts = m.end(0)
                yield code_print(level, ln, m.group(1).strip())
            if lasts:
                yield code_str(level, ln, line[lasts:])
                continue
            
            yield code_str(level, ln, line)
        fp.close()

    def render(self, **args):
        io = StringIO()
        args['stdout'] = io
        args['__builtins__'] = __builtins__
        eval(self.co, {}, args)
        io.seek(0)
        return io
Und nochmal die Benchmark Ergebnisse:

Code: Alles auswählen

from ttpl import Template
from timeit import Timer
print 'Test run:'
t = Template('./ttpl/test.tpl')
output = t.render(l = [1,'a'])
print output.read()
print 'Speed test:'  
t = Timer("t = Template('./ttpl/test.tpl')", "from ttpl import Template").timeit(10000)
print "Compiled 10000 templates in %f seconds (%d/s)" % (t, 10000/t)
t = Timer("t.render(l = [1,'a'])", """from ttpl import Template
t = Template('./ttpl/test.tpl')""").timeit(10000)
print "Rendered 10000 templates in %f seconds (%d/s)" % (t, 10000/t)
Das Wirft bei mir (AMD3000, 4GB Ram, Python2.6) folgendes aus:

Code: Alles auswählen

Test run:

<html>
  <head>
    <title>Test</title>
  </head>
  <body>
    <ul>
      <li>
        Zahl: 1
      </li>
      <li>
        Other: 'a'
      </li>
    </ul>
  </body>
</html>

Speed test:

Compiled 10000 templates in 9.270781 seconds (1078/s)
Rendered 10000 templates in 1.010130 seconds (9899/s)
Entwicklungszeit: Etwa 40 Minuten
Bottle: Micro Web Framework + Development Blog
Benutzeravatar
Defnull
User
Beiträge: 778
Registriert: Donnerstag 18. Juni 2009, 22:09
Wohnort: Göttingen
Kontaktdaten:

Hab das ganze etwas beschleunigt, indem ich Listen statt StringIO genutzt habe:

Compiled 10000 templates in 8.590348 seconds (1164/s)
Rendered 100000 templates in 1.832866 seconds (54559/s)

Faktor 54 beim Rendern. Die kleine Verbesserung beim Kompilieren kommt von kleineren Optimierungen in anderen Codeteilen.
Bottle: Micro Web Framework + Development Blog
stuhlbein
User
Beiträge: 89
Registriert: Freitag 9. Januar 2009, 16:08

Ziemlich beeindruckend, nur dass die Template-klasse keine strings verarbeitet, sondern direkt dateien, find ich persönlich etwas störend (das wäre als optionale wahl IMHO besser aufgehoben).

ansonsten echt gute arbeit! ;)
Benutzeravatar
Defnull
User
Beiträge: 778
Registriert: Donnerstag 18. Juni 2009, 22:09
Wohnort: Göttingen
Kontaktdaten:

Bitfish hat geschrieben:Ziemlich beeindruckend, nur dass die Template-klasse keine strings verarbeitet, sondern direkt dateien, find ich persönlich etwas störend (das wäre als optionale wahl IMHO besser aufgehoben).

ansonsten echt gute arbeit! ;)
Danke :) Die neueste Version schafft noch bessere Ergebnisse, da mehrere statische Zeilen nun in einem einzigen String zusammen gefasst werden, statt jede Zeile einzeln. Das hat den Code allerdings auf 91 Zeilen aufgepumpt. Für mehr Features wird es langsam eng :)

Code: Alles auswählen

import re

class TemplateError(Exception): pass

class BaseTemplate(object):
    def __init__(self, template):
        self.code = self.compile(template)
        self.co = compile(self.code, '<string>', 'exec')
        
    def compile(self, template):
        ''' Compiles a template provided as file object or list of lines into a python script '''
        pass

    def render(self, **args):
        ''' Returns the rendered template using keyword arguments as local variables. '''
        args['stdout'] = []
        args['__builtins__'] = __builtins__
        eval(self.co, {}, args)
        return "".join(args['stdout'])



class SimpleTemplate(BaseTemplate):

    re_block = re.compile(r'^\s*%\s*((if|elif|else|try|except|finally|for|while|with).*:)\s*$')
    re_end   = re.compile(r'^\s*%\s*end(.*?)\s*$')
    re_code  = re.compile(r'^\s*%\s*(.*?)\s*$')
    re_inc   = re.compile(r'\{\{(.*?)\}\}')

    def compile(self, template):
        return "\n".join(self._compile(template))
        
    def _compile(self, template):
        def code_str(level, line, value):
            value = "\n".join(value) + "\n"
            value = value.replace("'","\'").replace('\\','\\\\')
            return '    '*level + "stdout.append(r'''%s''')" % value
        def code_print(level, line, value):
            return '    '*level + "stdout.append(str(%s)) # Line: %d" % (value.strip(), line)
        def code_raw(level, line, value):
            return '    '*level + value.strip() + ' # Line: %d' % line

        level = 0
        ln = 0
        sbuffer = []
        for line in template.splitlines():
            ln += 1
            # Line with block starting code
            m = self.re_block.match(line)
            if m:
                if sbuffer:
                    yield code_str(level, ln, sbuffer)
                    sbuffer = []
                if m.group(2).strip().lower() in ('elif','else','except','finally'):
                    if level == 0:
                        raise TemplateError('#Unexpected end of block in line %d' % ln)
                    level -= 1
                yield code_raw(level, ln, m.group(1).strip())
                level += 1
                continue
            # Line with % end marker
            m = self.re_end.match(line)
            if m:
                if sbuffer:
                    yield code_str(level, ln, sbuffer)
                    sbuffer = []
                if level == 0:
                    raise TemplateError('#Unexpected end of block in line %d' % ln)
                level -= 1
                continue
            # Line with % marker
            m = self.re_code.match(line)
            if m:
                yield code_raw(level, ln, m.group(1).strip())
                continue
            # Line with inline code
            lasts = 0
            for m in self.re_inc.finditer(line):
                sbuffer.append(line[lasts:m.start(0)])
                yield code_str(level, ln, sbuffer)
                sbuffer = []
                lasts = m.end(0)
                yield code_print(level, ln, m.group(1))
            if lasts:
                sbuffer.append(line[lasts:])
                continue
            # Stupid line
            sbuffer.append(line)

        if sbuffer:
            yield code_str(level, ln, sbuffer)
Ich hab mal für den Benchmark die Mako-Template-Engine als Vergleich heran gezogen. Natürlich kann Mako deutlich mehr als meine Engine, aber der Unterschied ist trotzdem beachtlich :D

Speed test (BTPL):
Compiled 1000 templates in 0.631131 seconds (1584/s)
Rendered 1000 templates in 0.015639 seconds (63942/s)
Speed test (MAKO):
Compiled 1000 templates in 6.464512 seconds (154/s)
Rendered 1000 templates in 0.110056 seconds (9086/s)
Bottle: Micro Web Framework + Development Blog
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

SMA hat (diese Mal) keine eigene Sprache in Python geschrieben, sondern einfach die von Django abgeguckt. Wenn ich Whitespace-Problem lese, muss ich aber an HAML denken und es reizt mich, an dem nächsten verregneten Sommerabend mal diesen Ansatz auszuprobieren. Embrace the Whitespace! Da gibt es kein Problem.

Der Code ist beindruckend kompakt. Respekt. Könnte es noch etwas mehr bringen, wenn du dein Template mit `compile` gleich in ein Code-Objekt verwandelst, welches du dann mit `exec` ausführen kannst? Dann muss der Python-Compiler nicht jedes Mal bei `render` den Quelltext neu analysieren.

Stefan
Benutzeravatar
Defnull
User
Beiträge: 778
Registriert: Donnerstag 18. Juni 2009, 22:09
Wohnort: Göttingen
Kontaktdaten:

sma hat geschrieben:Wenn ich Whitespace-Problem lese, muss ich aber an HAML denken und es reizt mich, an dem nächsten verregneten Sommerabend mal diesen Ansatz auszuprobieren. Embrace the Whitespace! Da gibt es kein Problem.
Du schreibst die Engine, ich den Wrapper für Bottle :) Na los, morgen will ich Ergebnisse sehen :D
sma hat geschrieben:Der Code ist beindruckend kompakt. Respekt.
Das hört man wirklich gerne. Danke :)
sma hat geschrieben: Könnte es noch etwas mehr bringen, wenn du dein Template mit `compile` gleich in ein Code-Objekt verwandelst, welches du dann mit `exec` ausführen kannst? Dann muss der Python-Compiler nicht jedes Mal bei `render` den Quelltext neu analysieren.
Genau das passiert in Zeile 8 während des __init__ Aufrufs :)
Bottle: Micro Web Framework + Development Blog
Benutzeravatar
Defnull
User
Beiträge: 778
Registriert: Donnerstag 18. Juni 2009, 22:09
Wohnort: Göttingen
Kontaktdaten:

Die Engine ist nun in Bottle integriert:

Code: Alles auswählen

easy_install bottle

Code: Alles auswählen

from bottle import SimpleTemplate

t = SimpleTemplate('tenplate-string')
print t.render(parameter=value)
Bottle: Micro Web Framework + Development Blog
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

Defnull hat geschrieben:Genau das passiert in Zeile 8 während des __init__ Aufrufs :)
Nicht aufgepasst. Grummel.

Es hieß doch aber Mini-Template-Engine, oder? Ich habe deinen Code ein bisschen umgeschrieben und biete 35 Zeilen. Das Beispiel funktioniert, mehr habe ich nicht getestet. Mich würde interessieren, ob das jetzt schneller oder langsamer als deine fast 3x so lange Version ist.

Code: Alles auswählen

import re

_python = re.compile(r'\s*%\s*(?:(if|elif|else|try|except|finally|for|while|with)|(end)|(.*))')
_variable = re.compile(r'\{\{((?:"[^"]*|[^"}]+)*)\}\}|[^{]+|\{')
_dedent_keywords = ('elif', 'else', 'except', 'finally')

def template(source):
    def translate(lines):
        indent = 0
        for line in lines:
            m = _python.match(line)
            if m:
                keyword, end, statement = m.groups()
                if keyword:
                    if keyword in _dedent_keywords:
                        indent -= 1
                    yield " " * indent + line[m.start(1):]
                    indent += 1
                elif end:
                    indent -= 1
                else:
                    yield " " * indent + line[m.start(3):]
            else:
                yield " " * indent + "_output(%s)" % _variable.sub(
                    lambda m: (m.group(1) if m.group(1) else repr(m.group())) + ",",
                    line) 
    code = compile("\n".join(translate(source.splitlines())), "<template>", "exec")
    def render(**args):
        output = []
        def _output(*args):
            for arg in args: output.append(str(arg))
            output.append("\n")
        exec code in dict(args, _output=_output)
        return "".join(output)
    return render
Stefan
Benutzeravatar
Defnull
User
Beiträge: 778
Registriert: Donnerstag 18. Juni 2009, 22:09
Wohnort: Göttingen
Kontaktdaten:

sma hat geschrieben: Es hieß doch aber Mini-Template-Engine, oder? Ich habe deinen Code ein bisschen umgeschrieben und biete 35 Zeilen. Das Beispiel funktioniert, mehr habe ich nicht getestet. Mich würde interessieren, ob das jetzt schneller oder langsamer als deine fast 3x so lange Version ist.
Sehr geil! Ich werd es mir gleich mal genauer an schauen. Hab ich die Erlaubnis, es in Bottle ein zu bauen, wenn ich deinen Namen unter "Special Tranks to" aufführe? :)

Edit: Ich hab von deinem Ansatz ausgehend mal noch ein paar Sachen hinzu gefügt, die mir wichtig waren:

1) Zeilen-Treue: Zeilen Nummern im compilierten Template stimmen mit denen im Quelltext überein. So kann der Python Error Handler sogar die richtige Template-Zeile ausgeben, wenn man bei 'exec()' den Pfad zur Template Datei angibt.
2) Mehrere Zeilen Text ohne Python Code werden zu einem langen String zusammen gefasst, anstatt jede Zeile einzeln zu stdout hinzu zu fügen.

Hier das Ergebnis, wie es bereits in Bottle eingebaut, aber noch nicht veröffentlicht ist. Ich hoffe auf deine Zusage ;)

Code: Alles auswählen

class SimpleTemplate(BaseTemplate):

    re_python = re.compile(r'^\s*%\s*(?:(if|elif|else|try|except|finally|for|while|with)|(end.*)|(.*))')
    re_inline = re.compile(r'\{\{(.*?)\}\}')
    dedent_keywords = ('elif', 'else', 'except', 'finally')

    def __init__(self, template, filename='<template>'):
        self.code = "".join(self.translate(template))
        self.co = compile(self.code, filename, 'exec')

    def render(self, **args):
        ''' Returns the rendered template using keyword arguments as local variables. '''
        args['stdout'] = []
        args['__builtins__'] = __builtins__
        eval(self.co, {}, args)
        return ''.join(args['stdout'])

    def translate(self, template):
        indent = 0
        strbuffer = []
        code = []
        class PyStmt(str):
            def __repr__(self): return 'str(' + self + ')'
        def flush():
            if len(strbuffer):
                code.append(" " * indent + "stdout.append(%s)" % repr(''.join(strbuffer)))
                code.append("\n" * len(strbuffer)) # to preserve line numbers 
                del strbuffer[:]
        for line in template.splitlines(True):
            m = self.re_python.match(line)
            if m:
                flush()
                keyword, end, statement = m.groups()
                if keyword:
                    if keyword in self.dedent_keywords:
                        indent -= 1
                    code.append(" " * indent + line[m.start(1):])
                    indent += 1
                elif end:
                    indent -= 1
                    code.append(" " * indent + '#' + line[m.start(2):])
                elif statement:
                    code.append(" " * indent + line[m.start(3):])
            else:
                splits = self.re_inline.split(line) # text, (expr, text)*
                if len(splits) == 1:
                    strbuffer.append(line)
                else:
                    flush()
                    for i in xrange(1, len(splits), 2):
                        splits[i] = PyStmt(splits[i])
                    code.append(" " * indent + "stdout.extend(%s)\n" % repr(splits))
        return code
Und hier mal eine Beispiel-Fehlermeldung:

Code: Alles auswählen

Traceback (most recent call last):
  File "/home/marc/work/bottle/bottle.py", line 128, in WSGIHandler
    output = handler(**args)
  File "example.py", line 72, in template_test
    return template('example', title='Template Test', items=[1,2,3,'fly'])
  File "/home/marc/work/bottle/bottle.py", line 644, in template
    return TEMPLATES[template_name].render(**args)
  File "/home/marc/work/bottle/bottle.py", line 589, in render
    eval(self.co, {}, args)
  File "./example.tpl", line 14, in
    Zahl: {{ittem}}
NameError: name 'ittem' is not defined
Cool, oder? Er zeigt tatsächlich den Template-Code an, der zum Fehler geführt hat.
Bottle: Micro Web Framework + Development Blog
Benutzeravatar
Defnull
User
Beiträge: 778
Registriert: Donnerstag 18. Juni 2009, 22:09
Wohnort: Göttingen
Kontaktdaten:

Neues Feature: Mit "%include" kann man nun subtemplates einbinden:

Code: Alles auswählen

%include header title='Test Page', js_load=['jquery.js', 'default.js']
<p>Sime HTML</p>
%include foother
Das erste Wort nach %include gibt den Template-Namen an (der in allen in Bottle registrierten Pfaden gesucht wird). Nach kann beliebiger Code folgen, der direkt in den render(...) Aufruf geschrieben wird. Im Normalfall ist das eine Parameterliste. So was wie "%include name **locals()" wäre aber auch möglich und würde in "_subtemplate[name].render(**loals())" übersetzt werden.
Bottle: Micro Web Framework + Development Blog
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

Defnull hat geschrieben:Hab ich die Erlaubnis, es in Bottle ein zu bauen, wenn ich deinen Namen unter "Special Tranks to" aufführe? :)
Ja.

Stefan
Benutzeravatar
filchos
User
Beiträge: 35
Registriert: Dienstag 2. Juni 2009, 10:48
Wohnort: München
Kontaktdaten:

Hallo,

ich experimentiere gerade mit bottle (aktuelle Version von git), vor allem mit der netten, schlanken Templateengine. Ich finde allerdings einen Fehler nicht:

Pythonversion:

Code: Alles auswählen

items = range(20)
div = 3
found = filter(lambda i: i % div == 0, items)
print found
ergibt wie erwartet [0, 3, 6, 9, 12, 15, 18]

als Template:

Code: Alles auswählen

%items = range(20)
%div = 3
%found = filter(lambda i: i % div == 0, items)
{{found}}
Ich erhalte dort einen Fehler:

Error (500) on '/tpl': global name 'div' is not defined

wenn ich die Variable div in der filter-Funktion benutze. Mache ich einen Denkfehler oder ist das noch ein Bug.

Fragt sich Olaf

P.S.: Dies ist ein stark gekürztes Beispiel, welches in dieser Form in einem Template nichts zu suchen hat.
Benutzeravatar
Defnull
User
Beiträge: 778
Registriert: Donnerstag 18. Juni 2009, 22:09
Wohnort: Göttingen
Kontaktdaten:

Das ist ein Verhalten von "eval()", das ich so noch nicht kenne, aber offensichtlich wird "div" als lokale und nicht als globale Variable registriert.

Dein Beispiel produziert den folgenden Template-Code:

Code: Alles auswählen

>>> from bottle import SimpleTemplate
>>> print SimpleTemplate().translate(open('./tt.tpl').read())
items = range(20)
div = 3
found = filter(lambda i: i % div == 0, items)
stdout.extend(['', str(found), '\n'])
der ja richtig aus sieht, aber sich in eval() wohl anders verhält, als man erwarten würde... Hat jemand ne Idee, wie man das umgeht?


Unschöner Workaround:

Code: Alles auswählen

%items = range(20)
%global div
%div = 3
%found = filter(lambda i: i % div == 0, items)
{{found}}
Keine Angst, div bleibt im Template-Namensraum und leakt NICHT in den echten globalen Namensraum. Eval() scheint seine eigene Auffassung von 'global' zu haben.
Bottle: Micro Web Framework + Development Blog
Benutzeravatar
filchos
User
Beiträge: 35
Registriert: Dienstag 2. Juni 2009, 10:48
Wohnort: München
Kontaktdaten:

Hallo Defnull,

danke für die Idee mit der globalen Variablen.

Ich habe mir auch den erzeugten Code angeschaut, der direkt ausgeführt ja auch wunderbar funktioniert. Auf die Idee, dass eval() Eigenheiten, was die Namensräume angeht besitzt, bin ich gar nicht gekommen.

Grüße,
Olaf
Benutzeravatar
filchos
User
Beiträge: 35
Registriert: Dienstag 2. Juni 2009, 10:48
Wohnort: München
Kontaktdaten:

Nachtrag:

kann es sein (noch nicht in bottle getestet), dass das Problem in

Code: Alles auswählen

eval(self.co, args, globals())
steckt?

Test:

Code: Alles auswählen

>>> s = """items = range(20)
div = 3
found = filter(lambda i: i % div == 0, items)
print found"""
>>> c = compile(s, '<template>', 'exec')
>>> eval(c)
[0, 3, 6, 9, 12, 15, 18]
>>> eval(c, {}, globals()) 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<template>", line 3, in <module>
  File "<template>", line 3, in <lambda>
NameError: global name 'div' is not defined
>>> eval(c, locals(), globals())
[0, 3, 6, 9, 12, 15, 18]
Benutzeravatar
Defnull
User
Beiträge: 778
Registriert: Donnerstag 18. Juni 2009, 22:09
Wohnort: Göttingen
Kontaktdaten:

Du hast recht, ich hab da wohl was mit den globalen und lokalen Namensräumen durcheinander geworfen. Mit der aktuellen github-Version klappt es auch ohne den Workaround wie erwartet.

Code: Alles auswählen

marc@nava:~/work/bottle$ git diff HEAD^^
diff --git a/bottle.py b/bottle.py
index 557c455..d58646f 100644
--- a/bottle.py
+++ b/bottle.py
@@ -659,7 +659,7 @@ class SimpleTemplate(BaseTemplate):
         ''' Returns the rendered template using keyword arguments as local vari
         args['stdout'] = []
         args['_subtemplates'] = self.subtemplates
-        eval(self.co, args, globals())
+        exec self.co in args
         return ''.join(args['stdout'])
Bottle: Micro Web Framework + Development Blog
Benutzeravatar
filchos
User
Beiträge: 35
Registriert: Dienstag 2. Juni 2009, 10:48
Wohnort: München
Kontaktdaten:

OK Devnull,

Du warst schneller ;-).

Ich habe mit verschiedenen Varianten von eval() und exec herumgespielt, aber exec … in kannte ich noch nicht.

Danke für das schnelle Fixen.

Grüße,
Olaf
Antworten