Release 22.6 (#2487)

This commit is contained in:
Adam Hopkins 2022-06-28 15:25:46 +03:00 committed by GitHub
parent aba333bfb6
commit 13d5a44278
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 155 additions and 371 deletions

View File

@ -313,8 +313,10 @@ Version 21.3.0
`#2074 <https://github.com/sanic-org/sanic/pull/2074>`_ `#2074 <https://github.com/sanic-org/sanic/pull/2074>`_
Performance adjustments in ``handle_request_`` Performance adjustments in ``handle_request_``
Version 20.12.3 Version 20.12.3 🔷
--------------- ------------------
`Current LTS version`
**Bugfixes** **Bugfixes**
@ -348,8 +350,8 @@ Version 19.12.5
`#2027 <https://github.com/sanic-org/sanic/pull/2027>`_ `#2027 <https://github.com/sanic-org/sanic/pull/2027>`_
Remove old chardet requirement, add in hard multidict requirement Remove old chardet requirement, add in hard multidict requirement
Version 20.12.0 Version 20.12.0 🔹
--------------- -----------------
**Features** **Features**
@ -357,8 +359,8 @@ Version 20.12.0
`#1993 <https://github.com/sanic-org/sanic/pull/1993>`_ `#1993 <https://github.com/sanic-org/sanic/pull/1993>`_
Add disable app registry Add disable app registry
Version 20.12.0 Version 20.12.0 🔹
--------------- -----------------
**Features** **Features**

View File

@ -9,7 +9,7 @@ API
=== ===
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 3
👥 User Guide <https://sanicframework.org/guide/> 👥 User Guide <https://sanicframework.org/guide/>
sanic/api_reference sanic/api_reference

View File

@ -1,6 +1,7 @@
📜 Changelog 📜 Changelog
============ ============
.. mdinclude:: ./releases/22/22.6.md
.. mdinclude:: ./releases/22/22.3.md .. mdinclude:: ./releases/22/22.3.md
.. mdinclude:: ./releases/21/21.12.md .. mdinclude:: ./releases/21/21.12.md
.. mdinclude:: ./releases/21/21.9.md .. mdinclude:: ./releases/21/21.9.md

View File

@ -1,10 +1,12 @@
## Version 21.12.1 ## Version 21.12.1 🔷
_Current LTS version_
- [#2349](https://github.com/sanic-org/sanic/pull/2349) Only display MOTD on startup - [#2349](https://github.com/sanic-org/sanic/pull/2349) Only display MOTD on startup
- [#2354](https://github.com/sanic-org/sanic/pull/2354) Ignore name argument in Python 3.7 - [#2354](https://github.com/sanic-org/sanic/pull/2354) Ignore name argument in Python 3.7
- [#2355](https://github.com/sanic-org/sanic/pull/2355) Add config.update support for all config values - [#2355](https://github.com/sanic-org/sanic/pull/2355) Add config.update support for all config values
## Version 21.12.0 ## Version 21.12.0 🔹
### Features ### Features
- [#2260](https://github.com/sanic-org/sanic/pull/2260) Allow early Blueprint registrations to still apply later added objects - [#2260](https://github.com/sanic-org/sanic/pull/2260) Allow early Blueprint registrations to still apply later added objects

View File

@ -0,0 +1,42 @@
## Version 22.6.0 🔶
_Current version_
### Features
- [#2378](https://github.com/sanic-org/sanic/pull/2378) Introduce HTTP/3 and autogeneration of TLS certificates in `DEBUG` mode
- 👶 *EARLY RELEASE FEATURE*: Serving Sanic over HTTP/3 is an early release feature. It does not yet fully cover the HTTP/3 spec, but instead aims for feature parity with Sanic's existing HTTP/1.1 server. Websockets, WebTransport, push responses are examples of some features not yet implemented.
- 📦 *EXTRA REQUIREMENT*: Not all HTTP clients are capable of interfacing with HTTP/3 servers. You may need to install a [HTTP/3 capable client](https://curl.se/docs/http3.html).
- 📦 *EXTRA REQUIREMENT*: In order to use TLS autogeneration, you must install either [mkcert](https://github.com/FiloSottile/mkcert) or [trustme](https://github.com/python-trio/trustme).
- [#2416](https://github.com/sanic-org/sanic/pull/2416) Add message to `task.cancel`
- [#2420](https://github.com/sanic-org/sanic/pull/2420) Add exception aliases for more consistent naming with standard HTTP response types (`BadRequest`, `MethodNotAllowed`, `RangeNotSatisfiable`)
- [#2432](https://github.com/sanic-org/sanic/pull/2432) Expose ASGI `scope` as a property on the `Request` object
- [#2438](https://github.com/sanic-org/sanic/pull/2438) Easier access to websocket class for annotation: `from sanic import Websocket`
- [#2439](https://github.com/sanic-org/sanic/pull/2439) New API for reading form values with options: `Request.get_form`
- [#2447](https://github.com/sanic-org/sanic/pull/2447), [#2486](https://github.com/sanic-org/sanic/pull/2486) Improved API to support setting cache control headers
- [#2453](https://github.com/sanic-org/sanic/pull/2453) Move verbosity filtering to logger
- [#2475](https://github.com/sanic-org/sanic/pull/2475) Expose getter for current request using `Request.get_current()`
### Bugfixes
- [#2448](https://github.com/sanic-org/sanic/pull/2448) Fix to allow running with `pythonw.exe` or places where there is no `sys.stdout`
- [#2451](https://github.com/sanic-org/sanic/pull/2451) Trigger `http.lifecycle.request` signal in ASGI mode
- [#2455](https://github.com/sanic-org/sanic/pull/2455) Resolve typing of stacked route definitions
- [#2463](https://github.com/sanic-org/sanic/pull/2463) Properly catch websocket CancelledError in websocket handler in Python 3.7
### Deprecations and Removals
- [#2487](https://github.com/sanic-org/sanic/pull/2487) v22.6 deprecations and changes
1. Optional application registry
1. Execution of custom handlers after some part of response was sent
1. Configuring fallback handlers on the `ErrorHandler`
1. Custom `LOGO` setting
1. `sanic.response.stream`
1. `AsyncioServer.init`
### Developer infrastructure
- [#2449](https://github.com/sanic-org/sanic/pull/2449) Clean up `black` and `isort` config
- [#2479](https://github.com/sanic-org/sanic/pull/2479) Fix some flappy tests
### Improved Documentation
- [#2461](https://github.com/sanic-org/sanic/pull/2461) Update example to match current application naming standards
- [#2466](https://github.com/sanic-org/sanic/pull/2466) Better type annotation for `Extend`
- [#2485](https://github.com/sanic-org/sanic/pull/2485) Improved help messages in CLI

View File

@ -1 +1 @@
__version__ = "22.3.2" __version__ = "22.6.0"

View File

@ -169,7 +169,6 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
strict_slashes: bool = False, strict_slashes: bool = False,
log_config: Optional[Dict[str, Any]] = None, log_config: Optional[Dict[str, Any]] = None,
configure_logging: bool = True, configure_logging: bool = True,
register: Optional[bool] = None,
dumps: Optional[Callable[..., AnyStr]] = None, dumps: Optional[Callable[..., AnyStr]] = None,
) -> None: ) -> None:
super().__init__(name=name) super().__init__(name=name)
@ -218,20 +217,9 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
# Register alternative method names # Register alternative method names
self.go_fast = self.run self.go_fast = self.run
if register is not None:
deprecation(
"The register argument is deprecated and will stop working "
"in v22.6. After v22.6 all apps will be added to the Sanic "
"app registry.",
22.6,
)
self.config.REGISTER = register
if self.config.REGISTER:
self.__class__.register_app(self)
self.router.ctx.app = self self.router.ctx.app = self
self.signal_router.ctx.app = self self.signal_router.ctx.app = self
self.__class__.register_app(self)
if dumps: if dumps:
BaseHTTPResponse._dumps = dumps # type: ignore BaseHTTPResponse._dumps = dumps # type: ignore
@ -736,37 +724,24 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
"has at least partially been sent." "has at least partially been sent."
) )
# ----------------- deprecated -----------------
handler = self.error_handler._lookup( handler = self.error_handler._lookup(
exception, request.name if request else None exception, request.name if request else None
) )
if handler: if handler:
deprecation( logger.warning(
"An error occurred while handling the request after at " "An error occurred while handling the request after at "
"least some part of the response was sent to the client. " "least some part of the response was sent to the client. "
"Therefore, the response from your custom exception " "The response from your custom exception handler "
f"handler {handler.__name__} will not be sent to the " f"{handler.__name__} will not be sent to the client."
"client. Beginning in v22.6, Sanic will stop executing " "Exception handlers should only be used to generate the "
"custom exception handlers in this scenario. Exception " "exception responses. If you would like to perform any "
"handlers should only be used to generate the exception " "other action on a raised exception, consider using a "
"responses. If you would like to perform any other "
"action on a raised exception, please consider using a "
"signal handler like " "signal handler like "
'`@app.signal("http.lifecycle.exception")`\n' '`@app.signal("http.lifecycle.exception")`\n'
"For further information, please see the docs: " "For further information, please see the docs: "
"https://sanicframework.org/en/guide/advanced/" "https://sanicframework.org/en/guide/advanced/"
"signals.html", "signals.html",
22.6,
) )
try:
response = self.error_handler.response(request, exception)
if isawaitable(response):
response = await response
except BaseException as e:
logger.error("An error occurred in the exception handler.")
error_logger.exception(e)
# ----------------------------------------------
return return
# -------------------------------------------- # # -------------------------------------------- #
@ -1559,7 +1534,6 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
if self.state.primary: if self.state.primary:
# TODO: # TODO:
# - Raise warning if secondary apps have error handler config # - Raise warning if secondary apps have error handler config
ErrorHandler.finalize(self.error_handler, config=self.config)
if self.config.TOUCHUP: if self.config.TOUCHUP:
TouchUp.run(self) TouchUp.run(self)

View File

@ -36,7 +36,6 @@ DEFAULT_CONFIG = {
"NOISY_EXCEPTIONS": False, "NOISY_EXCEPTIONS": False,
"PROXIES_COUNT": None, "PROXIES_COUNT": None,
"REAL_IP_HEADER": None, "REAL_IP_HEADER": None,
"REGISTER": True,
"REQUEST_BUFFER_SIZE": 65536, # 64 KiB "REQUEST_BUFFER_SIZE": 65536, # 64 KiB
"REQUEST_MAX_HEADER_SIZE": 8192, # 8 KiB, but cannot exceed 16384 "REQUEST_MAX_HEADER_SIZE": 8192, # 8 KiB, but cannot exceed 16384
"REQUEST_ID_HEADER": "X-Request-ID", "REQUEST_ID_HEADER": "X-Request-ID",
@ -84,7 +83,6 @@ class Config(dict, metaclass=DescriptorMeta):
NOISY_EXCEPTIONS: bool NOISY_EXCEPTIONS: bool
PROXIES_COUNT: Optional[int] PROXIES_COUNT: Optional[int]
REAL_IP_HEADER: Optional[str] REAL_IP_HEADER: Optional[str]
REGISTER: bool
REQUEST_BUFFER_SIZE: int REQUEST_BUFFER_SIZE: int
REQUEST_MAX_HEADER_SIZE: int REQUEST_MAX_HEADER_SIZE: int
REQUEST_ID_HEADER: str REQUEST_ID_HEADER: str
@ -111,7 +109,6 @@ class Config(dict, metaclass=DescriptorMeta):
super().__init__({**DEFAULT_CONFIG, **defaults}) super().__init__({**DEFAULT_CONFIG, **defaults})
self._converters = [str, str_to_bool, float, int] self._converters = [str, str_to_bool, float, int]
self._LOGO = ""
if converters: if converters:
for converter in converters: for converter in converters:
@ -168,24 +165,14 @@ class Config(dict, metaclass=DescriptorMeta):
"REQUEST_MAX_SIZE", "REQUEST_MAX_SIZE",
): ):
self._configure_header_size() self._configure_header_size()
if attr == "LOGO":
self._LOGO = value if attr == "LOCAL_CERT_CREATOR" and not isinstance(
deprecation(
"Setting the config.LOGO is deprecated and will no longer "
"be supported starting in v22.6.",
22.6,
)
elif attr == "LOCAL_CERT_CREATOR" and not isinstance(
self.LOCAL_CERT_CREATOR, LocalCertCreator self.LOCAL_CERT_CREATOR, LocalCertCreator
): ):
self.LOCAL_CERT_CREATOR = LocalCertCreator[ self.LOCAL_CERT_CREATOR = LocalCertCreator[
self.LOCAL_CERT_CREATOR.upper() self.LOCAL_CERT_CREATOR.upper()
] ]
@property
def LOGO(self):
return self._LOGO
@property @property
def FALLBACK_ERROR_FORMAT(self) -> str: def FALLBACK_ERROR_FORMAT(self) -> str:
if self._FALLBACK_ERROR_FORMAT is _default: if self._FALLBACK_ERROR_FORMAT is _default:

View File

@ -1,21 +1,13 @@
from __future__ import annotations from __future__ import annotations
from typing import Dict, List, Optional, Tuple, Type, Union from typing import Dict, List, Optional, Tuple, Type
from sanic.config import Config from sanic.errorpages import BaseRenderer, TextRenderer, exception_response
from sanic.errorpages import (
DEFAULT_FORMAT,
BaseRenderer,
TextRenderer,
exception_response,
)
from sanic.exceptions import ( from sanic.exceptions import (
HeaderNotFound, HeaderNotFound,
InvalidRangeType, InvalidRangeType,
RangeNotSatisfiable, RangeNotSatisfiable,
SanicException,
) )
from sanic.helpers import Default, _default
from sanic.log import deprecation, error_logger from sanic.log import deprecation, error_logger
from sanic.models.handler_types import RouteHandler from sanic.models.handler_types import RouteHandler
from sanic.response import text from sanic.response import text
@ -36,91 +28,22 @@ class ErrorHandler:
def __init__( def __init__(
self, self,
fallback: Union[str, Default] = _default,
base: Type[BaseRenderer] = TextRenderer, base: Type[BaseRenderer] = TextRenderer,
): ):
self.cached_handlers: Dict[ self.cached_handlers: Dict[
Tuple[Type[BaseException], Optional[str]], Optional[RouteHandler] Tuple[Type[BaseException], Optional[str]], Optional[RouteHandler]
] = {} ] = {}
self.debug = False self.debug = False
self._fallback = fallback
self.base = base self.base = base
if fallback is not _default: @classmethod
self._warn_fallback_deprecation() def finalize(cls, *args, **kwargs):
@property
def fallback(self): # no cov
# This is for backwards compat and can be removed in v22.6
if self._fallback is _default:
return DEFAULT_FORMAT
return self._fallback
@fallback.setter
def fallback(self, value: str): # no cov
self._warn_fallback_deprecation()
if not isinstance(value, str):
raise SanicException(
f"Cannot set error handler fallback to: value={value}"
)
self._fallback = value
@staticmethod
def _warn_fallback_deprecation():
deprecation( deprecation(
"Setting the ErrorHandler fallback value directly is " "ErrorHandler.finalize is deprecated and no longer needed. "
"deprecated and no longer supported. This feature will " "Please remove update your code to remove it. ",
"be removed in v22.6. Instead, use " 22.12,
"app.config.FALLBACK_ERROR_FORMAT.",
22.6,
) )
@classmethod
def _get_fallback_value(cls, error_handler: ErrorHandler, config: Config):
if error_handler._fallback is not _default:
if config._FALLBACK_ERROR_FORMAT == error_handler._fallback:
return error_handler.fallback
error_logger.warning(
"Conflicting error fallback values were found in the "
"error handler and in the app.config while handling an "
"exception. Using the value from app.config."
)
return config.FALLBACK_ERROR_FORMAT
@classmethod
def finalize(
cls,
error_handler: ErrorHandler,
config: Config,
fallback: Optional[str] = None,
):
if fallback:
deprecation(
"Setting the ErrorHandler fallback value via finalize() "
"is deprecated and no longer supported. This feature will "
"be removed in v22.6. Instead, use "
"app.config.FALLBACK_ERROR_FORMAT.",
22.6,
)
if not fallback:
fallback = config.FALLBACK_ERROR_FORMAT
if fallback != DEFAULT_FORMAT:
if error_handler._fallback is not _default:
error_logger.warning(
f"Setting the fallback value to {fallback}. This changes "
"the current non-default value "
f"'{error_handler._fallback}'."
)
error_handler._fallback = fallback
if not isinstance(error_handler, cls):
error_logger.warning(
f"Error handler is non-conforming: {type(error_handler)}"
)
def _full_lookup(self, exception, route_name: Optional[str] = None): def _full_lookup(self, exception, route_name: Optional[str] = None):
return self.lookup(exception, route_name) return self.lookup(exception, route_name)
@ -237,7 +160,7 @@ class ErrorHandler:
:return: :return:
""" """
self.log(request, exception) self.log(request, exception)
fallback = ErrorHandler._get_fallback_value(self, request.app.config) fallback = request.app.config.FALLBACK_ERROR_FORMAT
return exception_response( return exception_response(
request, request,
exception, exception,

View File

@ -16,18 +16,6 @@ from typing import (
cast, cast,
) )
from aioquic.h0.connection import H0_ALPN, H0Connection
from aioquic.h3.connection import H3_ALPN, H3Connection
from aioquic.h3.events import (
DatagramReceived,
DataReceived,
H3Event,
HeadersReceived,
WebTransportStreamDataReceived,
)
from aioquic.quic.configuration import QuicConfiguration
from aioquic.tls import SessionTicket
from sanic.compat import Header from sanic.compat import Header
from sanic.constants import LocalCertCreator from sanic.constants import LocalCertCreator
from sanic.exceptions import PayloadTooLarge, SanicException, ServerError from sanic.exceptions import PayloadTooLarge, SanicException, ServerError
@ -40,14 +28,30 @@ from sanic.models.protocol_types import TransportProtocol
from sanic.models.server_types import ConnInfo from sanic.models.server_types import ConnInfo
try:
from aioquic.h0.connection import H0_ALPN, H0Connection
from aioquic.h3.connection import H3_ALPN, H3Connection
from aioquic.h3.events import (
DatagramReceived,
DataReceived,
H3Event,
HeadersReceived,
WebTransportStreamDataReceived,
)
from aioquic.quic.configuration import QuicConfiguration
from aioquic.tls import SessionTicket
HTTP3_AVAILABLE = True
except ModuleNotFoundError: # no cov
HTTP3_AVAILABLE = False
if TYPE_CHECKING: if TYPE_CHECKING:
from sanic import Sanic from sanic import Sanic
from sanic.request import Request from sanic.request import Request
from sanic.response import BaseHTTPResponse from sanic.response import BaseHTTPResponse
from sanic.server.protocols.http_protocol import Http3Protocol from sanic.server.protocols.http_protocol import Http3Protocol
HttpConnection = Union[H0Connection, H3Connection]
HttpConnection = Union[H0Connection, H3Connection]
class HTTP3Transport(TransportProtocol): class HTTP3Transport(TransportProtocol):
@ -269,12 +273,13 @@ class Http3:
Internal helper for managing the HTTP/3 request/response cycle Internal helper for managing the HTTP/3 request/response cycle
""" """
HANDLER_PROPERTY_MAPPING = { if HTTP3_AVAILABLE:
DataReceived: "stream_id", HANDLER_PROPERTY_MAPPING = {
HeadersReceived: "stream_id", DataReceived: "stream_id",
DatagramReceived: "flow_id", HeadersReceived: "stream_id",
WebTransportStreamDataReceived: "session_id", DatagramReceived: "flow_id",
} WebTransportStreamDataReceived: "session_id",
}
def __init__( def __init__(
self, self,

View File

@ -34,7 +34,7 @@ from sanic.exceptions import (
RangeNotSatisfiable, RangeNotSatisfiable,
) )
from sanic.handlers import ContentRangeHandler from sanic.handlers import ContentRangeHandler
from sanic.log import deprecation, error_logger from sanic.log import error_logger
from sanic.models.futures import FutureRoute, FutureStatic from sanic.models.futures import FutureRoute, FutureStatic
from sanic.models.handler_types import RouteHandler from sanic.models.handler_types import RouteHandler
from sanic.response import HTTPResponse, file, file_stream from sanic.response import HTTPResponse, file, file_stream
@ -1025,17 +1025,6 @@ class RouteMixin(metaclass=SanicMeta):
nonlocal types nonlocal types
with suppress(AttributeError): with suppress(AttributeError):
if node.value.func.id == "stream": # type: ignore
deprecation(
"The sanic.response.stream method has been "
"deprecated and will be removed in v22.6. Please "
"upgrade your application to use the new style "
"streaming pattern. See "
"https://sanicframework.org/en/guide/advanced/"
"streaming.html#response-streaming for more "
"information.",
22.6,
)
checks = [node.value.func.id] # type: ignore checks = [node.value.func.id] # type: ignore
if node.value.keywords: # type: ignore if node.value.keywords: # type: ignore
checks += [ checks += [
@ -1066,7 +1055,7 @@ class RouteMixin(metaclass=SanicMeta):
raise AttributeError( raise AttributeError(
"Cannot use restricted route context: " "Cannot use restricted route context: "
f"{restricted_arguments}. This limitation is only in place " f"{restricted_arguments}. This limitation is only in place "
"until v22.3 when the restricted names will no longer be in" "until v22.9 when the restricted names will no longer be in"
"conflict. See https://github.com/sanic-org/sanic/issues/2303 " "conflict. See https://github.com/sanic-org/sanic/issues/2303 "
"for more information." "for more information."
) )

View File

@ -565,11 +565,7 @@ class RunnerMixin(metaclass=SanicMeta):
if self.config.MOTD_DISPLAY: if self.config.MOTD_DISPLAY:
extra.update(self.config.MOTD_DISPLAY) extra.update(self.config.MOTD_DISPLAY)
logo = ( logo = get_logo(coffee=self.state.coffee)
get_logo(coffee=self.state.coffee)
if self.config.LOGO == "" or self.config.LOGO is True
else self.config.LOGO
)
MOTD.output(logo, serve_location, display, extra) MOTD.output(logo, serve_location, display, extra)

View File

@ -427,8 +427,7 @@ def redirect(
class ResponseStream: class ResponseStream:
""" """
ResponseStream is a compat layer to bridge the gap after the deprecation ResponseStream is a compat layer to bridge the gap after the deprecation
of StreamingHTTPResponse. In v22.6 it will be removed when: of StreamingHTTPResponse. It will be removed when:
- stream is removed
- file_stream is moved to new style streaming - file_stream is moved to new style streaming
- file and file_stream are combined into a single API - file and file_stream are combined into a single API
""" """
@ -556,38 +555,3 @@ async def file_stream(
headers=headers, headers=headers,
content_type=mime_type, content_type=mime_type,
) )
def stream(
streaming_fn: Callable[
[Union[BaseHTTPResponse, ResponseStream]], Coroutine[Any, Any, None]
],
status: int = 200,
headers: Optional[Dict[str, str]] = None,
content_type: str = "text/plain; charset=utf-8",
) -> ResponseStream:
"""Accepts a coroutine `streaming_fn` which can be used to
write chunks to a streaming response. Returns a `ResponseStream`.
Example usage::
@app.route("/")
async def index(request):
async def streaming_fn(response):
await response.write('foo')
await response.write('bar')
return stream(streaming_fn, content_type='text/plain')
:param streaming_fn: A coroutine accepts a response and
writes content to that response.
:param status: HTTP status.
:param content_type: Specific content_type.
:param headers: Custom Headers.
"""
return ResponseStream(
streaming_fn,
headers=headers,
content_type=content_type,
status=status,
)

View File

@ -5,7 +5,6 @@ import asyncio
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from sanic.exceptions import SanicException from sanic.exceptions import SanicException
from sanic.log import deprecation
if TYPE_CHECKING: if TYPE_CHECKING:
@ -35,15 +34,6 @@ class AsyncioServer:
self.serve_coro = serve_coro self.serve_coro = serve_coro
self.server = None self.server = None
@property
def init(self):
deprecation(
"AsyncioServer.init has been deprecated and will be removed "
"in v22.6. Use Sanic.state.is_started instead.",
22.6,
)
return self.app.state.is_started
def startup(self): def startup(self):
""" """
Trigger "before_server_start" events Trigger "before_server_start" events

View File

@ -2,8 +2,6 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Optional from typing import TYPE_CHECKING, Optional
from aioquic.h3.connection import H3_ALPN, H3Connection
from sanic.http.constants import HTTP from sanic.http.constants import HTTP
from sanic.http.http3 import Http3 from sanic.http.http3 import Http3
from sanic.touchup.meta import TouchUpMeta from sanic.touchup.meta import TouchUpMeta
@ -17,13 +15,6 @@ import sys
from asyncio import CancelledError from asyncio import CancelledError
from time import monotonic as current_time from time import monotonic as current_time
from aioquic.asyncio import QuicConnectionProtocol
from aioquic.quic.events import (
DatagramFrameReceived,
ProtocolNegotiated,
QuicEvent,
)
from sanic.exceptions import RequestTimeout, ServiceUnavailable from sanic.exceptions import RequestTimeout, ServiceUnavailable
from sanic.http import Http, Stage from sanic.http import Http, Stage
from sanic.log import Colors, error_logger, logger from sanic.log import Colors, error_logger, logger
@ -32,6 +23,21 @@ from sanic.request import Request
from sanic.server.protocols.base_protocol import SanicProtocol from sanic.server.protocols.base_protocol import SanicProtocol
ConnectionProtocol = type("ConnectionProtocol", (), {})
try:
from aioquic.asyncio import QuicConnectionProtocol
from aioquic.h3.connection import H3_ALPN, H3Connection
from aioquic.quic.events import (
DatagramFrameReceived,
ProtocolNegotiated,
QuicEvent,
)
ConnectionProtocol = QuicConnectionProtocol
except ModuleNotFoundError: # no cov
...
class HttpProtocolMixin: class HttpProtocolMixin:
__slots__ = () __slots__ = ()
__version__: HTTP __version__: HTTP
@ -278,7 +284,7 @@ class HttpProtocol(HttpProtocolMixin, SanicProtocol, metaclass=TouchUpMeta):
error_logger.exception("protocol.data_received") error_logger.exception("protocol.data_received")
class Http3Protocol(HttpProtocolMixin, QuicConnectionProtocol): class Http3Protocol(HttpProtocolMixin, ConnectionProtocol): # type: ignore
HTTP_CLASS = Http3 HTTP_CLASS = Http3
__version__ = HTTP.VERSION_3 __version__ = HTTP.VERSION_3

View File

@ -6,6 +6,7 @@ from ssl import SSLContext
from typing import TYPE_CHECKING, Dict, Optional, Type, Union from typing import TYPE_CHECKING, Dict, Optional, Type, Union
from sanic.config import Config from sanic.config import Config
from sanic.exceptions import ServerError
from sanic.http.constants import HTTP from sanic.http.constants import HTTP
from sanic.http.tls import get_ssl_context from sanic.http.tls import get_ssl_context
from sanic.server.events import trigger_events from sanic.server.events import trigger_events
@ -23,8 +24,6 @@ from functools import partial
from signal import SIG_IGN, SIGINT, SIGTERM, Signals from signal import SIG_IGN, SIGINT, SIGTERM, Signals
from signal import signal as signal_func from signal import signal as signal_func
from aioquic.asyncio import serve as quic_serve
from sanic.application.ext import setup_ext from sanic.application.ext import setup_ext
from sanic.compat import OS_IS_WINDOWS, ctrlc_workaround_for_windows from sanic.compat import OS_IS_WINDOWS, ctrlc_workaround_for_windows
from sanic.http.http3 import SessionTicketStore, get_config from sanic.http.http3 import SessionTicketStore, get_config
@ -39,6 +38,14 @@ from sanic.server.socket import (
) )
try:
from aioquic.asyncio import serve as quic_serve
HTTP3_AVAILABLE = True
except ModuleNotFoundError: # no cov
HTTP3_AVAILABLE = False
def serve( def serve(
host, host,
port, port,
@ -273,6 +280,10 @@ def _serve_http_3(
register_sys_signals: bool = True, register_sys_signals: bool = True,
run_multiple: bool = False, run_multiple: bool = False,
): ):
if not HTTP3_AVAILABLE:
raise ServerError(
"Cannot run HTTP/3 server without aioquic installed. "
)
protocol = partial(Http3Protocol, app=app) protocol = partial(Http3Protocol, app=app)
ticket_store = SessionTicketStore() ticket_store = SessionTicketStore()
ssl_context = get_ssl_context(app, ssl) ssl_context = get_ssl_context(app, ssl)

View File

@ -4,7 +4,6 @@ import re
from collections import Counter from collections import Counter
from inspect import isawaitable from inspect import isawaitable
from os import environ
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
import pytest import pytest
@ -113,19 +112,6 @@ def test_create_server_main_convenience(app, caplog):
) in caplog.record_tuples ) in caplog.record_tuples
def test_create_server_init(app, caplog):
loop = asyncio.get_event_loop()
asyncio_srv_coro = app.create_server(return_asyncio_server=True)
server = loop.run_until_complete(asyncio_srv_coro)
message = (
"AsyncioServer.init has been deprecated and will be removed in v22.6. "
"Use Sanic.state.is_started instead."
)
with pytest.warns(DeprecationWarning, match=message):
server.init
def test_app_loop_not_running(app): def test_app_loop_not_running(app):
with pytest.raises(SanicException) as excinfo: with pytest.raises(SanicException) as excinfo:
app.loop app.loop
@ -385,40 +371,6 @@ def test_get_app_default_ambiguous():
Sanic.get_app() Sanic.get_app()
def test_app_no_registry():
Sanic("no-register", register=False)
with pytest.raises(
SanicException, match='Sanic app name "no-register" not found.'
):
Sanic.get_app("no-register")
def test_app_no_registry_deprecation_message():
with pytest.warns(DeprecationWarning) as records:
Sanic("no-register", register=False)
Sanic("yes-register", register=True)
message = (
"[DEPRECATION v22.6] The register argument is deprecated and will "
"stop working in v22.6. After v22.6 all apps will be added to the "
"Sanic app registry."
)
assert len(records) == 2
for record in records:
assert record.message.args[0] == message
def test_app_no_registry_env():
environ["SANIC_REGISTER"] = "False"
Sanic("no-register")
with pytest.raises(
SanicException, match='Sanic app name "no-register" not found.'
):
Sanic.get_app("no-register")
del environ["SANIC_REGISTER"]
def test_app_set_attribute_warning(app): def test_app_set_attribute_warning(app):
message = ( message = (
"Setting variables on Sanic instances is not allowed. You should " "Setting variables on Sanic instances is not allowed. You should "

View File

@ -371,15 +371,6 @@ def test_update_from_lowercase_key(app: Sanic):
assert "test_setting_value" not in app.config assert "test_setting_value" not in app.config
def test_deprecation_notice_when_setting_logo(app: Sanic):
message = (
"Setting the config.LOGO is deprecated and will no longer be "
"supported starting in v22.6."
)
with pytest.warns(DeprecationWarning, match=message):
app.config.LOGO = "My Custom Logo"
def test_config_set_methods(app: Sanic, monkeypatch: MonkeyPatch): def test_config_set_methods(app: Sanic, monkeypatch: MonkeyPatch):
post_set = Mock() post_set = Mock()
monkeypatch.setattr(Config, "_post_set", post_set) monkeypatch.setattr(Config, "_post_set", post_set)

View File

@ -13,13 +13,7 @@ from sanic import Sanic, handlers
from sanic.exceptions import BadRequest, Forbidden, NotFound, ServerError from sanic.exceptions import BadRequest, Forbidden, NotFound, ServerError
from sanic.handlers import ErrorHandler from sanic.handlers import ErrorHandler
from sanic.request import Request from sanic.request import Request
from sanic.response import stream, text from sanic.response import text
async def sample_streaming_fn(response):
await response.write("foo,")
await asyncio.sleep(0.001)
await response.write("bar")
class ErrorWithRequestCtx(ServerError): class ErrorWithRequestCtx(ServerError):
@ -81,10 +75,10 @@ def exception_handler_app():
@exception_handler_app.exception(Forbidden) @exception_handler_app.exception(Forbidden)
async def async_handler_exception(request, exception): async def async_handler_exception(request, exception):
return stream( response = await request.respond(content_type="text/csv")
sample_streaming_fn, await response.send("foo,")
content_type="text/csv", await asyncio.sleep(0.001)
) await response.send("bar")
@exception_handler_app.middleware @exception_handler_app.middleware
async def some_request_middleware(request): async def some_request_middleware(request):
@ -183,7 +177,7 @@ def test_exception_handler_lookup(exception_handler_app: Sanic):
class ModuleNotFoundError(ImportError): class ModuleNotFoundError(ImportError):
pass pass
handler = ErrorHandler("auto") handler = ErrorHandler()
handler.add(ImportError, import_error_handler) handler.add(ImportError, import_error_handler)
handler.add(CustomError, custom_error_handler) handler.add(CustomError, custom_error_handler)
handler.add(ServerError, server_error_handler) handler.add(ServerError, server_error_handler)
@ -261,7 +255,6 @@ def test_exception_handler_response_was_sent(
_, response = app.test_client.get("/1") _, response = app.test_client.get("/1")
assert "some text" in response.text assert "some text" in response.text
# Change to assert warning not in the records in the future version.
message_in_records( message_in_records(
caplog.records, caplog.records,
( (

View File

@ -19,35 +19,6 @@ def test_logo_base(app, run_startup):
assert logs[0][2] == BASE_LOGO assert logs[0][2] == BASE_LOGO
def test_logo_false(app, run_startup):
app.config.LOGO = False
logs = run_startup(app)
banner, port = logs[1][2].rsplit(":", 1)
assert logs[0][1] == logging.INFO
assert banner == "Goin' Fast @ http://127.0.0.1"
assert int(port) > 0
def test_logo_true(app, run_startup):
app.config.LOGO = True
logs = run_startup(app)
assert logs[0][1] == logging.DEBUG
assert logs[0][2] == BASE_LOGO
def test_logo_custom(app, run_startup):
app.config.LOGO = "My Custom Logo"
logs = run_startup(app)
assert logs[0][1] == logging.DEBUG
assert logs[0][2] == "My Custom Logo"
def test_motd_with_expected_info(app, run_startup): def test_motd_with_expected_info(app, run_startup):
logs = run_startup(app) logs = run_startup(app)

View File

@ -3,7 +3,7 @@ import contextlib
import pytest import pytest
from sanic.response import stream, text from sanic.response import text
@pytest.mark.asyncio @pytest.mark.asyncio
@ -43,18 +43,16 @@ async def test_stream_request_cancel_when_conn_lost(app):
async def post(request, id): async def post(request, id):
assert isinstance(request.stream, asyncio.Queue) assert isinstance(request.stream, asyncio.Queue)
async def streaming(response): response = await request.respond()
while True:
body = await request.stream.get()
if body is None:
break
await response.write(body.decode("utf-8"))
await asyncio.sleep(1.0) await asyncio.sleep(1.0)
# at this point client is already disconnected # at this point client is already disconnected
app.ctx.still_serving_cancelled_request = True app.ctx.still_serving_cancelled_request = True
while True:
return stream(streaming) body = await request.stream.get()
if body is None:
break
await response.send(body.decode("utf-8"))
# schedule client call # schedule client call
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()

View File

@ -27,7 +27,6 @@ from sanic.response import (
file_stream, file_stream,
json, json,
raw, raw,
stream,
text, text,
) )
@ -49,10 +48,13 @@ def test_response_body_not_a_string(app):
assert b"Internal Server Error" in response.body assert b"Internal Server Error" in response.body
async def sample_streaming_fn(response): async def sample_streaming_fn(request, response=None):
await response.write("foo,") if not response:
response = await request.respond(content_type="text/csv")
await response.send("foo,")
await asyncio.sleep(0.001) await asyncio.sleep(0.001)
await response.write("bar") await response.send("bar")
await response.eof()
def test_method_not_allowed(): def test_method_not_allowed():
@ -217,10 +219,7 @@ def test_no_content(json_app):
def streaming_app(app): def streaming_app(app):
@app.route("/") @app.route("/")
async def test(request: Request): async def test(request: Request):
return stream( await sample_streaming_fn(request)
sample_streaming_fn,
content_type="text/csv",
)
return app return app
@ -229,11 +228,11 @@ def streaming_app(app):
def non_chunked_streaming_app(app): def non_chunked_streaming_app(app):
@app.route("/") @app.route("/")
async def test(request: Request): async def test(request: Request):
return stream( response = await request.respond(
sample_streaming_fn,
headers={"Content-Length": "7"}, headers={"Content-Length": "7"},
content_type="text/csv", content_type="text/csv",
) )
await sample_streaming_fn(request, response)
return app return app
@ -283,18 +282,6 @@ def test_non_chunked_streaming_returns_correct_content(
assert response.text == "foo,bar" assert response.text == "foo,bar"
def test_stream_response_with_cookies_legacy(app):
@app.route("/")
async def test(request: Request):
response = stream(sample_streaming_fn, content_type="text/csv")
response.cookies["test"] = "modified"
response.cookies["test"] = "pass"
return response
request, response = app.test_client.get("/")
assert response.cookies["test"] == "pass"
def test_stream_response_with_cookies(app): def test_stream_response_with_cookies(app):
@app.route("/") @app.route("/")
async def test(request: Request): async def test(request: Request):
@ -317,7 +304,7 @@ def test_stream_response_with_cookies(app):
def test_stream_response_without_cookies(app): def test_stream_response_without_cookies(app):
@app.route("/") @app.route("/")
async def test(request: Request): async def test(request: Request):
return stream(sample_streaming_fn, content_type="text/csv") await sample_streaming_fn(request)
request, response = app.test_client.get("/") request, response = app.test_client.get("/")
assert response.cookies == {} assert response.cookies == {}