A more useful command-line wsgiref.simple_server?

Masklinn <[email protected]> Thu, 29 Mar 2012 12:02:46 +0200
Newsgroups gmane.comp.python.web
Message-ID <[email protected]>
Moving here as suggested by Terry Reedy as this list may be more
interested than -ideas (note: some feedback already used to revise
the original proposal, and a very basic patch — with no tests — is
provided for the current CPython default branch)

Currently, calling wsgiref.simple_server simply mounts the (bundled)
demo app.

I think that's a bit of a lost opportunity: the community seems to have
mostly standardized on a wsgi script providing an application callable
in its global namespace (though details may differ, mod_wsgi does not
care for the script's name and mandates an `application` callable while
e.g. gunicorn wants a Python module and the callable name must be
configured), and it would be nice if simple_server could take such a
script and mount the application provided:

* This would allow testing that the script has no error without having
  to go through mounting it in e.g. mod_wsgi
* It would make trivial/test applications (e.g. dynamic responders to
  local JS) simpler to bootstrap as there would be no need for the
  half-dozen lines of wsgiref.simple_server bootstrapping and "hard"
  dependency on wsgiref,

   import wsgiref.simple_server

   def application(environ, start_response):
       'code'

   if __name__ == '__main__':
       httpd = make_server('', 8000, application)
       httpd.serve_forever()

 could become:

   def application(environ, start_response):
       'code'

Since wsgiref already supports `python -mwsgiref.simple_server`, the
changes would be pretty simple:

* an optional positional argument of the form `script[:app]`, the script
  is exec'd, the application (called "application" by default) is
  extracted and then mounted in simple_server. If no script is specified,
  just mount `demo_app` as before
* Add -H/--host -p/--port options to, respectively, the hostname and the
  port to bind the server to.
* The current -msimple_server uses `handle_request` and only replies once,
  to increase the usability of the CLI tool use `serve_forever` *when and
  only when the mounted application is not demo_app*. It also avoids
  opening a hardcoded example URL on launch.

This way the current sanity test/"PHPInfo" demo app works as it did before,
but it becomes possible to very easily serve a WSGI script with almost no
overhead in the script itself.

Attachment: patch performing the above-specified alterations, using
argparse for arguments parsing and generation of help.

_______________________________________________
Web-SIG mailing list
[email protected]
Web SIG: http://www.python.org/sigs/web-sig
Unsubscribe: http://mail.python.org/mailman/options/web-sig/gcpw-web-sig%40m.gmane.org
simple_server.patch (application/octet-stream, 1.8 KB)
diff --git a/Lib/wsgiref/simple_server.py b/Lib/wsgiref/simple_server.py
--- a/Lib/wsgiref/simple_server.py
+++ b/Lib/wsgiref/simple_server.py
@@ -15,6 +15,8 @@ import sys
 import urllib.parse
 from wsgiref.handlers import SimpleHandler
 
+import argparse
+
 __version__ = "0.2"
 __all__ = ['WSGIServer', 'WSGIRequestHandler', 'demo_app', 'make_server']
 
@@ -146,11 +148,36 @@ def make_server(
     server.set_app(app)
     return server
 
+def app_script(filename):
+    app_variable = 'application'
+    if ':' in filename:
+        filename, app_variable = filename.split(':')
+
+    variables = {}
+    exec(compile(open(filename).read(), filename, 'exec'), variables)
+
+    return variables[app_variable]
+
+parser = argparse.ArgumentParser(
+    description="Mount and serve a WSGI application")
+parser.add_argument('script',
+    type=app_script, default=demo_app, nargs='?',
+    metavar="script[:app]",
+    help="WSGI script to load the application from, loads demo_app if no script "
+         "is provided. By default, tries mounting the 'application' variable "
+         "from the script.")
+parser.add_argument('-H', '--host', help="Host to listen on",
+                    default='')
+parser.add_argument('-p', '--port', type=int, default=8000,
+                    help="Port to listen on (defaults to %(default)d)")
 
 if __name__ == '__main__':
-    httpd = make_server('', 8000, demo_app)
+    args = parser.parse_args()
+    httpd = make_server(args.host, args.port, args.script)
     sa = httpd.socket.getsockname()
     print("Serving HTTP on", sa[0], "port", sa[1], "...")
+    if args.script is not demo_app:
+        httpd.serve_forever()
     import webbrowser
     webbrowser.open('http://localhost:8000/xyz?abc')
     httpd.handle_request()  # serve one request, then exit