Version 23.12 (LTS)

Table of Contents

Introduction#

This is the final release of the version 23 release cycle. It is designated as a long-term support ("LTS") release, which means it will receive support for two years as stated in the support policy. If you run into any issues, please raise a concern on GitHub

What to know#

More details in the Changelog. Notable new or breaking features, and what to upgrade:

🎉 Documentation now using Sanic#

You can read all about how, but we converted this documentation site to using the SHH 🤫 stack.

👶 BETA Welcome to the Sanic interactive console#

That's right, Sanic now ships with a REPL!

When using the Sanic CLI, you can pass the --repl argument to automatically run an interactive console along side your application. This is extremely helpful while developing, and allows you access to the application instance, as well as a built-in client enabled to send HTTP requests to the running instance.

If you use the --dev flag, this feature is quasi-enabled by default. While it will not outright run the REPL, it will start the process and allow you to enter the REPL at anytime by hitting <ENTER> on your keyboard.

This is still in BETA mode. We would appreciate you letting us know about any any enhancement requests or issues.

Python 3.12 support#

We have added Python 3.12 to the supported versions.

Start and restart arbitrary processes#

Using the multiplexer, you can now start and restart arbitrary or pre-existing processes. This enabled the following new features in the way the multiplexer and worker manager operate:

  1. multiplexer.restart("<process name>") will now restart a targeted single process
  2. multiplexer.manage(...) is a new method that works exactly like manager.manage(...)
  3. The manage methods now have additional keyword arguments:
    • tracked - whether the state of the process is tracked after the process completes
    • restartable - whether the process should be allowed to be restarted
    • auto_start - whether the process should be started immediately after it is created
def task(n: int = 10, **kwargs):
    print("TASK STARTED", kwargs)
    for i in range(n):
        print(f"Running task - Step {i+1} of {n}")
        sleep(1)

@app.get("/restart")
async def restart_handler(request: Request):
    request.app.m.restart("Sanic-TEST-0")
    return json({"foo": request.app.m.name})


@app.get("/start")
async def start_handler(request: Request):
    request.app.m.manage("NEW", task, kwargs={"n": 7}, workers=2)
    return json({"foo": request.app.m.name})

@app.main_process_ready
def start_process(app: Sanic):
    app.manager.manage("TEST", task, kwargs={"n": 3}, restartable=True)

Prioritized listeners and signals#

In v22.9 Sanic added prioritization to middleware to allow arbitrary ordering of middleware. This same concept has now been extended to listeners and signals. This will allow a priority number to be assigned at creation time that will override its default position in the execution timeline.

@app.before_server_start(priority=3)
async def sample(app):
    ...

The higher the number, the higher priority it will receive. Overall the rules for deciding the order of execution are as follows:

  1. Priority in descending order
  2. Application listeners before Blueprint listeners
  3. Registration order

Remember, some listeners are executed in reverse order

Websocket signals#

We have added three new signals for websockets:

  1. websocket.handler.before
  2. websocket.handler.after
  3. websocket.handler.exception
@app.signal("websocket.handler.before")
async def ws_before(request: Request, websocket: Websocket):
    ...

@app.signal("websocket.handler.after")
async def ws_after(request: Request, websocket: Websocket):
    ...
    
@app.signal("websocket.handler.exception")
async def ws_exception(
    request: Request, websocket: Websocket, exception: Exception
):
    ...

Simplified signals#

Sanic has always enforced a three part naming convention for signals: one.two.three. However, now you can create simpler names that are only a single part.

@app.signal("foo")
async def foo():
    ...

You can make that part dynamic just like with regular signals and routes:

@app.signal("<thing>")
async def handler(**kwargs):
    print("foobar signal received")
    print(kwargs)


@app.route("/")
async def test(request: Request):
    await request.app.dispatch("foobar")
    return json({"hello": "world"})

If you need to have multiple dynamic signals, then you should use the longer three-part format.

The event method has been updated#

A number of changes have been made to both app.event() and blueprint.event().

  • condition and exclusive are keywords to control matching conditions (similar to the signal() methods)
  • You can pass either a str or an Enum (just like signal())
  • returns a copy of the context that was passed to the dispatch() method

Reload trigger gets changed files#

The files changed by the reloader are now injected into the listener. This will allow the trigger to do something with knowledge of what those changed files were.

@app.after_reload_trigger
async def after_reload_trigger(_, changed):
    print(changed)

Thank you#

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

@ahopkins @ChihweiLHBird @freddiewanah @gluhar2006 @iAndriy @MichaelHinrichs @prryplatypus @SaidBySolo @sjsadowski @talljosh @tjni @Tronic


If you enjoy the project, please consider contributing. Of course we love code contributions, but we also love contributions in any form. Consider writing some documentation, showing off use cases, joining conversations and making your voice known, and if you are able: financial contributions.