From cdbf0c4425584439c6773e7aed90b50628c1430c Mon Sep 17 00:00:00 2001 From: Adam Hopkins Date: Wed, 31 Dec 2025 11:13:06 +0200 Subject: [PATCH 1/5] Remove v3.9 and add v3.14 --- .github/workflows/tests.yml | 11 +- README.rst | 4 +- guide/content/en/guide/introduction.md | 2 +- guide/webapp/display/base.py | 2 +- pyproject.toml | 2 +- sanic/app.py | 251 ++++++++++--------- sanic/application/motd.py | 5 +- sanic/application/state.py | 12 +- sanic/asgi.py | 8 +- sanic/base/root.py | 4 +- sanic/blueprints.py | 62 +++-- sanic/cli/app.py | 5 +- sanic/cli/arguments.py | 7 +- sanic/cli/console.py | 18 +- sanic/cli/daemon.py | 15 +- sanic/cli/inspector_client.py | 4 +- sanic/compat.py | 8 +- sanic/config.py | 54 ++-- sanic/cookies/request.py | 8 +- sanic/cookies/response.py | 20 +- sanic/exceptions.py | 86 +++---- sanic/handlers/directory.py | 8 +- sanic/handlers/error.py | 12 +- sanic/headers.py | 40 +-- sanic/http/http1.py | 4 +- sanic/http/http3.py | 21 +- sanic/http/stream.py | 10 +- sanic/http/tls/context.py | 24 +- sanic/http/tls/creators.py | 12 +- sanic/logging/setup.py | 4 +- sanic/middleware.py | 5 +- sanic/mixins/base.py | 10 +- sanic/mixins/commands.py | 6 +- sanic/mixins/exceptions.py | 6 +- sanic/mixins/listeners.py | 40 +-- sanic/mixins/middleware.py | 8 +- sanic/mixins/routes.py | 122 ++++----- sanic/mixins/signals.py | 12 +- sanic/mixins/startup.py | 103 ++++---- sanic/mixins/static.py | 25 +- sanic/models/asgi.py | 10 +- sanic/models/ctx_types.py | 6 +- sanic/models/futures.py | 24 +- sanic/models/handler_types.py | 22 +- sanic/models/http_types.py | 9 +- sanic/models/protocol_types.py | 17 +- sanic/models/server_types.py | 8 +- sanic/pages/css.py | 3 +- sanic/request/form.py | 2 +- sanic/request/parameters.py | 12 +- sanic/request/types.py | 97 ++++--- sanic/response/convenience.py | 54 ++-- sanic/response/types.py | 60 +++-- sanic/router.py | 20 +- sanic/server/async_server.py | 2 +- sanic/server/events.py | 6 +- sanic/server/protocols/base_protocol.py | 10 +- sanic/server/protocols/http_protocol.py | 19 +- sanic/server/protocols/websocket_protocol.py | 22 +- sanic/server/runners.py | 10 +- sanic/server/socket.py | 8 +- sanic/server/websockets/connection.py | 19 +- sanic/server/websockets/frame.py | 8 +- sanic/server/websockets/impl.py | 52 ++-- sanic/signals.py | 36 +-- sanic/utils.py | 3 +- sanic/views.py | 22 +- sanic/worker/daemon.py | 13 +- sanic/worker/inspector.py | 8 +- sanic/worker/loader.py | 8 +- sanic/worker/manager.py | 16 +- sanic/worker/multiplexer.py | 6 +- sanic/worker/process.py | 2 + sanic/worker/restarter.py | 4 +- sanic/worker/serve.py | 22 +- sanic/worker/state.py | 5 +- setup.py | 4 +- tox.ini | 6 +- 78 files changed, 840 insertions(+), 875 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a3bb725d87..4270b197c0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -24,20 +24,18 @@ jobs: fail-fast: true matrix: config: - - { python-version: "3.9", tox-env: security } - { python-version: "3.10", tox-env: security } - { python-version: "3.11", tox-env: security } - { python-version: "3.12", tox-env: security } - { python-version: "3.13", tox-env: security } - - { python-version: "3.13", tox-env: lint } + - { python-version: "3.14", tox-env: security } + - { python-version: "3.14", tox-env: lint } # - { python-version: "3.10", tox-env: docs } - - { python-version: "3.9", tox-env: type-checking } - { python-version: "3.10", tox-env: type-checking } - { python-version: "3.11", tox-env: type-checking } - { python-version: "3.12", tox-env: type-checking } - { python-version: "3.13", tox-env: type-checking } - - { python-version: "3.9", tox-env: py39, max-attempts: 3 } - - { python-version: "3.9", tox-env: py39-no-ext, max-attempts: 3 } + - { python-version: "3.14", tox-env: type-checking } - { python-version: "3.10", tox-env: py310, max-attempts: 3 } - { python-version: "3.10", tox-env: py310-no-ext, max-attempts: 3 } - { python-version: "3.11", tox-env: py311, max-attempts: 3 } @@ -46,7 +44,8 @@ jobs: - { python-version: "3.12", tox-env: py312-no-ext, max-attempts: 3 } - { python-version: "3.13", tox-env: py313, max-attempts: 3 } - { python-version: "3.13", tox-env: py313-no-ext, max-attempts: 3 } - - { python-version: "3.9", tox-env: py39-no-ext, platform: windows-latest, ignore-errors: true } + - { python-version: "3.14", tox-env: py314, max-attempts: 3 } + - { python-version: "3.14", tox-env: py314-no-ext, max-attempts: 3 } - { python-version: "3.10", tox-env: py310-no-ext, platform: windows-latest, ignore-errors: true } - { python-version: "3.11", tox-env: py311-no-ext, platform: windows-latest, ignore-errors: true } steps: diff --git a/README.rst b/README.rst index 42f54d976a..5bc1beb8ce 100644 --- a/README.rst +++ b/README.rst @@ -58,7 +58,7 @@ Sanic | Build fast. Run fast. .. end-badges -Sanic is a **Python 3.9+** web server and web framework that's written to go fast. It allows the usage of the ``async/await`` syntax added in Python 3.5, which makes your code non-blocking and speedy. +Sanic is a **Python 3.10+** web server and web framework that's written to go fast. It allows the usage of the ``async/await`` syntax added in Python 3.5, which makes your code non-blocking and speedy. Sanic is also ASGI compliant, so you can deploy it with an `alternative ASGI webserver `_. @@ -129,7 +129,7 @@ And, we can verify it is working: ``curl localhost:8000 -i`` **Now, let's go build something fast!** -Minimum Python version is 3.9. +Minimum Python version is 3.10. Documentation ------------- diff --git a/guide/content/en/guide/introduction.md b/guide/content/en/guide/introduction.md index 8d7e959e2b..002d4476a8 100644 --- a/guide/content/en/guide/introduction.md +++ b/guide/content/en/guide/introduction.md @@ -1,6 +1,6 @@ # Introduction -Sanic is a Python 3.9+ web server and web framework that’s written to go fast. It allows the usage of the async/await syntax added in Python 3.5, which makes your code non-blocking and speedy. +Sanic is a Python 3.10+ web server and web framework that's written to go fast. It allows the usage of the async/await syntax added in Python 3.5, which makes your code non-blocking and speedy. .. attrs:: :class: introduction-table diff --git a/guide/webapp/display/base.py b/guide/webapp/display/base.py index f5e1ad3eb7..f653a7279d 100644 --- a/guide/webapp/display/base.py +++ b/guide/webapp/display/base.py @@ -38,7 +38,7 @@ def _head(self) -> list[Builder]: E.meta( name="description", content=( - "Sanic is a Python 3.9+ web server and " + "Sanic is a Python 3.10+ web server and " "web framework that's written to go fast." ), ), diff --git a/pyproject.toml b/pyproject.toml index 12b080dbd3..0ab5ed94c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [tool.ruff] -target-version = "py39" +target-version = "py310" line-length = 79 [tool.ruff.lint] diff --git a/sanic/app.py b/sanic/app.py index 4462771d96..98ddaf5179 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -32,12 +32,9 @@ AnyStr, Callable, ClassVar, - Deque, Generic, Literal, - Optional, TypeVar, - Union, cast, overload, ) @@ -64,6 +61,7 @@ from sanic.helpers import Default, _default from sanic.http import Stage from sanic.log import LOGGING_CONFIG_DEFAULTS, error_logger, logger +from sanic.logging.deprecation import deprecation from sanic.logging.setup import setup_logging from sanic.middleware import Middleware, MiddlewareLocation from sanic.mixins.commands import CommandMixin @@ -232,40 +230,40 @@ def __init__( name: str, config: None = None, ctx: None = None, - router: Optional[Router] = None, - signal_router: Optional[SignalRouter] = None, - error_handler: Optional[ErrorHandler] = None, - env_prefix: Optional[str] = SANIC_PREFIX, - request_class: Optional[type[Request]] = None, + router: Router | None = None, + signal_router: SignalRouter | None = None, + error_handler: ErrorHandler | None = None, + env_prefix: str | None = SANIC_PREFIX, + request_class: type[Request] | None = None, strict_slashes: bool = False, - log_config: Optional[dict[str, Any]] = None, + log_config: dict[str, Any] | None = None, configure_logging: bool = True, - dumps: Optional[Callable[..., AnyStr]] = None, - loads: Optional[Callable[..., Any]] = None, + dumps: Callable[..., AnyStr] | None = None, + loads: Callable[..., Any] | None = None, inspector: bool = False, - inspector_class: Optional[type[Inspector]] = None, - certloader_class: Optional[type[CertLoader]] = None, + inspector_class: type[Inspector] | None = None, + certloader_class: type[CertLoader] | None = None, ) -> None: ... @overload def __init__( self: Sanic[config_type, SimpleNamespace], name: str, - config: Optional[config_type] = None, + config: config_type | None = None, ctx: None = None, - router: Optional[Router] = None, - signal_router: Optional[SignalRouter] = None, - error_handler: Optional[ErrorHandler] = None, - env_prefix: Optional[str] = SANIC_PREFIX, - request_class: Optional[type[Request]] = None, + router: Router | None = None, + signal_router: SignalRouter | None = None, + error_handler: ErrorHandler | None = None, + env_prefix: str | None = SANIC_PREFIX, + request_class: type[Request] | None = None, strict_slashes: bool = False, - log_config: Optional[dict[str, Any]] = None, + log_config: dict[str, Any] | None = None, configure_logging: bool = True, - dumps: Optional[Callable[..., AnyStr]] = None, - loads: Optional[Callable[..., Any]] = None, + dumps: Callable[..., AnyStr] | None = None, + loads: Callable[..., Any] | None = None, inspector: bool = False, - inspector_class: Optional[type[Inspector]] = None, - certloader_class: Optional[type[CertLoader]] = None, + inspector_class: type[Inspector] | None = None, + certloader_class: type[CertLoader] | None = None, ) -> None: ... @overload @@ -273,61 +271,61 @@ def __init__( self: Sanic[Config, ctx_type], name: str, config: None = None, - ctx: Optional[ctx_type] = None, - router: Optional[Router] = None, - signal_router: Optional[SignalRouter] = None, - error_handler: Optional[ErrorHandler] = None, - env_prefix: Optional[str] = SANIC_PREFIX, - request_class: Optional[type[Request]] = None, + ctx: ctx_type | None = None, + router: Router | None = None, + signal_router: SignalRouter | None = None, + error_handler: ErrorHandler | None = None, + env_prefix: str | None = SANIC_PREFIX, + request_class: type[Request] | None = None, strict_slashes: bool = False, - log_config: Optional[dict[str, Any]] = None, + log_config: dict[str, Any] | None = None, configure_logging: bool = True, - dumps: Optional[Callable[..., AnyStr]] = None, - loads: Optional[Callable[..., Any]] = None, + dumps: Callable[..., AnyStr] | None = None, + loads: Callable[..., Any] | None = None, inspector: bool = False, - inspector_class: Optional[type[Inspector]] = None, - certloader_class: Optional[type[CertLoader]] = None, + inspector_class: type[Inspector] | None = None, + certloader_class: type[CertLoader] | None = None, ) -> None: ... @overload def __init__( self: Sanic[config_type, ctx_type], name: str, - config: Optional[config_type] = None, - ctx: Optional[ctx_type] = None, - router: Optional[Router] = None, - signal_router: Optional[SignalRouter] = None, - error_handler: Optional[ErrorHandler] = None, - env_prefix: Optional[str] = SANIC_PREFIX, - request_class: Optional[type[Request]] = None, + config: config_type | None = None, + ctx: ctx_type | None = None, + router: Router | None = None, + signal_router: SignalRouter | None = None, + error_handler: ErrorHandler | None = None, + env_prefix: str | None = SANIC_PREFIX, + request_class: type[Request] | None = None, strict_slashes: bool = False, - log_config: Optional[dict[str, Any]] = None, + log_config: dict[str, Any] | None = None, configure_logging: bool = True, - dumps: Optional[Callable[..., AnyStr]] = None, - loads: Optional[Callable[..., Any]] = None, + dumps: Callable[..., AnyStr] | None = None, + loads: Callable[..., Any] | None = None, inspector: bool = False, - inspector_class: Optional[type[Inspector]] = None, - certloader_class: Optional[type[CertLoader]] = None, + inspector_class: type[Inspector] | None = None, + certloader_class: type[CertLoader] | None = None, ) -> None: ... def __init__( self, name: str, - config: Optional[config_type] = None, - ctx: Optional[ctx_type] = None, - router: Optional[Router] = None, - signal_router: Optional[SignalRouter] = None, - error_handler: Optional[ErrorHandler] = None, - env_prefix: Optional[str] = SANIC_PREFIX, - request_class: Optional[type[Request]] = None, + config: config_type | None = None, + ctx: ctx_type | None = None, + router: Router | None = None, + signal_router: SignalRouter | None = None, + error_handler: ErrorHandler | None = None, + env_prefix: str | None = SANIC_PREFIX, + request_class: type[Request] | None = None, strict_slashes: bool = False, - log_config: Optional[dict[str, Any]] = None, + log_config: dict[str, Any] | None = None, configure_logging: bool = True, - dumps: Optional[Callable[..., AnyStr]] = None, - loads: Optional[Callable[..., Any]] = None, + dumps: Callable[..., AnyStr] | None = None, + loads: Callable[..., Any] | None = None, inspector: bool = False, - inspector_class: Optional[type[Inspector]] = None, - certloader_class: Optional[type[CertLoader]] = None, + inspector_class: type[Inspector] | None = None, + certloader_class: type[CertLoader] | None = None, ) -> None: super().__init__(name=name) # logging @@ -349,16 +347,16 @@ def __init__( self.config.INSPECTOR = inspector # Then we can do the rest - self._asgi_app: Optional[ASGIApp] = None - self._asgi_lifespan: Optional[Lifespan] = None + self._asgi_app: ASGIApp | None = None + self._asgi_lifespan: Lifespan | None = None self._asgi_client: Any = None self._blueprint_order: list[Blueprint] = [] self._delayed_tasks: list[str] = [] self._future_registry: FutureRegistry = FutureRegistry() - self._inspector: Optional[Inspector] = None - self._manager: Optional[WorkerManager] = None + self._inspector: Inspector | None = None + self._manager: WorkerManager | None = None self._state: ApplicationState = ApplicationState(app=self) - self._task_registry: dict[str, Union[Task, None]] = {} + self._task_registry: dict[str, Task | None] = {} self._test_client: Any = None self._test_manager: Any = None self.asgi = False @@ -372,16 +370,16 @@ def __init__( self.error_handler: ErrorHandler = error_handler or ErrorHandler() self.inspector_class: type[Inspector] = inspector_class or Inspector self.listeners: dict[str, list[ListenerType[Any]]] = defaultdict(list) - self.named_request_middleware: dict[str, Deque[Middleware]] = {} - self.named_response_middleware: dict[str, Deque[Middleware]] = {} + self.named_request_middleware: dict[str, deque[Middleware]] = {} + self.named_response_middleware: dict[str, deque[Middleware]] = {} self.repl_ctx: REPLContext = REPLContext() self.request_class = request_class or Request - self.request_middleware: Deque[Middleware] = deque() - self.response_middleware: Deque[Middleware] = deque() + self.request_middleware: deque[Middleware] = deque() + self.response_middleware: deque[Middleware] = deque() self.router: Router = router or Router() self.shared_ctx: SharedContext = SharedContext() self.signal_router: SignalRouter = signal_router or SignalRouter() - self.sock: Optional[socket] = None + self.sock: socket | None = None self.strict_slashes: bool = strict_slashes self.websocket_enabled: bool = False self.websocket_tasks: set[Future[Any]] = set() @@ -418,10 +416,7 @@ def loop(self) -> AbstractEventLoop: try: return get_running_loop() except RuntimeError: # no cov - if sys.version_info > (3, 10): - return asyncio.get_event_loop_policy().get_event_loop() - else: - return asyncio.get_event_loop() + return asyncio.get_event_loop_policy().get_event_loop() # -------------------------------------------------------------------- # # Registration @@ -467,11 +462,11 @@ def register_listener( def register_middleware( self, - middleware: Union[MiddlewareType, Middleware], + middleware: MiddlewareType | Middleware, attach_to: str = "request", *, - priority: Union[Default, int] = _default, - ) -> Union[MiddlewareType, Middleware]: + priority: Default | int = _default, + ) -> MiddlewareType | Middleware: """Register a middleware to be called before a request is handled. Args: @@ -516,7 +511,7 @@ def register_named_middleware( route_names: Iterable[str], attach_to: str = "request", *, - priority: Union[Default, int] = _default, + priority: Default | int = _default, ): """Used to register named middleqare (middleware typically on blueprints) @@ -567,7 +562,7 @@ def register_named_middleware( def _apply_exception_handler( self, handler: FutureException, - route_names: Optional[list[str]] = None, + route_names: list[str] | None = None, ): """Decorate a function to be registered as a handler for exceptions @@ -624,7 +619,7 @@ def _apply_route( def _apply_middleware( self, middleware: FutureMiddleware, - route_names: Optional[list[str]] = None, + route_names: list[str] | None = None, ): with self.amend(): if route_names: @@ -651,8 +646,8 @@ def dispatch( self, event: str, *, - condition: Optional[dict[str, str]] = None, - context: Optional[dict[str, Any]] = None, + condition: dict[str, str] | None = None, + context: dict[str, Any] | None = None, fail_not_found: bool = True, inline: Literal[True], reverse: bool = False, @@ -663,8 +658,8 @@ def dispatch( self, event: str, *, - condition: Optional[dict[str, str]] = None, - context: Optional[dict[str, Any]] = None, + condition: dict[str, str] | None = None, + context: dict[str, Any] | None = None, fail_not_found: bool = True, inline: Literal[False] = False, reverse: bool = False, @@ -674,12 +669,12 @@ def dispatch( self, event: str, *, - condition: Optional[dict[str, str]] = None, - context: Optional[dict[str, Any]] = None, + condition: dict[str, str] | None = None, + context: dict[str, Any] | None = None, fail_not_found: bool = True, inline: bool = False, reverse: bool = False, - ) -> Coroutine[Any, Any, Awaitable[Union[Task, Any]]]: + ) -> Coroutine[Any, Any, Awaitable[Task | Any]]: """Dispatches an event to the signal router. Args: @@ -725,10 +720,10 @@ async def handle_registration(request): async def event( self, - event: Union[str, Enum], - timeout: Optional[Union[int, float]] = None, + event: str | Enum, + timeout: int | float | None = None, *, - condition: Optional[dict[str, Any]] = None, + condition: dict[str, Any] | None = None, exclusive: bool = True, ) -> None: """Wait for a specific event to be triggered. @@ -853,13 +848,13 @@ def enable_websocket(self, enable: bool = True) -> None: def blueprint( self, - blueprint: Union[Blueprint, Iterable[Blueprint], BlueprintGroup], + blueprint: Blueprint | Iterable[Blueprint] | BlueprintGroup, *, - url_prefix: Optional[str] = None, - version: Optional[Union[int, float, str]] = None, - strict_slashes: Optional[bool] = None, - version_prefix: Optional[str] = None, - name_prefix: Optional[str] = None, + url_prefix: str | None = None, + version: int | float | str | None = None, + strict_slashes: bool | None = None, + version_prefix: str | None = None, + name_prefix: str | None = None, ) -> None: """Register a blueprint on the application. @@ -1302,13 +1297,12 @@ async def handle_request(self, request: Request) -> None: # no cov # Define `response` var here to remove warnings about # allocation before assignment below. - response: Optional[ - Union[ - BaseHTTPResponse, - Coroutine[Any, Any, Optional[BaseHTTPResponse]], - ResponseStream, - ] - ] = None + response: ( + BaseHTTPResponse + | Coroutine[Any, Any, BaseHTTPResponse | None] + | ResponseStream + | None + ) = None run_middleware = True try: await self.dispatch( @@ -1620,6 +1614,17 @@ async def _listener( try: maybe_coro = listener(app) # type: ignore except TypeError: + name = getattr( + listener, + "__qualname__", + getattr(getattr(listener, "func", None), "__qualname__", None), + ) + deprecation( + f"Passing the loop argument to listeners is deprecated. " + f"Your listener{f' {name!r}' if name else ''} should only " + "accept the app argument.", + 26.6, + ) maybe_coro = listener(app, loop) # type: ignore if maybe_coro and isawaitable(maybe_coro): await maybe_coro @@ -1665,7 +1670,7 @@ def _loop_add_task( app, loop, *, - name: Optional[str] = None, + name: str | None = None, register: bool = True, ) -> Task: tsk: Task = task @@ -1705,7 +1710,7 @@ async def dispatch_delayed_tasks( async def run_delayed_task( app: Sanic, loop: AbstractEventLoop, - task: Union[Future[Any], Task[Any], Awaitable[Any]], + task: Future[Any] | Task[Any] | Awaitable[Any], ) -> None: """Executes a delayed task within the context of a given app and loop. @@ -1728,11 +1733,11 @@ async def run_delayed_task( def add_task( self, - task: Union[Future[Any], Coroutine[Any, Any, Any], Awaitable[Any]], + task: Future[Any] | Coroutine[Any, Any, Any] | Awaitable[Any], *, - name: Optional[str] = None, + name: str | None = None, register: bool = True, - ) -> Optional[Task[Any]]: + ) -> Task[Any] | None: """Schedule a task to run later, after the loop has started. While this is somewhat similar to `asyncio.create_task`, it can be @@ -1782,16 +1787,14 @@ def get_task( @overload def get_task( self, name: str, *, raise_exception: Literal[False] - ) -> Optional[Task]: ... + ) -> Task | None: ... @overload - def get_task( - self, name: str, *, raise_exception: bool - ) -> Optional[Task]: ... + def get_task(self, name: str, *, raise_exception: bool) -> Task | None: ... def get_task( self, name: str, *, raise_exception: bool = True - ) -> Optional[Task]: + ) -> Task | None: """Get a named task. This method is used to get a task by its name. Optionally, you can @@ -1817,7 +1820,7 @@ def get_task( async def cancel_task( self, name: str, - msg: Optional[str] = None, + msg: str | None = None, *, raise_exception: bool = True, ) -> None: @@ -1852,10 +1855,10 @@ async def before_start(app): """ # noqa: E501 task = self.get_task(name, raise_exception=raise_exception) if task and not task.cancelled(): - args: tuple[str, ...] = () - if msg: - args = (msg,) - task.cancel(*args) + if msg and sys.version_info < (3, 14): + task.cancel(msg) + else: + task.cancel() try: await task except CancelledError: @@ -1879,7 +1882,7 @@ def purge_tasks(self) -> None: } def shutdown_tasks( - self, timeout: Optional[float] = None, increment: float = 0.1 + self, timeout: float | None = None, increment: float = 0.1 ) -> None: """Cancel all tasks except the server task. @@ -1949,7 +1952,7 @@ async def __call__(self, scope, receive, send): # Configuration # -------------------------------------------------------------------- # - def update_config(self, config: Union[bytes, str, dict, Any]) -> None: + def update_config(self, config: bytes | str | dict | Any) -> None: """Update the application configuration. This method is used to update the application configuration. It can @@ -2044,9 +2047,9 @@ def ext(self) -> Extend: def extend( self, *, - extensions: Optional[list[type[Extension]]] = None, + extensions: list[type[Extension]] | None = None, built_in_extensions: bool = True, - config: Optional[Union[Config, dict[str, Any]]] = None, + config: Config | dict[str, Any] | None = None, **kwargs, ) -> Extend: """Extend Sanic with additional functionality using Sanic Extensions. @@ -2165,7 +2168,7 @@ def unregister_app(cls, app: Sanic) -> None: @classmethod def get_app( - cls, name: Optional[str] = None, *, force_create: bool = False + cls, name: str | None = None, *, force_create: bool = False ) -> Sanic: """Retrieve an instantiated Sanic instance by name. @@ -2426,7 +2429,7 @@ async def _server_event( self, concern: str, action: str, - loop: Optional[AbstractEventLoop] = None, + loop: AbstractEventLoop | None = None, ) -> None: event = f"server.{concern}.{action}" if action not in ("before", "after") or concern not in ( @@ -2457,7 +2460,7 @@ async def _server_event( def refresh( self, - passthru: Optional[dict[str, Any]] = None, + passthru: dict[str, Any] | None = None, ) -> Sanic: """Refresh the application instance. **This is used internally by Sanic**. diff --git a/sanic/application/motd.py b/sanic/application/motd.py index 7a0aa8e759..bfe4f95463 100644 --- a/sanic/application/motd.py +++ b/sanic/application/motd.py @@ -1,7 +1,6 @@ from abc import ABC, abstractmethod from shutil import get_terminal_size from textwrap import indent, wrap -from typing import Optional from sanic import __version__ from sanic.helpers import is_atty @@ -13,7 +12,7 @@ class MOTD(ABC): def __init__( self, - logo: Optional[str], + logo: str | None, serve_location: str, data: dict[str, str], extra: dict[str, str], @@ -32,7 +31,7 @@ def display(self): @classmethod def output( cls, - logo: Optional[str], + logo: str | None, serve_location: str, data: dict[str, str], extra: dict[str, str], diff --git a/sanic/application/state.py b/sanic/application/state.py index 56b44aff11..627550e8b5 100644 --- a/sanic/application/state.py +++ b/sanic/application/state.py @@ -6,7 +6,7 @@ from pathlib import Path from socket import socket from ssl import SSLContext -from typing import TYPE_CHECKING, Any, Optional, Union +from typing import TYPE_CHECKING, Any from sanic.application.constants import Mode, Server, ServerStage from sanic.log import VerbosityFilter, logger @@ -23,7 +23,7 @@ class ApplicationServerInfo: settings: dict[str, Any] stage: ServerStage = field(default=ServerStage.STOPPED) - server: Optional[AsyncioServer] = field(default=None) + server: AsyncioServer | None = field(default=None) @dataclass @@ -40,9 +40,9 @@ class ApplicationState: fast: bool = field(default=False) host: str = field(default="") port: int = field(default=0) - ssl: Optional[SSLContext] = field(default=None) - sock: Optional[socket] = field(default=None) - unix: Optional[str] = field(default=None) + ssl: SSLContext | None = field(default=None) + sock: socket | None = field(default=None) + unix: str | None = field(default=None) mode: Mode = field(default=Mode.PRODUCTION) reload_dirs: set[Path] = field(default_factory=set) auto_reload: bool = field(default=False) @@ -71,7 +71,7 @@ def __setattr__(self, name: str, value: Any) -> None: if self._init and hasattr(self, f"set_{name}"): getattr(self, f"set_{name}")(value) - def set_mode(self, value: Union[str, Mode]): + def set_mode(self, value: str | Mode): if hasattr(self.app, "error_handler"): self.app.error_handler.debug = self.app.debug if getattr(self.app, "configure_logging", False) and self.app.debug: diff --git a/sanic/asgi.py b/sanic/asgi.py index 497073127e..4a3b5894cc 100644 --- a/sanic/asgi.py +++ b/sanic/asgi.py @@ -2,7 +2,7 @@ import warnings -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING from sanic.compat import Header from sanic.exceptions import BadRequest, ServerError @@ -109,9 +109,9 @@ class ASGIApp: request: Request transport: MockTransport lifespan: Lifespan - ws: Optional[WebSocketConnection] + ws: WebSocketConnection | None stage: Stage - response: Optional[BaseHTTPResponse] + response: BaseHTTPResponse | None @classmethod async def create( @@ -189,7 +189,7 @@ async def create( return instance - async def read(self) -> Optional[bytes]: + async def read(self) -> bytes | None: """ Read and stream the body in chunks from an incoming ASGI message. """ diff --git a/sanic/base/root.py b/sanic/base/root.py index e4f0ff911e..3d6e33d03e 100644 --- a/sanic/base/root.py +++ b/sanic/base/root.py @@ -1,6 +1,6 @@ import re -from typing import Any, Optional +from typing import Any from sanic.base.meta import SanicMeta from sanic.exceptions import SanicException @@ -29,7 +29,7 @@ class BaseSanic( __slots__ = ("name",) def __init__( - self, name: Optional[str] = None, *args: Any, **kwargs: Any + self, name: str | None = None, *args: Any, **kwargs: Any ) -> None: class_name = self.__class__.__name__ diff --git a/sanic/blueprints.py b/sanic/blueprints.py index 10622305f0..d572b4580e 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -13,8 +13,6 @@ TYPE_CHECKING, Any, Callable, - Optional, - Union, overload, ) @@ -115,10 +113,10 @@ class Blueprint(BaseSanic): def __init__( self, name: str, - url_prefix: Optional[str] = None, - host: Optional[Union[list[str], str]] = None, - version: Optional[Union[int, str, float]] = None, - strict_slashes: Optional[bool] = None, + url_prefix: str | None = None, + host: list[str] | str | None = None, + version: int | str | float | None = None, + strict_slashes: bool | None = None, version_prefix: str = "/v", ): super().__init__(name=name) @@ -201,11 +199,11 @@ def reset(self) -> None: def copy( self, name: str, - url_prefix: Optional[Union[str, Default]] = _default, - version: Optional[Union[int, str, float, Default]] = _default, - version_prefix: Union[str, Default] = _default, - allow_route_overwrite: Union[bool, Default] = _default, - strict_slashes: Optional[Union[bool, Default]] = _default, + url_prefix: str | Default | None = _default, + version: int | str | float | Default | None = _default, + version_prefix: str | Default = _default, + allow_route_overwrite: bool | Default = _default, + strict_slashes: bool | Default | None = _default, with_registration: bool = True, with_ctx: bool = False, ): @@ -270,12 +268,12 @@ def copy( @staticmethod def group( - *blueprints: Union[Blueprint, BlueprintGroup], - url_prefix: Optional[str] = None, - version: Optional[Union[int, str, float]] = None, - strict_slashes: Optional[bool] = None, + *blueprints: Blueprint | BlueprintGroup, + url_prefix: str | None = None, + version: int | str | float | None = None, + strict_slashes: bool | None = None, version_prefix: str = "/v", - name_prefix: Optional[str] = "", + name_prefix: str | None = "", ) -> BlueprintGroup: """Group multiple blueprints (or other blueprint groups) together. @@ -525,9 +523,9 @@ async def dispatch(self, *args, **kwargs): def event( self, event: str, - timeout: Optional[Union[int, float]] = None, + timeout: int | float | None = None, *, - condition: Optional[dict[str, Any]] = None, + condition: dict[str, Any] | None = None, ): """Wait for a signal event to be dispatched. @@ -579,7 +577,7 @@ def _extract_value(*values): return value @staticmethod - def _setup_uri(base: str, prefix: Optional[str]): + def _setup_uri(base: str, prefix: str | None): uri = base if prefix: uri = prefix @@ -693,11 +691,11 @@ async def group_middleware(request): def __init__( self, - url_prefix: Optional[str] = None, - version: Optional[Union[int, str, float]] = None, - strict_slashes: Optional[bool] = None, + url_prefix: str | None = None, + version: int | str | float | None = None, + strict_slashes: bool | None = None, version_prefix: str = "/v", - name_prefix: Optional[str] = "", + name_prefix: str | None = "", ): self._blueprints: list[Blueprint] = [] self._url_prefix = url_prefix @@ -707,7 +705,7 @@ def __init__( self._name_prefix = name_prefix @property - def url_prefix(self) -> Optional[Union[int, str, float]]: + def url_prefix(self) -> int | str | float | None: """The URL prefix for the Blueprint Group. Returns: @@ -727,7 +725,7 @@ def blueprints(self) -> list[Blueprint]: return self._blueprints @property - def version(self) -> Optional[Union[str, int, float]]: + def version(self) -> str | int | float | None: """API Version for the Blueprint Group, if any. Returns: @@ -736,7 +734,7 @@ def version(self) -> Optional[Union[str, int, float]]: return self._version @property - def strict_slashes(self) -> Optional[bool]: + def strict_slashes(self) -> bool | None: """Whether to enforce strict slashes for the Blueprint Group. Returns: @@ -754,7 +752,7 @@ def version_prefix(self) -> str: return self._version_prefix @property - def name_prefix(self) -> Optional[str]: + def name_prefix(self) -> str | None: """Name prefix for the Blueprint Group. This is mainly needed when blueprints are copied in order to @@ -780,8 +778,8 @@ def __getitem__(self, item: int) -> Blueprint: ... def __getitem__(self, item: slice) -> MutableSequence[Blueprint]: ... def __getitem__( - self, item: Union[int, slice] - ) -> Union[Blueprint, MutableSequence[Blueprint]]: + self, item: int | slice + ) -> Blueprint | MutableSequence[Blueprint]: """Get the Blueprint object at the specified index. This method returns a blueprint inside the group specified by @@ -807,8 +805,8 @@ def __setitem__(self, index: slice, item: Iterable[Blueprint]) -> None: ... def __setitem__( self, - index: Union[int, slice], - item: Union[Blueprint, Iterable[Blueprint]], + index: int | slice, + item: Blueprint | Iterable[Blueprint], ) -> None: """Set the Blueprint object at the specified index. @@ -844,7 +842,7 @@ def __delitem__(self, index: int) -> None: ... @overload def __delitem__(self, index: slice) -> None: ... - def __delitem__(self, index: Union[int, slice]) -> None: + def __delitem__(self, index: int | slice) -> None: """Delete the Blueprint object at the specified index. Abstract method implemented to turn the `BlueprintGroup` class diff --git a/sanic/cli/app.py b/sanic/cli/app.py index 0a82b994b2..929b5cce5b 100644 --- a/sanic/cli/app.py +++ b/sanic/cli/app.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import shutil import sys @@ -5,7 +7,6 @@ from argparse import Namespace from functools import partial from textwrap import indent -from typing import Union from sanic.app import Sanic from sanic.application.logo import get_logo @@ -452,7 +453,7 @@ def _get_app(self, app_loader: AppLoader): def _build_run_kwargs(self): for group in self.groups: group.prepare(self.args) - ssl: Union[None, dict, str, list] = [] + ssl: None | dict | str | list = [] if self.args.tlshost: ssl.append(None) if self.args.cert is not None or self.args.key is not None: diff --git a/sanic/cli/arguments.py b/sanic/cli/arguments.py index 5605b06666..b3b84a1a17 100644 --- a/sanic/cli/arguments.py +++ b/sanic/cli/arguments.py @@ -1,7 +1,6 @@ from __future__ import annotations from argparse import ArgumentParser, _ArgumentGroup -from typing import Optional, Union from sanic_routing import __version__ as __routing_version__ @@ -11,14 +10,14 @@ class Group: - name: Optional[str] - container: Union[ArgumentParser, _ArgumentGroup] + name: str | None + container: ArgumentParser | _ArgumentGroup _registry: list[type[Group]] = [] def __init_subclass__(cls) -> None: Group._registry.append(cls) - def __init__(self, parser: ArgumentParser, title: Optional[str]): + def __init__(self, parser: ArgumentParser, title: str | None): self.parser = parser if title: diff --git a/sanic/cli/console.py b/sanic/cli/console.py index 4bb39a8251..f52fa15003 100644 --- a/sanic/cli/console.py +++ b/sanic/cli/console.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import atexit import concurrent.futures import sys @@ -20,7 +22,7 @@ from collections.abc import Sequence from contextlib import suppress from types import FunctionType -from typing import Any, NamedTuple, Optional, Union +from typing import Any, NamedTuple import sanic @@ -57,8 +59,8 @@ def __init__(self, app: Sanic): file=sys.stderr, ) -repl_app: Optional[Sanic] = None -repl_response: Optional[HTTPResponse] = None +repl_app: Sanic | None = None +repl_response: HTTPResponse | None = None class REPLProtocol(TransportProtocol): @@ -82,9 +84,9 @@ class Result(NamedTuple): def make_request( url: str = "/", - headers: Optional[Union[dict[str, Any], Sequence[tuple[str, str]]]] = None, + headers: dict[str, Any] | Sequence[tuple[str, str]] | None = None, method: str = "GET", - body: Optional[str] = None, + body: str | None = None, ): assert repl_app, "No Sanic app has been registered." headers = headers or {} @@ -113,9 +115,9 @@ async def respond(request) -> HTTPResponse: async def do( url: str = "/", - headers: Optional[Union[dict[str, Any], Sequence[tuple[str, str]]]] = None, + headers: dict[str, Any] | Sequence[tuple[str, str]] | None = None, method: str = "GET", - body: Optional[str] = None, + body: str | None = None, ) -> Result: request = make_request(url, headers, method, body) response = await respond(request) @@ -130,7 +132,7 @@ def _variable_description(name: str, desc: str, type_desc: str) -> str: class SanicREPL(InteractiveConsole): - def __init__(self, app: Sanic, start: Optional[Default] = None): + def __init__(self, app: Sanic, start: Default | None = None): global repl_app repl_app = app locals_available = { diff --git a/sanic/cli/daemon.py b/sanic/cli/daemon.py index 5e78a7f039..061de588d7 100644 --- a/sanic/cli/daemon.py +++ b/sanic/cli/daemon.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import signal import sys @@ -5,7 +7,6 @@ from argparse import ArgumentParser from contextlib import suppress from pathlib import Path -from typing import Optional from sanic.worker.daemon import Daemon @@ -40,8 +41,8 @@ def make_restart_parser(parser: ArgumentParser) -> None: def resolve_target( - pid: Optional[int], pidfile: Optional[str] -) -> tuple[int, Optional[Path]]: + pid: int | None, pidfile: str | None +) -> tuple[int, Path | None]: """ Resolve a PID from either a direct PID or a pidfile path. @@ -68,7 +69,7 @@ def resolve_target( def _terminate_process( - pid: int, sig: signal.Signals, pidfile: Optional[Path] = None + pid: int, sig: signal.Signals, pidfile: Path | None = None ) -> None: """Send a signal to terminate a process and clean up pidfile.""" sig_name = sig.name @@ -108,20 +109,20 @@ def _terminate_process( pass -def kill_daemon(pid: int, pidfile: Optional[Path] = None) -> None: +def kill_daemon(pid: int, pidfile: Path | None = None) -> None: """Force kill a daemon process with SIGKILL.""" _terminate_process(pid, signal.SIGKILL, pidfile) def stop_daemon( - pid: int, pidfile: Optional[Path] = None, force: bool = False + pid: int, pidfile: Path | None = None, force: bool = False ) -> None: """Stop a daemon process gracefully (SIGTERM) or forcefully (SIGKILL).""" sig = signal.SIGKILL if force else signal.SIGTERM _terminate_process(pid, sig, pidfile) -def status_daemon(pid: int, pidfile: Optional[Path] = None) -> bool: +def status_daemon(pid: int, pidfile: Path | None = None) -> bool: """ Check if a daemon process is running. diff --git a/sanic/cli/inspector_client.py b/sanic/cli/inspector_client.py index f3b8d3ab3b..01c6c14cb4 100644 --- a/sanic/cli/inspector_client.py +++ b/sanic/cli/inspector_client.py @@ -4,7 +4,7 @@ from http.client import RemoteDisconnected from textwrap import indent -from typing import Any, Optional +from typing import Any from urllib.error import URLError from urllib.request import Request as URequest from urllib.request import urlopen @@ -27,7 +27,7 @@ def __init__( port: int, secure: bool, raw: bool, - api_key: Optional[str], + api_key: str | None, ) -> None: self.scheme = "https" if secure else "http" self.host = host diff --git a/sanic/compat.py b/sanic/compat.py index ce476dc41a..a044853f99 100644 --- a/sanic/compat.py +++ b/sanic/compat.py @@ -7,7 +7,7 @@ from collections.abc import Awaitable from contextlib import contextmanager from enum import Enum -from typing import Literal, Union +from typing import Literal from multidict import CIMultiDict # type: ignore @@ -15,9 +15,9 @@ from sanic.log import error_logger -StartMethod = Union[ - Default, Literal["fork"], Literal["forkserver"], Literal["spawn"] -] +StartMethod = ( + Default | Literal["fork"] | Literal["forkserver"] | Literal["spawn"] +) OS_IS_WINDOWS = os.name == "nt" PYPY_IMPLEMENTATION = platform.python_implementation() == "PyPy" diff --git a/sanic/config.py b/sanic/config.py index f9b36c50c0..270017b13b 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -5,7 +5,7 @@ from inspect import getmembers, isclass, isdatadescriptor from os import environ from pathlib import Path -from typing import Any, Callable, Literal, Optional, Union +from typing import Any, Callable, Literal from warnings import filterwarnings from sanic.constants import LocalCertCreator @@ -16,14 +16,14 @@ from sanic.utils import load_module_from_file_location, str_to_bool -FilterWarningType = Union[ - Literal["default"], - Literal["error"], - Literal["ignore"], - Literal["always"], - Literal["module"], - Literal["once"], -] +FilterWarningType = ( + Literal["default"] + | Literal["error"] + | Literal["ignore"] + | Literal["always"] + | Literal["module"] + | Literal["once"] +) SANIC_PREFIX = "SANIC_" @@ -141,28 +141,28 @@ class Config(dict, metaclass=DescriptorMeta): EVENT_AUTOREGISTER: bool DEPRECATION_FILTER: FilterWarningType FORWARDED_FOR_HEADER: str - FORWARDED_SECRET: Optional[str] + FORWARDED_SECRET: str | None GRACEFUL_SHUTDOWN_TIMEOUT: float GRACEFUL_TCP_CLOSE_TIMEOUT: float INSPECTOR: bool INSPECTOR_HOST: str INSPECTOR_PORT: int - INSPECTOR_TLS_KEY: Union[Path, str, Default] - INSPECTOR_TLS_CERT: Union[Path, str, Default] + INSPECTOR_TLS_KEY: Path | str | Default + INSPECTOR_TLS_CERT: Path | str | Default INSPECTOR_API_KEY: str KEEP_ALIVE_TIMEOUT: int KEEP_ALIVE: bool - LOCAL_CERT_CREATOR: Union[str, LocalCertCreator] - LOCAL_TLS_KEY: Union[Path, str, Default] - LOCAL_TLS_CERT: Union[Path, str, Default] + LOCAL_CERT_CREATOR: str | LocalCertCreator + LOCAL_TLS_KEY: Path | str | Default + LOCAL_TLS_CERT: Path | str | Default LOCALHOST: str - LOG_EXTRA: Union[Default, bool] + LOG_EXTRA: Default | bool MOTD: bool MOTD_DISPLAY: dict[str, str] NO_COLOR: bool NOISY_EXCEPTIONS: bool - PROXIES_COUNT: Optional[int] - REAL_IP_HEADER: Optional[str] + PROXIES_COUNT: int | None + REAL_IP_HEADER: str | None REQUEST_BUFFER_SIZE: int REQUEST_MAX_HEADER_SIZE: int REQUEST_ID_HEADER: str @@ -171,21 +171,19 @@ class Config(dict, metaclass=DescriptorMeta): RESPONSE_TIMEOUT: int SERVER_NAME: str TLS_CERT_PASSWORD: str - TOUCHUP: Union[Default, bool] - USE_UVLOOP: Union[Default, bool] + TOUCHUP: Default | bool + USE_UVLOOP: Default | bool WEBSOCKET_MAX_SIZE: int WEBSOCKET_PING_INTERVAL: int WEBSOCKET_PING_TIMEOUT: int def __init__( self, - defaults: Optional[ - dict[str, Union[str, bool, int, float, None]] - ] = None, - env_prefix: Optional[str] = SANIC_PREFIX, - keep_alive: Optional[bool] = None, + defaults: dict[str, str | bool | int | float | None] | None = None, + env_prefix: str | None = SANIC_PREFIX, + keep_alive: bool | None = None, *, - converters: Optional[Sequence[Callable[[str], Any]]] = None, + converters: Sequence[Callable[[str], Any]] | None = None, ): defaults = defaults or {} self.defaults = {**DEFAULT_CONFIG, **defaults} @@ -321,7 +319,7 @@ def _configure_warnings(self): module=r"sanic.*", ) - def _check_error_format(self, format: Optional[str] = None): + def _check_error_format(self, format: str | None = None): check_error_format(format or self.FALLBACK_ERROR_FORMAT) def load_environment_vars(self, prefix=SANIC_PREFIX): @@ -382,7 +380,7 @@ def load_environment_vars(self, prefix=SANIC_PREFIX): except ValueError: pass - def update_config(self, config: Union[bytes, str, dict[str, Any], Any]): + def update_config(self, config: bytes | str | dict[str, Any] | Any): """Update app.config. .. note:: diff --git a/sanic/cookies/request.py b/sanic/cookies/request.py index adbe3501bf..f98eb457b7 100644 --- a/sanic/cookies/request.py +++ b/sanic/cookies/request.py @@ -1,6 +1,6 @@ import re -from typing import Any, Optional +from typing import Any from sanic.cookies.response import Cookie from sanic.request.parameters import RequestParameters @@ -124,7 +124,7 @@ class CookieRequestParameters(RequestParameters): ``` """ # noqa: E501 - def __getitem__(self, key: str) -> Optional[str]: + def __getitem__(self, key: str) -> str | None: try: value = self._get_prefixed_cookie(key) except KeyError: @@ -137,14 +137,14 @@ def __getattr__(self, key: str) -> str: key = key.rstrip("_").replace("_", "-") return str(self.get(key, "")) - def get(self, name: str, default: Optional[Any] = None) -> Optional[Any]: + def get(self, name: str, default: Any | None = None) -> Any | None: try: return self._get_prefixed_cookie(name)[0] except KeyError: return super().get(name, default) def getlist( - self, name: str, default: Optional[list[Any]] = None + self, name: str, default: list[Any] | None = None ) -> list[Any]: try: return self._get_prefixed_cookie(name) diff --git a/sanic/cookies/response.py b/sanic/cookies/response.py index da896685ea..38dba02468 100644 --- a/sanic/cookies/response.py +++ b/sanic/cookies/response.py @@ -4,7 +4,7 @@ import string from datetime import datetime -from typing import TYPE_CHECKING, Union, cast +from typing import TYPE_CHECKING, Literal, cast from sanic.exceptions import ServerError @@ -12,17 +12,15 @@ if TYPE_CHECKING: from sanic.compat import Header -from typing import Literal - -SameSite = Union[ - Literal["Strict"], - Literal["Lax"], - Literal["None"], - Literal["strict"], - Literal["lax"], - Literal["none"], -] +SameSite = ( + Literal["Strict"] + | Literal["Lax"] + | Literal["None"] + | Literal["strict"] + | Literal["lax"] + | Literal["none"] +) DEFAULT_MAX_AGE = 0 SAMESITE_VALUES = ("strict", "lax", "none") diff --git a/sanic/exceptions.py b/sanic/exceptions.py index 3a2ee14b23..e085f3cf5a 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -1,7 +1,7 @@ from asyncio import CancelledError from collections.abc import Sequence from os import PathLike -from typing import Any, Optional, Union +from typing import Any from sanic.helpers import STATUS_CODES from sanic.models.protocol_types import Range @@ -54,19 +54,19 @@ class SanicException(Exception): """ # noqa: E501 status_code: int = 500 - quiet: Optional[bool] = False + quiet: bool | None = False headers: dict[str, str] = {} message: str = "" def __init__( self, - message: Optional[Union[str, bytes]] = None, - status_code: Optional[int] = None, + message: str | bytes | None = None, + status_code: int | None = None, *, - quiet: Optional[bool] = None, - context: Optional[dict[str, Any]] = None, - extra: Optional[dict[str, Any]] = None, - headers: Optional[dict[str, str]] = None, + quiet: bool | None = None, + context: dict[str, Any] | None = None, + extra: dict[str, Any] | None = None, + headers: dict[str, str] | None = None, ) -> None: self.context = context self.extra = extra @@ -103,12 +103,12 @@ class HTTPException(SanicException): def __init__( self, - message: Optional[Union[str, bytes]] = None, + message: str | bytes | None = None, *, - quiet: Optional[bool] = None, - context: Optional[dict[str, Any]] = None, - extra: Optional[dict[str, Any]] = None, - headers: Optional[dict[str, Any]] = None, + quiet: bool | None = None, + context: dict[str, Any] | None = None, + extra: dict[str, Any] | None = None, + headers: dict[str, Any] | None = None, ) -> None: super().__init__( message, @@ -187,14 +187,14 @@ class MethodNotAllowed(HTTPException): def __init__( self, - message: Optional[Union[str, bytes]] = None, + message: str | bytes | None = None, method: str = "", - allowed_methods: Optional[Sequence[str]] = None, + allowed_methods: Sequence[str] | None = None, *, - quiet: Optional[bool] = None, - context: Optional[dict[str, Any]] = None, - extra: Optional[dict[str, Any]] = None, - headers: Optional[dict[str, Any]] = None, + quiet: bool | None = None, + context: dict[str, Any] | None = None, + extra: dict[str, Any] | None = None, + headers: dict[str, Any] | None = None, ): super().__init__( message, @@ -308,14 +308,14 @@ class FileNotFound(NotFound): def __init__( self, - message: Optional[Union[str, bytes]] = None, - path: Optional[PathLike] = None, - relative_url: Optional[str] = None, + message: str | bytes | None = None, + path: PathLike | None = None, + relative_url: str | None = None, *, - quiet: Optional[bool] = None, - context: Optional[dict[str, Any]] = None, - extra: Optional[dict[str, Any]] = None, - headers: Optional[dict[str, Any]] = None, + quiet: bool | None = None, + context: dict[str, Any] | None = None, + extra: dict[str, Any] | None = None, + headers: dict[str, Any] | None = None, ): super().__init__( message, @@ -438,13 +438,13 @@ class RangeNotSatisfiable(HTTPException): def __init__( self, - message: Optional[Union[str, bytes]] = None, - content_range: Optional[Range] = None, + message: str | bytes | None = None, + content_range: Range | None = None, *, - quiet: Optional[bool] = None, - context: Optional[dict[str, Any]] = None, - extra: Optional[dict[str, Any]] = None, - headers: Optional[dict[str, Any]] = None, + quiet: bool | None = None, + context: dict[str, Any] | None = None, + extra: dict[str, Any] | None = None, + headers: dict[str, Any] | None = None, ): super().__init__( message, @@ -530,12 +530,12 @@ class PyFileError(SanicException): def __init__( self, file, - status_code: Optional[int] = None, + status_code: int | None = None, *, - quiet: Optional[bool] = None, - context: Optional[dict[str, Any]] = None, - extra: Optional[dict[str, Any]] = None, - headers: Optional[dict[str, Any]] = None, + quiet: bool | None = None, + context: dict[str, Any] | None = None, + extra: dict[str, Any] | None = None, + headers: dict[str, Any] | None = None, ): super().__init__( "could not execute config file %s" % file, @@ -612,13 +612,13 @@ class Unauthorized(HTTPException): def __init__( self, - message: Optional[Union[str, bytes]] = None, - scheme: Optional[str] = None, + message: str | bytes | None = None, + scheme: str | None = None, *, - quiet: Optional[bool] = None, - context: Optional[dict[str, Any]] = None, - extra: Optional[dict[str, Any]] = None, - headers: Optional[dict[str, Any]] = None, + quiet: bool | None = None, + context: dict[str, Any] | None = None, + extra: dict[str, Any] | None = None, + headers: dict[str, Any] | None = None, **challenges, ): super().__init__( diff --git a/sanic/handlers/directory.py b/sanic/handlers/directory.py index 526a364017..34e7a749c2 100644 --- a/sanic/handlers/directory.py +++ b/sanic/handlers/directory.py @@ -5,7 +5,7 @@ from operator import itemgetter from pathlib import Path from stat import S_ISDIR -from typing import Optional, Union, cast +from typing import cast from sanic.exceptions import NotFound from sanic.pages.directory_page import DirectoryPage, FileInfo @@ -20,7 +20,7 @@ class DirectoryHandler: uri (str): The URI to serve the files at. directory (Path): The directory to serve files from. directory_view (bool): Whether to show a directory listing or not. - index (Optional[Union[str, Sequence[str]]]): The index file(s) to + index (str | Sequence[str] | None): The index file(s) to serve if the directory is requested. Defaults to None. """ @@ -29,7 +29,7 @@ def __init__( uri: str, directory: Path, directory_view: bool = False, - index: Optional[Union[str, Sequence[str]]] = None, + index: str | Sequence[str] | None = None, ) -> None: if isinstance(index, str): index = [index] @@ -81,7 +81,7 @@ def _index(self, location: Path, path: str, debug: bool): page = DirectoryPage(self._iter_files(location), path, debug) return html(page.render()) - def _prepare_file(self, path: Path) -> dict[str, Union[int, str]]: + def _prepare_file(self, path: Path) -> dict[str, int | str]: stat = path.stat() modified = ( datetime.fromtimestamp(stat.st_mtime) diff --git a/sanic/handlers/error.py b/sanic/handlers/error.py index 3f58b551ba..bda367e129 100644 --- a/sanic/handlers/error.py +++ b/sanic/handlers/error.py @@ -1,7 +1,5 @@ from __future__ import annotations -from typing import Optional - from sanic.errorpages import BaseRenderer, TextRenderer, exception_response from sanic.exceptions import ServerError from sanic.log import error_logger @@ -28,17 +26,17 @@ def __init__( base: type[BaseRenderer] = TextRenderer, ): self.cached_handlers: dict[ - tuple[type[BaseException], Optional[str]], Optional[RouteHandler] + tuple[type[BaseException], str | None], RouteHandler | None ] = {} self.debug = False self.base = base - def _full_lookup(self, exception, route_name: Optional[str] = None): + def _full_lookup(self, exception, route_name: str | None = None): return self.lookup(exception, route_name) def _add( self, - key: tuple[type[BaseException], Optional[str]], + key: tuple[type[BaseException], str | None], handler: RouteHandler, ) -> None: if key in self.cached_handlers: @@ -53,7 +51,7 @@ def _add( raise ServerError(message) self.cached_handlers[key] = handler - def add(self, exception, handler, route_names: Optional[list[str]] = None): + def add(self, exception, handler, route_names: list[str] | None = None): """Add a new exception handler to an already existing handler object. Args: @@ -72,7 +70,7 @@ def add(self, exception, handler, route_names: Optional[list[str]] = None): else: self._add((exception, None), handler) - def lookup(self, exception, route_name: Optional[str] = None): + def lookup(self, exception, route_name: str | None = None): """Lookup the existing instance of `ErrorHandler` and fetch the registered handler for a specific type of exception. This method leverages a dict lookup to speedup the retrieval process. diff --git a/sanic/headers.py b/sanic/headers.py index 588c9426f2..e064d6d047 100644 --- a/sanic/headers.py +++ b/sanic/headers.py @@ -3,7 +3,7 @@ import re from collections.abc import Iterable -from typing import Any, Optional, Union +from typing import Any from urllib.parse import unquote from sanic.exceptions import InvalidHeader @@ -15,7 +15,7 @@ # across the application (in request.py for example) HeaderIterable = Iterable[tuple[str, Any]] # Values convertible to str HeaderBytesIterable = Iterable[tuple[bytes, bytes]] -Options = dict[str, Union[int, str]] # key=value fields in various headers +Options = dict[str, int | str] # key=value fields in various headers OptionsIterable = Iterable[tuple[str, str]] # May contain duplicate keys _token, _quoted = r"([\w!#$%&'*+\-.^_`|~]+)", r'"([^"]*)"' @@ -86,8 +86,8 @@ def __eq__(self, other): def match( self, - mime_with_params: Union[str, MediaType], - ) -> Optional[MediaType]: + mime_with_params: str | MediaType, + ) -> MediaType | None: """Match this media type against another media type. Check if this media type matches the given mime type/subtype. @@ -142,7 +142,7 @@ def has_wildcard(self) -> bool: return any(part == "*" for part in (self.subtype, self.type)) @classmethod - def _parse(cls, mime_with_params: str) -> Optional[MediaType]: + def _parse(cls, mime_with_params: str) -> MediaType | None: mtype = mime_with_params.strip() if "/" not in mime_with_params: return None @@ -172,7 +172,7 @@ class Matched: header (MediaType): The header to match against, if any. """ - def __init__(self, mime: str, header: Optional[MediaType]): + def __init__(self, mime: str, header: MediaType | None): self.mime = mime self.header = header @@ -214,7 +214,7 @@ def _compare(self, other) -> tuple[bool, Matched]: f"mime types of '{self.mime}' and '{other}'" ) - def match(self, other: Union[str, Matched]) -> Optional[Matched]: + def match(self, other: str | Matched) -> Matched | None: """Match this MIME string against another MIME string. Check if this MIME string matches the given MIME string. Wildcards are supported both ways on both type and subtype. @@ -295,7 +295,7 @@ def __str__(self): return ", ".join(str(m) for m in self) -def parse_accept(accept: Optional[str]) -> AcceptList: +def parse_accept(accept: str | None) -> AcceptList: """Parse an Accept header and order the acceptable media types according to RFC 7231, s. 5.3.2 https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2 @@ -345,7 +345,7 @@ def parse_content_header(value: str) -> tuple[str, Options]: """ pos = value.find(";") if pos == -1: - options: dict[str, Union[int, str]] = {} + options: dict[str, int | str] = {} else: options = { m.group(1).lower(): (m.group(2) or m.group(3)) @@ -365,7 +365,7 @@ def parse_content_header(value: str) -> tuple[str, Options]: _rparam = re.compile(f"(?:{_token}|{_quoted})={_token}\\s*($|[;,])", re.ASCII) -def parse_forwarded(headers, config) -> Optional[Options]: +def parse_forwarded(headers, config) -> Options | None: """Parse RFC 7239 Forwarded headers. The value of `by` or `secret` must match `config.FORWARDED_SECRET` :return: dict with keys and values, or None if nothing matched @@ -379,7 +379,7 @@ def parse_forwarded(headers, config) -> Optional[Options]: return None # Loop over = elements from right to left sep = pos = None - options: list[tuple[str, str]] = [] + options_list: list[tuple[str, str]] = [] found = False for m in _rparam.finditer(header[::-1]): # Start of new element? (on parser skips and non-semicolon right sep) @@ -388,22 +388,22 @@ def parse_forwarded(headers, config) -> Optional[Options]: if found: break # Clear values and parse as new element - del options[:] + del options_list[:] pos = m.end() val_token, val_quoted, key, sep = m.groups() key = key.lower()[::-1] val = (val_token or val_quoted.replace('"\\', '"'))[::-1] - options.append((key, val)) + options_list.append((key, val)) if key in ("secret", "by") and val == secret: found = True # Check if we would return on next round, to avoid useless parse if found and sep != ";": break # If secret was found, return the matching options in left-to-right order - return fwd_normalize(reversed(options)) if found else None + return fwd_normalize(reversed(options_list)) if found else None -def parse_xforwarded(headers, config) -> Optional[Options]: +def parse_xforwarded(headers, config) -> Options | None: """Parse traditional proxy headers.""" real_ip_header = config.REAL_IP_HEADER proxies_count = config.PROXIES_COUNT @@ -450,7 +450,7 @@ def fwd_normalize(fwd: OptionsIterable) -> Options: Returns: Options: A dict of normalized key-value pairs. """ - ret: dict[str, Union[int, str]] = {} + ret: dict[str, int | str] = {} for key, val in fwd: if val is not None: try: @@ -487,7 +487,7 @@ def fwd_normalize_address(addr: str) -> str: return addr.lower() -def parse_host(host: str) -> tuple[Optional[str], Optional[int]]: +def parse_host(host: str) -> tuple[str | None, int | None]: """Split host:port into hostname and port. Args: @@ -529,9 +529,9 @@ def format_http1_response(status: int, headers: HeaderBytesIterable) -> bytes: def parse_credentials( - header: Optional[str], - prefixes: Optional[Union[list, tuple, set]] = None, -) -> tuple[Optional[str], Optional[str]]: + header: str | None, + prefixes: list | tuple | set | None = None, +) -> tuple[str | None, str | None]: """Parses any header with the aim to retrieve any credentials from it. Args: diff --git a/sanic/http/http1.py b/sanic/http/http1.py index 5e6e830048..511d0a0808 100644 --- a/sanic/http/http1.py +++ b/sanic/http/http1.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -496,7 +496,7 @@ async def __aiter__(self): if data: yield data - async def read(self) -> Optional[bytes]: # no cov + async def read(self) -> bytes | None: # no cov """Read some bytes of request body.""" # Send a 100-continue if needed diff --git a/sanic/http/http3.py b/sanic/http/http3.py index 7dae67336a..2fcc98759c 100644 --- a/sanic/http/http3.py +++ b/sanic/http/http3.py @@ -4,14 +4,7 @@ from abc import ABC, abstractmethod from ssl import SSLContext -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Optional, - Union, - cast, -) +from typing import TYPE_CHECKING, Any, Callable, cast from sanic.compat import Header from sanic.constants import LocalCertCreator @@ -53,7 +46,7 @@ from sanic.response import BaseHTTPResponse from sanic.server.protocols.http_protocol import Http3Protocol - HttpConnection = Union[H0Connection, H3Connection] + HttpConnection = H0Connection | H3Connection class HTTP3Transport(TransportProtocol): @@ -106,11 +99,11 @@ def __init__(self, *args, **kwargs) -> None: self.request_body = None self.stage = Stage.IDLE self.headers_sent = False - self.response: Optional[BaseHTTPResponse] = None + self.response: BaseHTTPResponse | None = None self.request_max_size = self.protocol.request_max_size self.request_bytes = 0 - async def run(self, exception: Optional[Exception] = None): + async def run(self, exception: Exception | None = None): """Handle the request and response cycle.""" self.stage = Stage.HANDLER self.head_only = self.request.method.upper() == "HEAD" @@ -396,13 +389,11 @@ def __init__(self) -> None: def add(self, ticket: SessionTicket) -> None: self.tickets[ticket.ticket] = ticket - def pop(self, label: bytes) -> Optional[SessionTicket]: + def pop(self, label: bytes) -> SessionTicket | None: return self.tickets.pop(label, None) -def get_config( - app: Sanic, ssl: Union[SanicSSLContext, CertSelector, SSLContext] -): +def get_config(app: Sanic, ssl: SanicSSLContext | CertSelector | SSLContext): # TODO: # - proper selection needed if service with multiple certs insted of # just taking the first diff --git a/sanic/http/stream.py b/sanic/http/stream.py index 104654cf60..027b621c81 100644 --- a/sanic/http/stream.py +++ b/sanic/http/stream.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional, Union +from typing import TYPE_CHECKING from sanic.http.constants import Stage @@ -12,11 +12,11 @@ class Stream: stage: Stage - response: Optional[BaseHTTPResponse] + response: BaseHTTPResponse | None protocol: HttpProtocol - url: Optional[str] - request_body: Optional[bytes] - request_max_size: Union[int, float] + url: str | None + request_body: bytes | None + request_max_size: int | float __touchup__: tuple[str, ...] = tuple() __slots__ = ("request_max_size",) diff --git a/sanic/http/tls/context.py b/sanic/http/tls/context.py index 3be0ecd735..3200f17376 100644 --- a/sanic/http/tls/context.py +++ b/sanic/http/tls/context.py @@ -4,7 +4,7 @@ import ssl from collections.abc import Iterable -from typing import Any, Optional, Union +from typing import Any from sanic.log import logger @@ -22,9 +22,9 @@ def create_context( - certfile: Optional[str] = None, - keyfile: Optional[str] = None, - password: Optional[str] = None, + certfile: str | None = None, + keyfile: str | None = None, + password: str | None = None, purpose: ssl.Purpose = ssl.Purpose.CLIENT_AUTH, ) -> ssl.SSLContext: """Create a context with secure crypto and HTTP/1.1 in protocols.""" @@ -40,8 +40,8 @@ def create_context( def shorthand_to_ctx( - ctxdef: Union[None, ssl.SSLContext, dict, str], -) -> Optional[ssl.SSLContext]: + ctxdef: None | ssl.SSLContext | dict | str, +) -> ssl.SSLContext | None: """Convert an ssl argument shorthand to an SSLContext object.""" if ctxdef is None or isinstance(ctxdef, ssl.SSLContext): return ctxdef @@ -56,8 +56,8 @@ def shorthand_to_ctx( def process_to_context( - ssldef: Union[None, ssl.SSLContext, dict, str, list, tuple], -) -> Optional[ssl.SSLContext]: + ssldef: None | ssl.SSLContext | dict | str | list | tuple, +) -> ssl.SSLContext | None: """Process app.run ssl argument from easy formats to full SSLContext.""" return ( CertSelector(map(shorthand_to_ctx, ssldef)) @@ -101,9 +101,7 @@ def find_cert(self: CertSelector, server_name: str): raise ValueError(f"No certificate found matching hostname {server_name!r}") -def match_hostname( - ctx: Union[ssl.SSLContext, CertSelector], hostname: str -) -> bool: +def match_hostname(ctx: ssl.SSLContext | CertSelector, hostname: str) -> bool: """Match names from CertSelector against a received hostname.""" # Local certs are considered trusted, so this can be less pedantic # and thus faster than the deprecated ssl.match_hostname function is. @@ -120,7 +118,7 @@ def match_hostname( def selector_sni_callback( sslobj: ssl.SSLObject, server_name: str, ctx: CertSelector -) -> Optional[int]: +) -> int | None: """Select a certificate matching the SNI.""" # Call server_name_callback to store the SNI on sslobj server_name_callback(sslobj, server_name, ctx) @@ -191,7 +189,7 @@ class CertSelector(ssl.SSLContext): def __new__(cls, ctxs): return super().__new__(cls) - def __init__(self, ctxs: Iterable[Optional[ssl.SSLContext]]): + def __init__(self, ctxs: Iterable[ssl.SSLContext | None]): super().__init__() self.sni_callback = selector_sni_callback # type: ignore self.sanic_select = [] diff --git a/sanic/http/tls/creators.py b/sanic/http/tls/creators.py index 7ba89b6bac..8ece9c5494 100644 --- a/sanic/http/tls/creators.py +++ b/sanic/http/tls/creators.py @@ -9,7 +9,7 @@ from pathlib import Path from tempfile import mkdtemp from types import ModuleType -from typing import TYPE_CHECKING, Optional, Union, cast +from typing import TYPE_CHECKING, cast from sanic.application.constants import Mode from sanic.application.spinner import loading @@ -47,7 +47,7 @@ ] -def _make_path(maybe_path: Union[Path, str], tmpdir: Optional[Path]) -> Path: +def _make_path(maybe_path: Path | str, tmpdir: Path | None) -> Path: if isinstance(maybe_path, Path): return maybe_path else: @@ -60,9 +60,7 @@ def _make_path(maybe_path: Union[Path, str], tmpdir: Optional[Path]) -> Path: return path -def get_ssl_context( - app: Sanic, ssl: Optional[ssl.SSLContext] -) -> ssl.SSLContext: +def get_ssl_context(app: Sanic, ssl: ssl.SSLContext | None) -> ssl.SSLContext: if ssl: return ssl @@ -126,7 +124,7 @@ def select( local_tls_key, local_tls_cert, ) -> CertCreator: - creator: Optional[CertCreator] = None + creator: CertCreator | None = None cert_creator_options: tuple[ tuple[type[CertCreator], LocalCertCreator], ... @@ -160,7 +158,7 @@ def select( @staticmethod def _try_select( app: Sanic, - creator: Optional[CertCreator], + creator: CertCreator | None, creator_class: type[CertCreator], creator_requirement: LocalCertCreator, creator_requested: LocalCertCreator, diff --git a/sanic/logging/setup.py b/sanic/logging/setup.py index 8674ad8199..5552a7cd37 100644 --- a/sanic/logging/setup.py +++ b/sanic/logging/setup.py @@ -1,8 +1,6 @@ import logging import os -from typing import Union - from sanic.helpers import Default, _default from sanic.log import ( access_logger, @@ -24,7 +22,7 @@ def setup_logging( debug: bool, no_color: bool = False, - log_extra: Union[bool, Default] = _default, + log_extra: bool | Default = _default, ) -> None: if AutoFormatter.SETUP: return diff --git a/sanic/middleware.py b/sanic/middleware.py index 150e2ccf81..65098e1bad 100644 --- a/sanic/middleware.py +++ b/sanic/middleware.py @@ -4,7 +4,6 @@ from collections.abc import Sequence from enum import IntEnum, auto from itertools import count -from typing import Deque, Union from sanic.models.handler_types import MiddlewareType @@ -70,9 +69,9 @@ def order(self) -> tuple[int, int]: @classmethod def convert( cls, - *middleware_collections: Sequence[Union[Middleware, MiddlewareType]], + *middleware_collections: Sequence[Middleware | MiddlewareType], location: MiddlewareLocation, - ) -> Deque[Middleware]: + ) -> deque[Middleware]: """Convert middleware collections to a deque of Middleware objects. Args: diff --git a/sanic/mixins/base.py b/sanic/mixins/base.py index d0261fa661..66b3cc855d 100644 --- a/sanic/mixins/base.py +++ b/sanic/mixins/base.py @@ -1,4 +1,6 @@ -from typing import Optional, Protocol, Union +from __future__ import annotations + +from typing import Protocol from sanic.base.meta import SanicMeta @@ -15,12 +17,12 @@ class BaseMixin(metaclass=SanicMeta): """Base class for various mixins.""" name: str - strict_slashes: Optional[bool] + strict_slashes: bool | None def _generate_name( - self, *objects: Union[NameProtocol, DunderNameProtocol, str] + self, *objects: NameProtocol | DunderNameProtocol | str ) -> str: - name: Optional[str] = None + name: str | None = None for obj in objects: if not obj: continue diff --git a/sanic/mixins/commands.py b/sanic/mixins/commands.py index d553fca125..02d12ed62b 100644 --- a/sanic/mixins/commands.py +++ b/sanic/mixins/commands.py @@ -2,7 +2,7 @@ from functools import wraps from inspect import isawaitable -from typing import Callable, Optional, Union +from typing import Callable from sanic.base.meta import SanicMeta from sanic.models.futures import FutureCommand @@ -13,8 +13,8 @@ def __init__(self, *args, **kwargs) -> None: self._future_commands: set[FutureCommand] = set() def command( - self, maybe_func: Optional[Callable] = None, *, name: str = "" - ) -> Union[Callable, Callable[[Callable], Callable]]: + self, maybe_func: Callable | None = None, *, name: str = "" + ) -> Callable | Callable[[Callable], Callable]: def decorator(f): @wraps(f) async def decorated_function(*args, **kwargs): diff --git a/sanic/mixins/exceptions.py b/sanic/mixins/exceptions.py index 1b9873094b..c3ed52fdc9 100644 --- a/sanic/mixins/exceptions.py +++ b/sanic/mixins/exceptions.py @@ -1,4 +1,6 @@ -from typing import Any, Callable, Union +from __future__ import annotations + +from typing import Any, Callable from sanic.base.meta import SanicMeta from sanic.models.futures import FutureException @@ -13,7 +15,7 @@ def _apply_exception_handler(self, handler: FutureException): def exception( self, - *exceptions: Union[type[Exception], list[type[Exception]]], + *exceptions: type[Exception] | list[type[Exception]], apply: bool = True, ) -> Callable: """Decorator used to register an exception handler for the current application or blueprint instance. diff --git a/sanic/mixins/listeners.py b/sanic/mixins/listeners.py index 4e10dfa6bd..25e3d2f7ac 100644 --- a/sanic/mixins/listeners.py +++ b/sanic/mixins/listeners.py @@ -1,6 +1,8 @@ +from __future__ import annotations + from enum import Enum, auto from functools import partial -from typing import Callable, Optional, Union, cast, overload +from typing import Callable, cast, overload from sanic.base.meta import SanicMeta from sanic.exceptions import BadRequest @@ -54,15 +56,15 @@ def listener( def listener( self, - listener_or_event: Union[ListenerType[Sanic], str], - event_or_none: Optional[str] = None, + listener_or_event: ListenerType[Sanic] | str, + event_or_none: str | None = None, apply: bool = True, *, priority: int = 0, - ) -> Union[ - ListenerType[Sanic], - Callable[[ListenerType[Sanic]], ListenerType[Sanic]], - ]: + ) -> ( + ListenerType[Sanic] + | Callable[[ListenerType[Sanic]], ListenerType[Sanic]] + ): """Create a listener for a specific event in the application's lifecycle. See [Listeners](/en/guide/basics/listeners) for more details. @@ -138,7 +140,7 @@ def register_listener( def _setup_listener( self, - listener: Optional[ListenerType[Sanic]], + listener: ListenerType[Sanic] | None, event: str, priority: int, ) -> ListenerType[Sanic]: @@ -150,7 +152,7 @@ def _setup_listener( ) def main_process_start( - self, listener: Optional[ListenerType[Sanic]], *, priority: int = 0 + self, listener: ListenerType[Sanic] | None, *, priority: int = 0 ) -> ListenerType[Sanic]: """Decorator for registering a listener for the main_process_start event. @@ -174,7 +176,7 @@ async def on_main_process_start(app: Sanic): return self._setup_listener(listener, "main_process_start", priority) def main_process_ready( - self, listener: Optional[ListenerType[Sanic]], *, priority: int = 0 + self, listener: ListenerType[Sanic] | None, *, priority: int = 0 ) -> ListenerType[Sanic]: """Decorator for registering a listener for the main_process_ready event. @@ -199,7 +201,7 @@ async def on_main_process_ready(app: Sanic): return self._setup_listener(listener, "main_process_ready", priority) def main_process_stop( - self, listener: Optional[ListenerType[Sanic]], *, priority: int = 0 + self, listener: ListenerType[Sanic] | None, *, priority: int = 0 ) -> ListenerType[Sanic]: """Decorator for registering a listener for the main_process_stop event. @@ -222,7 +224,7 @@ async def on_main_process_stop(app: Sanic): return self._setup_listener(listener, "main_process_stop", priority) def reload_process_start( - self, listener: Optional[ListenerType[Sanic]], *, priority: int = 0 + self, listener: ListenerType[Sanic] | None, *, priority: int = 0 ) -> ListenerType[Sanic]: """Decorator for registering a listener for the reload_process_start event. @@ -245,7 +247,7 @@ async def on_reload_process_start(app: Sanic): return self._setup_listener(listener, "reload_process_start", priority) def reload_process_stop( - self, listener: Optional[ListenerType[Sanic]], *, priority: int = 0 + self, listener: ListenerType[Sanic] | None, *, priority: int = 0 ) -> ListenerType[Sanic]: """Decorator for registering a listener for the reload_process_stop event. @@ -268,7 +270,7 @@ async def on_reload_process_stop(app: Sanic): return self._setup_listener(listener, "reload_process_stop", priority) def before_reload_trigger( - self, listener: Optional[ListenerType[Sanic]], *, priority: int = 0 + self, listener: ListenerType[Sanic] | None, *, priority: int = 0 ) -> ListenerType[Sanic]: """Decorator for registering a listener for the before_reload_trigger event. @@ -294,7 +296,7 @@ async def on_before_reload_trigger(app: Sanic): ) def after_reload_trigger( - self, listener: Optional[ListenerType[Sanic]], *, priority: int = 0 + self, listener: ListenerType[Sanic] | None, *, priority: int = 0 ) -> ListenerType[Sanic]: """Decorator for registering a listener for the after_reload_trigger event. @@ -319,7 +321,7 @@ async def on_after_reload_trigger(app: Sanic, changed: set[str]): def before_server_start( self, - listener: Optional[ListenerType[Sanic]] = None, + listener: ListenerType[Sanic] | None = None, *, priority: int = 0, ) -> ListenerType[Sanic]: @@ -347,7 +349,7 @@ async def on_before_server_start(app: Sanic): return self._setup_listener(listener, "before_server_start", priority) def after_server_start( - self, listener: Optional[ListenerType[Sanic]], *, priority: int = 0 + self, listener: ListenerType[Sanic] | None, *, priority: int = 0 ) -> ListenerType[Sanic]: """Decorator for registering a listener for the after_server_start event. @@ -377,7 +379,7 @@ async def on_after_server_start(app: Sanic): return self._setup_listener(listener, "after_server_start", priority) def before_server_stop( - self, listener: Optional[ListenerType[Sanic]], *, priority: int = 0 + self, listener: ListenerType[Sanic] | None, *, priority: int = 0 ) -> ListenerType[Sanic]: """Decorator for registering a listener for the before_server_stop event. @@ -404,7 +406,7 @@ async def on_before_server_stop(app: Sanic): return self._setup_listener(listener, "before_server_stop", priority) def after_server_stop( - self, listener: Optional[ListenerType[Sanic]], *, priority: int = 0 + self, listener: ListenerType[Sanic] | None, *, priority: int = 0 ) -> ListenerType[Sanic]: """Decorator for registering a listener for the after_server_stop event. diff --git a/sanic/mixins/middleware.py b/sanic/mixins/middleware.py index 0747178ce7..6e1b0567bc 100644 --- a/sanic/mixins/middleware.py +++ b/sanic/mixins/middleware.py @@ -1,7 +1,9 @@ +from __future__ import annotations + from collections import deque from functools import partial from operator import attrgetter -from typing import Callable, Union, overload +from typing import Callable, overload from sanic.base.meta import SanicMeta from sanic.middleware import Middleware, MiddlewareLocation @@ -40,12 +42,12 @@ def middleware( def middleware( self, - middleware_or_request: Union[MiddlewareType, str], + middleware_or_request: MiddlewareType | str, attach_to: str = "request", apply: bool = True, *, priority: int = 0, - ) -> Union[MiddlewareType, Callable[[MiddlewareType], MiddlewareType]]: + ) -> MiddlewareType | Callable[[MiddlewareType], MiddlewareType]: """Decorator for registering middleware. Decorate and register middleware to be called before a request is diff --git a/sanic/mixins/routes.py b/sanic/mixins/routes.py index 7cd5412205..9e97b89928 100644 --- a/sanic/mixins/routes.py +++ b/sanic/mixins/routes.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from ast import NodeVisitor, Return, parse from collections.abc import Iterable from contextlib import suppress @@ -6,8 +8,6 @@ from typing import ( Any, Callable, - Optional, - Union, cast, ) @@ -23,7 +23,7 @@ RouteWrapper = Callable[ - [RouteHandler], Union[RouteHandler, tuple[Route, RouteHandler]] + [RouteHandler], RouteHandler | tuple[Route, RouteHandler] ] @@ -38,20 +38,20 @@ def _apply_route(self, route: FutureRoute) -> list[Route]: def route( self, uri: str, - methods: Optional[Iterable[str]] = None, - host: Optional[Union[str, list[str]]] = None, - strict_slashes: Optional[bool] = None, + methods: Iterable[str] | None = None, + host: str | list[str] | None = None, + strict_slashes: bool | None = None, stream: bool = False, - version: Optional[Union[int, str, float]] = None, - name: Optional[str] = None, + version: int | str | float | None = None, + name: str | None = None, ignore_body: bool = False, apply: bool = True, - subprotocols: Optional[list[str]] = None, + subprotocols: list[str] | None = None, websocket: bool = False, unquote: bool = False, static: bool = False, version_prefix: str = "/v", - error_format: Optional[str] = None, + error_format: str | None = None, **ctx_kwargs: Any, ) -> RouteWrapper: """Decorate a function to be registered as a route. @@ -215,13 +215,13 @@ def add_route( handler: RouteHandler, uri: str, methods: Iterable[str] = frozenset({"GET"}), - host: Optional[Union[str, list[str]]] = None, - strict_slashes: Optional[bool] = None, - version: Optional[Union[int, str, float]] = None, - name: Optional[str] = None, + host: str | list[str] | None = None, + strict_slashes: bool | None = None, + version: int | str | float | None = None, + name: str | None = None, stream: bool = False, version_prefix: str = "/v", - error_format: Optional[str] = None, + error_format: str | None = None, unquote: bool = False, **ctx_kwargs: Any, ) -> RouteHandler: @@ -309,13 +309,13 @@ async def custom_middleware(request): def get( self, uri: str, - host: Optional[Union[str, list[str]]] = None, - strict_slashes: Optional[bool] = None, - version: Optional[Union[int, str, float]] = None, - name: Optional[str] = None, + host: str | list[str] | None = None, + strict_slashes: bool | None = None, + version: int | str | float | None = None, + name: str | None = None, ignore_body: bool = True, version_prefix: str = "/v", - error_format: Optional[str] = None, + error_format: str | None = None, **ctx_kwargs: Any, ) -> RouteHandler: """Decorate a function handler to create a route definition using the **GET** HTTP method. @@ -362,13 +362,13 @@ def get( def post( self, uri: str, - host: Optional[Union[str, list[str]]] = None, - strict_slashes: Optional[bool] = None, + host: str | list[str] | None = None, + strict_slashes: bool | None = None, stream: bool = False, - version: Optional[Union[int, str, float]] = None, - name: Optional[str] = None, + version: int | str | float | None = None, + name: str | None = None, version_prefix: str = "/v", - error_format: Optional[str] = None, + error_format: str | None = None, **ctx_kwargs: Any, ) -> RouteHandler: """Decorate a function handler to create a route definition using the **POST** HTTP method. @@ -413,13 +413,13 @@ def post( def put( self, uri: str, - host: Optional[Union[str, list[str]]] = None, - strict_slashes: Optional[bool] = None, + host: str | list[str] | None = None, + strict_slashes: bool | None = None, stream: bool = False, - version: Optional[Union[int, str, float]] = None, - name: Optional[str] = None, + version: int | str | float | None = None, + name: str | None = None, version_prefix: str = "/v", - error_format: Optional[str] = None, + error_format: str | None = None, **ctx_kwargs: Any, ) -> RouteHandler: """Decorate a function handler to create a route definition using the **PUT** HTTP method. @@ -464,13 +464,13 @@ def put( def head( self, uri: str, - host: Optional[Union[str, list[str]]] = None, - strict_slashes: Optional[bool] = None, - version: Optional[Union[int, str, float]] = None, - name: Optional[str] = None, + host: str | list[str] | None = None, + strict_slashes: bool | None = None, + version: int | str | float | None = None, + name: str | None = None, ignore_body: bool = True, version_prefix: str = "/v", - error_format: Optional[str] = None, + error_format: str | None = None, **ctx_kwargs: Any, ) -> RouteHandler: """Decorate a function handler to create a route definition using the **HEAD** HTTP method. @@ -517,13 +517,13 @@ def head( def options( self, uri: str, - host: Optional[Union[str, list[str]]] = None, - strict_slashes: Optional[bool] = None, - version: Optional[Union[int, str, float]] = None, - name: Optional[str] = None, + host: str | list[str] | None = None, + strict_slashes: bool | None = None, + version: int | str | float | None = None, + name: str | None = None, ignore_body: bool = True, version_prefix: str = "/v", - error_format: Optional[str] = None, + error_format: str | None = None, **ctx_kwargs: Any, ) -> RouteHandler: """Decorate a function handler to create a route definition using the **OPTIONS** HTTP method. @@ -570,13 +570,13 @@ def options( def patch( self, uri: str, - host: Optional[Union[str, list[str]]] = None, - strict_slashes: Optional[bool] = None, + host: str | list[str] | None = None, + strict_slashes: bool | None = None, stream=False, - version: Optional[Union[int, str, float]] = None, - name: Optional[str] = None, + version: int | str | float | None = None, + name: str | None = None, version_prefix: str = "/v", - error_format: Optional[str] = None, + error_format: str | None = None, **ctx_kwargs: Any, ) -> RouteHandler: """Decorate a function handler to create a route definition using the **PATCH** HTTP method. @@ -621,13 +621,13 @@ def patch( def delete( self, uri: str, - host: Optional[Union[str, list[str]]] = None, - strict_slashes: Optional[bool] = None, - version: Optional[Union[int, str, float]] = None, - name: Optional[str] = None, + host: str | list[str] | None = None, + strict_slashes: bool | None = None, + version: int | str | float | None = None, + name: str | None = None, ignore_body: bool = False, version_prefix: str = "/v", - error_format: Optional[str] = None, + error_format: str | None = None, **ctx_kwargs: Any, ) -> RouteHandler: """Decorate a function handler to create a route definition using the **DELETE** HTTP method. @@ -671,14 +671,14 @@ def delete( def websocket( self, uri: str, - host: Optional[Union[str, list[str]]] = None, - strict_slashes: Optional[bool] = None, - subprotocols: Optional[list[str]] = None, - version: Optional[Union[int, str, float]] = None, - name: Optional[str] = None, + host: str | list[str] | None = None, + strict_slashes: bool | None = None, + subprotocols: list[str] | None = None, + version: int | str | float | None = None, + name: str | None = None, apply: bool = True, version_prefix: str = "/v", - error_format: Optional[str] = None, + error_format: str | None = None, **ctx_kwargs: Any, ): """Decorate a function to be registered as a websocket route. @@ -725,13 +725,13 @@ def add_websocket_route( self, handler, uri: str, - host: Optional[Union[str, list[str]]] = None, - strict_slashes: Optional[bool] = None, + host: str | list[str] | None = None, + strict_slashes: bool | None = None, subprotocols=None, - version: Optional[Union[int, str, float]] = None, - name: Optional[str] = None, + version: int | str | float | None = None, + name: str | None = None, version_prefix: str = "/v", - error_format: Optional[str] = None, + error_format: str | None = None, **ctx_kwargs: Any, ): """A helper method to register a function as a websocket route. diff --git a/sanic/mixins/signals.py b/sanic/mixins/signals.py index 7c4d57adda..98d68211c4 100644 --- a/sanic/mixins/signals.py +++ b/sanic/mixins/signals.py @@ -2,7 +2,7 @@ from collections.abc import Coroutine from enum import Enum -from typing import Any, Callable, Optional, Union +from typing import Any, Callable from sanic.base.meta import SanicMeta from sanic.models.futures import FutureSignal @@ -20,10 +20,10 @@ def _apply_signal(self, signal: FutureSignal) -> Signal: def signal( self, - event: Union[str, Enum], + event: str | Enum, *, apply: bool = True, - condition: Optional[dict[str, Any]] = None, + condition: dict[str, Any] | None = None, exclusive: bool = True, priority: int = 0, ) -> Callable[[SignalHandler], SignalHandler]: @@ -70,9 +70,9 @@ def decorator(handler: SignalHandler): def add_signal( self, - handler: Optional[Callable[..., Any]], - event: Union[str, Enum], - condition: Optional[dict[str, Any]] = None, + handler: Callable[..., Any] | None, + event: str | Enum, + condition: dict[str, Any] | None = None, exclusive: bool = True, ) -> Callable[..., Any]: """Registers a signal handler for a specific event. diff --git a/sanic/mixins/startup.py b/sanic/mixins/startup.py index a6b3c26aa8..989c7ce3aa 100644 --- a/sanic/mixins/startup.py +++ b/sanic/mixins/startup.py @@ -34,8 +34,6 @@ Callable, ClassVar, Literal, - Optional, - Union, cast, ) @@ -78,7 +76,7 @@ SANIC_PACKAGES = ("sanic-routing", "sanic-testing", "sanic-ext") -HTTPVersion = Union[HTTP, Literal[1], Literal[3]] +HTTPVersion = HTTP | Literal[1] | Literal[3] class StartupMixin(metaclass=SanicMeta): @@ -153,28 +151,28 @@ def make_coffee(self, *args, **kwargs): def run( self, - host: Optional[str] = None, - port: Optional[int] = None, + host: str | None = None, + port: int | None = None, *, dev: bool = False, debug: bool = False, - auto_reload: Optional[bool] = None, + auto_reload: bool | None = None, version: HTTPVersion = HTTP.VERSION_1, - ssl: Union[None, SSLContext, dict, str, list, tuple] = None, - sock: Optional[socket] = None, + ssl: None | SSLContext | dict | str | list | tuple = None, + sock: socket | None = None, workers: int = 1, - protocol: Optional[type[Protocol]] = None, + protocol: type[Protocol] | None = None, backlog: int = 100, register_sys_signals: bool = True, - access_log: Optional[bool] = None, - unix: Optional[str] = None, - loop: Optional[AbstractEventLoop] = None, - reload_dir: Optional[Union[list[str], str]] = None, - noisy_exceptions: Optional[bool] = None, + access_log: bool | None = None, + unix: str | None = None, + loop: AbstractEventLoop | None = None, + reload_dir: list[str] | str | None = None, + noisy_exceptions: bool | None = None, motd: bool = True, fast: bool = False, verbosity: int = 0, - motd_display: Optional[dict[str, str]] = None, + motd_display: dict[str, str] | None = None, auto_tls: bool = False, single_process: bool = False, ) -> None: @@ -283,28 +281,28 @@ async def handler(request: Request): def prepare( self, - host: Optional[str] = None, - port: Optional[int] = None, + host: str | None = None, + port: int | None = None, *, dev: bool = False, debug: bool = False, - auto_reload: Optional[bool] = None, + auto_reload: bool | None = None, version: HTTPVersion = HTTP.VERSION_1, - ssl: Union[None, SSLContext, dict, str, list, tuple] = None, - sock: Optional[socket] = None, + ssl: None | SSLContext | dict | str | list | tuple = None, + sock: socket | None = None, workers: int = 1, - protocol: Optional[type[Protocol]] = None, + protocol: type[Protocol] | None = None, backlog: int = 100, register_sys_signals: bool = True, - access_log: Optional[bool] = None, - unix: Optional[str] = None, - loop: Optional[AbstractEventLoop] = None, - reload_dir: Optional[Union[list[str], str]] = None, - noisy_exceptions: Optional[bool] = None, + access_log: bool | None = None, + unix: str | None = None, + loop: AbstractEventLoop | None = None, + reload_dir: list[str] | str | None = None, + noisy_exceptions: bool | None = None, motd: bool = True, fast: bool = False, verbosity: int = 0, - motd_display: Optional[dict[str, str]] = None, + motd_display: dict[str, str] | None = None, coffee: bool = False, auto_tls: bool = False, single_process: bool = False, @@ -465,20 +463,20 @@ def prepare( async def create_server( self, - host: Optional[str] = None, - port: Optional[int] = None, + host: str | None = None, + port: int | None = None, *, debug: bool = False, - ssl: Union[None, SSLContext, dict, str, list, tuple] = None, - sock: Optional[socket] = None, - protocol: Optional[type[Protocol]] = None, + ssl: None | SSLContext | dict | str | list | tuple = None, + sock: socket | None = None, + protocol: type[Protocol] | None = None, backlog: int = 100, - access_log: Optional[bool] = None, - unix: Optional[str] = None, + access_log: bool | None = None, + unix: str | None = None, return_asyncio_server: bool = True, - asyncio_server_kwargs: Optional[dict[str, Any]] = None, - noisy_exceptions: Optional[bool] = None, - ) -> Optional[AsyncioServer]: + asyncio_server_kwargs: dict[str, Any] | None = None, + noisy_exceptions: bool | None = None, + ) -> AsyncioServer | None: """ Low level API for creating a Sanic Server instance. @@ -631,15 +629,15 @@ def stop(self, terminate: bool = True, unregister: bool = False) -> None: def _helper( self, - host: Optional[str] = None, - port: Optional[int] = None, + host: str | None = None, + port: int | None = None, debug: bool = False, version: HTTPVersion = HTTP.VERSION_1, - ssl: Union[None, SSLContext, dict, str, list, tuple] = None, - sock: Optional[socket] = None, - unix: Optional[str] = None, + ssl: None | SSLContext | dict | str | list | tuple = None, + sock: socket | None = None, + unix: str | None = None, workers: int = 1, - loop: Optional[AbstractEventLoop] = None, + loop: AbstractEventLoop | None = None, protocol: type[Protocol] = HttpProtocol, backlog: int = 100, register_sys_signals: bool = True, @@ -722,7 +720,7 @@ def _helper( def motd( self, - server_settings: Optional[dict[str, Any]] = None, + server_settings: dict[str, Any] | None = None, ) -> None: """Outputs the message of the day (MOTD). @@ -751,7 +749,7 @@ def motd( MOTD.output(logo, serve_location, display, extra) def get_motd_data( - self, server_settings: Optional[dict[str, Any]] = None + self, server_settings: dict[str, Any] | None = None ) -> tuple[dict[str, Any], dict[str, Any]]: """Retrieves the message of the day (MOTD) data. @@ -840,7 +838,7 @@ def serve_location(self) -> str: @staticmethod def get_server_location( - server_settings: Optional[dict[str, Any]] = None, + server_settings: dict[str, Any] | None = None, ) -> str: """Using the server settings, retrieve the server location. @@ -875,8 +873,8 @@ def get_server_location( @staticmethod def get_address( - host: Optional[str], - port: Optional[int], + host: str | None, + port: int | None, version: HTTPVersion = HTTP.VERSION_1, auto_tls: bool = False, ) -> tuple[str, int]: @@ -950,10 +948,10 @@ def _get_context(cls) -> BaseContext: @classmethod def serve( cls, - primary: Optional[Sanic] = None, + primary: Sanic | None = None, *, - app_loader: Optional[AppLoader] = None, - factory: Optional[Callable[[], Sanic]] = None, + app_loader: AppLoader | None = None, + factory: Callable[[], Sanic] | None = None, ) -> None: """Serve one or more Sanic applications. @@ -1216,7 +1214,7 @@ def _get_process_states(worker_state) -> list[str]: ] @classmethod - def serve_single(cls, primary: Optional[Sanic] = None) -> None: + def serve_single(cls, primary: Sanic | None = None) -> None: """Serve a single process of a Sanic application. Similar to `serve`, but only serves a single process. When used, @@ -1314,7 +1312,6 @@ def serve_single(cls, primary: Optional[Sanic] = None) -> None: async def _start_servers( self, primary: Sanic, - _, apps: list[Sanic], ) -> None: for app in apps: diff --git a/sanic/mixins/static.py b/sanic/mixins/static.py index 17329661aa..defc8641d3 100644 --- a/sanic/mixins/static.py +++ b/sanic/mixins/static.py @@ -1,9 +1,10 @@ +from __future__ import annotations + from collections.abc import Sequence from email.utils import formatdate from functools import partial, wraps from os import PathLike, path from pathlib import Path, PurePath -from typing import Optional, Union from urllib.parse import unquote from sanic_routing.route import Route @@ -31,20 +32,20 @@ def _apply_static(self, static: FutureStatic) -> Route: def static( self, uri: str, - file_or_directory: Union[PathLike, str], + file_or_directory: PathLike | str, pattern: str = r"/?.+", use_modified_since: bool = True, use_content_range: bool = False, - stream_large_files: Union[bool, int] = False, + stream_large_files: bool | int = False, name: str = "static", - host: Optional[str] = None, - strict_slashes: Optional[bool] = None, - content_type: Optional[str] = None, + host: str | None = None, + strict_slashes: bool | None = None, + content_type: str | None = None, apply: bool = True, - resource_type: Optional[str] = None, - index: Optional[Union[str, Sequence[str]]] = None, + resource_type: str | None = None, + index: str | Sequence[str] | None = None, directory_view: bool = False, - directory_handler: Optional[DirectoryHandler] = None, + directory_handler: DirectoryHandler | None = None, ): """Register a root to serve files from. The input can either be a file or a directory. @@ -252,10 +253,10 @@ async def _static_request_handler( file_or_directory: str, use_modified_since: bool, use_content_range: bool, - stream_large_files: Union[bool, int], + stream_large_files: bool | int, directory_handler: DirectoryHandler, - content_type: Optional[str] = None, - __file_uri__: Optional[str] = None, + content_type: str | None = None, + __file_uri__: str | None = None, ): not_found = FileNotFound( "File not found", diff --git a/sanic/models/asgi.py b/sanic/models/asgi.py index c358b09c27..2a2f19e36e 100644 --- a/sanic/models/asgi.py +++ b/sanic/models/asgi.py @@ -1,7 +1,7 @@ import asyncio from collections.abc import Awaitable, MutableMapping -from typing import Any, Callable, Optional, Union +from typing import Any, Callable from sanic.exceptions import BadRequest from sanic.models.protocol_types import TransportProtocol @@ -48,7 +48,7 @@ async def drain(self) -> None: class MockTransport(TransportProtocol): # no cov - _protocol: Optional[MockProtocol] + _protocol: MockProtocol | None def __init__( self, scope: ASGIScope, receive: ASGIReceive, send: ASGISend @@ -57,16 +57,14 @@ def __init__( self._receive = receive self._send = send self._protocol = None - self.loop: Optional[asyncio.AbstractEventLoop] = None + self.loop: asyncio.AbstractEventLoop | None = None def get_protocol(self) -> MockProtocol: # type: ignore if not self._protocol: self._protocol = MockProtocol(self, self.loop) return self._protocol - def get_extra_info( - self, info: str, default=None - ) -> Optional[Union[str, bool]]: + def get_extra_info(self, info: str, default=None) -> str | bool | None: if info == "peername": return self.scope.get("client") elif info == "sslcontext": diff --git a/sanic/models/ctx_types.py b/sanic/models/ctx_types.py index 2a0de84c76..7f5d2e4c09 100644 --- a/sanic/models/ctx_types.py +++ b/sanic/models/ctx_types.py @@ -1,4 +1,4 @@ -from typing import Any, NamedTuple, Optional +from typing import Any, NamedTuple class REPLLocal(NamedTuple): @@ -21,8 +21,8 @@ def __init__(self): def add( self, var: Any, - name: Optional[str] = None, - desc: Optional[str] = None, + name: str | None = None, + desc: str | None = None, ): """Add a local variable to be available in REPL context. diff --git a/sanic/models/futures.py b/sanic/models/futures.py index 374802771a..673ef09a58 100644 --- a/sanic/models/futures.py +++ b/sanic/models/futures.py @@ -1,6 +1,6 @@ from collections.abc import Iterable from pathlib import Path -from typing import Callable, NamedTuple, Optional, Union +from typing import Callable, NamedTuple from sanic.handlers.directory import DirectoryHandler from sanic.models.handler_types import ( @@ -15,19 +15,19 @@ class FutureRoute(NamedTuple): handler: str uri: str - methods: Optional[Iterable[str]] - host: Union[str, list[str]] + methods: Iterable[str] | None + host: str | list[str] strict_slashes: bool stream: bool - version: Optional[int] + version: int | None name: str ignore_body: bool websocket: bool - subprotocols: Optional[list[str]] + subprotocols: list[str] | None unquote: bool static: bool version_prefix: str - error_format: Optional[str] + error_format: str | None route_context: HashableDict @@ -53,19 +53,19 @@ class FutureStatic(NamedTuple): pattern: str use_modified_since: bool use_content_range: bool - stream_large_files: Union[bool, int] + stream_large_files: bool | int name: str - host: Optional[str] - strict_slashes: Optional[bool] - content_type: Optional[str] - resource_type: Optional[str] + host: str | None + strict_slashes: bool | None + content_type: str | None + resource_type: str | None directory_handler: DirectoryHandler class FutureSignal(NamedTuple): handler: SignalHandler event: str - condition: Optional[dict[str, str]] + condition: dict[str, str] | None exclusive: bool priority: int diff --git a/sanic/models/handler_types.py b/sanic/models/handler_types.py index 1f9abaeb98..2b7eadeea2 100644 --- a/sanic/models/handler_types.py +++ b/sanic/models/handler_types.py @@ -1,6 +1,6 @@ from asyncio.events import AbstractEventLoop from collections.abc import Coroutine -from typing import Any, Callable, Optional, TypeVar, Union +from typing import Any, Callable, TypeVar import sanic @@ -11,20 +11,20 @@ Sanic = TypeVar("Sanic", bound="sanic.Sanic") Request = TypeVar("Request", bound="request.Request") -MiddlewareResponse = Union[ - Optional[HTTPResponse], Coroutine[Any, Any, Optional[HTTPResponse]] -] +MiddlewareResponse = ( + HTTPResponse | None | Coroutine[Any, Any, HTTPResponse | None] +) RequestMiddlewareType = Callable[[Request], MiddlewareResponse] ResponseMiddlewareType = Callable[ [Request, BaseHTTPResponse], MiddlewareResponse ] ErrorMiddlewareType = Callable[ - [Request, BaseException], Optional[Coroutine[Any, Any, None]] -] -MiddlewareType = Union[RequestMiddlewareType, ResponseMiddlewareType] -ListenerType = Union[ - Callable[[Sanic], Optional[Coroutine[Any, Any, None]]], - Callable[[Sanic, AbstractEventLoop], Optional[Coroutine[Any, Any, None]]], + [Request, BaseException], Coroutine[Any, Any, None] | None ] -RouteHandler = Callable[..., Coroutine[Any, Any, Optional[HTTPResponse]]] +MiddlewareType = RequestMiddlewareType | ResponseMiddlewareType +ListenerType = ( + Callable[[Sanic], Coroutine[Any, Any, None] | None] + | Callable[[Sanic, AbstractEventLoop], Coroutine[Any, Any, None] | None] +) +RouteHandler = Callable[..., Coroutine[Any, Any, HTTPResponse | None]] SignalHandler = Callable[..., Coroutine[Any, Any, None]] diff --git a/sanic/models/http_types.py b/sanic/models/http_types.py index 595eaf0e61..fc86b6ca64 100644 --- a/sanic/models/http_types.py +++ b/sanic/models/http_types.py @@ -2,15 +2,14 @@ from base64 import b64decode from dataclasses import dataclass, field -from typing import Optional @dataclass() class Credentials: - auth_type: Optional[str] - token: Optional[str] - _username: Optional[str] = field(default=None) - _password: Optional[str] = field(default=None) + auth_type: str | None + token: str | None + _username: str | None = field(default=None) + _password: str | None = field(default=None) def __post_init__(self): if self._auth_is_basic: diff --git a/sanic/models/protocol_types.py b/sanic/models/protocol_types.py index 19417153bb..7ff220e2b1 100644 --- a/sanic/models/protocol_types.py +++ b/sanic/models/protocol_types.py @@ -1,7 +1,7 @@ from __future__ import annotations from asyncio import BaseTransport -from typing import TYPE_CHECKING, Optional, Union +from typing import TYPE_CHECKING, Protocol if TYPE_CHECKING: @@ -9,20 +9,17 @@ from sanic.models.asgi import ASGIScope -from typing import Protocol - - class HTMLProtocol(Protocol): - def __html__(self) -> Union[str, bytes]: ... + def __html__(self) -> str | bytes: ... - def _repr_html_(self) -> Union[str, bytes]: ... + def _repr_html_(self) -> str | bytes: ... class Range(Protocol): - start: Optional[int] - end: Optional[int] - size: Optional[int] - total: Optional[int] + start: int | None + end: int | None + size: int | None + total: int | None __slots__ = () diff --git a/sanic/models/server_types.py b/sanic/models/server_types.py index d08bcf8e83..88b9d5b79c 100644 --- a/sanic/models/server_types.py +++ b/sanic/models/server_types.py @@ -2,7 +2,7 @@ from ssl import SSLContext, SSLObject from types import SimpleNamespace -from typing import Any, Optional +from typing import Any from sanic.models.protocol_types import TransportProtocol @@ -35,7 +35,7 @@ class ConnInfo: def __init__(self, transport: TransportProtocol, unix=None): self.ctx = SimpleNamespace() self.lost = False - self.peername: Optional[tuple[str, int]] = None + self.peername: tuple[str, int] | None = None self.server = self.client = "" self.server_port = self.client_port = 0 self.client_ip = "" @@ -44,8 +44,8 @@ def __init__(self, transport: TransportProtocol, unix=None): self.server_name = "" self.cert: dict[str, Any] = {} self.network_paths: list[Any] = [] - sslobj: Optional[SSLObject] = transport.get_extra_info("ssl_object") # type: ignore - sslctx: Optional[SSLContext] = transport.get_extra_info("ssl_context") # type: ignore + sslobj: SSLObject | None = transport.get_extra_info("ssl_object") # type: ignore + sslctx: SSLContext | None = transport.get_extra_info("ssl_context") # type: ignore if sslobj: self.ssl = True self.server_name = getattr(sslobj, "sanic_server_name", None) or "" diff --git a/sanic/pages/css.py b/sanic/pages/css.py index 8852d31a02..519c0bbe90 100644 --- a/sanic/pages/css.py +++ b/sanic/pages/css.py @@ -1,12 +1,11 @@ from abc import ABCMeta from pathlib import Path -from typing import Optional CURRENT_DIR = Path(__file__).parent -def _extract_style(maybe_style: Optional[str], name: str) -> str: +def _extract_style(maybe_style: str | None, name: str) -> str: if maybe_style is not None: maybe_path = Path(maybe_style) if maybe_path.exists(): diff --git a/sanic/request/form.py b/sanic/request/form.py index 3f3a416660..192f71e44e 100644 --- a/sanic/request/form.py +++ b/sanic/request/form.py @@ -37,7 +37,7 @@ def parse_multipart_form(body, boundary): boundary (bytes): Bytes multipart boundary. Returns: - Tuple[RequestParameters, RequestParameters]: A tuple containing fields and files as `RequestParameters`. + tuple[RequestParameters, RequestParameters]: A tuple containing fields and files as `RequestParameters`. """ # noqa: E501 files = {} fields = {} diff --git a/sanic/request/parameters.py b/sanic/request/parameters.py index f4e83b2e9e..5d5b9e913b 100644 --- a/sanic/request/parameters.py +++ b/sanic/request/parameters.py @@ -1,31 +1,31 @@ from __future__ import annotations -from typing import Any, Optional +from typing import Any class RequestParameters(dict): """Hosts a dict with lists as values where get returns the first value of the list and getlist returns the whole shebang""" # noqa: E501 - def get(self, name: str, default: Optional[Any] = None) -> Optional[Any]: + def get(self, name: str, default: Any | None = None) -> Any | None: """Return the first value, either the default or actual Args: name (str): The name of the parameter - default (Optional[Any], optional): The default value. Defaults to None. + default (Any | None, optional): The default value. Defaults to None. Returns: - Optional[Any]: The first value of the list + Any | None: The first value of the list """ # noqa: E501 return super().get(name, [default])[0] def getlist( - self, name: str, default: Optional[list[Any]] = None + self, name: str, default: list[Any] | None = None ) -> list[Any]: """Return the entire list Args: name (str): The name of the parameter - default (Optional[List[Any]], optional): The default value. Defaults to None. + default (list[Any] | None, optional): The default value. Defaults to None. Returns: list[Any]: The entire list of values or [] if not found diff --git a/sanic/request/types.py b/sanic/request/types.py index 0cf064e4aa..187978f1aa 100644 --- a/sanic/request/types.py +++ b/sanic/request/types.py @@ -1,16 +1,14 @@ from __future__ import annotations from asyncio import BaseProtocol +from collections import defaultdict from contextvars import ContextVar from inspect import isawaitable from types import SimpleNamespace from typing import ( TYPE_CHECKING, Any, - DefaultDict, Generic, - Optional, - Union, cast, ) @@ -30,7 +28,6 @@ import uuid -from collections import defaultdict from urllib.parse import parse_qs, parse_qsl, urlunparse from httptools import parse_url @@ -161,8 +158,8 @@ def __init__( except HttpParserInvalidURLError: url = url_bytes.decode(errors="backslashreplace") raise BadURL(f"Bad URL: {url}") - self._id: Optional[Union[uuid.UUID, str, int]] = None - self._name: Optional[str] = None + self._id: uuid.UUID | str | int | None = None + self._name: str | None = None self._stream_id = stream_id self.app = app @@ -174,29 +171,29 @@ def __init__( # Init but do not inhale self.body = b"" - self.conn_info: Optional[ConnInfo] = None - self._ctx: Optional[ctx_type] = None - self.parsed_accept: Optional[AcceptList] = None - self.parsed_args: DefaultDict[ + self.conn_info: ConnInfo | None = None + self._ctx: ctx_type | None = None + self.parsed_accept: AcceptList | None = None + self.parsed_args: defaultdict[ tuple[bool, bool, str, str], RequestParameters ] = defaultdict(RequestParameters) - self.parsed_cookies: Optional[RequestParameters] = None - self.parsed_credentials: Optional[Credentials] = None - self.parsed_files: Optional[RequestParameters] = None - self.parsed_form: Optional[RequestParameters] = None - self.parsed_forwarded: Optional[Options] = None + self.parsed_cookies: RequestParameters | None = None + self.parsed_credentials: Credentials | None = None + self.parsed_files: RequestParameters | None = None + self.parsed_form: RequestParameters | None = None + self.parsed_forwarded: Options | None = None self.parsed_json = None - self.parsed_not_grouped_args: DefaultDict[ + self.parsed_not_grouped_args: defaultdict[ tuple[bool, bool, str, str], list[tuple[str, str]] ] = defaultdict(list) - self.parsed_token: Optional[str] = None + self.parsed_token: str | None = None self._request_middleware_started = False self._response_middleware_started = False self.responded: bool = False - self.route: Optional[Route] = None - self.stream: Optional[Stream] = None + self.route: Route | None = None + self.stream: Stream | None = None self._match_info: dict[str, Any] = {} - self._protocol: Optional[BaseProtocol] = None + self._protocol: BaseProtocol | None = None def __repr__(self): class_name = self.__class__.__name__ @@ -251,14 +248,14 @@ def format(self, record): return request @classmethod - def generate_id(*_) -> Union[uuid.UUID, str, int]: + def generate_id(*_) -> uuid.UUID | str | int: """Generate a unique ID for the request. This method is called to generate a unique ID for each request. By default, it returns a `uuid.UUID` instance. Returns: - Union[uuid.UUID, str, int]: A unique ID for the request. + uuid.UUID | str | int: A unique ID for the request. """ return uuid.uuid4() @@ -320,11 +317,11 @@ def reset_response(self) -> None: async def respond( self, - response: Optional[BaseHTTPResponse] = None, + response: BaseHTTPResponse | None = None, *, status: int = 200, - headers: Optional[Union[Header, dict[str, str]]] = None, - content_type: Optional[str] = None, + headers: Header | dict[str, str] | None = None, + content_type: str | None = None, ): """Respond to the request without returning. @@ -365,8 +362,8 @@ async def add_header(_, response: HTTPResponse): Args: response (ResponseType): Response instance to send. status (int): Status code to return in the response. - headers (Optional[Dict[str, str]]): Headers to return in the response, defaults to None. - content_type (Optional[str]): Content-Type header of the response, defaults to None. + headers (dict[str, str] | None): Headers to return in the response, defaults to None. + content_type (str | None): Content-Type header of the response, defaults to None. Returns: FinalResponseType: Final response being sent (may be different from the @@ -424,7 +421,7 @@ async def receive_body(self): self.body = b"".join([data async for data in self.stream]) @property - def name(self) -> Optional[str]: + def name(self) -> str | None: """The route name In the following pattern: @@ -434,7 +431,7 @@ def name(self) -> Optional[str]: ``` Returns: - Optional[str]: The route name + str | None: The route name """ if self._name: return self._name @@ -443,20 +440,20 @@ def name(self) -> Optional[str]: return None @property - def endpoint(self) -> Optional[str]: + def endpoint(self) -> str | None: """Alias of `sanic.request.Request.name` Returns: - Optional[str]: The route name + str | None: The route name """ return self.name @property - def uri_template(self) -> Optional[str]: + def uri_template(self) -> str | None: """The defined URI template Returns: - Optional[str]: The defined URI template + str | None: The defined URI template """ if self.route: return f"/{self.route.path}" @@ -494,7 +491,7 @@ def request_line(self) -> bytes: return bytes(reqline) @property - def id(self) -> Optional[Union[uuid.UUID, str, int]]: + def id(self) -> uuid.UUID | str | int | None: """A request ID passed from the client, or generated from the backend. By default, this will look in a request header defined at: @@ -521,7 +518,7 @@ def generate_id(self): ``` Returns: - Optional[Union[uuid.UUID, str, int]]: A request ID passed from the + uuid.UUID | str | int | None: A request ID passed from the client, or generated from the backend. """ if not self._id: @@ -593,11 +590,11 @@ def accept(self) -> AcceptList: return self.parsed_accept @property - def token(self) -> Optional[str]: + def token(self) -> str | None: """Attempt to return the auth header token. Returns: - Optional[str]: The auth header token + str | None: The auth header token """ if self.parsed_token is None: prefixes = ("Bearer", "Token") @@ -608,14 +605,14 @@ def token(self) -> Optional[str]: return self.parsed_token @property - def credentials(self) -> Optional[Credentials]: + def credentials(self) -> Credentials | None: """Attempt to return the auth header value. Covers NoAuth, Basic Auth, Bearer Token, Api Token authentication schemas. Returns: - Optional[Credentials]: A Credentials object with token, or username + Credentials | None: A Credentials object with token, or username and password related to the request """ if self.parsed_credentials is None: @@ -633,14 +630,14 @@ def credentials(self) -> Optional[Credentials]: def get_form( self, keep_blank_values: bool = False - ) -> Optional[RequestParameters]: + ) -> RequestParameters | None: """Method to extract and parse the form data from a request. Args: keep_blank_values (bool): Whether to discard blank values from the form data. Returns: - Optional[RequestParameters]: The parsed form data. + RequestParameters | None: The parsed form data. """ # noqa: E501 self.parsed_form = RequestParameters() self.parsed_files = RequestParameters() @@ -670,11 +667,11 @@ def get_form( return self.parsed_form @property - def form(self) -> Optional[RequestParameters]: + def form(self) -> RequestParameters | None: """The request body parsed as form data Returns: - Optional[RequestParameters]: The request body parsed as form data + RequestParameters | None: The request body parsed as form data """ if self.parsed_form is None: self.get_form() @@ -682,11 +679,11 @@ def form(self) -> Optional[RequestParameters]: return self.parsed_form @property - def files(self) -> Optional[RequestParameters]: + def files(self) -> RequestParameters | None: """The request body parsed as uploaded files Returns: - Optional[RequestParameters]: The request body parsed as uploaded files + RequestParameters | None: The request body parsed as uploaded files """ # noqa: E501 if self.parsed_files is None: self.form # compute form to get files @@ -840,7 +837,7 @@ def match_info(self) -> dict[str, Any]: """Matched path parameters after resolving route Returns: - Dict[str, Any]: Matched path parameters after resolving route + dict[str, Any]: Matched path parameters after resolving route """ return self._match_info @@ -867,11 +864,11 @@ def port(self) -> int: return self.conn_info.client_port if self.conn_info else 0 @property - def socket(self) -> Union[tuple[str, int], tuple[None, None]]: + def socket(self) -> tuple[str, int] | tuple[None, None]: """Information about the connected socket if available Returns: - Tuple[Optional[str], Optional[int]]: Information about the + tuple[str, int] | tuple[None, None]: Information about the connected socket if available, in the form of a tuple of (ip, port) """ @@ -891,11 +888,11 @@ def path(self) -> str: return self._parsed_url.path.decode("utf-8") @property - def network_paths(self) -> Optional[list[Any]]: + def network_paths(self) -> list[Any] | None: """Access the network paths if available Returns: - Optional[List[Any]]: Access the network paths if available + list[Any] | None: Access the network paths if available """ if self.conn_info is None: return None diff --git a/sanic/response/convenience.py b/sanic/response/convenience.py index 0c10fa4068..da82db92ae 100644 --- a/sanic/response/convenience.py +++ b/sanic/response/convenience.py @@ -6,7 +6,7 @@ from os import path from pathlib import PurePath from time import time -from typing import Any, AnyStr, Callable, Optional, Union +from typing import Any, AnyStr, Callable from urllib.parse import quote_plus from sanic.compat import Header, open_async, stat_async @@ -19,7 +19,7 @@ def empty( - status: int = 204, headers: Optional[dict[str, str]] = None + status: int = 204, headers: dict[str, str] | None = None ) -> HTTPResponse: """Returns an empty response to the client. @@ -36,9 +36,9 @@ def empty( def json( body: Any, status: int = 200, - headers: Optional[dict[str, str]] = None, + headers: dict[str, str] | None = None, content_type: str = "application/json", - dumps: Optional[Callable[..., AnyStr]] = None, + dumps: Callable[..., AnyStr] | None = None, **kwargs: Any, ) -> JSONResponse: """Returns response object with body in json format. @@ -67,7 +67,7 @@ def json( def text( body: str, status: int = 200, - headers: Optional[dict[str, str]] = None, + headers: dict[str, str] | None = None, content_type: str = "text/plain; charset=utf-8", ) -> HTTPResponse: """Returns response object with body in text format. @@ -95,9 +95,9 @@ def text( def raw( - body: Optional[AnyStr], + body: AnyStr | None, status: int = 200, - headers: Optional[dict[str, str]] = None, + headers: dict[str, str] | None = None, content_type: str = DEFAULT_HTTP_CONTENT_TYPE, ) -> HTTPResponse: """Returns response object without encoding the body. @@ -120,9 +120,9 @@ def raw( def html( - body: Union[str, bytes, HTMLProtocol], + body: str | bytes | HTMLProtocol, status: int = 200, - headers: Optional[dict[str, str]] = None, + headers: dict[str, str] | None = None, ) -> HTTPResponse: """Returns response object with body in html format. @@ -151,8 +151,8 @@ def html( async def validate_file( - request_headers: Header, last_modified: Union[datetime, float, int] -) -> Optional[HTTPResponse]: + request_headers: Header, last_modified: datetime | float | int +) -> HTTPResponse | None: """Validate file based on request headers. Args: @@ -204,17 +204,17 @@ async def validate_file( async def file( - location: Union[str, PurePath], + location: str | PurePath, status: int = 200, - request_headers: Optional[Header] = None, + request_headers: Header | None = None, validate_when_requested: bool = True, - mime_type: Optional[str] = None, - headers: Optional[dict[str, str]] = None, - filename: Optional[str] = None, - last_modified: Optional[Union[datetime, float, int, Default]] = _default, - max_age: Optional[Union[float, int]] = None, - no_store: Optional[bool] = None, - _range: Optional[Range] = None, + mime_type: str | None = None, + headers: dict[str, str] | None = None, + filename: str | None = None, + last_modified: datetime | float | int | Default | None = _default, + max_age: float | int | None = None, + no_store: bool | None = None, + _range: Range | None = None, ) -> HTTPResponse: """Return a response object with file data. @@ -303,7 +303,7 @@ async def file( def redirect( to: str, - headers: Optional[dict[str, str]] = None, + headers: dict[str, str] | None = None, status: int = 302, content_type: str = "text/html; charset=utf-8", ) -> HTTPResponse: @@ -332,13 +332,13 @@ def redirect( async def file_stream( - location: Union[str, PurePath], + location: str | PurePath, status: int = 200, chunk_size: int = 4096, - mime_type: Optional[str] = None, - headers: Optional[dict[str, str]] = None, - filename: Optional[str] = None, - _range: Optional[Range] = None, + mime_type: str | None = None, + headers: dict[str, str] | None = None, + filename: str | None = None, + _range: Range | None = None, ) -> ResponseStream: """Return a streaming response object with file data. @@ -400,7 +400,7 @@ async def _streaming_fn(response): def guess_content_type( - file_path: Union[str, PurePath], + file_path: str | PurePath, fallback: str = DEFAULT_HTTP_CONTENT_TYPE, ) -> str: """Guess the content type (rather than MIME only) by the file extension.""" diff --git a/sanic/response/types.py b/sanic/response/types.py index 7a1b9c45ce..e9b6fd5c30 100644 --- a/sanic/response/types.py +++ b/sanic/response/types.py @@ -7,9 +7,7 @@ Any, AnyStr, Callable, - Optional, TypeVar, - Union, ) from sanic.compat import Header @@ -45,18 +43,18 @@ class BaseHTTPResponse: def __init__(self): self.asgi: bool = False - self.body: Optional[bytes] = None - self.content_type: Optional[str] = None - self.stream: Optional[Union[Http, ASGIApp, HTTPReceiver]] = None + self.body: bytes | None = None + self.content_type: str | None = None + self.stream: Http | ASGIApp | HTTPReceiver | None = None self.status: int = None self.headers = Header({}) - self._cookies: Optional[CookieJar] = None + self._cookies: CookieJar | None = None def __repr__(self): class_name = self.__class__.__name__ return f"<{class_name}: {self.status} {self.content_type}>" - def _encode_body(self, data: Optional[str | bytes]): + def _encode_body(self, data: str | bytes | None): if data is None: return b"" return data.encode() if hasattr(data, "encode") else data # type: ignore @@ -93,8 +91,8 @@ def processed_headers(self) -> Iterator[tuple[bytes, bytes]]: async def send( self, - data: Optional[AnyStr] = None, - end_stream: Optional[bool] = None, + data: AnyStr | None = None, + end_stream: bool | None = None, ) -> None: """Send any pending response headers and the given data as body. @@ -127,14 +125,14 @@ def add_cookie( value: str, *, path: str = "/", - domain: Optional[str] = None, + domain: str | None = None, secure: bool = True, - max_age: Optional[int] = None, - expires: Optional[datetime] = None, + max_age: int | None = None, + expires: datetime | None = None, httponly: bool = False, - samesite: Optional[SameSite] = "Lax", + samesite: SameSite | None = "Lax", partitioned: bool = False, - comment: Optional[str] = None, + comment: str | None = None, host_prefix: bool = False, secure_prefix: bool = False, ) -> Cookie: @@ -181,7 +179,7 @@ def delete_cookie( key: str, *, path: str = "/", - domain: Optional[str] = None, + domain: str | None = None, host_prefix: bool = False, secure_prefix: bool = False, ) -> None: @@ -225,14 +223,14 @@ class HTTPResponse(BaseHTTPResponse): def __init__( self, - body: Optional[Any] = None, + body: Any = None, status: int = 200, - headers: Optional[Union[Header, dict[str, str]]] = None, - content_type: Optional[str] = None, + headers: Header | dict[str, str] | None = None, + content_type: str | None = None, ): super().__init__() - self.content_type: Optional[str] = content_type + self.content_type: str | None = content_type self.body = self._encode_body(body) self.status = status self.headers = Header(headers or {}) @@ -276,11 +274,11 @@ class JSONResponse(HTTPResponse): def __init__( self, - body: Optional[Any] = None, + body: Any = None, status: int = 200, - headers: Optional[Union[Header, dict[str, str]]] = None, + headers: Header | dict[str, str] | None = None, content_type: str = "application/json", - dumps: Optional[Callable[..., AnyStr]] = None, + dumps: Callable[..., AnyStr] | None = None, **kwargs: Any, ): self._initialized = False @@ -309,7 +307,7 @@ def _check_body_not_manually_set(self): ) @property - def raw_body(self) -> Optional[Any]: + def raw_body(self) -> Any: """Returns the raw body, as long as body has not been manually set previously. NOTE: This object should not be mutated, as it will not be @@ -333,7 +331,7 @@ def raw_body(self, value: Any): self._raw_body = value @property # type: ignore - def body(self) -> Optional[bytes]: # type: ignore + def body(self) -> bytes | None: # type: ignore """Returns the response body. Returns: @@ -342,7 +340,7 @@ def body(self) -> Optional[bytes]: # type: ignore return self._body @body.setter - def body(self, value: Optional[bytes]): + def body(self, value: bytes | None): self._body = value if not self._initialized: return @@ -351,7 +349,7 @@ def body(self, value: Optional[bytes]): def set_body( self, body: Any, - dumps: Optional[Callable[..., AnyStr]] = None, + dumps: Callable[..., AnyStr] | None = None, **dumps_kwargs: Any, ) -> None: """Set the response body to the given value, using the given dumps function @@ -498,12 +496,12 @@ class ResponseStream: def __init__( self, streaming_fn: Callable[ - [Union[BaseHTTPResponse, ResponseStream]], + [BaseHTTPResponse | ResponseStream], Coroutine[Any, Any, None], ], status: int = 200, - headers: Optional[Union[Header, dict[str, str]]] = None, - content_type: Optional[str] = None, + headers: Header | dict[str, str] | None = None, + content_type: str | None = None, ): if headers is None: headers = Header() @@ -513,8 +511,8 @@ def __init__( self.status = status self.headers = headers or Header() self.content_type = content_type - self.request: Optional[Request] = None - self._cookies: Optional[CookieJar] = None + self.request: Request | None = None + self._cookies: CookieJar | None = None async def write(self, message: str): await self.response.send(message) diff --git a/sanic/router.py b/sanic/router.py index 70cc9cb15d..b674d12308 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -3,7 +3,7 @@ from collections.abc import Iterable from functools import lru_cache from inspect import signature -from typing import Any, Optional, Union +from typing import Any from uuid import UUID from sanic_routing import BaseRouter @@ -29,7 +29,7 @@ class Router(BaseRouter): ALLOWED_METHODS = HTTP_METHODS def _get( - self, path: str, method: str, host: Optional[str] + self, path: str, method: str, host: str | None ) -> tuple[Route, RouteHandler, dict[str, Any]]: try: return self.resolve( @@ -50,7 +50,7 @@ def _get( @lru_cache(maxsize=ROUTER_CACHE_SIZE) def get( # type: ignore - self, path: str, method: str, host: Optional[str] + self, path: str, method: str, host: str | None ) -> tuple[Route, RouteHandler, dict[str, Any]]: """Retrieve a `Route` object containing the details about how to handle a response for a given request @@ -80,18 +80,18 @@ def add( # type: ignore uri: str, methods: Iterable[str], handler: RouteHandler, - host: Optional[Union[str, Iterable[str]]] = None, + host: str | Iterable[str] | None = None, strict_slashes: bool = False, stream: bool = False, ignore_body: bool = False, - version: Optional[Union[str, float, int]] = None, - name: Optional[str] = None, + version: str | float | int | None = None, + name: str | None = None, unquote: bool = False, static: bool = False, version_prefix: str = "/v", overwrite: bool = False, - error_format: Optional[str] = None, - ) -> Union[Route, list[Route]]: + error_format: str | None = None, + ) -> Route | list[Route]: """Add a handler to the router Args: @@ -165,8 +165,8 @@ def add( # type: ignore @lru_cache(maxsize=ROUTER_CACHE_SIZE) def find_route_by_view_name( - self, view_name: str, name: Optional[str] = None - ) -> Optional[Route]: + self, view_name: str, name: str | None = None + ) -> Route | None: """Find a route in the router based on the specified view name. Args: diff --git a/sanic/server/async_server.py b/sanic/server/async_server.py index 754a6e4e06..34e7276bbc 100644 --- a/sanic/server/async_server.py +++ b/sanic/server/async_server.py @@ -67,7 +67,7 @@ def close(self): if self.server: self.server.close() coro = self.wait_closed() - task = asyncio.ensure_future(coro, loop=self.loop) + task = asyncio.ensure_future(coro) return task def start_serving(self): diff --git a/sanic/server/events.py b/sanic/server/events.py index 6a767a56dc..9dc8bfbde5 100644 --- a/sanic/server/events.py +++ b/sanic/server/events.py @@ -2,7 +2,7 @@ from collections.abc import Iterable from inspect import isawaitable -from typing import TYPE_CHECKING, Any, Callable, Optional +from typing import TYPE_CHECKING, Any, Callable if TYPE_CHECKING: @@ -10,9 +10,9 @@ def trigger_events( - events: Optional[Iterable[Callable[..., Any]]], + events: Iterable[Callable[..., Any]] | None, loop, - app: Optional[Sanic] = None, + app: Sanic | None = None, **kwargs, ): """Trigger event callbacks (functions or async) diff --git a/sanic/server/protocols/base_protocol.py b/sanic/server/protocols/base_protocol.py index a6dae610b3..cf9a35927f 100644 --- a/sanic/server/protocols/base_protocol.py +++ b/sanic/server/protocols/base_protocol.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING from sanic.exceptions import RequestCancelled @@ -47,14 +47,14 @@ def __init__( self.loop = loop self.app: Sanic = app self.signal = signal or Signal() - self.transport: Optional[Transport] = None + self.transport: Transport | None = None self.connections = connections if connections is not None else set() - self.conn_info: Optional[ConnInfo] = None + self.conn_info: ConnInfo | None = None self._can_write = asyncio.Event() self._can_write.set() self._unix = unix self._time = 0.0 # type: float - self._task = None # type: Optional[asyncio.Task] + self._task: asyncio.Task | None = None self._data_received = asyncio.Event() @property @@ -82,7 +82,7 @@ async def receive_more(self): self._data_received.clear() await self._data_received.wait() - def close(self, timeout: Optional[float] = None): + def close(self, timeout: float | None = None): """ Attempt close the connection. """ diff --git a/sanic/server/protocols/http_protocol.py b/sanic/server/protocols/http_protocol.py index 4cc4c3f8ef..d28f0863f7 100644 --- a/sanic/server/protocols/http_protocol.py +++ b/sanic/server/protocols/http_protocol.py @@ -1,6 +1,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional +import sys + +from typing import TYPE_CHECKING from sanic.http.constants import HTTP from sanic.http.http3 import Http3 @@ -60,7 +62,7 @@ def _setup_connection(self, *args, **kwargs): ... def _setup(self): - self.request: Optional[Request] = None + self.request: Request | None = None self.access_log = self.app.config.ACCESS_LOG self.request_handler = self.app.handle_request self.error_handler = self.app.error_handler @@ -242,13 +244,14 @@ def check_timeouts(self): _interval, self.check_timeouts ) return - cancel_msg_args = () - cancel_msg_args = ("Cancel connection task with a timeout",) - self._task.cancel(*cancel_msg_args) + if sys.version_info < (3, 14): + self._task.cancel("Cancel connection task with a timeout") + else: + self._task.cancel() except Exception: error_logger.exception("protocol.check_timeouts") - def close(self, timeout: Optional[float] = None): + def close(self, timeout: float | None = None): """ Requires to prevent checking timeouts for closed connections """ @@ -329,7 +332,7 @@ def __init__(self, *args, app: Sanic, **kwargs) -> None: self.app = app super().__init__(*args, **kwargs) self._setup() - self._connection: Optional[H3Connection] = None + self._connection: H3Connection | None = None def quic_event_received(self, event: QuicEvent) -> None: logger.debug( @@ -353,5 +356,5 @@ def quic_event_received(self, event: QuicEvent) -> None: self._http.http_event_received(http_event) @property - def connection(self) -> Optional[H3Connection]: + def connection(self) -> H3Connection | None: return self._connection diff --git a/sanic/server/protocols/websocket_protocol.py b/sanic/server/protocols/websocket_protocol.py index 5a3e70f2e2..33c7142de3 100644 --- a/sanic/server/protocols/websocket_protocol.py +++ b/sanic/server/protocols/websocket_protocol.py @@ -1,5 +1,5 @@ from collections.abc import Sequence -from typing import Optional, cast +from typing import cast try: # websockets >= 11.0 @@ -41,19 +41,19 @@ def __init__( self, *args, websocket_timeout: float = 10.0, - websocket_max_size: Optional[int] = None, - websocket_ping_interval: Optional[float] = 20.0, - websocket_ping_timeout: Optional[float] = 20.0, + websocket_max_size: int | None = None, + websocket_ping_interval: float | None = 20.0, + websocket_ping_timeout: float | None = 20.0, **kwargs, ): super().__init__(*args, **kwargs) - self.websocket: Optional[WebsocketImplProtocol] = None + self.websocket: WebsocketImplProtocol | None = None self.websocket_timeout = websocket_timeout self.websocket_max_size = websocket_max_size self.websocket_ping_interval = websocket_ping_interval self.websocket_ping_timeout = websocket_ping_timeout - self.websocket_url: Optional[str] = None - self.websocket_peer: Optional[str] = None + self.websocket_url: str | None = None + self.websocket_peer: str | None = None def connection_lost(self, exc): if self.websocket is not None: @@ -71,13 +71,13 @@ def data_received(self, data): # That will (hopefully) upgrade it to a websocket. super().data_received(data) - def eof_received(self) -> Optional[bool]: + def eof_received(self) -> bool | None: if self.websocket is not None: return self.websocket.eof_received() else: return False - def close(self, timeout: Optional[float] = None): + def close(self, timeout: float | None = None): # Called by HttpProtocol at the end of connection_task # If we've upgraded to websocket, we do our own closing if self.websocket is not None: @@ -109,7 +109,7 @@ def sanic_request_to_ws_request(request: Request): ) async def websocket_handshake( - self, request, subprotocols: Optional[Sequence[str]] = None + self, request, subprotocols: Sequence[str] | None = None ): # let the websockets package do the handshake with the client try: @@ -117,7 +117,7 @@ async def websocket_handshake( # subprotocols can be a set or frozenset, # but ServerProtocol needs a list subprotocols = cast( - Optional[Sequence[Subprotocol]], + Sequence[Subprotocol] | None, list( [ Subprotocol(subprotocol) diff --git a/sanic/server/runners.py b/sanic/server/runners.py index df742e9e5d..57e50a1262 100644 --- a/sanic/server/runners.py +++ b/sanic/server/runners.py @@ -1,7 +1,7 @@ from __future__ import annotations from ssl import SSLContext -from typing import TYPE_CHECKING, Optional, Union +from typing import TYPE_CHECKING from sanic.config import Config from sanic.exceptions import ServerError @@ -43,9 +43,9 @@ def serve( host, port, app: Sanic, - ssl: Optional[SSLContext] = None, - sock: Optional[socket.socket] = None, - unix: Optional[str] = None, + ssl: SSLContext | None = None, + sock: socket.socket | None = None, + unix: str | None = None, reuse_port: bool = False, loop=None, protocol: type[asyncio.Protocol] = HttpProtocol, @@ -352,7 +352,7 @@ def _serve_http_3( def _build_protocol_kwargs( protocol: type[asyncio.Protocol], config: Config -) -> dict[str, Union[int, float]]: +) -> dict[str, int | float]: if hasattr(protocol, "websocket_handshake"): return { "websocket_max_size": config.WEBSOCKET_MAX_SIZE, diff --git a/sanic/server/socket.py b/sanic/server/socket.py index e40c4a7029..62055ffa00 100644 --- a/sanic/server/socket.py +++ b/sanic/server/socket.py @@ -6,7 +6,7 @@ from ipaddress import ip_address from pathlib import Path -from typing import Any, Optional, Union +from typing import Any from sanic.http.constants import HTTP @@ -36,7 +36,7 @@ def bind_socket(host: str, port: int, *, backlog=100) -> socket.socket: def bind_unix_socket( - path: Union[Path, str], *, mode=0o666, backlog=100 + path: Path | str, *, mode=0o666, backlog=100 ) -> socket.socket: """Create unix socket. :param path: filesystem path @@ -78,7 +78,7 @@ def bind_unix_socket( return sock -def remove_unix_socket(path: Optional[Union[Path, str]]) -> None: +def remove_unix_socket(path: Path | str | None) -> None: """Remove dead unix socket during server exit.""" if not path: return @@ -97,7 +97,7 @@ def remove_unix_socket(path: Optional[Union[Path, str]]) -> None: def configure_socket( server_settings: dict[str, Any], -) -> Optional[socket.SocketType]: +) -> socket.SocketType | None: # Create a listening socket or use the one in settings if server_settings.get("version") is HTTP.VERSION_3: return None diff --git a/sanic/server/websockets/connection.py b/sanic/server/websockets/connection.py index 4f37f98aa9..118f9dda48 100644 --- a/sanic/server/websockets/connection.py +++ b/sanic/server/websockets/connection.py @@ -1,10 +1,5 @@ from collections.abc import Awaitable, MutableMapping -from typing import ( - Any, - Callable, - Optional, - Union, -) +from typing import Any, Callable from sanic.exceptions import InvalidUsage @@ -26,14 +21,14 @@ def __init__( self, send: Callable[[ASGIMessage], Awaitable[None]], receive: Callable[[], Awaitable[ASGIMessage]], - subprotocols: Optional[list[str]] = None, + subprotocols: list[str] | None = None, ) -> None: self._send = send self._receive = receive self._subprotocols = subprotocols or [] - async def send(self, data: Union[str, bytes], *args, **kwargs) -> None: - message: dict[str, Union[str, bytes]] = {"type": "websocket.send"} + async def send(self, data: str | bytes, *args, **kwargs) -> None: + message: dict[str, str | bytes] = {"type": "websocket.send"} if isinstance(data, bytes): message.update({"bytes": data}) @@ -42,7 +37,7 @@ async def send(self, data: Union[str, bytes], *args, **kwargs) -> None: await self._send(message) - async def recv(self, *args, **kwargs) -> Optional[Union[str, bytes]]: + async def recv(self, *args, **kwargs) -> str | bytes | None: message = await self._receive() if message["type"] == "websocket.receive": @@ -60,7 +55,7 @@ async def recv(self, *args, **kwargs) -> Optional[Union[str, bytes]]: receive = recv - async def accept(self, subprotocols: Optional[list[str]] = None) -> None: + async def accept(self, subprotocols: list[str] | None = None) -> None: subprotocol = None if subprotocols: for subp in subprotocols: @@ -83,5 +78,5 @@ def subprotocols(self): return self._subprotocols @subprotocols.setter - def subprotocols(self, subprotocols: Optional[list[str]] = None): + def subprotocols(self, subprotocols: list[str] | None = None): self._subprotocols = subprotocols or [] diff --git a/sanic/server/websockets/frame.py b/sanic/server/websockets/frame.py index 5493c5233d..9938a950c6 100644 --- a/sanic/server/websockets/frame.py +++ b/sanic/server/websockets/frame.py @@ -2,7 +2,7 @@ import codecs from collections.abc import AsyncIterator -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING from websockets.frames import Frame, Opcode from websockets.typing import Data @@ -46,10 +46,10 @@ class WebsocketFrameAssembler: message_fetched: asyncio.Event completed_queue: asyncio.Queue get_in_progress: bool - decoder: Optional[codecs.IncrementalDecoder] + decoder: codecs.IncrementalDecoder | None # For streaming chunks rather than messages: chunks: list[Data] - chunks_queue: Optional[asyncio.Queue[Optional[Data]]] + chunks_queue: asyncio.Queue[Data | None] | None paused: bool def __init__(self, protocol) -> None: @@ -86,7 +86,7 @@ def __init__(self, protocol) -> None: # Flag to indicate we've paused the protocol self.paused = False - async def get(self, timeout: Optional[float] = None) -> Optional[Data]: + async def get(self, timeout: float | None = None) -> Data | None: """ Read the next message. :meth:`get` returns a single :class:`str` or :class:`bytes`. diff --git a/sanic/server/websockets/impl.py b/sanic/server/websockets/impl.py index c399832c17..607d060a5f 100644 --- a/sanic/server/websockets/impl.py +++ b/sanic/server/websockets/impl.py @@ -2,10 +2,6 @@ import secrets from collections.abc import AsyncIterator, Iterable, Mapping, Sequence -from typing import ( - Optional, - Union, -) from websockets.exceptions import ( ConnectionClosed, @@ -38,35 +34,35 @@ class WebsocketImplProtocol: ws_proto: ServerProtocol - io_proto: Optional[SanicProtocol] - loop: Optional[asyncio.AbstractEventLoop] + io_proto: SanicProtocol | None + loop: asyncio.AbstractEventLoop | None max_queue: int close_timeout: float - ping_interval: Optional[float] - ping_timeout: Optional[float] + ping_interval: float | None + ping_timeout: float | None assembler: WebsocketFrameAssembler - # Dict[bytes, asyncio.Future[None]] + # dict[bytes, asyncio.Future[None]] pings: dict[bytes, asyncio.Future] conn_mutex: asyncio.Lock recv_lock: asyncio.Lock - recv_cancel: Optional[asyncio.Future] + recv_cancel: asyncio.Future | None process_event_mutex: asyncio.Lock can_pause: bool - # Optional[asyncio.Future[None]] - data_finished_fut: Optional[asyncio.Future] - # Optional[asyncio.Future[None]] - pause_frame_fut: Optional[asyncio.Future] - # Optional[asyncio.Future[None]] - connection_lost_waiter: Optional[asyncio.Future] - keepalive_ping_task: Optional[asyncio.Task] - auto_closer_task: Optional[asyncio.Task] + # asyncio.Future[None] | None + data_finished_fut: asyncio.Future | None + # asyncio.Future[None] | None + pause_frame_fut: asyncio.Future | None + # asyncio.Future[None] | None + connection_lost_waiter: asyncio.Future | None + keepalive_ping_task: asyncio.Task | None + auto_closer_task: asyncio.Task | None def __init__( self, ws_proto, max_queue=None, - ping_interval: Optional[float] = 20, - ping_timeout: Optional[float] = 20, + ping_interval: float | None = 20, + ping_timeout: float | None = 20, close_timeout: float = 10, loop=None, ): @@ -128,13 +124,13 @@ def resume_frames(self): async def connection_made( self, io_proto: SanicProtocol, - loop: Optional[asyncio.AbstractEventLoop] = None, + loop: asyncio.AbstractEventLoop | None = None, ): if not loop: try: loop = getattr(io_proto, "loop") except AttributeError: - loop = asyncio.get_event_loop() + loop = asyncio.get_running_loop() if not loop: # This catch is for mypy type checker # to assert loop is not None here. @@ -510,7 +506,7 @@ async def close(self, code: int = 1000, reason: str = "") -> None: data_to_send = self.ws_proto.data_to_send() await self.send_data(data_to_send) - async def recv(self, timeout: Optional[float] = None) -> Optional[Data]: + async def recv(self, timeout: float | None = None) -> Data | None: """ Receive the next message. Return a :class:`str` for a text frame and :class:`bytes` for a binary @@ -542,7 +538,7 @@ async def recv(self, timeout: Optional[float] = None) -> Optional[Data]: raise WebsocketClosed( "Cannot receive from websocket interface after it is closed." ) - assembler_get: Optional[asyncio.Task] = None + assembler_get: asyncio.Task | None = None try: self.recv_cancel = asyncio.Future() assembler_get = asyncio.create_task(self.assembler.get(timeout)) @@ -598,7 +594,7 @@ async def recv_burst(self, max_recv=256) -> Sequence[Data]: "Cannot receive from websocket interface after it is closed." ) messages = [] - assembler_get: Optional[asyncio.Task] = None + assembler_get: asyncio.Task | None = None try: # Prevent pausing the transport when we're # receiving a burst of messages @@ -676,7 +672,7 @@ async def recv_streaming(self) -> AsyncIterator[Data]: self.recv_cancel = None self.recv_lock.release() - async def send(self, message: Union[Data, Iterable[Data]]) -> None: + async def send(self, message: Data | Iterable[Data]) -> None: """ Send a message. A string (:class:`str`) is sent as a `Text frame`_. A bytestring or @@ -727,7 +723,7 @@ async def send(self, message: Union[Data, Iterable[Data]]) -> None: else: raise TypeError("Websocket data must be bytes, str.") - async def ping(self, data: Optional[Data] = None) -> asyncio.Future: + async def ping(self, data: Data | None = None) -> asyncio.Future: """ Send a ping. Return an :class:`~asyncio.Future` that will be resolved when the @@ -851,7 +847,7 @@ async def async_eof_received(self, data_to_send, events_to_process): # This will fail the connection appropriately SanicProtocol.close(self.io_proto, timeout=1.0) - def eof_received(self) -> Optional[bool]: + def eof_received(self) -> bool | None: self.ws_proto.receive_eof() data_to_send = self.ws_proto.data_to_send() events_to_process = self.ws_proto.events_received() diff --git a/sanic/signals.py b/sanic/signals.py index c1a78e8868..52cebfffc0 100644 --- a/sanic/signals.py +++ b/sanic/signals.py @@ -6,7 +6,7 @@ from dataclasses import dataclass from enum import Enum from inspect import isawaitable -from typing import Any, Optional, Union, cast +from typing import Any, cast from sanic_routing import BaseRouter, Route, RouteGroup from sanic_routing.exceptions import NotFound @@ -94,10 +94,10 @@ class SignalWaiter: signal: Signal event_definition: str trigger: str = "" - requirements: Optional[dict[str, str]] = None + requirements: dict[str, str] | None = None exclusive: bool = True - future: Optional[asyncio.Future] = None + future: asyncio.Future | None = None async def wait(self): """Block until the signal is next dispatched. @@ -138,7 +138,7 @@ def __init__(self) -> None: self.ctx.loop = None @staticmethod - def format_event(event: Union[str, Enum]) -> str: + def format_event(event: str | Enum) -> str: """Ensure event strings in proper format Args: @@ -155,8 +155,8 @@ def format_event(event: Union[str, Enum]) -> str: def get( # type: ignore self, - event: Union[str, Enum], - condition: Optional[dict[str, str]] = None, + event: str | Enum, + condition: dict[str, str] | None = None, ): """Get the handlers for a signal @@ -182,7 +182,7 @@ def get( # type: ignore ) except NotFound: message = "Could not find signal %s" - terms: list[Union[str, Optional[dict[str, str]]]] = [event] + terms: list[str | dict[str, str] | None] = [event] if extra: message += " with %s" terms.append(extra) @@ -205,8 +205,8 @@ def get( # type: ignore async def _dispatch( self, event: str, - context: Optional[dict[str, Any]] = None, - condition: Optional[dict[str, str]] = None, + context: dict[str, Any] | None = None, + condition: dict[str, str] | None = None, fail_not_found: bool = True, reverse: bool = False, ) -> Any: @@ -264,14 +264,14 @@ async def _dispatch( async def dispatch( self, - event: Union[str, Enum], + event: str | Enum, *, - context: Optional[dict[str, Any]] = None, - condition: Optional[dict[str, str]] = None, + context: dict[str, Any] | None = None, + condition: dict[str, str] | None = None, fail_not_found: bool = True, inline: bool = False, reverse: bool = False, - ) -> Union[asyncio.Task, Any]: + ) -> asyncio.Task | Any: """Dispatch a signal to all handlers that match the event Args: @@ -308,10 +308,10 @@ async def dispatch( def get_waiter( self, - event: Union[str, Enum], - condition: Optional[dict[str, Any]] = None, + event: str | Enum, + condition: dict[str, Any] | None = None, exclusive: bool = True, - ) -> Optional[SignalWaiter]: + ) -> SignalWaiter | None: event_definition = self.format_event(event) name, trigger, _ = self._get_event_parts(event_definition) signal = cast(Signal, self.name_index.get(name)) @@ -345,8 +345,8 @@ def _get_event_parts(self, event: str) -> tuple[str, str, str]: def add( # type: ignore self, handler: SignalHandler, - event: Union[str, Enum], - condition: Optional[dict[str, Any]] = None, + event: str | Enum, + condition: dict[str, Any] | None = None, exclusive: bool = True, *, priority: int = 0, diff --git a/sanic/utils.py b/sanic/utils.py index 422492619e..79c166ccc4 100644 --- a/sanic/utils.py +++ b/sanic/utils.py @@ -4,7 +4,6 @@ from os import environ as os_environ from pathlib import Path from re import findall as re_findall -from typing import Union from sanic.exceptions import LoadFileException, PyFileError from sanic.helpers import import_string @@ -53,7 +52,7 @@ def str_to_bool(val: str) -> bool: def load_module_from_file_location( - location: Union[bytes, str, Path], encoding: str = "utf8", *args, **kwargs + location: bytes | str | Path, encoding: str = "utf8", *args, **kwargs ): # noqa """Returns loaded module provided as a file path. diff --git a/sanic/views.py b/sanic/views.py index b77f493896..fbbed78a46 100644 --- a/sanic/views.py +++ b/sanic/views.py @@ -5,8 +5,6 @@ TYPE_CHECKING, Any, Callable, - Optional, - Union, ) from sanic.models.handler_types import RouteHandler @@ -118,13 +116,13 @@ def get(self, request: Request): def __init_subclass__( cls, - attach: Optional[Union[Sanic, Blueprint]] = None, + attach: Sanic | Blueprint | None = None, uri: str = "", methods: Iterable[str] = frozenset({"GET"}), - host: Optional[str] = None, - strict_slashes: Optional[bool] = None, - version: Optional[int] = None, - name: Optional[str] = None, + host: str | None = None, + strict_slashes: bool | None = None, + version: int | None = None, + name: str | None = None, stream: bool = False, version_prefix: str = "/v", **kwargs: Any, @@ -204,13 +202,13 @@ def view(*args, **kwargs): @classmethod def attach( cls, - to: Union[Sanic, Blueprint], + to: Sanic | Blueprint, uri: str, methods: Iterable[str] = frozenset({"GET"}), - host: Optional[str] = None, - strict_slashes: Optional[bool] = None, - version: Optional[int] = None, - name: Optional[str] = None, + host: str | None = None, + strict_slashes: bool | None = None, + version: int | None = None, + name: str | None = None, stream: bool = False, version_prefix: str = "/v", ) -> None: diff --git a/sanic/worker/daemon.py b/sanic/worker/daemon.py index 08d1c82f74..2f43b3fa92 100644 --- a/sanic/worker/daemon.py +++ b/sanic/worker/daemon.py @@ -11,7 +11,6 @@ from dataclasses import dataclass from pathlib import Path -from typing import Optional from sanic.compat import OS_IS_WINDOWS from sanic.log import logger @@ -119,12 +118,12 @@ class Daemon: def __init__( self, - pidfile: Optional[str] = None, - logfile: Optional[str] = None, - user: Optional[str] = None, - group: Optional[str] = None, - name: Optional[str] = None, - lockfile: Optional[str] = None, + pidfile: str | None = None, + logfile: str | None = None, + user: str | None = None, + group: str | None = None, + name: str | None = None, + lockfile: str | None = None, ): if OS_IS_WINDOWS: raise DaemonError( diff --git a/sanic/worker/inspector.py b/sanic/worker/inspector.py index a809e4b1fa..89a4dea90c 100644 --- a/sanic/worker/inspector.py +++ b/sanic/worker/inspector.py @@ -6,7 +6,7 @@ from multiprocessing.connection import Connection from os import environ from pathlib import Path -from typing import Any, Union +from typing import Any from sanic.exceptions import Unauthorized from sanic.helpers import Default @@ -45,8 +45,8 @@ def __init__( host: str, port: int, api_key: str, - tls_key: Union[Path, str, Default], - tls_cert: Union[Path, str, Default], + tls_key: Path | str | Default, + tls_cert: Path | str | Default, ): self._publisher = publisher self.app_info = app_info @@ -133,7 +133,7 @@ def reload(self, zero_downtime: bool = False) -> None: message += ":STARTUP_FIRST" self._publisher.send(message) - def scale(self, replicas: Union[str, int]) -> str: + def scale(self, replicas: str | int) -> str: """Scale the number of workers Args: diff --git a/sanic/worker/loader.py b/sanic/worker/loader.py index 9b81cfdd46..287055f3f7 100644 --- a/sanic/worker/loader.py +++ b/sanic/worker/loader.py @@ -8,7 +8,7 @@ from inspect import isfunction from pathlib import Path from ssl import SSLContext -from typing import TYPE_CHECKING, Any, Callable, Optional, Union, cast +from typing import TYPE_CHECKING, Any, Callable, cast from sanic.http.tls.context import process_to_context from sanic.http.tls.creators import MkcertCreator, TrustmeCreator @@ -41,7 +41,7 @@ def __init__( as_factory: bool = False, as_simple: bool = False, args: Any = None, - factory: Optional[Callable[[], SanicApp]] = None, + factory: Callable[[], SanicApp] | None = None, ) -> None: self.module_input = module_input self.module_name = "" @@ -139,9 +139,7 @@ class CertLoader: def __init__( self, - ssl_data: Optional[ - Union[SSLContext, dict[str, Union[str, os.PathLike]]] - ], + ssl_data: SSLContext | dict[str, str | os.PathLike] | None, ): self._ssl_data = ssl_data self._creator_class = None diff --git a/sanic/worker/manager.py b/sanic/worker/manager.py index 1f7df04dfa..5f7e99feb3 100644 --- a/sanic/worker/manager.py +++ b/sanic/worker/manager.py @@ -12,7 +12,7 @@ from random import choice from signal import SIGINT, SIGTERM, Signals from signal import signal as signal_func -from typing import Any, Callable, Optional +from typing import Any, Callable from sanic.compat import OS_IS_WINDOWS from sanic.exceptions import ServerKilled @@ -99,7 +99,7 @@ def manage( func: Callable[..., Any], kwargs: dict[str, Any], transient: bool = False, - restartable: Optional[bool] = None, + restartable: bool | None = None, tracked: bool = True, auto_start: bool = True, workers: int = 1, @@ -167,7 +167,7 @@ def create_server(self) -> Worker: ident=f"{WorkerProcess.SERVER_IDENTIFIER}{server_number:2}", ) - def shutdown_server(self, name: Optional[str] = None) -> None: + def shutdown_server(self, name: str | None = None) -> None: """Shutdown a server process. Args: @@ -240,7 +240,7 @@ def cleanup(self): def restart( self, - process_names: Optional[list[str]] = None, + process_names: list[str] | None = None, restart_order=RestartOrder.SHUTDOWN_FIRST, **kwargs, ): @@ -463,7 +463,7 @@ def _cleanup_non_tracked_workers(self) -> None: for worker in to_remove: self.remove_worker(worker) - def _poll_monitor(self) -> Optional[MonitorCycle]: + def _poll_monitor(self) -> MonitorCycle | None: if self.monitor_subscriber.poll(0.1): message = self.monitor_subscriber.recv() logger.debug(f"Monitor message: {message}", extra={"verbosity": 2}) @@ -488,7 +488,7 @@ def _poll_monitor(self) -> Optional[MonitorCycle]: def _handle_terminate(self) -> None: self.shutdown() - def _handle_message(self, message: str) -> Optional[MonitorCycle]: + def _handle_message(self, message: str) -> MonitorCycle | None: logger.debug( "Incoming monitor message: %s", message, @@ -501,7 +501,7 @@ def _handle_message(self, message: str) -> Optional[MonitorCycle]: processes = split_message[0] reloaded_files = split_message[1] if len(split_message) > 1 else None - process_names: Optional[list[str]] = [ + process_names: list[str] | None = [ name.strip() for name in processes.split(",") ] if process_names and "__ALL_PROCESSES__" in process_names: @@ -525,7 +525,7 @@ def _handle_manage( func: Callable[..., Any], kwargs: dict[str, Any], transient: bool, - restartable: Optional[bool], + restartable: bool | None, tracked: bool, auto_start: bool, workers: int, diff --git a/sanic/worker/multiplexer.py b/sanic/worker/multiplexer.py index 399071e276..8cbb18c03a 100644 --- a/sanic/worker/multiplexer.py +++ b/sanic/worker/multiplexer.py @@ -1,6 +1,8 @@ +from __future__ import annotations + from multiprocessing.connection import Connection from os import environ, getpid -from typing import Any, Callable, Optional +from typing import Any, Callable from sanic.log import Colors, logger from sanic.worker.process import ProcessState @@ -45,7 +47,7 @@ def manage( func: Callable[..., Any], kwargs: dict[str, Any], transient: bool = False, - restartable: Optional[bool] = None, + restartable: bool | None = None, tracked: bool = False, auto_start: bool = True, workers: int = 1, diff --git a/sanic/worker/process.py b/sanic/worker/process.py index 6142362412..e6c51a04e5 100644 --- a/sanic/worker/process.py +++ b/sanic/worker/process.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os from collections.abc import MutableMapping diff --git a/sanic/worker/restarter.py b/sanic/worker/restarter.py index d9e98044cb..762b107580 100644 --- a/sanic/worker/restarter.py +++ b/sanic/worker/restarter.py @@ -1,4 +1,4 @@ -from typing import Optional +from __future__ import annotations from sanic.log import error_logger from sanic.worker.constants import RestartOrder @@ -10,7 +10,7 @@ def restart( self, transient_processes: list[WorkerProcess], durable_processes: list[WorkerProcess], - process_names: Optional[list[str]] = None, + process_names: list[str] | None = None, restart_order=RestartOrder.SHUTDOWN_FIRST, **kwargs, ) -> None: diff --git a/sanic/worker/serve.py b/sanic/worker/serve.py index 98a988f3ae..b7594e1250 100644 --- a/sanic/worker/serve.py +++ b/sanic/worker/serve.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import asyncio import os import socket @@ -6,7 +8,7 @@ from functools import partial from multiprocessing.connection import Connection from ssl import SSLContext -from typing import Any, Optional, Union +from typing import Any from sanic.application.constants import ServerStage from sanic.application.state import ApplicationServerInfo @@ -25,15 +27,13 @@ def worker_serve( host, port, app_name: str, - monitor_publisher: Optional[Connection], + monitor_publisher: Connection | None, app_loader: AppLoader, - worker_state: Optional[dict[str, Any]] = None, - server_info: Optional[dict[str, list[ApplicationServerInfo]]] = None, - ssl: Optional[ - Union[SSLContext, dict[str, Union[str, os.PathLike[str]]]] - ] = None, - sock: Optional[socket.socket] = None, - unix: Optional[str] = None, + worker_state: dict[str, Any] | None = None, + server_info: dict[str, list[ApplicationServerInfo]] | None = None, + ssl: SSLContext | dict[str, str | os.PathLike[str]] | None = None, + sock: socket.socket | None = None, + unix: str | None = None, reuse_port: bool = False, loop=None, protocol: type[asyncio.Protocol] = HttpProtocol, @@ -46,8 +46,8 @@ def worker_serve( state=None, asyncio_server_kwargs=None, version=HTTP.VERSION_1, - config: Optional[Union[bytes, str, dict[str, Any], Any]] = None, - passthru: Optional[dict[str, Any]] = None, + config: bytes | str | dict[str, Any] | Any | None = None, + passthru: dict[str, Any] | None = None, ): try: from sanic import Sanic diff --git a/sanic/worker/state.py b/sanic/worker/state.py index c3c32d890d..9aa25b66ac 100644 --- a/sanic/worker/state.py +++ b/sanic/worker/state.py @@ -1,11 +1,10 @@ +from __future__ import annotations + from collections.abc import ItemsView, Iterator, KeysView, Mapping, ValuesView from collections.abc import Mapping as MappingType from typing import Any -dict - - class WorkerState(Mapping): RESTRICTED = ( "health", diff --git a/setup.py b/setup.py index d57dcc6d75..e3f60e5f8d 100644 --- a/setup.py +++ b/setup.py @@ -94,16 +94,16 @@ def str_to_bool(val: str) -> bool: "packages": find_packages(exclude=("tests", "tests.*")), "package_data": {"sanic": ["py.typed", "pages/styles/*"]}, "platforms": "any", - "python_requires": ">=3.9", + "python_requires": ">=3.10", "classifiers": [ "Development Status :: 4 - Beta", "Environment :: Web Environment", "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", ], "entry_points": {"console_scripts": ["sanic = sanic.__main__:main"]}, } diff --git a/tox.ini b/tox.ini index 6c00fbd1b7..90fc498d89 100644 --- a/tox.ini +++ b/tox.ini @@ -1,11 +1,11 @@ [tox] -envlist = py39, py310, py311, py312, py313, pyNightly, pypy310, {py39,py310,py311,py312,py313,pyNightly,pypy310}-no-ext, lint, check, security, docs, type-checking +envlist = py310, py311, py312, py313, py314, pyNightly, pypy310, {py310,py311,py312,py313,py314,pyNightly,pypy310}-no-ext, lint, check, security, docs, type-checking [testenv] usedevelop = true setenv = - {py39,py310,py311,py312,py313,pyNightly}-no-ext: SANIC_NO_UJSON=1 - {py39,py310,py311,py312,py313,pyNightly}-no-ext: SANIC_NO_UVLOOP=1 + {py310,py311,py312,py313,py314,pyNightly}-no-ext: SANIC_NO_UJSON=1 + {py310,py311,py312,py313,py314,pyNightly}-no-ext: SANIC_NO_UVLOOP=1 extras = test, http3 deps = httpx>=0.23 From f8b2bfc75d4e05aa9f89cc685dc7ee926c458c70 Mon Sep 17 00:00:00 2001 From: Adam Hopkins Date: Wed, 31 Dec 2025 11:28:13 +0200 Subject: [PATCH 2/5] Update test with new version of tracerite header checks --- tests/test_exceptions_handler.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_exceptions_handler.py b/tests/test_exceptions_handler.py index e59a96cd52..3f8bb075d6 100644 --- a/tests/test_exceptions_handler.py +++ b/tests/test_exceptions_handler.py @@ -151,9 +151,9 @@ def test_chained_exception_handler(exception_handler_app: Sanic): assert "GET /6" in html # Both exceptions should be present in the traceback headers - h3_texts = [h3.text for h3 in soup.select("h3")] - assert any("ZeroDivisionError" in text for text in h3_texts) - assert any("ValueError" in text for text in h3_texts) + header_texts = [h.text for h in soup.select("h2, h3")] + assert any("ZeroDivisionError" in text for text in header_texts) + assert any("ValueError" in text for text in header_texts) def test_exception_handler_lookup(exception_handler_app: Sanic): From 68b823f3f8b4ec7ec392e732ce4be8154acbf8d0 Mon Sep 17 00:00:00 2001 From: Adam Hopkins Date: Wed, 31 Dec 2025 11:33:01 +0200 Subject: [PATCH 3/5] squash --- sanic/server/websockets/impl.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/sanic/server/websockets/impl.py b/sanic/server/websockets/impl.py index 607d060a5f..83026c4002 100644 --- a/sanic/server/websockets/impl.py +++ b/sanic/server/websockets/impl.py @@ -1,16 +1,11 @@ import asyncio import secrets - from collections.abc import AsyncIterator, Iterable, Mapping, Sequence -from websockets.exceptions import ( - ConnectionClosed, - ConnectionClosedError, - ConnectionClosedOK, -) +from websockets.exceptions import (ConnectionClosed, ConnectionClosedError, + ConnectionClosedOK) from websockets.frames import Frame, Opcode - try: # websockets >= 11.0 from websockets.protocol import Event, State # type: ignore from websockets.server import ServerProtocol # type: ignore @@ -18,15 +13,14 @@ from websockets.connection import Event, State # type: ignore from websockets.server import ServerConnection as ServerProtocol -from websockets.typing import Data - from sanic.log import websockets_logger from sanic.server.protocols.base_protocol import SanicProtocol +from websockets.typing import Data + from ...exceptions import ServerError, WebsocketClosed from .frame import WebsocketFrameAssembler - OPEN = State.OPEN CLOSING = State.CLOSING CLOSED = State.CLOSED @@ -130,7 +124,7 @@ async def connection_made( try: loop = getattr(io_proto, "loop") except AttributeError: - loop = asyncio.get_running_loop() + loop = asyncio.get_event_loop() if not loop: # This catch is for mypy type checker # to assert loop is not None here. From 2e23df6b77771da2a2f9b324bb38cec435c4d59a Mon Sep 17 00:00:00 2001 From: Adam Hopkins Date: Wed, 31 Dec 2025 11:44:44 +0200 Subject: [PATCH 4/5] Use explicit loop on server shutdown --- sanic/server/async_server.py | 6 ++---- sanic/server/websockets/impl.py | 14 ++++++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/sanic/server/async_server.py b/sanic/server/async_server.py index 34e7276bbc..d9dd5e6287 100644 --- a/sanic/server/async_server.py +++ b/sanic/server/async_server.py @@ -1,7 +1,5 @@ from __future__ import annotations -import asyncio - from typing import TYPE_CHECKING from sanic.exceptions import SanicException @@ -67,7 +65,7 @@ def close(self): if self.server: self.server.close() coro = self.wait_closed() - task = asyncio.ensure_future(coro) + task = self.loop.create_task(coro) return task def start_serving(self): @@ -107,7 +105,7 @@ def __await__(self): """ Starts the asyncio server, returns AsyncServerCoro """ - task = asyncio.ensure_future(self.serve_coro) + task = self.loop.create_task(self.serve_coro) while not task.done(): yield self.server = task.result() diff --git a/sanic/server/websockets/impl.py b/sanic/server/websockets/impl.py index 83026c4002..d68110773b 100644 --- a/sanic/server/websockets/impl.py +++ b/sanic/server/websockets/impl.py @@ -1,11 +1,16 @@ import asyncio import secrets + from collections.abc import AsyncIterator, Iterable, Mapping, Sequence -from websockets.exceptions import (ConnectionClosed, ConnectionClosedError, - ConnectionClosedOK) +from websockets.exceptions import ( + ConnectionClosed, + ConnectionClosedError, + ConnectionClosedOK, +) from websockets.frames import Frame, Opcode + try: # websockets >= 11.0 from websockets.protocol import Event, State # type: ignore from websockets.server import ServerProtocol # type: ignore @@ -13,14 +18,15 @@ from websockets.connection import Event, State # type: ignore from websockets.server import ServerConnection as ServerProtocol +from websockets.typing import Data + from sanic.log import websockets_logger from sanic.server.protocols.base_protocol import SanicProtocol -from websockets.typing import Data - from ...exceptions import ServerError, WebsocketClosed from .frame import WebsocketFrameAssembler + OPEN = State.OPEN CLOSING = State.CLOSING CLOSED = State.CLOSED From ddfd4200dfeb984af809e0c91356c84fb92b223b Mon Sep 17 00:00:00 2001 From: Adam Hopkins Date: Wed, 31 Dec 2025 13:14:51 +0200 Subject: [PATCH 5/5] Correct py3.14 issues --- sanic/cli/executor.py | 12 +++++++++++- sanic/compat.py | 17 +++++++++++++++++ sanic/mixins/static.py | 11 ++++++++++- tests/test_deprecation.py | 2 ++ 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/sanic/cli/executor.py b/sanic/cli/executor.py index 0ed09537ab..1798355e94 100644 --- a/sanic/cli/executor.py +++ b/sanic/cli/executor.py @@ -83,8 +83,18 @@ def _add_arguments(self, parser: ArgumentParser, func: Callable) -> None: kwargs = {} if param.default is not param.empty: kwargs["default"] = param.default + # In Python 3.14+, argparse validates help strings and rejects + # non-string types. Convert annotations to string representation. + help_text = None + if param.annotation is not param.empty: + if isinstance(param.annotation, str): + help_text = param.annotation + else: + help_text = getattr( + param.annotation, "__name__", str(param.annotation) + ) parser.add_argument( f"--{param.name}", - help=param.annotation, + help=help_text, **kwargs, ) diff --git a/sanic/compat.py b/sanic/compat.py index a044853f99..f608909b64 100644 --- a/sanic/compat.py +++ b/sanic/compat.py @@ -22,6 +22,7 @@ OS_IS_WINDOWS = os.name == "nt" PYPY_IMPLEMENTATION = platform.python_implementation() == "PyPy" UVLOOP_INSTALLED = False +PYTHON_314_OR_LATER = sys.version_info >= (3, 14) try: import uvloop # type: ignore # noqa @@ -177,3 +178,19 @@ def ctrlc_handler(sig, frame): die = False signal.signal(signal.SIGINT, ctrlc_handler) app.add_task(stay_active) + + +def clear_function_annotate(*funcs): + """Clear __annotate__ on functions for Python 3.14+ pickle compatibility. + + In Python 3.14, PEP 649 adds __annotate__ to functions with annotations. + When methods are used in functools.partial and pickled, the __annotate__ + function can cause PicklingError because pickle cannot locate it by name. + + This function sets __annotate__ to None on the given functions to avoid + pickle issues. + """ + if PYTHON_314_OR_LATER: + for func in funcs: + if hasattr(func, "__annotate__") and func.__annotate__ is not None: + func.__annotate__ = None diff --git a/sanic/mixins/static.py b/sanic/mixins/static.py index defc8641d3..cb4cfded0c 100644 --- a/sanic/mixins/static.py +++ b/sanic/mixins/static.py @@ -10,7 +10,7 @@ from sanic_routing.route import Route from sanic.base.meta import SanicMeta -from sanic.compat import stat_async +from sanic.compat import clear_function_annotate, stat_async from sanic.exceptions import FileNotFound, HeaderNotFound, RangeNotSatisfiable from sanic.handlers import ContentRangeHandler from sanic.handlers.directory import DirectoryHandler @@ -370,3 +370,12 @@ async def _get_file_path(self, file_or_directory, __file_uri__, not_found): ) raise not_found return file_path + + +# Clear __annotate__ on methods that may be pickled via functools.partial +# to avoid PicklingError in Python 3.14+ (PEP 649) +clear_function_annotate( + StaticHandleMixin._static_request_handler, + StaticHandleMixin._get_file_path, + StaticHandleMixin._register_static, +) diff --git a/tests/test_deprecation.py b/tests/test_deprecation.py index 04817f88db..b79c70b6bc 100644 --- a/tests/test_deprecation.py +++ b/tests/test_deprecation.py @@ -16,5 +16,7 @@ def test_deprecation(): ) def test_deprecation_filter(app: Sanic, filter, expected, recwarn): app.config.DEPRECATION_FILTER = filter + # Clear any warnings captured during test setup (e.g., from pytest_sanic) + recwarn.clear() deprecation("hello", 9.9) assert len(recwarn) == expected