# HG changeset patch # User Tero Marttila # Date 1292677787 -7200 # Node ID b28a1681e79b8a70226ee214fe26b766cf86e2bc Initial layout, with hello-world diff -r 000000000000 -r b28a1681e79b .hgignore --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.hgignore Sat Dec 18 15:09:47 2010 +0200 @@ -0,0 +1,8 @@ +syntax: regexp + +# tempfiles +\.pyc$ +\.sw[op]$ + +# random env stuff +^lib diff -r 000000000000 -r b28a1681e79b bin/wsgi-dev.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/wsgi-dev.py Sat Dec 18 15:09:47 2010 +0200 @@ -0,0 +1,51 @@ +#!/usr/bin/env python +""" + Simple test server/environment for WSGI development +""" + +import werkzeug + +# app import +from svv import wsgi + +import optparse, logging + + +if __name__ == '__main__' : + parser = optparse.OptionParser() + parser.add_option('-q', '--quiet', action='store_true', help='More output') + parser.add_option('-v', '--verbose', action='store_true', help='More output') + parser.add_option('-p', '--port', type='int', help='Local port to run on', default=8080, metavar='PORT') + parser.add_option('-B', '--bind', help="Local address to listen on", default='localhost', metavar='HOST') + + (options, args) = parser.parse_args() + + if options.quiet : + level = logging.WARN + + elif options.verbose : + level = logging.DEBUG + + else : + # default + level = logging.INFO + + bind = options.bind + port = options.port + + assert not args + + logging.basicConfig(format="[%(levelname)5s] %(funcName)25s : %(message)s", level=level) + + app = wsgi.WSGIApp( + # params + ) + + # run + werkzeug.run_simple(bind, port, app, use_reloader=True, use_debugger=True, + static_files = { + # static resources mounted off app /static + '/static': 'static/', + }, + ) + diff -r 000000000000 -r b28a1681e79b static/layout.css --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/static/layout.css Sat Dec 18 15:09:47 2010 +0200 @@ -0,0 +1,127 @@ +/* + * Structural layout, i.e. positioning of layout items + */ + +body +{ + padding: 0px; + margin: 0px; +} + +/* Full-width decorative header at top */ +div#header +{ + padding: 40px; + + text-align: left; + + font-size: 300%; + font-weight: bold; + + border-bottom: 1px dotted #aaa; +} + +/* + * Container for horizontal layout + * XXX: for equal-height columns, I believe; see #menu + */ +div#container +{ + overflow: hidden; +} + +/* Left column in content layout, below header */ +div#menu +{ + float: left; + + /* Evil? */ + padding-bottom: 2000px; + margin-bottom: -2000px; + + border-right: 1px dotted #aaa; +} + +div#menu ul +{ + margin: 0px; + padding: 0px; + + list-style-type: none; + + width: 180px; +} + +div#menu li a +{ + display: block; + + padding: 10px; + padding-left: 20px; +} + +/* +div#menu li a:hover +{ + background-color: #ddd; + text-decoration: none; +} +*/ + +/* Content-width compact navigation menu below header and next to menu*/ +div#nav +{ + margin-left: 180px; + + padding: 0px; + + border-bottom: 1px dotted #aaa; +} + +div#nav ul +{ + list-style-type: none; + white-space: nowrap; + + padding: 5px 0px; + margin: 0px; +} + +div#nav li +{ + display: inline; + + padding: 0px; + + border-left: 1px dotted #aaa; +} + +div#nav li:first-child +{ + border-left: none; +} + +div#nav li a +{ + padding: 5px 10px; + +} + +/* Main content view */ +div#content +{ + margin-left: 180px; +} + +/* Full-width Footer at bottom of page */ +div#footer +{ + padding: 10px; + + border-top: 1px dotted #aaa; + + font-size: x-small; + font-style: italic; + + text-align: center; +} diff -r 000000000000 -r b28a1681e79b static/style.css --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/static/style.css Sat Dec 18 15:09:47 2010 +0200 @@ -0,0 +1,42 @@ +/* + * General element styles + */ + +/* Links */ +a { + color: black; + text-decoration: none; + font-weight: bold; +} + +a:hover { + text-decoration: underline; +} + +/* Page heading */ +h1 { + font-size: xx-large; + + text-align: center; +} + +/* Section heading */ +h2 { + font-size: large; + + margin-left: 0px; + padding: 5px; + width: 100%; + + background-color: #e5e5e5; + + border: 1px dashed #c5c5c5; +} + +/* Paragraph/list/etc. heading */ +h3 { + font-size: medium; + font-style: italic; +} + + diff -r 000000000000 -r b28a1681e79b svv/html.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/svv/html.py Sat Dec 18 15:09:47 2010 +0200 @@ -0,0 +1,414 @@ +""" + Generating XHTML output from nested python objects + + XXX: use a 'real' XML builder? + + To use: + + >>> from html import tags + >>> str(tags.a(href="http://www.google.com")("Google !")) + '\\n\\tGoogle <this>!\\n\\n' +""" + +from cgi import escape +import itertools as _itertools, types as _types + +class IRenderable (object) : + """ + Something that's renderable as the contents of a HTML tag. + + This is just used by Container for rendering Tags as actual HTML, vs just plain strs as escaped data. + + Additionally, some str vs unicode vs file stuff.. + """ + + def render_raw_lines (self, indent=u'\t') : + """ + Render the indented lines for tag and contents, without newlines + """ + + abstract + + def render_lines (self, indent=u'\t', newline=u'\n') : + """ + Render full output lines with given newlines + + >>> list(Tag('xx', 'yy').render_lines()) + [u'\\n', u'\\tyy\\n', u'\\n'] + """ + + for line in self.render_raw_lines(indent=indent) : + yield line + newline + + def render_unicode (self, **render_opts) : + """ + Render full tag as a single unicode string + + >>> Tag('xx', 'yy').render_unicode() + u'\\n\\tyy\\n\\n' + """ + + return "".join(self.render_lines(**render_opts)) + + def render_str (self, encoding='ascii', **render_opts) : + """ + Render full tag as an encoded string + + >>> Tag('xx', 'yy').render_str() + '\\n\\tyy\\n\\n' + """ + + return self.render_unicode(**render_opts).encode(encoding) + + def render_out (self, stream, encoding=None, **render_opts) : + """ + Render output into the given stream, encoding using the given encoding if given. + + >>> from StringIO import StringIO; buf = StringIO(); Tag('xx', 'yy').render_out(buf, 'ascii'); buf.getvalue() + '\\n\\tyy\\n\\n' + """ + + for line in self.render_lines(**render_opts) : + if encoding : + line = line.encode(encoding) + + stream.write(line) + + def render_file (self, file, encoding=None, **render_opts) : + """ + Render output to given file, overwriteing anything already there + """ + + self.render_out(file.open_write(encoding), **render_opts) + + # default output + __str__ = render_str + __unicode__ = render_unicode + + # default .render method + render = render_unicode + +class Container (IRenderable) : + """ + A container holds a sequence of other renderable items. + + This is just used as the superclass for Tag, and just serves to gives us useful handling for e.g. generators as + tag contents (iterate through them instead of repr'ing them). + """ + + @classmethod + def process_contents (cls, contents) : + """ + Postprocess contents iterable to return new list. + + Items that are None will be omitted from the return value. + + Certain core sequence types will be recognized and flattened for output: tuples, lists, and generators. + + >>> list(Container.process_contents([])) + [] + >>> list(Container.process_contents([None])) + [] + >>> list(Container.process_contents([u'foo'])) + [u'foo'] + """ + + for content in contents : + if content is None : + continue + + # Hardcoded list of special-case nested contents + elif isinstance(content, (_types.TupleType, _types.ListType, _types.GeneratorType)) : + for subcontent in cls.process_contents(content) : + yield subcontent + + else : + # normal, handle as IRenderable/unicode data + yield content + + def __init__ (self, *contents) : + """ + Construct this container with the given sub-items + """ + + # store postprocessed + self.contents = list(self.process_contents(contents)) + + def render_raw_lines (self, **render_opts) : + """ + Render our contents as a series of non-indented lines, with the contents handling indentation themselves. + + >>> list(Container(5).render_raw_lines()) + [u'5'] + >>> list(Container('line1', 'line2').render_raw_lines()) + [u'line1', u'line2'] + >>> list(Container('a', Tag('b', 'bb'), 'c').render_raw_lines()) + [u'a', u'', u'\\tbb', u'', u'c'] + >>> list(Container(Tag('hr'), Tag('foo')('bar')).render_raw_lines()) + [u'
', u'', u'\\tbar', u''] + """ + + for content in self.contents : + if isinstance(content, IRenderable) : + # sub-items + for line in content.render_raw_lines(**render_opts) : + yield line + + else : + # escape raw values + yield escape(unicode(content)) + + def __repr__ (self) : + return 'Container(%s)' % ', '.join(repr(c) for c in self.contents) + +class Tag (Container) : + """ + A HTML tag, with attributes and contents, which can a mixture of data and other renderables(tags). + + This is the core object, and the ~only one you really need to pay attention to. + + Provides various kinds of rendering output via IRenderable. + """ + + @staticmethod + def process_attrs (attrs) : + """ + Postprocess attributes. + + Key-value pairs where the value is None will be ommitted, and any trailing underscores in the key omitted. + + TODO: only remove one underscore + + >>> dict(Tag.process_attrs(dict())) + {} + >>> dict(Tag.process_attrs(dict(foo='bar'))) + {'foo': 'bar'} + >>> dict(Tag.process_attrs(dict(class_='bar', frob=None))) + {'class': 'bar'} + """ + + return ((k.rstrip('_'), v) for k, v in attrs.iteritems() if v is not None) + + def __init__ (self, name, *contents, **attrs) : + """ + Construct tag with given name/attributes or contents. + + The filtering rules desribed in process_contents/process_attrs apply. + + >>> Tag('foo') + Tag('foo') + >>> Tag('foo', 'quux') + Tag('foo', 'quux') + >>> Tag('foo', 'quux', bar=5) + Tag('foo', 'quux', bar=5) + >>> Tag('foo', class_='ten') + Tag('foo', class='ten') + """ + + # store contents as container + super(Tag, self).__init__(*contents) + + # store postprocessed stuff + self.name = name + self.attrs = dict(self.process_attrs(attrs)) + + def __call__ (self, *contents, **attrs) : + """ + Return a new Tag with this tag's attributes and contents, as well as the given attributes/contents. + + The filtering rules desribed in process_contents/process_attrs apply. + + >>> Tag('foo')('bar') + Tag('foo', 'bar') + >>> Tag('a', href='index.html')("Home") + Tag('a', 'Home', href='index.html') + >>> Tag('bar', None)(5, foo=None, class_='bar') + Tag('bar', 5, class='bar') + >>> Tag('a')('b')('c')(asdf=5) + Tag('a', 'b', 'c', asdf=5) + >>> t1 = Tag('a'); t2 = t1('b'); t1 + Tag('a') + """ + + # merge attrs/contents + # XXX: new_attrs is not an iterator... + new_attrs = dict(_itertools.chain(self.attrs.iteritems(), attrs.iteritems())) + new_contents = _itertools.chain(self.contents, contents) + + # build new tag + return Tag(self.name, *new_contents, **new_attrs) + + @staticmethod + def format_attr (name, value) : + """ + Format a single HTML tag attribute + + >>> Tag.format_attr('name', 'value') + u'name="value"' + >>> Tag.format_attr('this', '') + u'this="<a"b>"' + >>> Tag.format_attr('xx', 1337) + u'xx="1337"' + """ + + return u'%s="%s"' % (name, escape(unicode(value), True)) + + def render_attrs (self) : + """ + Return the HTML attributes string + + >>> Tag('x', foo=5, bar='<').render_attrs() + u'foo="5" bar="<"' + """ + + return " ".join(self.format_attr(n, v) for n, v in self.attrs.iteritems()) + + def render_raw_lines (self, indent=u'\t') : + """ + Render the tag and indented content + + >>> list(Tag('xx', 'yy', zz='foo').render_raw_lines(indent=' ')) + [u'', u' yy', u''] + """ + + # render attr string, including preceding space + attrs_stuff = (" " + self.render_attrs()) if self.attrs else "" + + if self.contents : + # wrapping tags + yield u"<%s%s>" % (self.name, attrs_stuff) + + # subcontents + for line in super(Tag, self).render_raw_lines(indent=indent) : + yield indent + line + + yield u"" % (self.name, ) + + else : + # singleton tag + yield u"<%s%s />" % (self.name, attrs_stuff) + + + def __repr__ (self) : + return 'Tag(%s)' % ', '.join( + [ + repr(self.name) + ] + [ + repr(c) for c in self.contents + ] + [ + '%s=%r' % (name, value) for name, value in self.attrs.iteritems() + ] + ) + +class Text (IRenderable) : + """ + Raw HTML text + """ + + def __init__ (self, line) : + """ + Initialize to render as the given lines + """ + + self.lines = [line] + + def render_raw_lines (self, indent=u'\t') : + return self.lines + +class Document (IRenderable) : + """ + A full XHTML document with XML header, doctype, head and body. + + XXX: current rendering is a bit of a kludge + + + + + + + ... + + + ... + + + """ + + def __init__ (self, + head, body, + xml_version='1.0', xml_encoding='utf-8', + doctype='html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"', + html_xmlns='http://www.w3.org/1999/xhtml', html_lang='en' + ) : + # store + self.xml_version = xml_version + self.xml_encoding = xml_encoding + self.doctype = doctype + + # build the document + self.document = Tag('html', **{'xmlns': html_xmlns, 'xml:lang': html_lang})( + Tag('head', head), + Tag('body', body), + ) + + def render_raw_lines (self, **render_opts) : + """ + Render the two header lines, and then the document + """ + + yield '' % (self.xml_version, self.xml_encoding) + yield '' % (self.doctype) + + for line in self.document.render_raw_lines(**render_opts) : + yield line + + def _check_encoding (self, encoding) : + if encoding and encoding != self.xml_encoding : + raise ValueError("encoding mismatch: %r should be %r" % (encoding, self.xml_encoding)) + + def render_str (self, encoding=None, **render_opts) : + """ + Wrap render_str to verify that the right encoding is used + """ + + self._check_encoding(encoding) + + return super(XHTMLDocument, self).render_str(self.xml_encoding, **render_opts) + + def render_out (self, stream, encoding=None, **render_opts) : + """ + Wrap render_out to verify that the right encoding is used + """ + + self._check_encoding(encoding) + + return super(XHTMLDocument, self).render_out(stream, self.xml_encoding, **render_opts) + +class TagFactory (object) : + """ + Build Tags with names give as attribute names + """ + + def __getattr__ (self, name) : + """ + Get a Tag object with the given name, but no contents + + >>> TagFactory().a(href='bar')('quux') + Tag('a', 'quux', href='bar') + """ + + return Tag(name) + +# pretty names +container = Container +tag = Tag +tags = TagFactory() +raw = Text +document = Document + +# testing +if __name__ == '__main__' : + import doctest + + doctest.testmod() + diff -r 000000000000 -r b28a1681e79b svv/wsgi.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/svv/wsgi.py Sat Dec 18 15:09:47 2010 +0200 @@ -0,0 +1,134 @@ +import werkzeug +from werkzeug import exceptions +from werkzeug import Request, Response +from werkzeug.routing import Map, Rule + +import logging + +# logging +log = logging.getLogger('svv.wsgi') + +class AppHandler (object): + """ + Per-request handler context + """ + + # default content type for response + CONTENT_TYPE = 'text/html' + + def __init__ (self, request) : + """ + Initialize for processing the given Request, to prepare for action-method later on + """ + + self.request = request + + def respond (self, url_values) : + """ + Handle request that was mapped to ourselves via the URL routing, using given dict of values from URL. + """ + + # render + html = unicode(self.render(**url_values)) + + # XXX: unicode + return Response(html, mimetype='text/html') + + def render (self, **args) : + """ + Handling a GET request, returning the proper response HTML. + """ + + raise NotImplementedError() + +import html +from html import tags + +class Index (AppHandler) : + def render (self) : + title = "Index" + css = ["/static/layout.css", "/static/style.css"] + + head = ( + tags.title(title), + (tags.link(rel='Stylesheet', type="text/css", href=src) for src in css), + ) + + header = ("Foo List") + nav = [tags.ul(tags.li(tags.a(href='#')(name)) for name in ['Nav A', 'Nav B', 'Nav C'])] + menu = [tags.ul(tags.li(tags.a(href='#')(name)) for name in ['Menu A', 'Menu B', 'Menu C'])] + footer = ("Copyright?") + + content = ( + tags.ul(tags.li("Item #%d" % i) for i in xrange(10)) + ) + + layout = ( + tags.div(id='header')(header), + tags.div(id='container')( + tags.div(id='menu')(menu), + tags.div(id='nav')(nav), + tags.div(id='content')(content), + ), + tags.div(id='footer')(footer), + ) + + return html.document(head, layout) + +class WSGIApp (object) : + """ + Top-level WSGI handler impl + """ + + # URL paths + # map to AppHandler-endpoint + URLS = Map(( + Rule('/', endpoint=Index), + )) + + def __init__ (self) : + pass + + # wrap to use werkzeug's Request/Response + @Request.application + def __call__ (self, req) : + """ + Main WSGI entry point, error handling + """ + + try : + # wrapped handler + response = self.request(req) + + except exceptions.HTTPException, e : + # format properly as response, also includes redirects + return e.get_response(req.environ) + + # XXX: except Exception, e : + # XXX: we want to trap errors in prod, but not in dev? + + else : + # a-ok + return response + + def request (self, req) : + """ + Wrapped request handler, URL mapping + """ + + # map URLs against this request + urls = self.URLS.bind_to_environ(req) + + # lookup matching URL for handler type and matched values from URL + url_handler, url_values = urls.match() + + # the per-request handler (from endpoint) + req_handler = url_handler(req) + + # XXX: per-method thing? + response = req_handler.respond(url_values) + + # ok + return response + +