From cbbdfab6a4370b71203a9a480a78331ee03a9019 Mon Sep 17 00:00:00 2001 From: RaNaN Date: Mon, 17 Aug 2009 15:02:59 +0200 Subject: pyLoad Test Webinterface ! --- module/remote/SocketServer.py | 2 +- module/web/WebServer.py | 70 ++ module/web/__init__.py | 0 module/web/bottle.py | 883 +++++++++++++++++++++ module/web/static/default.css | 178 +++++ module/web/static/default/head-login.png | Bin 0 -> 1288 bytes .../web/static/default/head-menu-development.png | Bin 0 -> 1324 bytes module/web/static/default/head-menu-download.png | Bin 0 -> 721 bytes module/web/static/default/head-menu-home.png | Bin 0 -> 920 bytes module/web/static/default/head-menu-index.png | Bin 0 -> 482 bytes module/web/static/default/head-menu-news.png | Bin 0 -> 628 bytes module/web/static/default/head-menu-recent.png | Bin 0 -> 932 bytes module/web/static/default/head-menu-wiki.png | Bin 0 -> 1204 bytes module/web/static/default/head-search-noshadow.png | Bin 0 -> 1187 bytes module/web/static/default/head_bg1.png | Bin 0 -> 125 bytes module/web/static/default/page-tools-backlinks.png | Bin 0 -> 540 bytes module/web/static/default/page-tools-edit.png | Bin 0 -> 574 bytes module/web/static/default/page-tools-revisions.png | Bin 0 -> 603 bytes .../pyload-logo-edited3.5-new-font-small.png | Bin 0 -> 8457 bytes module/web/static/default/tab-background.png | Bin 0 -> 179 bytes module/web/static/default/tabs-border-bottom.png | Bin 0 -> 163 bytes module/web/static/default/user-actions-logout.png | Bin 0 -> 799 bytes module/web/static/default/user-actions-profile.png | Bin 0 -> 628 bytes module/web/static/mootools-1.2.3-core.js | 335 ++++++++ module/web/static/pyload.ico | Bin 0 -> 7206 bytes module/web/templates/default.tpl | 107 +++ module/web/templates/footer.tpl | 2 + module/web/templates/header.tpl | 20 + 28 files changed, 1596 insertions(+), 1 deletion(-) create mode 100644 module/web/WebServer.py create mode 100644 module/web/__init__.py create mode 100644 module/web/bottle.py create mode 100644 module/web/static/default.css create mode 100644 module/web/static/default/head-login.png create mode 100644 module/web/static/default/head-menu-development.png create mode 100644 module/web/static/default/head-menu-download.png create mode 100644 module/web/static/default/head-menu-home.png create mode 100644 module/web/static/default/head-menu-index.png create mode 100644 module/web/static/default/head-menu-news.png create mode 100644 module/web/static/default/head-menu-recent.png create mode 100644 module/web/static/default/head-menu-wiki.png create mode 100644 module/web/static/default/head-search-noshadow.png create mode 100644 module/web/static/default/head_bg1.png create mode 100644 module/web/static/default/page-tools-backlinks.png create mode 100644 module/web/static/default/page-tools-edit.png create mode 100644 module/web/static/default/page-tools-revisions.png create mode 100644 module/web/static/default/pyload-logo-edited3.5-new-font-small.png create mode 100644 module/web/static/default/tab-background.png create mode 100644 module/web/static/default/tabs-border-bottom.png create mode 100644 module/web/static/default/user-actions-logout.png create mode 100644 module/web/static/default/user-actions-profile.png create mode 100644 module/web/static/mootools-1.2.3-core.js create mode 100644 module/web/static/pyload.ico create mode 100644 module/web/templates/default.tpl create mode 100644 module/web/templates/footer.tpl create mode 100644 module/web/templates/header.tpl (limited to 'module') diff --git a/module/remote/SocketServer.py b/module/remote/SocketServer.py index e396a3507..d2fa912dd 100644 --- a/module/remote/SocketServer.py +++ b/module/remote/SocketServer.py @@ -20,7 +20,7 @@ class ServerThread(threading.Thread): def __init__(self, pycore): threading.Thread.__init__(self) self.setDaemon(True) - self.server = MainServerSocket(int(pycore.config['port']), pycore) + self.server = MainServerSocket(int(pycore.config['remote']['port']), pycore) def run(self): asyncore.loop() diff --git a/module/web/WebServer.py b/module/web/WebServer.py new file mode 100644 index 000000000..d2bf9b3a8 --- /dev/null +++ b/module/web/WebServer.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +#Copyright (C) 2009 RaNaN +# +#This program is free software; you can redistribute it and/or modify +#it under the terms of the GNU General Public License as published by +#the Free Software Foundation; either version 3 of the License, +#or (at your option) any later version. +# +#This program is distributed in the hope that it will be useful, +#but WITHOUT ANY WARRANTY; without even the implied warranty of +#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +#See the GNU General Public License for more details. +# +#You should have received a copy of the GNU General Public License +# along with this program; if not, see . +# +### + +import threading + +import bottle +from bottle import abort +from bottle import db +from bottle import debug +from bottle import request +from bottle import response +from bottle import redirect +from bottle import route +from bottle import run +from bottle import send_file +from bottle import template +from bottle import validate + + +debug(True) +core = None + +PATH = "./module/web/" + +@route('/') +def hello_world(): + return template('default', string=str(core.get_downloads())) + +@route('/favicon.ico') +def favicon(): + send_file('pyload.ico', PATH + 'static/') + +@route('static/:section/:filename') +def static_folder(section, filename): + send_file(filename, root=(PATH + 'static/' + section)) + +@route('/static/:filename') +def static_file(filename): + send_file(filename, root=(PATH + 'static/')) + +class WebServer(threading.Thread): + def __init__(self, pycore): + threading.Thread.__init__(self) + + global core + core = pycore + self.core = pycore + self.setDaemon(True) + + bottle.TEMPLATE_PATH.append('./module/web/templates/%s.tpl') + + def run(self): + run(host='localhost', port=8080) \ No newline at end of file diff --git a/module/web/__init__.py b/module/web/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/module/web/bottle.py b/module/web/bottle.py new file mode 100644 index 000000000..8335f112b --- /dev/null +++ b/module/web/bottle.py @@ -0,0 +1,883 @@ +# -*- coding: utf-8 -*- +""" +Bottle is a fast and simple mirco-framework for small web-applications. It +offers request dispatching (Routes) with url parameter support, Templates, +key/value Databases, a build-in HTTP Server and adapters for many third party +WSGI/HTTP-server and template engines. All in a single file and with no +dependencies other than the Python Standard Library. + +Homepage and documentation: http://wiki.github.com/defnull/bottle + +Special thanks to Stefan Matthias Aust [http://github.com/sma] + for his contribution to SimpelTemplate + +Licence (MIT) +------------- + + Copyright (c) 2009, Marcel Hellkamp. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + + +Example +------- + + from bottle import route, run, request, response, send_file, abort + + @route('/') + def hello_world(): + return 'Hello World!' + + @route('/hello/:name') + def hello_name(name): + return 'Hello %s!' % name + + @route('/hello', method='POST') + def hello_post(): + name = request.POST['name'] + return 'Hello %s!' % name + + @route('/static/:filename#.*#') + def static_file(filename): + send_file(filename, root='/path/to/static/files/') + + run(host='localhost', port=8080) + +""" + +__author__ = 'Marcel Hellkamp' +__version__ = '0.5.7' +__license__ = 'MIT' + +import sys +import cgi +import mimetypes +import os +import os.path +import traceback +import re +import random +import threading +import time +from wsgiref.headers import Headers as HeaderWrapper +from Cookie import SimpleCookie +import anydbm as dbm + +try: + from urlparse import parse_qs +except ImportError: + from cgi import parse_qs + +try: + import cPickle as pickle +except ImportError: + import pickle as pickle + +try: + try: + from json import dumps as json_dumps + except ImportError: + from simplejson import dumps as json_dumps +except ImportError: + json_dumps = None + + + + + + +# Exceptions and Events + +class BottleException(Exception): + """ A base class for exceptions used by bottle.""" + pass + + +class HTTPError(BottleException): + """ A way to break the execution and instantly jump to an error handler. """ + def __init__(self, status, text): + self.output = text + self.http_status = int(status) + + def __repr__(self): + return "HTTPError(%d,%s)" % (self.http_status, repr(self.output)) + + def __str__(self): + out = [] + status = self.http_status + name = HTTP_CODES.get(status,'Unknown').title() + url = request.path + out.append('') + out.append('Error %d: %s' % (status, name)) + out.append('

Error %d: %s

' % (status, name)) + out.append('

Sorry, the requested URL "%s" caused an error.

' % url) + out.append(''.join(list(self.output))) + out.append('') + return "\n".join(out) + + +class BreakTheBottle(BottleException): + """ Not an exception, but a straight jump out of the controller code. + + Causes the Bottle to instantly call start_response() and return the + content of output """ + def __init__(self, output): + self.output = output + + + + + + +# WSGI abstraction: Request and response management + +_default_app = None +def default_app(newapp = None): + ''' Returns the current default app or sets a new one. + Defaults to an instance of Bottle ''' + global _default_app + if newapp: + _default_app = newapp + if not _default_app: + _default_app = Bottle() + return _default_app + + +class Bottle(object): + + def __init__(self, catchall=True, debug=False, optimize=False, autojson=True): + self.simple_routes = {} + self.regexp_routes = {} + self.error_handler = {} + self.optimize = optimize + self.debug = debug + self.autojson = autojson + self.catchall = catchall + + def match_url(self, url, method='GET'): + """Returns the first matching handler and a parameter dict or (None, None) """ + url = url.strip().lstrip("/ ") + # Search for static routes first + route = self.simple_routes.get(method,{}).get(url,None) + if route: + return (route, {}) + + routes = self.regexp_routes.get(method,[]) + for i in range(len(routes)): + match = routes[i][0].match(url) + if match: + handler = routes[i][1] + if i > 0 and self.optimize and random.random() <= 0.001: + routes[i-1], routes[i] = routes[i], routes[i-1] + return (handler, match.groupdict()) + return (None, None) + + def add_route(self, route, handler, method='GET', simple=False): + """ Adds a new route to the route mappings. """ + method = method.strip().upper() + route = route.strip().lstrip('$^/ ').rstrip('$^ ') + if re.match(r'^(\w+/)*\w*$', route) or simple: + self.simple_routes.setdefault(method, {})[route] = handler + else: + route = re.sub(r':([a-zA-Z_]+)(?P[^\w/])(?P.+?)(?P=uniq)',r'(?P<\1>\g)',route) + route = re.sub(r':([a-zA-Z_]+)',r'(?P<\1>[^/]+)', route) + route = re.compile('^%s$' % route) + self.regexp_routes.setdefault(method, []).append([route, handler]) + + def route(self, url, **kargs): + """ Decorator for request handler. Same as add_route(url, handler, **kargs).""" + def wrapper(handler): + self.add_route(url, handler, **kargs) + return handler + return wrapper + + def set_error_handler(self, code, handler): + """ Adds a new error handler. """ + code = int(code) + self.error_handler[code] = handler + + def error(self, code=500): + """ Decorator for error handler. Same as set_error_handler(code, handler).""" + def wrapper(handler): + self.set_error_handler(code, handler) + return handler + return wrapper + + def __call__(self, environ, start_response): + """ The bottle WSGI-interface .""" + request.bind(environ) + response.bind() + try: # Unhandled Exceptions + try: # Bottle Error Handling + handler, args = self.match_url(request.path, request.method) + if not handler: raise HTTPError(404, "Not found") + output = handler(**args) + db.close() + except BreakTheBottle, e: + output = e.output + except HTTPError, e: + response.status = e.http_status + output = self.error_handler.get(response.status, str)(e) + # output casting + if hasattr(output, 'read'): + output = environ.get('wsgi.file_wrapper', lambda x: iter(lambda: x.read(8192), ''))(output) + elif self.autojson and json_dumps and isinstance(output, dict): + output = json_dumps(output) + response.content_type = 'application/json' + if isinstance(output, str): + response.header['Content-Length'] = str(len(output)) + output = [output] + except (KeyboardInterrupt, SystemExit, MemoryError): + raise + except Exception, e: + response.status = 500 + if self.catchall: + err = "Unhandled Exception: %s\n" % (repr(e)) + if self.debug: + err += "

Traceback:

\n
\n"
+                    err += traceback.format_exc(10)
+                    err += "\n
" + output = str(HTTPError(500, err)) + request._environ['wsgi.errors'].write(err) + else: + raise + status = '%d %s' % (response.status, HTTP_CODES[response.status]) + start_response(status, response.wsgiheaders()) + return output + + + +class Request(threading.local): + """ Represents a single request using thread-local namespace. """ + + def bind(self, environ): + """ Binds the enviroment of the current request to this request handler """ + self._environ = environ + self._GET = None + self._POST = None + self._GETPOST = None + self._COOKIES = None + self.path = self._environ.get('PATH_INFO', '/').strip() + if not self.path.startswith('/'): + self.path = '/' + self.path + + @property + def method(self): + ''' Returns the request method (GET,POST,PUT,DELETE,...) ''' + return self._environ.get('REQUEST_METHOD', 'GET').upper() + + @property + def query_string(self): + ''' Content of QUERY_STRING ''' + return self._environ.get('QUERY_STRING', '') + + @property + def input_length(self): + ''' Content of CONTENT_LENGTH ''' + try: + return int(self._environ.get('CONTENT_LENGTH', '0')) + except ValueError: + return 0 + + @property + def GET(self): + """Returns a dict with GET parameters.""" + if self._GET is None: + data = parse_qs(self.query_string, keep_blank_values=True) + self._GET = {} + for key, value in data.iteritems(): + if len(value) == 1: + self._GET[key] = value[0] + else: + self._GET[key] = value + return self._GET + + @property + def POST(self): + """Returns a dict with parsed POST or PUT data.""" + if self._POST is None: + data = cgi.FieldStorage(fp=self._environ['wsgi.input'], environ=self._environ, keep_blank_values=True) + self._POST = {} + for item in data.list: + name = item.name + if not item.filename: + item = item.value + self._POST.setdefault(name, []).append(item) + for key in self._POST: + if len(self._POST[key]) == 1: + self._POST[key] = self._POST[key][0] + return self._POST + + @property + def params(self): + ''' Returns a mix of GET and POST data. POST overwrites GET ''' + if self._GETPOST is None: + self._GETPOST = dict(self.GET) + self._GETPOST.update(dict(self.POST)) + return self._GETPOST + + @property + def COOKIES(self): + """Returns a dict with COOKIES.""" + if self._COOKIES is None: + raw_dict = SimpleCookie(self._environ.get('HTTP_COOKIE','')) + self._COOKIES = {} + for cookie in raw_dict.itervalues(): + self._COOKIES[cookie.key] = cookie.value + return self._COOKIES + + +class Response(threading.local): + """ Represents a single response using thread-local namespace. """ + + def bind(self): + """ Clears old data and creates a brand new Response object """ + self._COOKIES = None + self.status = 200 + self.header_list = [] + self.header = HeaderWrapper(self.header_list) + self.content_type = 'text/html' + self.error = None + + def wsgiheaders(self): + ''' Returns a wsgi conform list of header/value pairs ''' + for c in self.COOKIES.itervalues(): + self.header.add_header('Set-Cookie', c.OutputString()) + return [(h.title(), str(v)) for h, v in self.header.items()] + + @property + def COOKIES(self): + if not self._COOKIES: + self._COOKIES = SimpleCookie() + return self._COOKIES + + def set_cookie(self, key, value, **kargs): + """ Sets a Cookie. Optional settings: expires, path, comment, domain, max-age, secure, version, httponly """ + self.COOKIES[key] = value + for k in kargs: + self.COOKIES[key][k] = kargs[k] + + def get_content_type(self): + '''Gives access to the 'Content-Type' header and defaults to 'text/html'.''' + return self.header['Content-Type'] + + def set_content_type(self, value): + self.header['Content-Type'] = value + + content_type = property(get_content_type, set_content_type, None, get_content_type.__doc__) + + +def abort(code=500, text='Unknown Error: Appliction stopped.'): + """ Aborts execution and causes a HTTP error. """ + raise HTTPError(code, text) + + +def redirect(url, code=307): + """ Aborts execution and causes a 307 redirect """ + response.status = code + response.header['Location'] = url + raise BreakTheBottle("") + + +def send_file(filename, root, guessmime = True, mimetype = 'text/plain'): + """ Aborts execution and sends a static files as response. """ + root = os.path.abspath(root) + '/' + filename = os.path.normpath(filename).strip('/') + filename = os.path.join(root, filename) + + if not filename.startswith(root): + abort(401, "Access denied.") + if not os.path.exists(filename) or not os.path.isfile(filename): + abort(404, "File does not exist.") + if not os.access(filename, os.R_OK): + abort(401, "You do not have permission to access this file.") + + if guessmime: + guess = mimetypes.guess_type(filename)[0] + if guess: + response.content_type = guess + elif mimetype: + response.content_type = mimetype + elif mimetype: + response.content_type = mimetype + + stats = os.stat(filename) + # TODO: HTTP_IF_MODIFIED_SINCE -> 304 (Thu, 02 Jul 2009 23:16:31 CEST) + if 'Content-Length' not in response.header: + response.header['Content-Length'] = stats.st_size + if 'Last-Modified' not in response.header: + ts = time.gmtime(stats.st_mtime) + ts = time.strftime("%a, %d %b %Y %H:%M:%S +0000", ts) + response.header['Last-Modified'] = ts + + raise BreakTheBottle(open(filename, 'r')) + + + + + + +# Decorators + +def validate(**vkargs): + ''' Validates and manipulates keyword arguments by user defined callables + and handles ValueError and missing arguments by raising HTTPError(403) ''' + def decorator(func): + def wrapper(**kargs): + for key in vkargs: + if key not in kargs: + abort(403, 'Missing parameter: %s' % key) + try: + kargs[key] = vkargs[key](kargs[key]) + except ValueError, e: + abort(403, 'Wrong parameter format for: %s' % key) + return func(**kargs) + return wrapper + return decorator + + +def route(url, **kargs): + """ Decorator for request handler. Same as add_route(url, handler, **kargs).""" + return default_app().route(url, **kargs) + + +def error(code=500): + """ Decorator for error handler. Same as set_error_handler(code, handler).""" + return default_app().error(code) + + + + + + +# Server adapter + +class ServerAdapter(object): + def __init__(self, host='127.0.0.1', port=8080, **kargs): + self.host = host + self.port = int(port) + self.options = kargs + + def __repr__(self): + return "%s (%s:%d)" % (self.__class__.__name__, self.host, self.port) + + def run(self, handler): + pass + + +class WSGIRefServer(ServerAdapter): + def run(self, handler): + from wsgiref.simple_server import make_server + srv = make_server(self.host, self.port, handler) + srv.serve_forever() + + +class CherryPyServer(ServerAdapter): + def run(self, handler): + from cherrypy import wsgiserver + server = wsgiserver.CherryPyWSGIServer((self.host, self.port), handler) + server.start() + + +class FlupServer(ServerAdapter): + def run(self, handler): + from flup.server.fcgi import WSGIServer + WSGIServer(handler, bindAddress=(self.host, self.port)).run() + + +class PasteServer(ServerAdapter): + def run(self, handler): + from paste import httpserver + from paste.translogger import TransLogger + app = TransLogger(handler) + httpserver.serve(app, host=self.host, port=str(self.port)) + + +class FapwsServer(ServerAdapter): + """ Extreamly fast Webserver using libev (see http://william-os4y.livejournal.com/) + Experimental ... """ + def run(self, handler): + import fapws._evwsgi as evwsgi + from fapws import base + import sys + evwsgi.start(self.host, self.port) + evwsgi.set_base_module(base) + def app(environ, start_response): + environ['wsgi.multiprocess'] = False + return handler(environ, start_response) + evwsgi.wsgi_cb(('',app)) + evwsgi.run() + + +def run(app=None, server=WSGIRefServer, host='127.0.0.1', port=8080, **kargs): + """ Runs bottle as a web server, using Python's built-in wsgiref implementation by default. + + You may choose between WSGIRefServer, CherryPyServer, FlupServer and + PasteServer or write your own server adapter. + """ + if not app: + app = default_app() + + quiet = bool('quiet' in kargs and kargs['quiet']) + + # Instanciate server, if it is a class instead of an instance + if isinstance(server, type) and issubclass(server, ServerAdapter): + server = server(host=host, port=port, **kargs) + + if not isinstance(server, ServerAdapter): + raise RuntimeError("Server must be a subclass of ServerAdapter") + + if not quiet: + print 'Bottle server starting up (using %s)...' % repr(server) + print 'Listening on http://%s:%d/' % (server.host, server.port) + print 'Use Ctrl-C to quit.' + print + + try: + server.run(app) + except KeyboardInterrupt: + print "Shuting down..." + + + + + + +# Templates + +class BaseTemplate(object): + def __init__(self, template='', filename=None): + self.source = filename + if self.source: + fp = open(filename) + template = fp.read() + fp.close() + self.parse(template) + + def parse(self, template): raise NotImplementedError + def render(self, **args): raise NotImplementedError + + @classmethod + def find(cls, name): + for path in TEMPLATE_PATH: + if os.path.isfile(path % name): + return cls(filename = path % name) + return None + + +class MakoTemplate(BaseTemplate): + def parse(self, template): + from mako.template import Template + self.tpl = Template(template) + + def render(self, **args): + return self.tpl.render(**args) + + +class CheetahTemplate(BaseTemplate): + def parse(self, template): + from Cheetah.Template import Template + self.context = threading.local() + self.context.vars = {} + self.tpl = Template(source = template, searchList=[self.context.vars]) + + def render(self, **args): + self.context.vars.update(args) + out = str(self.tpl) + self.context.vars.clear() + return out + + +class SimpleTemplate(BaseTemplate): + re_python = re.compile(r'^\s*%\s*(?:(if|elif|else|try|except|finally|for|while|with|def|class)|(include)|(end)|(.*))') + re_inline = re.compile(r'\{\{(.*?)\}\}') + dedent_keywords = ('elif', 'else', 'except', 'finally') + + def translate(self, template): + indent = 0 + strbuffer = [] + code = [] + self.subtemplates = {} + class PyStmt(str): + def __repr__(self): return 'str(' + self + ')' + def flush(allow_nobreak=False): + if len(strbuffer): + if allow_nobreak and strbuffer[-1].endswith("\\\\\n"): + strbuffer[-1]=strbuffer[-1][:-3] + code.append(" " * indent + "stdout.append(%s)" % repr(''.join(strbuffer))) + code.append((" " * indent + "\n") * len(strbuffer)) # to preserve line numbers + del strbuffer[:] + for line in template.splitlines(True): + m = self.re_python.match(line) + if m: + flush(allow_nobreak=True) + keyword, include, end, statement = m.groups() + if keyword: + if keyword in self.dedent_keywords: + indent -= 1 + code.append(" " * indent + line[m.start(1):]) + indent += 1 + elif include: + tmp = line[m.end(2):].strip().split(None, 1) + name = tmp[0] + args = tmp[1:] and tmp[1] or '' + self.subtemplates[name] = SimpleTemplate.find(name) + code.append(" " * indent + "stdout.append(_subtemplates[%s].render(%s))\n" % (repr(name), args)) + elif end: + indent -= 1 + code.append(" " * indent + '#' + line[m.start(3):]) + elif statement: + code.append(" " * indent + line[m.start(4):]) + else: + splits = self.re_inline.split(line) # text, (expr, text)* + if len(splits) == 1: + strbuffer.append(line) + else: + flush() + for i in range(1, len(splits), 2): + splits[i] = PyStmt(splits[i]) + splits = [x for x in splits if bool(x)] + code.append(" " * indent + "stdout.extend(%s)\n" % repr(splits)) + flush() + return ''.join(code) + + def parse(self, template): + code = self.translate(template) + self.co = compile(code, self.source or '