[pypy-svn] r38903 - in pypy/dist/pypy/tool/build: . test

guido at codespeak.net guido at codespeak.net
Thu Feb 15 15:56:53 CET 2007


Author: guido
Date: Thu Feb 15 15:56:52 2007
New Revision: 38903

Added:
   pypy/dist/pypy/tool/build/test/test_webserver.py
   pypy/dist/pypy/tool/build/webapp.py
   pypy/dist/pypy/tool/build/webserver.py
Log:
First bits of a small webserver thingie for the build tool to present status
pages: a BaseHTTPRequestHandler that publishes an object tree.


Added: pypy/dist/pypy/tool/build/test/test_webserver.py
==============================================================================
--- (empty file)
+++ pypy/dist/pypy/tool/build/test/test_webserver.py	Thu Feb 15 15:56:52 2007
@@ -0,0 +1,104 @@
+import py
+
+from pypy.tool.build.webserver import Handler, Resource, Collection, \
+                                      HTTPError
+
+class NonInitHandler(Handler):
+    request_version = '1.0'
+
+    def __init__(self):
+        pass
+
+    def log_request(self, code='-', size='-'):
+        pass
+
+class SomePage(Resource):
+    def handle(self, server, path, query):
+        return ('text/plain', 'foo')
+
+def build_app_structure():
+    app = Collection()
+    app.sub = Collection()
+    app.sub = Collection()
+    app.sub.index = SomePage()
+    return app
+
+class TestCollection(object):
+    def test_traverse(self):
+        app = build_app_structure()
+        assert app.traverse(['sub', 'index'], '/sub/index') is app.sub.index
+        assert app.traverse(['sub', ''], '/sub/') is app.sub.index
+        try:
+            app.traverse(['sub'], '/sub')
+        except HTTPError, e:
+            assert e.status == 301
+            assert e.data == '/sub/'
+        else:
+            py.test.fail('should have redirected')
+        # 404 errors (first -> no index)
+        py.test.raises(HTTPError, "app.traverse([''], '/')")
+        py.test.raises(HTTPError, "app.traverse(['other', ''], '/other/')")
+
+class TestResource(object):
+    pass
+
+class TestHandler(object):
+    def setup_method(self, method):
+        self.handler = NonInitHandler()
+        self.handler.wfile = self.wfile = py.std.StringIO.StringIO()
+
+    def test_process_path(self):
+        path, query = self.handler.process_path('')
+        assert path == ''
+        assert query == ''
+    
+        path, query = self.handler.process_path('/foo')
+        assert path == '/foo'
+        assert query == ''
+
+        path, query = self.handler.process_path('/foo?bar')
+        assert path == '/foo'
+        assert query == 'bar'
+
+        py.test.raises(ValueError, "self.handler.process_path('/foo?bar?baz')")
+
+    def test_find_resource(self):
+        app = build_app_structure()
+        self.handler.application = app
+        assert self.handler.find_resource('/sub/index', '') is app.sub.index
+        assert self.handler.find_resource('/sub/', '') is app.sub.index
+        try:
+            self.handler.find_resource('/sub', '')
+        except HTTPError, e:
+            assert e.status == 301
+            assert e.data == '/sub/'
+        else:
+            py.test.raises('should have raised a redirect')
+        try:
+            self.handler.find_resource('', '')
+        except HTTPError, e:
+            assert e.status == 301
+            assert e.data == '/'
+        else:
+            py.test.raises('should have raised a redirect')
+        py.test.raises(HTTPError, "self.handler.find_resource('/foo/', '')")
+
+    def test_response(self):
+        self.handler.response(200, {'Content-Type': 'text/plain'}, 'foo')
+        response = self.wfile.getvalue()
+        assert response.startswith('HTTP/1.0 200 OK')
+        assert 'Content-Type: text/plain\r\n' in response
+        assert 'Content-Length: 3\r\n' in response
+        assert response.endswith('\r\n\r\nfoo')
+
+    def test_get_response_file(self):
+        rfile = py.std.StringIO.StringIO()
+        rfile.write('foo\nbar\nbaz')
+        rfile.seek(0)
+        self.handler.response(200, {'Content-Type': 'text/plain'}, rfile)
+        response = self.wfile.getvalue()
+        assert response.endswith('\r\n\r\nfoo\nbar\nbaz')
+
+    def test_get_response_wrong_body(self):
+        py.test.raises(ValueError, "self.handler.response(200, {}, u'xxx')")
+

Added: pypy/dist/pypy/tool/build/webapp.py
==============================================================================
--- (empty file)
+++ pypy/dist/pypy/tool/build/webapp.py	Thu Feb 15 15:56:52 2007
@@ -0,0 +1,35 @@
+""" a web server that displays status info of the meta server and builds """
+
+from pypy.tool.build.webserver import HTTPError, Resource, Collection, Handler
+
+class IndexPage(Resource):
+    """ the index page """
+    def handle(self, handler, path, query):
+        return {'Content-Type': 'text/html'}, """\
+<html>
+  <head>
+    <title>Build meta server web interface (temp index page)</title>
+  </head>
+  <body>
+    <a href="/serverstatus">server status</a>
+  </body>
+</html>
+"""
+
+class ServerStatus(Resource):
+    """ a page displaying overall meta server statistics """
+    def handle(self, handler, path, query):
+        return {'Content-Type': 'text/plain'}, 'foo'
+
+class Application(Collection):
+    """ the application root """
+    index = IndexPage()
+    serverstatus = ServerStatus()
+
+class AppHandler(Handler):
+    application = Application() # shared by all instances!
+
+if __name__ == '__main__':
+    from pypy.tool.build.webserver import run_server
+    run_server(('', 8080), AppHandler)
+

Added: pypy/dist/pypy/tool/build/webserver.py
==============================================================================
--- (empty file)
+++ pypy/dist/pypy/tool/build/webserver.py	Thu Feb 15 15:56:52 2007
@@ -0,0 +1,175 @@
+from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
+
+# some generic stuff to make working with BaseHTTPServer a bit nicer...
+
+# XXX note that this has some overlap with pypy/translator/js/lib/server.py,
+# and should perhaps at some point be merged... (the main reason why I started
+# this instead of using server.py is because the latter is mostly geared
+# towards some tricks (transparent AJAX), and doesn't utilize or abstract the
+# HTTP support in a very clean way, hopefully I can find a model here to
+# improve server.py)
+
+# XXX this needs to be built using httplib.responses in Python > 2.5 :(
+HTTP_STATUS_MESSAGES = {
+    200: 'OK',
+    204: 'No Content',
+    301: 'Moved permanently',
+    302: 'Found',
+    304: 'Not modified',
+    401: 'Unauthorized',
+    403: 'Forbidden',
+    404: 'Not found',
+    500: 'Server error',
+    501: 'Not implemented',
+}
+
+class HTTPError(Exception):
+    """ raised on HTTP errors """
+    def __init__(self, status, data=None):
+        self.status = status
+        self.message = HTTP_STATUS_MESSAGES[status]
+        self.data = data
+
+    def __str__(self):
+        data = ''
+        if self.data:
+            data = ' (%s)' % (self.data,)
+        return '<HTTPException %s "%s"%s>' % (self.status, self.message, data)
+
+class Resource(object):
+    """ an HTTP resource
+    
+        essentially this is an object with a path that does not end on a slash,
+        and no support for PATH_INFO
+    """
+
+    def handle(self, handler, path, query):
+        """ handle a single request to self 
+        
+            returns a tuple (content_type, data) where data is either a string
+            (non-unicode!) or a file-like object with a read() method that
+            accepts an integer (size) argument
+        """
+        raise NotImplemented('abstract base class')
+
+class Collection(Resource):
+    """ an HTTP collection
+    
+        essentially this is a container object that has a path that ends on a
+        slash, and support for PATH_INFO (so can have (virtual or not)
+        children)
+    """
+
+    def traverse(self, path, orgpath):
+        """ traverse path relative to self
+
+            'path' is the path requested by the client, split on '/', but
+            relative from the current object: parent Collection items may have
+            removed items (they will have, actually, unless 'self' is the root
+            of the website) from the beginning on traversal to 'self'
+
+            path is split on '/', the first item is removed and used to do
+            a lookup on self, if that fails a 404 is raised, if successful
+            the item is used to continue traversal (if the object found is
+            a Collection type) or to handle the request (if the object found
+            is a Resource type)
+
+            if path equals '', a lookup for 'index' is done
+        """
+        name = path.pop(0)
+        if name == '':
+            name = 'index'
+        resource = getattr(self, name, None)
+        if resource is None or not isinstance(resource, Resource):
+            raise HTTPError(404)
+        if path:
+            if not isinstance(resource, Collection):
+                raise HTTPError(500) # no PATH_INFO allowed for non-Collection
+            return resource.traverse(path, orgpath)
+        else:
+            if isinstance(resource, Collection):
+                # targeting a collection directly: redirect to its 'index'
+                raise HTTPError(301, orgpath + '/')
+            return resource
+
+class Handler(BaseHTTPRequestHandler):
+    application = None # attach web root (Collection object) here!!
+    bufsize = 1024
+    
+    def do_GET(self, send_body=True):
+        """ perform a request """
+        path, query = self.process_path(self.path)
+        try:
+            resource = self.find_resource(path, query)
+            headers, data = resource.handle(self, path, query)
+        except HTTPError, e:
+            status = e.status
+            headers, data = self.process_http_error(e)
+        else:
+            status = 200
+            if not 'content-type' in [k.lower() for k in headers]:
+                headers['Content-Type'] = 'text/html; charset=UTF-8'
+        self.response(status, headers, data)
+
+    do_POST = do_GET
+
+    def do_HEAD(self):
+        return self.do_GET(False)
+
+    def process_path(self, path):
+        """ split the path in a path and a query part
+
+            returns a tuple (path, query), where path is a string and
+            query a dictionary containing the GET vars (URL decoded and such)
+        """
+        path = path.split('?')
+        if len(path) > 2:
+            raise ValueError('illegal path %s' % (path,))
+        p = path[0]
+        q = len(path) > 1 and path[1] or ''
+        return p, q
+
+    def find_resource(self, path, query):
+        """ find the resource for a given path
+        """
+        if not path:
+            raise HTTPError(301, '/')
+        assert path.startswith('/')
+        chunks = path.split('/')
+        chunks.pop(0) # empty item
+        return self.application.traverse(chunks, path)
+
+    def process_http_error(self, e):
+        headers = {'Content-Type': 'text/plain'} # XXX need more headers here?
+        if e.status in [301, 302]:
+            headers['Location'] = e.data
+            self.response(e.status, e.message, headers,
+                          'Redirecting to %s' % (e.data,))
+        else:
+            self.response(e.status, e.message, headers,
+                          'Error: %s (%s)' % (e.status, e.message))
+    
+    def response(self, status, headers, body):
+        self.send_response(status)
+        if (isinstance(body, str) and
+                not 'content-length' in [k.lower() for k in headers]):
+            headers['Content-Length'] = len(body)
+        for keyword, value in headers.iteritems():
+            self.send_header(keyword, value)
+        self.end_headers()
+        if isinstance(body, str):
+            self.wfile.write(body)
+        elif hasattr(body, 'read'):
+            while 1:
+                data = body.read(self.bufsize)
+                if data == '':
+                    break
+                self.wfile.write(data)
+        else:
+            raise ValueError('body is not a plain string or file-like object')
+
+def run_server(address, handler):
+    server = HTTPServer(address, handler)
+    server.serve_forever()
+
+



More information about the Pypy-commit mailing list