Welcome to my Sanic Web Tutorial where you will learn to use the async web framework Sanic to create and deploy your own website.
Assumptions
I assume that you are familiar with using Python and virtualenv, but you do not need to be an expert. You should be able to use this tutorial even if you are new to web development.
I'm developing on a Ubuntu 19.10 machine running Python 3.8, so if you are on a different OS, you might need to make minor changes.
Hello World with Sanic
Start by creating a folder for your new web application and in there create a virtualenv and install Sanic with pip:
$ mkdir sanic_web_tutorial
$ virtualenv venv -p python3
$ source venv/bin/activate
$ pip install sanic
Now we will create our main application file and put some boilerplate Python code in there.
$ touch main.py
Open main.py in an editor and enter this:
from sanic import Sanic
from sanic import response
app = Sanic(name='My Sanic Web App')
@app.route("/")
async def index(request):
return response.text("Hello World")
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
Now, start the web application by entering:
$ python main.py
[2020-02-07 23:35:35 +0800] [6539] [INFO] Goin' Fast @ http://0.0.0.0:8000
[2020-02-07 23:35:35 +0800] [6539] [INFO] Starting worker [6539]
Go to http://0.0.0.0:8000 in your browser to see you web application in action with the text "Hello World" returned.
What goes on in "Hello World"
Lets dig into this tiny Hello World application to understand what exactly goes on.
First, we import two things from the sanic framework:
Sanic: The web application class and
response: which will help us create something that the application can return to the user.
Next, we instantiate the application with app = Sanic(name='My Sanic Web App')
. We must
provide a 'name' otherwise we will get a warning (or the instantiation will fail).
With @app.route("/")
we define a HTTP endpoint, in this case the /
. Once our browser hits this endpoint, the
function that is defined below it will be executed:
async def index(request):
return response.text("Hello World")
This defines an async function called 'index'. We could make this example even more simple by removing the 'async' keyword and simply have:
async def index(request):
return response.text("Hello World")
then our endpoint wouldn't be syncronious though, not async.
The @app.route
decorater for the app.add_route
method, and instead of using the decorater, we could have just
used this method like this:
async def index(request):
return response.html("<h1>Hello World</h1>")
app.add_route(index, "/")
One advantage of using the add_route
method instead of the decorator is that you can add multiple routes to
the same function. Say, we want both /
and /main
to return the same page, we can do this with 2 add_route()
calls:
async def index(request):
return response.html("<h1>Hello World</h1>")
app.add_route(index, "/")
app.add_route(index, "/main")
No automatic server restart on changes
Be mindful, that the running python main.py doesn't restart automatically when you make changes in the code like you
might be used to from Flask or Django. You'll have to re-run python main.py yourself.
In the index
function we are simply returning a text response with the value "Hello World". If we wanted to return
html instead of text, we could have written the function as
@app.route("/")
async def index(request):
return response.html("<h1>Hello World</h1>")
The only difference between these two response types is the Content-Type
, where for text it is text/plain
while for the
html response it is text/html
.
There are other repsonse types available as well, like response.json
and response.file
, but we will get back to them in time.
Finally, we instantiate the application with
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
This is similar to how Flask works (if you are familiar with Flask). We specify the host and port and with __name__
we can
run it with python main.py
.
Testing
We could write unittests using Python's unittest module, but we will use pytest (and pytest-sanic) instead as this allows us to write shorter and nicer tests. Additionally, pytest enables more advanced testing options going forward.
First, install pytest-sanic into the virtualenv and create a test file:
$ pip install pytest-sanic
$ touch test.py
In test.py, write a simple test to check that the application returns a 200 on the router /
:
from main import app
def test__index__get_request__returns_200():
request, response = app.test_client.get('/')
assert response.status == 200
run the test by typing
$ pytest test.py
================================================ test session starts ================================================
platform linux -- Python 3.7.3, pytest-5.3.5, py-1.8.1, pluggy-0.13.1
rootdir: /sanic_web_tutorial
plugins: sanic-1.1.2
collected 1 item
test.py . [100%]
================================================= warnings summary ==================================================
test.py::test__index__get_request__returns_200
/sanic_web_tutorial/venv/lib/python3.7/site-packages/httpx/client.py:234: UserWarning: Passing a 'verify' argument when making a request on a client is due to be deprecated. Instantiate a new client instead, passing any 'verify' arguments to the client itself.
"Passing a 'verify' argument when making a request on a client "
-- Docs: https://docs.pytest.org/en/latest/warnings.html
=========================================== 1 passed, 1 warning in 0.02s ============================================
Ignore the warning for now. The test passes.
Save
Finally, before closing everything, do a
$ pip freeze > requirements.txt
to create a reusable requirements file. If you are on a Ubuntu system, you might have to manually open the just created
requirements.txt file and delete the line pkg-resources==0.0.0
. On my system (Ubuntu), this line is wrongly created when
running pip freeze (bug in Ubuntu). If you don't delete it, then you will get an error the next time you try and install
the requirements with pip install -r requirements.txt
.