Version 21.3

Table of Contents

Introduction#

Sanic is now faster.

Well, it already was fast. But with the first iteration of the v21 release, we incorporated a few major milestones that have made some tangible improvements. These encompass some ideas that have been in the works for years, and have finally made it into the released version.

Breaking changes

Version 21.3 introduces a lot of new features. But, it also includes some breaking changes. This is why these changes were introduced after the last LTS. If you rely upon something that has been removed, you should continue to use v20.12LTS until you are able to upgrade.

pip install "sanic>=20.12,<20.13"
pip freeze > requirements.txt

For most typical installations, you should be able to upgrade without a problem.

What to know#

Notable new or breaking features, and what to upgrade...

Python 3.7+ Only#

This version drops Python 3.6 support. Version 20.12LTS will continue to support Python 3.6 until its EOL in December, 2022, and version 19.12LTS will support it until its EOL in December, 2021.

Read more about our LTS policy.

Streaming as first class citizen#

The biggest speed improvement came from unifying the request/response cycle into a single flow. Previously, there was a difference between regular cycles, and streaming cycles. This has been simplified under the hood, even though the API is staying the same right now for compatibility. The net benefit is that all requests now should see a new benefit.

Read more about streaming changes.

Router overhaul#

The old Sanic router was based upon regular expressions. In addition it suffered from a number of quirks that made it hard to modify at run time, and resulted in some performance issues. This change has been years in the making and now converts the router to a compiled tree at startup. Look for additional improvements throughout the year.

The outward facing API has kept backwards compatibility. However, if you were accessing anything inside the router specifically, you many notice some changes. For example:

  1. Router.get() has a new return value
  2. Route is now a proper class object and not a namedtuple
  3. If building the router manually, you will need to call Router.finalize() before it is usable
  4. There is a new <date:ymd> pattern that can be matched in your routes
  5. You cannot startup an application without at least one route defined

The router is now located in its own repository: sanic-org/sanic-router and is also its own standalone package on PyPI.

Signals API ⭐️#

BETA Feature: API to be finalized in v21.6

A side benefit of the new router is that it can do double duty also powering the new signals API. This feature is being released for public usage now, and likely the public API will not change in its final form.

The core ideas of this feature are:

  1. to allow the developer greater control and access to plugging into the server and request lifecycles,
  2. to provide new tools to synchronize and send messages through your application, and
  3. to ultimately further increase performance.

The API introduces three new methods:

  • @app.signal(...) - For defining a signal handler. It looks and operates very much like a route. Whenever that signal is dispatched, this handler will be executed.
  • app.event(...) - An awaitable that can be used anywhere in your application to pause execution until the event is triggered.
  • app.dispatch(...) - Trigger an event and cause the signal handlers to execute.
@app.signal("foo.bar.<thing>")
async def signal_handler(thing, **kwargs):
    print(f"[signal_handler] {thing=}", kwargs)

async def wait_for_event(app):
    while True:
        print("> waiting")
        await app.event("foo.bar.*")
        print("> event found\n")

@app.after_server_start
async def after_server_start(app, loop):
    app.add_task(wait_for_event(app))

@app.get("/")
async def trigger(request):
    await app.dispatch("foo.bar.baz")
    return response.text("Done.")

Route naming#

Routes used to be referenced by both route.name and route.endpoint. While similar, they were slightly different. Now, all routes will be consistently namespaced and referenced.

<app name>.[optional:<blueprint name>.]<handler name>

This new "name" is assigned to the property route.name. We are deprecating route.endpoint, and will remove that property in v21.9. Until then, it will be an alias for route.name.

In addition, naming prefixes that had been in use for things like static, websocket, and blueprint routes have been removed.

New decorators#

Several new convenience decorators to help IDEs with autocomplete.

# Alias to @app.listener("...")
@app.before_server_start
@app.after_server_stop
@app.before_server_start
@app.after_server_stop

# Alias to @app.middleware("...")
@app.on_request
@app.on_response

Unquote in route#

If you have a route that uses non-ascii characters, Sanic will no longer unquote the text for you. You will need to specifically tell the route definition that it should do so.

@app.route("/overload/<param>", methods=["GET"], unquote=True)
async def handler2(request, param):
    return text("OK2 " + param)

request, response = app.test_client.get("/overload/您好")
assert response.text == "OK2 您好"

If you forget to do so, your text will remain encoded.

Alter Request.match_info#

The match_info has always provided the data for the matched path parameters. You now have access to modify that, for example in middleware.

@app.on_request
def convert_to_snake_case(request):
    request.match_info = to_snake(request.match_info)

Version types in routes#

The version argument in routes can now be:

  • str
  • int
  • float
@app.route("/foo", version="2.1.1")
@app.route("/foo", version=2)
@app.route("/foo", version=2.1)

Safe method handling with body#

Route handlers for GET, HEAD, OPTIONS and DELETE will not decode any HTTP body passed to it. You can override this:

@app.delete(..., ignore_body=False)

Application, Blueprint and Blueprint Group parity#

The Sanic and Blueprint classes share a common base. Previously they duplicated a lot of functionality, that lead to slightly different implementations between them. Now that they both inherit the same base class, developers and plugins should have a more consistent API to work with.

Also, Blueprint Groups now also support common URL extensions like the version and strict_slashes keyword arguments.

Dropped httpx from dependencies#

There is no longer a dependency on httpx.

Removed testing library#

Sanic internal testing client has been removed. It is now located in its own repository: sanic-org/sanic-testing and is also its own standalone package on PyPI.

If you have sanic-testing installed, it will be available and usable on your Sanic() application instances as before. So, the only change you will need to make is to add sanic-testing to your test suite requirements.

Application and connection level context (ctx) objects#

Version 19.9 added the request.ctx API. This helpful construct easily allows for attaching properties and data to a request object (for example, in middleware), and reusing the information elsewhere int he application.

Similarly, this concept is being extended in two places:

  1. the application instance, and
  2. a transport connection.

Application context#

A common use case is to attach properties to the app instance. For the sake of consistency, and to avoid the issue of name collision with Sanic properties, the ctx object now exists on Sanic instances.

@app.before_server_startup
async def startup_db(app, _):
    # WRONG
    app.db = await connect_to_db()

    # CORRECT
    app.ctx.db = await connect_to_db()

Connection context#

When a client sends a keep alive header, Sanic will attempt to keep the transport socket open for a period of time. That transport object now has a ctx object available on it. This effectively means that multiple requests from a single client (where the transport layer is being reused) may share state.

@app.on_request
async def increment_foo(request):
    if not hasattr(request.conn_info.ctx, "foo"):
        request.conn_info.ctx.foo = 0
    request.conn_info.ctx.foo += 1

@app.get("/")
async def count_foo(request):
    return text(f"request.conn_info.ctx.foo={request.conn_info.ctx.foo}")
$ curl localhost:8000 localhost:8000 localhost:8000
request.conn_info.ctx.foo=1
request.conn_info.ctx.foo=2
request.conn_info.ctx.foo=3

Warning

Connection level context is an experimental feature, and should be finalized in v21.6.

News#

A NEW frontpage 🎉#

We have split the documentation into two. The docstrings inside the codebase will still continue to build sphinx docs to ReadTheDocs. However, it will be limited to API documentation. The new frontpage will house the "Sanic User Guide".

The new site runs on Vuepress. Contributions are welcome. We also invite help in translating the documents.

As a part of this, we also freshened up the RTD documentation and changed it to API docs only.

Chat has moved to Discord#

The Gitter chatroom has taken one step closer to being phased out. In its place we opened a Discord server.

Open Collective#

The Sanic Community Organization has opened a page on Open Collective to enable anyone that would like to financially support the development of Sanic.

2021 Release Managers#

Thank you to @sjsadowski and @yunstanford for acting as release managers for both 2019 and 2020. This year's release managers are @ahopkins and @vltr.

Thank you#

Thank you to everyone that participated in this release: :clap:

@ahopkins @akshgpt7 @artcg @ashleysommer @elis-k @harshanarayana @sjsadowski @tronic @vltr,

To @ConnorZhang and @ZinkLu for translating our documents into Chinese,


Make sure to checkout the changelog to get links to all the PRs, etc.