Version 23.6

Table of Contents

Introduction#

This is the second release of the version 23 release cycle. 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...

Remove Python 3.7 support#

Python 3.7 is due to reach its scheduled upstream end-of-life on 2023-06-27. Sanic is now dropping support for Python 3.7, and requires Python 3.8 or newer.

See #2777.

Resolve pypy compatibility issues#

A small patch was added to the os module to once again allow for Sanic to run with PyPy. This workaround replaces the missing readlink function (missing in PyPy os module) with the function os.path.realpath, which serves to the same purpose.

See #2782.

Add custom typing to config and ctx objects#

The sanic.Sanic and sanic.Request object have become generic types that will make it more convenient to have fully typed config and ctx objects.

In the most simple form, the Sanic object is typed as:

from sanic import Sanic
app = Sanic("test")
reveal_type(app)  # N: Revealed type is "sanic.app.Sanic[sanic.config.Config, types.SimpleNamespace]"

Note

It should be noted, there is no requirement to use the generic types. The default types are sanic.config.Config and types.SimpleNamespace. This new feature is just an option for those that want to use it and existing types of app: Sanic and request: Request should work just fine.

Now it is possible to have a fully-type app.config, app.ctx, and request.ctx objects though generics. This allows for better integration with auto completion tools in IDEs improving the developer experience.

from sanic import Request, Sanic
from sanic.config import Config

class CustomConfig(Config):
    pass

class Foo:
    pass

class RequestContext:
    foo: Foo

class CustomRequest(Request[Sanic[CustomConfig, Foo], RequestContext]):
    @staticmethod
    def make_context() -> RequestContext:
        ctx = RequestContext()
        ctx.foo = Foo()
        return ctx

app = Sanic(
    "test", config=CustomConfig(), ctx=Foo(), request_class=CustomRequest
)

@app.get("/")
async def handler(request: CustomRequest):
   ...

As a side effect, now request.ctx is lazy initialized, which should reduce some overhead when the request.ctx is unused.

One further change you may have noticed in the above snippet is the make_context method. This new method can be used by custom Request types to inject an object different from a SimpleNamespace similar to how Sanic has allowed custom application context objects for a while.

For a more thorough discussion, see custom typed application and custom typed request.

See #2785.

Universal exception signal#

A new exception signal added for ALL exceptions raised while the server is running: "server.exception.reporting". This is a universal signal that will be emitted for any exception raised, and dispatched as its own task. This means that it will not block the request handler, and will not be affected by any middleware.

This is useful for catching exceptions that may occur outside of the request handler (for example in signals, or in a background task), and it intended for use to create a consistent error handling experience for the user.

from sanic.signals import Event

@app.signal(Event.SERVER_LIFECYCLE_EXCEPTION)
async def catch_any_exception(app: Sanic, exception: Exception):
    app.ctx.my_error_reporter_utility.error(exception)

This pattern can be simplified with a new decorator @app.report_exception:

@app.report_exception
async def catch_any_exception(app: Sanic, exception: Exception):
    print("Caught exception:", exception)

It should be pointed out that this happens in a background task and is NOT for manipulation of an error response. It is only for reporting, logging, or other purposes that should be triggered when an application error occurs.

See #2724 and #2792.

Add name prefixing to BP groups#

Sanic had been raising a warning on duplicate route names for a while, and started to enforce route name uniqueness in v23.3. This created a complication for blueprint composition.

New name prefixing parameter for blueprints groups has been added to alleviate this issue. It allows nesting of blueprints and groups to make them composable.

The addition is the new name_prefix parameter as shown in this snippet.

bp1 = Blueprint("bp1", url_prefix="/bp1")
bp2 = Blueprint("bp2", url_prefix="/bp2")

bp1.add_route(lambda _: ..., "/", name="route1")
bp2.add_route(lambda _: ..., "/", name="route2")

group_a = Blueprint.group(
    bp1, bp2, url_prefix="/group-a", name_prefix="group-a"
)
group_b = Blueprint.group(
    bp1, bp2, url_prefix="/group-b", name_prefix="group-b"
)

app = Sanic("TestApp")
app.blueprint(group_a)
app.blueprint(group_b)

The routes built will be named as follows:

  • TestApp.group-a_bp1.route1
  • TestApp.group-a_bp2.route2
  • TestApp.group-b_bp1.route1
  • TestApp.group-b_bp2.route2

See #2727.

Add request.client_ip#

Sanic has introduced request.client_ip, a new accessor that provides client's IP address from both local and proxy data. It allows running the application directly on Internet or behind a proxy. This is equivalent to request.remote_addr or request.ip, providing the client IP regardless of how the application is deployed.

See #2790.

Increase of KEEP_ALIVE_TIMEOUT default to 120 seconds#

The default KEEP_ALIVE_TIMEOUT value changed from 5 seconds to 120 seconds. It is of course still configurable, but this change should improve performance on long latency connections, where reconnecting is expensive, and better fits typical user flow browsing pages with longer-than-5-second intervals.

Sanic has historically used 5 second timeouts to quickly close idle connections. The chosen value of 120 seconds is indeed larger than Nginx default of 75, and is the same value that Caddy server has by default.

Related to #2531 and #2681.

See #2670.

Set multiprocessing start method early#

Due to how Python handles multiprocessing, it may be confusing to some users how to properly create synchronization primitives. This is due to how Sanic creates the multiprocessing context. This change sets the start method early so that any primitives created will properly attach to the correct context.

For most users, this should not be noticeable or impactful. But, it should make creation of something like this easier and work as expected.

from multiprocessing import Queue

@app.main_process_start
async def main_process_start(app):
    app.shared_ctx.queue = Queue()

See #2776.

Thank you#

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

@ahopkins @ChihweiLHBird @chuckds @deounix @guacs @liamcoatman @moshe742 @prryplatypus @SaidBySolo @Thirumalai @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.