Initial layout, with hello-world
authorTero Marttila <terom@fixme.fi>
Sat, 18 Dec 2010 15:09:47 +0200
changeset 0 b28a1681e79b
child 1 06451697083a
Initial layout, with hello-world
.hgignore
bin/wsgi-dev.py
setup.py
static/layout.css
static/style.css
svv/__init__.py
svv/html.py
svv/wsgi.py
--- /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
--- /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/',
+            },
+    )
+
--- /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;
+}
--- /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;
+}
+
+
--- /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 <this>!"))
+    '<a href="http://www.google.com">\\n\\tGoogle &lt;this&gt;!\\n</a>\\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'<xx>\\n', u'\\tyy\\n', u'</xx>\\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'<xx>\\n\\tyy\\n</xx>\\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()
+            '<xx>\\n\\tyy\\n</xx>\\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()
+            '<xx>\\n\\tyy\\n</xx>\\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'<b>', u'\\tbb', u'</b>', u'c']
+            >>> list(Container(Tag('hr'), Tag('foo')('bar')).render_raw_lines())
+            [u'<hr />', u'<foo>', u'\\tbar', u'</foo>']
+        """
+
+        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', '<a"b>')
+            u'this="&lt;a&quot;b&gt;"'
+            >>> 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="&lt;"'
+        """
+
+        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'<xx zz="foo">', u' yy', u'</xx>']
+        """
+
+        # 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"</%s>" % (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
+
+        <?xml version="..." encoding="..." ?>
+        <!DOCTYPE ...>
+        
+        <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
+            <head>
+                ...
+            </head>
+            <body>
+                ...
+            </body>
+        </html>
+    """
+
+    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 '<?xml version="%s" encoding="%s" ?>' % (self.xml_version, self.xml_encoding)
+        yield '<!DOCTYPE %s>' % (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()
+
--- /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
+
+