So you want to start developing a Python application do ya? Let’s assume we
are planning to use a smaller framework like Flask
which is a framework I’m really loving at the moment.
Update (6th of April 2014): In the past, I had found Flask’s in-built
development server to be a bit unstable which is why I put together this entry
but this is no longer the case. As such, I now recommend using the dev server
that comes with Flask for development purposes. However, if you prefer to use
another server instead, please read on.
In this article, I’ll show you how to setup various web servers to serve Flask
applications using WSGI which may also be suitable for production use later on.
The requirements I have for a development server are as follows:
- It must print access and error log output to stdout in realtime
- It must display stdout when the print function is used in the code (which is
really handy for debugging)
- The server should reliably reload automatically when any code is changed
- Exceptions should be printed to stderr or the browser if something goes wrong
- The web server needs to be easy to setup
- (Bonus) It would be ideal if the web server works with Jython 2.5.x too
So let’s get into it.
CherryPy
CherryPy’s web server is very well regarded and was
one of the first I looked at. My main criticism of CherryPy was its
documentation which I found extremely difficult to read through and grasp.
With a bit of digging around, I managed to get everything to work. You must
use the Paste library for logging.
Installation of CherryPy is super easy:
1
|
pip install cherrypy paste
|
And here’s the Flask script:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
#!/usr/bin/env python
from flask import Flask
import cherrypy
from paste.translogger import TransLogger
app = Flask(__name__)
app.debug = True
@app.route("/")
def hello():
return "Hello World!"
def run_server():
# Enable WSGI access logging via Paste
app_logged = TransLogger(app)
# Mount the WSGI callable object (app) on the root directory
cherrypy.tree.graft(app_logged, '/')
# Set the configuration of the web server
cherrypy.config.update({
'engine.autoreload_on': True,
'log.screen': True,
'server.socket_port': 5000,
'server.socket_host': '0.0.0.0'
})
# Start the CherryPy WSGI web server
cherrypy.engine.start()
cherrypy.engine.block()
if __name__ == "__main__":
run_server()
|
With a bit of extra effort, we can customise the access logging from Paste to
be consistent with CherryPy if desired:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
|
#!/usr/bin/env python
import time
from flask import Flask
import cherrypy
from paste.translogger import TransLogger
app = Flask(__name__)
app.debug = True
@app.route("/")
def hello():
return "Hello World!"
class FotsTransLogger(TransLogger):
def write_log(self, environ, method, req_uri, start, status, bytes):
""" We'll override the write_log function to remove the time offset so
that the output aligns nicely with CherryPy's web server logging
i.e.
[08/Jan/2013:23:50:03] ENGINE Serving on 0.0.0.0:5000
[08/Jan/2013:23:50:03] ENGINE Bus STARTED
[08/Jan/2013:23:50:45 +1100] REQUES GET 200 / (192.168.172.1) 830
becomes
[08/Jan/2013:23:50:03] ENGINE Serving on 0.0.0.0:5000
[08/Jan/2013:23:50:03] ENGINE Bus STARTED
[08/Jan/2013:23:50:45] REQUES GET 200 / (192.168.172.1) 830
"""
if bytes is None:
bytes = '-'
remote_addr = '-'
if environ.get('HTTP_X_FORWARDED_FOR'):
remote_addr = environ['HTTP_X_FORWARDED_FOR']
elif environ.get('REMOTE_ADDR'):
remote_addr = environ['REMOTE_ADDR']
d = {
'REMOTE_ADDR': remote_addr,
'REMOTE_USER': environ.get('REMOTE_USER') or '-',
'REQUEST_METHOD': method,
'REQUEST_URI': req_uri,
'HTTP_VERSION': environ.get('SERVER_PROTOCOL'),
'time': time.strftime('%d/%b/%Y:%H:%M:%S', start),
'status': status.split(None, 1)[0],
'bytes': bytes,
'HTTP_REFERER': environ.get('HTTP_REFERER', '-'),
'HTTP_USER_AGENT': environ.get('HTTP_USER_AGENT', '-'),
}
message = self.format % d
self.logger.log(self.logging_level, message)
def run_server():
# Enable custom Paste access logging
log_format = (
'[%(time)s] REQUES %(REQUEST_METHOD)s %(status)s %(REQUEST_URI)s '
'(%(REMOTE_ADDR)s) %(bytes)s'
)
app_logged = FotsTransLogger(app, format=log_format)
# Mount the WSGI callable object (app) on the root directory
cherrypy.tree.graft(app_logged, '/')
# Set the configuration of the web server
cherrypy.config.update({
'engine.autoreload_on': True,
'log.screen': True,
'server.socket_port': 5000,
'server.socket_host': '0.0.0.0'
})
# Start the CherryPy WSGI web server
cherrypy.engine.start()
cherrypy.engine.block()
if __name__ == "__main__":
run_server()
|
Gevent
http://www.gevent.org/ appears to be one of the fastest WSGI web
servers out there and provides all the features we are after too!
Installing Gevent is a bit more of a pain due to the libevent dependency:
1
2
|
sudo apt-get install build-essential python-dev libevent-dev
pip install gevent
|
And here’s the Flask script:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
#!/usr/bin/env python
from flask import Flask
import gevent.wsgi
import gevent.monkey
import werkzeug.serving
gevent.monkey.patch_all()
app = Flask(__name__)
app.debug = True
@app.route("/")
def hello():
return "Hello World!"
@werkzeug.serving.run_with_reloader
def run_server():
ws = gevent.wsgi.WSGIServer(listener=('0.0.0.0', 5000),
application=app)
ws.serve_forever()
if __name__ == "__main__":
run_server()
|
Gunicorn
Gunicorn is a production-ready web server for Python.
I must commend the designer of the site who shows that even Python-related
sites can look beautiful! Unfortunately, Gunicorn (being a server aimed at
production use) makes it a lot harder to get auto-restart capabilities as this
functionality is not natively included.
Installing Gunicorn is painless:
To make it all happen with Gunicorn, we’re going to need
supervisor and
watchdog to monitor for changes and
trigger a restart of Gunicorn.
These tools rely on several C libraries, so there’s a bit more to it than just
using pip:
1
2
|
sudo apt-get install build-essential python-dev libyaml-dev
pip install supervisor watchdog
|
The supervisor configuration is as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
[supervisord]
logfile=test.log
loglevel=debug
nodaemon=true
[program:test]
autostart=true
command=gunicorn --pid /tmp/flask-project.pid --workers 4 --log-level debug -b 0.0.0.0:5000 test:app
[program:test-reloader]
autostart=true
autorestart=false
command=watchmedo shell-command --patterns="*.py;*.html;*.css;*.js" --recursive --command='kill -HUP $(cat /tmp/flask-project.pid)'
|
The Python script stays super clean and simple which is nice:
1
2
3
4
5
6
7
8
9
10
11
|
#!/usr/bin/env python
from flask import Flask
app = Flask(__name__)
app.debug = True
@app.route("/")
def hello():
return "Hello World!"
|
We can now launch the web server using supervisor as follows:
1
|
supervisord -c test.conf
|
Overall though, I don’t see myself using Gunicorn for development purposes due
to the added complexity involved. Another point worth noting is that
print statements to stdout do not appear on the console with Gunicorn,
unlike the rest of the web servers tested here.
Rocket
Rocket is a newer pure Python WSGI web server
which is also production ready. I thought it would be worth giving it a try
too.
Installing Rocket is very simple
And here’s the Flask script:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
#!/usr/bin/env python
import logging
import sys
from flask import Flask
from rocket import Rocket
app = Flask(__name__)
app.debug = True
@app.route("/")
def hello():
return "Hello World!"
def run_server():
# Setup logging
log = logging.getLogger('Rocket')
log.setLevel(logging.INFO)
log.addHandler(logging.StreamHandler(sys.stdout))
# Set the configuration of the web server
server = Rocket(interfaces=('0.0.0.0', 5000), method='wsgi',
app_info={"wsgi_app": app})
# Start the Rocket web server
server.start()
if __name__ == "__main__":
run_server()
|
Unfortunately, Rocket (much like Gunicorn) is primarily aimed at production
deployments, so it doesn’t include an auto-restart feature.
To restart it, you may send a SIGUSR1 to the pid:
You may shutdown the process using SIGTERM:
Tornado
Tornado appears to be well respected too and has
no C dependencies.
Note: The latest Tornado 3.x has a significantly changed API and therefore
the code below will not work with it. I may look into rewriting the code below
to work with Tornado 3.x when I have a spare moment.
Installing Tornado is as easy as CherryPy:
1
|
pip install tornado==2.4
|
And here’s the Flask script:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
#!/usr/bin/env python
from flask import Flask
import tornado.wsgi
import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.autoreload
app = Flask(__name__)
app.debug = True
@app.route("/")
def hello():
return "Hello World!"
def run_server():
# Create the HTTP server
http_server = tornado.httpserver.HTTPServer(
tornado.wsgi.WSGIContainer(app)
)
http_server.listen(5000)
# Reads args given at command line (this also enables logging to stderr)
tornado.options.parse_command_line()
# Start the I/O loop with autoreload
io_loop = tornado.ioloop.IOLoop.instance()
tornado.autoreload.start(io_loop)
try:
io_loop.start()
except KeyboardInterrupt:
pass
if __name__ == "__main__":
run_server()
|
Further WSGI Debugging with Werkzeug
Flask’s Werkzeug has an awesome debugging module which you will lose access to
when not using the default web server. But don’t fear, we can add it back in!
1
2
3
4
5
6
7
8
|
...
from werkzeug.debug import DebuggedApplication
...
def run_server():
# Enable the Werkzeug Debugger
app_debug = DebuggedApplication(app, evalex=True)
...
|
Now simply ensure that you pass app_debug into sebsequent functions instead
of app as we did above.
Further WSGI Logging with wsgilog
An additional module you can plug in into your application is
wsgilog which provides further logging
options for capturing output of WSGI applications.
Installation goes something like this:
You may use it as follows:
1
2
3
4
5
6
7
8
9
|
...
import wsgilog
app = Flask(__name__)
app.debug = True
app_logged_wsgi = wsgilog.WsgiLog(app, tohtml=True, tofile='wsgi.log',
tostream=True, toprint=True)
...
|
Now when initialising the web server, pass in app_logged_wsgi instead of
app.
Final Words
To summarise, the following web servers failed to meet one or more criteria
above:
- Gunicorn: Does not display stdout via the print statement. Gunicorn is
also more work to setup for a development server compared to the rest.
- Rocket: Doesn’t include auto-restart ability, but is less troublesome to
work with in comparison to Gunicorn.
As far as Jython is concerned, I’m sorry to say that none of the web servers
worked with it. I also tried them with 2.7b1 and still no dice.