8.3 KiB
Version 23.6
.. toc::
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]"
.. tip:: 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.
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.
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: 👏
@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.