v23.3 Deprecation Removal (#2717)

This commit is contained in:
Adam Hopkins 2023-03-26 15:24:08 +03:00 committed by GitHub
parent a8c2d77c91
commit d680af3709
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 180 additions and 598 deletions

View File

@ -64,12 +64,7 @@ from sanic.exceptions import (
from sanic.handlers import ErrorHandler from sanic.handlers import ErrorHandler
from sanic.helpers import Default, _default from sanic.helpers import Default, _default
from sanic.http import Stage from sanic.http import Stage
from sanic.log import ( from sanic.log import LOGGING_CONFIG_DEFAULTS, error_logger, logger
LOGGING_CONFIG_DEFAULTS,
deprecation,
error_logger,
logger,
)
from sanic.middleware import Middleware, MiddlewareLocation from sanic.middleware import Middleware, MiddlewareLocation
from sanic.mixins.listeners import ListenerEvent from sanic.mixins.listeners import ListenerEvent
from sanic.mixins.startup import StartupMixin from sanic.mixins.startup import StartupMixin
@ -1584,17 +1579,19 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta):
self.signalize(self.config.TOUCHUP) self.signalize(self.config.TOUCHUP)
self.finalize() self.finalize()
route_names = [route.name for route in self.router.routes] route_names = [route.extra.ident for route in self.router.routes]
duplicates = { duplicates = {
name for name in route_names if route_names.count(name) > 1 name for name in route_names if route_names.count(name) > 1
} }
if duplicates: if duplicates:
names = ", ".join(duplicates) names = ", ".join(duplicates)
deprecation( message = (
f"Duplicate route names detected: {names}. In the future, " f"Duplicate route names detected: {names}. You should rename "
"Sanic will enforce uniqueness in route naming.", "one or more of them explicitly by using the `name` param, "
23.3, "or changing the implicit name derived from the class and "
"function name. For more details, please see ___."
) )
raise ServerError(message)
Sanic._check_uvloop_conflict() Sanic._check_uvloop_conflict()

View File

@ -93,6 +93,7 @@ class Blueprint(BaseSanic):
"_future_listeners", "_future_listeners",
"_future_exceptions", "_future_exceptions",
"_future_signals", "_future_signals",
"copied_from",
"ctx", "ctx",
"exceptions", "exceptions",
"host", "host",
@ -118,6 +119,7 @@ class Blueprint(BaseSanic):
): ):
super().__init__(name=name) super().__init__(name=name)
self.reset() self.reset()
self.copied_from = ""
self.ctx = SimpleNamespace() self.ctx = SimpleNamespace()
self.host = host self.host = host
self.strict_slashes = strict_slashes self.strict_slashes = strict_slashes
@ -213,6 +215,7 @@ class Blueprint(BaseSanic):
self.reset() self.reset()
new_bp = deepcopy(self) new_bp = deepcopy(self)
new_bp.name = name new_bp.name = name
new_bp.copied_from = self.name
if not isinstance(url_prefix, Default): if not isinstance(url_prefix, Default):
new_bp.url_prefix = url_prefix new_bp.url_prefix = url_prefix
@ -352,6 +355,16 @@ class Blueprint(BaseSanic):
registered.add(apply_route) registered.add(apply_route)
route = app._apply_route(apply_route) route = app._apply_route(apply_route)
# If it is a copied BP, then make sure all of the names of routes
# matchup with the new BP name
if self.copied_from:
for r in route:
r.name = r.name.replace(self.copied_from, self.name)
r.extra.ident = r.extra.ident.replace(
self.copied_from, self.name
)
operation = ( operation = (
routes.extend if isinstance(route, list) else routes.append routes.extend if isinstance(route, list) else routes.append
) )

View File

@ -1,4 +1,3 @@
import logging
import os import os
import shutil import shutil
import sys import sys
@ -6,7 +5,7 @@ import sys
from argparse import Namespace from argparse import Namespace
from functools import partial from functools import partial
from textwrap import indent from textwrap import indent
from typing import List, Union, cast from typing import List, Union
from sanic.app import Sanic from sanic.app import Sanic
from sanic.application.logo import get_logo from sanic.application.logo import get_logo
@ -14,7 +13,7 @@ from sanic.cli.arguments import Group
from sanic.cli.base import SanicArgumentParser, SanicHelpFormatter from sanic.cli.base import SanicArgumentParser, SanicHelpFormatter
from sanic.cli.inspector import make_inspector_parser from sanic.cli.inspector import make_inspector_parser
from sanic.cli.inspector_client import InspectorClient from sanic.cli.inspector_client import InspectorClient
from sanic.log import Colors, error_logger from sanic.log import error_logger
from sanic.worker.loader import AppLoader from sanic.worker.loader import AppLoader
@ -103,10 +102,6 @@ Or, a path to a directory to run as a simple HTTP server:
self.args.target, self.args.factory, self.args.simple, self.args self.args.target, self.args.factory, self.args.simple, self.args
) )
if self.args.inspect or self.args.inspect_raw or self.args.trigger:
self._inspector_legacy(app_loader)
return
try: try:
app = self._get_app(app_loader) app = self._get_app(app_loader)
kwargs = self._build_run_kwargs() kwargs = self._build_run_kwargs()
@ -117,38 +112,10 @@ Or, a path to a directory to run as a simple HTTP server:
app.prepare(**kwargs, version=http_version) app.prepare(**kwargs, version=http_version)
if self.args.single: if self.args.single:
serve = Sanic.serve_single serve = Sanic.serve_single
elif self.args.legacy:
serve = Sanic.serve_legacy
else: else:
serve = partial(Sanic.serve, app_loader=app_loader) serve = partial(Sanic.serve, app_loader=app_loader)
serve(app) serve(app)
def _inspector_legacy(self, app_loader: AppLoader):
host = port = None
target = cast(str, self.args.target)
if ":" in target:
maybe_host, maybe_port = target.rsplit(":", 1)
if maybe_port.isnumeric():
host, port = maybe_host, int(maybe_port)
if not host:
app = self._get_app(app_loader)
host, port = app.config.INSPECTOR_HOST, app.config.INSPECTOR_PORT
action = self.args.trigger or "info"
InspectorClient(
str(host), int(port or 6457), False, self.args.inspect_raw, ""
).do(action)
sys.stdout.write(
f"\n{Colors.BOLD}{Colors.YELLOW}WARNING:{Colors.END} "
"You are using the legacy CLI command that will be removed in "
f"{Colors.RED}v23.3{Colors.END}. See "
"https://sanic.dev/en/guide/release-notes/v22.12.html"
"#deprecations-and-removals or checkout the new "
"style commands:\n\n\t"
f"{Colors.YELLOW}sanic inspect --help{Colors.END}\n"
)
def _inspector(self): def _inspector(self):
args = sys.argv[2:] args = sys.argv[2:]
self.args, unknown = self.parser.parse_known_args(args=args) self.args, unknown = self.parser.parse_known_args(args=args)
@ -202,8 +169,6 @@ Or, a path to a directory to run as a simple HTTP server:
) )
error_logger.error(message) error_logger.error(message)
sys.exit(1) sys.exit(1)
if self.args.inspect or self.args.inspect_raw:
logging.disable(logging.CRITICAL)
def _get_app(self, app_loader: AppLoader): def _get_app(self, app_loader: AppLoader):
try: try:
@ -251,7 +216,6 @@ Or, a path to a directory to run as a simple HTTP server:
"workers": self.args.workers, "workers": self.args.workers,
"auto_tls": self.args.auto_tls, "auto_tls": self.args.auto_tls,
"single_process": self.args.single, "single_process": self.args.single,
"legacy": self.args.legacy,
} }
for maybe_arg in ("auto_reload", "dev"): for maybe_arg in ("auto_reload", "dev"):

View File

@ -93,32 +93,6 @@ class ApplicationGroup(Group):
"a directory\n(module arg should be a path)" "a directory\n(module arg should be a path)"
), ),
) )
group.add_argument(
"--inspect",
dest="inspect",
action="store_true",
help=("Inspect the state of a running instance, human readable"),
)
group.add_argument(
"--inspect-raw",
dest="inspect_raw",
action="store_true",
help=("Inspect the state of a running instance, JSON output"),
)
group.add_argument(
"--trigger-reload",
dest="trigger",
action="store_const",
const="reload",
help=("Trigger worker processes to reload"),
)
group.add_argument(
"--trigger-shutdown",
dest="trigger",
action="store_const",
const="shutdown",
help=("Trigger all processes to shutdown"),
)
class HTTPVersionGroup(Group): class HTTPVersionGroup(Group):
@ -247,11 +221,6 @@ class WorkerGroup(Group):
action="store_true", action="store_true",
help="Do not use multiprocessing, run server in a single process", help="Do not use multiprocessing, run server in a single process",
) )
self.container.add_argument(
"--legacy",
action="store_true",
help="Use the legacy server manager",
)
self.add_bool_arguments( self.add_bool_arguments(
"--access-logs", "--access-logs",
dest="access_log", dest="access_log",

View File

@ -3,7 +3,8 @@ from __future__ import annotations
from typing import Dict, List, Optional, Tuple, Type from typing import Dict, List, Optional, Tuple, Type
from sanic.errorpages import BaseRenderer, TextRenderer, exception_response from sanic.errorpages import BaseRenderer, TextRenderer, exception_response
from sanic.log import deprecation, error_logger from sanic.exceptions import ServerError
from sanic.log import 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
@ -43,16 +44,11 @@ class ErrorHandler:
if name is None: if name is None:
name = "__ALL_ROUTES__" name = "__ALL_ROUTES__"
error_logger.warning( message = (
f"Duplicate exception handler definition on: route={name} " f"Duplicate exception handler definition on: route={name} "
f"and exception={exc}" f"and exception={exc}"
) )
deprecation( raise ServerError(message)
"A duplicate exception handler definition was discovered. "
"This may cause unintended consequences. A warning has been "
"issued now, but it will not be allowed starting in v23.3.",
23.3,
)
self.cached_handlers[key] = handler self.cached_handlers[key] = handler
def add(self, exception, handler, route_names: Optional[List[str]] = None): def add(self, exception, handler, route_names: Optional[List[str]] = None):

View File

@ -47,17 +47,16 @@ from sanic.helpers import Default, _default
from sanic.http.constants import HTTP from sanic.http.constants import HTTP
from sanic.http.tls import get_ssl_context, process_to_context from sanic.http.tls import get_ssl_context, process_to_context
from sanic.http.tls.context import SanicSSLContext from sanic.http.tls.context import SanicSSLContext
from sanic.log import Colors, deprecation, error_logger, logger from sanic.log import Colors, error_logger, logger
from sanic.models.handler_types import ListenerType from sanic.models.handler_types import ListenerType
from sanic.server import Signal as ServerSignal from sanic.server import Signal as ServerSignal
from sanic.server import try_use_uvloop from sanic.server import try_use_uvloop
from sanic.server.async_server import AsyncioServer from sanic.server.async_server import AsyncioServer
from sanic.server.events import trigger_events from sanic.server.events import trigger_events
from sanic.server.legacy import watchdog
from sanic.server.loop import try_windows_loop from sanic.server.loop import try_windows_loop
from sanic.server.protocols.http_protocol import HttpProtocol from sanic.server.protocols.http_protocol import HttpProtocol
from sanic.server.protocols.websocket_protocol import WebSocketProtocol from sanic.server.protocols.websocket_protocol import WebSocketProtocol
from sanic.server.runners import serve, serve_multiple, serve_single from sanic.server.runners import serve
from sanic.server.socket import configure_socket, remove_unix_socket from sanic.server.socket import configure_socket, remove_unix_socket
from sanic.worker.loader import AppLoader from sanic.worker.loader import AppLoader
from sanic.worker.manager import WorkerManager from sanic.worker.manager import WorkerManager
@ -135,7 +134,6 @@ class StartupMixin(metaclass=SanicMeta):
motd_display: Optional[Dict[str, str]] = None, motd_display: Optional[Dict[str, str]] = None,
auto_tls: bool = False, auto_tls: bool = False,
single_process: bool = False, single_process: bool = False,
legacy: bool = False,
) -> None: ) -> None:
""" """
Run the HTTP Server and listen until keyboard interrupt or term Run the HTTP Server and listen until keyboard interrupt or term
@ -197,13 +195,10 @@ class StartupMixin(metaclass=SanicMeta):
motd_display=motd_display, motd_display=motd_display,
auto_tls=auto_tls, auto_tls=auto_tls,
single_process=single_process, single_process=single_process,
legacy=legacy,
) )
if single_process: if single_process:
serve = self.__class__.serve_single serve = self.__class__.serve_single
elif legacy:
serve = self.__class__.serve_legacy
else: else:
serve = self.__class__.serve serve = self.__class__.serve
serve(primary=self) # type: ignore serve(primary=self) # type: ignore
@ -235,7 +230,6 @@ class StartupMixin(metaclass=SanicMeta):
coffee: bool = False, coffee: bool = False,
auto_tls: bool = False, auto_tls: bool = False,
single_process: bool = False, single_process: bool = False,
legacy: bool = False,
) -> None: ) -> None:
if version == 3 and self.state.server_info: if version == 3 and self.state.server_info:
raise RuntimeError( raise RuntimeError(
@ -264,13 +258,10 @@ class StartupMixin(metaclass=SanicMeta):
"or auto-reload" "or auto-reload"
) )
if single_process and legacy: if register_sys_signals is False and not single_process:
raise RuntimeError("Cannot run single process and legacy mode")
if register_sys_signals is False and not (single_process or legacy):
raise RuntimeError( raise RuntimeError(
"Cannot run Sanic.serve with register_sys_signals=False. " "Cannot run Sanic.serve with register_sys_signals=False. "
"Use either Sanic.serve_single or Sanic.serve_legacy." "Use Sanic.serve_single."
) )
if motd_display: if motd_display:
@ -956,76 +947,6 @@ class StartupMixin(metaclass=SanicMeta):
cls._cleanup_env_vars() cls._cleanup_env_vars()
cls._cleanup_apps() cls._cleanup_apps()
@classmethod
def serve_legacy(cls, primary: Optional[Sanic] = None) -> None:
apps = list(cls._app_registry.values())
if not primary:
try:
primary = apps[0]
except IndexError:
raise RuntimeError("Did not find any applications.")
reloader_start = primary.listeners.get("reload_process_start")
reloader_stop = primary.listeners.get("reload_process_stop")
# We want to run auto_reload if ANY of the applications have it enabled
if (
cls.should_auto_reload()
and os.environ.get("SANIC_SERVER_RUNNING") != "true"
): # no cov
loop = new_event_loop()
trigger_events(reloader_start, loop, primary)
reload_dirs: Set[Path] = primary.state.reload_dirs.union(
*(app.state.reload_dirs for app in apps)
)
watchdog(1.0, reload_dirs)
trigger_events(reloader_stop, loop, primary)
return
# This exists primarily for unit testing
if not primary.state.server_info: # no cov
for app in apps:
app.state.server_info.clear()
return
primary_server_info = primary.state.server_info[0]
primary.before_server_start(partial(primary._start_servers, apps=apps))
deprecation(
f"{Colors.YELLOW}Running {Colors.SANIC}Sanic {Colors.YELLOW}w/ "
f"LEGACY manager.{Colors.END} Support for will be dropped in "
"version 23.3.",
23.3,
)
try:
primary_server_info.stage = ServerStage.SERVING
if primary.state.workers > 1 and os.name != "posix": # no cov
logger.warning(
f"Multiprocessing is currently not supported on {os.name},"
" using workers=1 instead"
)
primary.state.workers = 1
if primary.state.workers == 1:
serve_single(primary_server_info.settings)
elif primary.state.workers == 0:
raise RuntimeError("Cannot serve with no workers")
else:
serve_multiple(
primary_server_info.settings, primary.state.workers
)
except BaseException:
error_logger.exception(
"Experienced exception while trying to serve"
)
raise
finally:
primary_server_info.stage = ServerStage.STOPPED
logger.info("Server Stopped")
cls._cleanup_env_vars()
cls._cleanup_apps()
async def _start_servers( async def _start_servers(
self, self,
primary: Sanic, primary: Sanic,

View File

@ -3,7 +3,7 @@ from functools import partial, wraps
from mimetypes import guess_type from mimetypes import guess_type
from os import PathLike, path from os import PathLike, path
from pathlib import Path, PurePath from pathlib import Path, PurePath
from typing import Optional, Sequence, Set, Union, cast from typing import Optional, Sequence, Set, Union
from urllib.parse import unquote from urllib.parse import unquote
from sanic_routing.route import Route from sanic_routing.route import Route
@ -14,7 +14,7 @@ from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE
from sanic.exceptions import FileNotFound, HeaderNotFound, RangeNotSatisfiable from sanic.exceptions import FileNotFound, HeaderNotFound, RangeNotSatisfiable
from sanic.handlers import ContentRangeHandler from sanic.handlers import ContentRangeHandler
from sanic.handlers.directory import DirectoryHandler from sanic.handlers.directory import DirectoryHandler
from sanic.log import deprecation, error_logger from sanic.log import error_logger
from sanic.mixins.base import BaseMixin from sanic.mixins.base import BaseMixin
from sanic.models.futures import FutureStatic from sanic.models.futures import FutureStatic
from sanic.request import Request from sanic.request import Request
@ -31,7 +31,7 @@ class StaticMixin(BaseMixin, metaclass=SanicMeta):
def static( def static(
self, self,
uri: str, uri: str,
file_or_directory: Union[PathLike, str, bytes], file_or_directory: Union[PathLike, str],
pattern: str = r"/?.+", pattern: str = r"/?.+",
use_modified_since: bool = True, use_modified_since: bool = True,
use_content_range: bool = False, use_content_range: bool = False,
@ -94,14 +94,12 @@ class StaticMixin(BaseMixin, metaclass=SanicMeta):
f"Static route must be a valid path, not {file_or_directory}" f"Static route must be a valid path, not {file_or_directory}"
) )
if isinstance(file_or_directory, bytes): try:
deprecation( file_or_directory = Path(file_or_directory)
"Serving a static directory with a bytes string is " except TypeError:
"deprecated and will be removed in v22.9.", raise TypeError(
22.9, "Static file or directory must be a path-like object or string"
) )
file_or_directory = cast(str, file_or_directory.decode())
file_or_directory = Path(file_or_directory)
if directory_handler and (directory_view or index): if directory_handler and (directory_view or index):
raise ValueError( raise ValueError(

View File

@ -55,7 +55,7 @@ from sanic.headers import (
parse_xforwarded, parse_xforwarded,
) )
from sanic.http import Stage from sanic.http import Stage
from sanic.log import deprecation, error_logger from sanic.log import error_logger
from sanic.models.protocol_types import TransportProtocol from sanic.models.protocol_types import TransportProtocol
from sanic.response import BaseHTTPResponse, HTTPResponse from sanic.response import BaseHTTPResponse, HTTPResponse
@ -205,16 +205,6 @@ class Request:
def generate_id(*_): def generate_id(*_):
return uuid.uuid4() return uuid.uuid4()
@property
def request_middleware_started(self):
deprecation(
"Request.request_middleware_started has been deprecated and will"
"be removed. You should set a flag on the request context using"
"either middleware or signals if you need this feature.",
23.3,
)
return self._request_middleware_started
@property @property
def stream_id(self): def stream_id(self):
""" """

View File

@ -44,7 +44,9 @@ class Router(BaseRouter):
raise MethodNotAllowed( raise MethodNotAllowed(
f"Method {method} not allowed for URL {path}", f"Method {method} not allowed for URL {path}",
method=method, method=method,
allowed_methods=e.allowed_methods, allowed_methods=tuple(e.allowed_methods)
if e.allowed_methods
else None,
) from None ) from None
@lru_cache(maxsize=ROUTER_CACHE_SIZE) @lru_cache(maxsize=ROUTER_CACHE_SIZE)
@ -133,7 +135,16 @@ class Router(BaseRouter):
if host: if host:
params.update({"requirements": {"host": host}}) params.update({"requirements": {"host": host}})
ident = name
if len(hosts) > 1:
ident = (
f"{name}_{host.replace('.', '_')}"
if name
else "__unnamed__"
)
route = super().add(**params) # type: ignore route = super().add(**params) # type: ignore
route.extra.ident = ident
route.extra.ignore_body = ignore_body route.extra.ignore_body = ignore_body
route.extra.stream = stream route.extra.stream = stream
route.extra.hosts = hosts route.extra.hosts = hosts

View File

@ -2,7 +2,7 @@ from sanic.models.server_types import ConnInfo, Signal
from sanic.server.async_server import AsyncioServer from sanic.server.async_server import AsyncioServer
from sanic.server.loop import try_use_uvloop from sanic.server.loop import try_use_uvloop
from sanic.server.protocols.http_protocol import HttpProtocol from sanic.server.protocols.http_protocol import HttpProtocol
from sanic.server.runners import serve, serve_multiple, serve_single from sanic.server.runners import serve
__all__ = ( __all__ = (
@ -11,7 +11,5 @@ __all__ = (
"HttpProtocol", "HttpProtocol",
"Signal", "Signal",
"serve", "serve",
"serve_multiple",
"serve_single",
"try_use_uvloop", "try_use_uvloop",
) )

View File

@ -1,123 +0,0 @@
import itertools
import os
import signal
import subprocess
import sys
from time import sleep
def _iter_module_files():
"""This iterates over all relevant Python files.
It goes through all
loaded files from modules, all files in folders of already loaded modules
as well as all files reachable through a package.
"""
# The list call is necessary on Python 3 in case the module
# dictionary modifies during iteration.
for module in list(sys.modules.values()):
if module is None:
continue
filename = getattr(module, "__file__", None)
if filename:
old = None
while not os.path.isfile(filename):
old = filename
filename = os.path.dirname(filename)
if filename == old:
break
else:
if filename[-4:] in (".pyc", ".pyo"):
filename = filename[:-1]
yield filename
def _get_args_for_reloading():
"""Returns the executable."""
main_module = sys.modules["__main__"]
mod_spec = getattr(main_module, "__spec__", None)
if sys.argv[0] in ("", "-c"):
raise RuntimeError(
f"Autoreloader cannot work with argv[0]={sys.argv[0]!r}"
)
if mod_spec:
# Parent exe was launched as a module rather than a script
return [sys.executable, "-m", mod_spec.name] + sys.argv[1:]
return [sys.executable] + sys.argv
def restart_with_reloader(changed=None):
"""Create a new process and a subprocess in it with the same arguments as
this one.
"""
reloaded = ",".join(changed) if changed else ""
return subprocess.Popen( # nosec B603
_get_args_for_reloading(),
env={
**os.environ,
"SANIC_SERVER_RUNNING": "true",
"SANIC_RELOADER_PROCESS": "true",
"SANIC_RELOADED_FILES": reloaded,
},
)
def _check_file(filename, mtimes):
need_reload = False
mtime = os.stat(filename).st_mtime
old_time = mtimes.get(filename)
if old_time is None:
mtimes[filename] = mtime
elif mtime > old_time:
mtimes[filename] = mtime
need_reload = True
return need_reload
def watchdog(sleep_interval, reload_dirs):
"""Watch project files, restart worker process if a change happened.
:param sleep_interval: interval in second.
:return: Nothing
"""
def interrupt_self(*args):
raise KeyboardInterrupt
mtimes = {}
signal.signal(signal.SIGTERM, interrupt_self)
if os.name == "nt":
signal.signal(signal.SIGBREAK, interrupt_self)
worker_process = restart_with_reloader()
try:
while True:
changed = set()
for filename in itertools.chain(
_iter_module_files(),
*(d.glob("**/*") for d in reload_dirs),
):
try:
if _check_file(filename, mtimes):
path = (
filename
if isinstance(filename, str)
else filename.resolve()
)
changed.add(str(path))
except OSError:
continue
if changed:
worker_process.terminate()
worker_process.wait()
worker_process = restart_with_reloader(changed)
sleep(sleep_interval)
except KeyboardInterrupt:
pass
finally:
worker_process.terminate()
worker_process.wait()

View File

@ -9,19 +9,17 @@ from sanic.config import Config
from sanic.exceptions import ServerError 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
if TYPE_CHECKING: if TYPE_CHECKING:
from sanic.app import Sanic from sanic.app import Sanic
import asyncio import asyncio
import multiprocessing
import os import os
import socket import socket
from functools import partial from functools import partial
from signal import SIG_IGN, SIGINT, SIGTERM, Signals from signal import SIG_IGN, SIGINT, SIGTERM
from signal import signal as signal_func from signal import signal as signal_func
from sanic.application.ext import setup_ext from sanic.application.ext import setup_ext
@ -31,11 +29,7 @@ from sanic.log import error_logger, server_logger
from sanic.models.server_types import Signal from sanic.models.server_types import Signal
from sanic.server.async_server import AsyncioServer from sanic.server.async_server import AsyncioServer
from sanic.server.protocols.http_protocol import Http3Protocol, HttpProtocol from sanic.server.protocols.http_protocol import Http3Protocol, HttpProtocol
from sanic.server.socket import ( from sanic.server.socket import bind_unix_socket, remove_unix_socket
bind_socket,
bind_unix_socket,
remove_unix_socket,
)
try: try:
@ -319,94 +313,6 @@ def _serve_http_3(
) )
def serve_single(server_settings):
main_start = server_settings.pop("main_start", None)
main_stop = server_settings.pop("main_stop", None)
if not server_settings.get("run_async"):
# create new event_loop after fork
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
server_settings["loop"] = loop
trigger_events(main_start, server_settings["loop"])
serve(**server_settings)
trigger_events(main_stop, server_settings["loop"])
server_settings["loop"].close()
def serve_multiple(server_settings, workers):
"""Start multiple server processes simultaneously. Stop on interrupt
and terminate signals, and drain connections when complete.
:param server_settings: kw arguments to be passed to the serve function
:param workers: number of workers to launch
:param stop_event: if provided, is used as a stop signal
:return:
"""
server_settings["reuse_port"] = True
server_settings["run_multiple"] = True
main_start = server_settings.pop("main_start", None)
main_stop = server_settings.pop("main_stop", None)
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
trigger_events(main_start, loop)
# Create a listening socket or use the one in settings
sock = server_settings.get("sock")
unix = server_settings["unix"]
backlog = server_settings["backlog"]
if unix:
sock = bind_unix_socket(unix, backlog=backlog)
server_settings["unix"] = unix
if sock is None:
sock = bind_socket(
server_settings["host"], server_settings["port"], backlog=backlog
)
sock.set_inheritable(True)
server_settings["sock"] = sock
server_settings["host"] = None
server_settings["port"] = None
processes = []
def sig_handler(signal, frame):
server_logger.info(
"Received signal %s. Shutting down.", Signals(signal).name
)
for process in processes:
os.kill(process.pid, SIGTERM)
signal_func(SIGINT, lambda s, f: sig_handler(s, f))
signal_func(SIGTERM, lambda s, f: sig_handler(s, f))
mp = multiprocessing.get_context("fork")
for _ in range(workers):
process = mp.Process(
target=serve,
kwargs=server_settings,
)
process.daemon = True
process.start()
processes.append(process)
for process in processes:
process.join()
# the above processes will block this until they're stopped
for process in processes:
process.terminate()
trigger_events(main_stop, loop)
sock.close()
loop.close()
remove_unix_socket(unix)
def _build_protocol_kwargs( def _build_protocol_kwargs(
protocol: Type[asyncio.Protocol], config: Config protocol: Type[asyncio.Protocol], config: Config
) -> Dict[str, Union[int, float]]: ) -> Dict[str, Union[int, float]]:

View File

@ -29,7 +29,7 @@ except ImportError: # websockets >= 11.0
from websockets.typing import Data from websockets.typing import Data
from sanic.log import deprecation, error_logger, logger from sanic.log import error_logger, logger
from sanic.server.protocols.base_protocol import SanicProtocol from sanic.server.protocols.base_protocol import SanicProtocol
from ...exceptions import ServerError, WebsocketClosed from ...exceptions import ServerError, WebsocketClosed
@ -99,15 +99,6 @@ class WebsocketImplProtocol:
def subprotocol(self): def subprotocol(self):
return self.ws_proto.subprotocol return self.ws_proto.subprotocol
@property
def connection(self):
deprecation(
"The connection property has been deprecated and will be removed. "
"Please use the ws_proto property instead going forward.",
22.6,
)
return self.ws_proto
def pause_frames(self): def pause_frames(self):
if not self.can_pause: if not self.can_pause:
return False return False

View File

@ -116,7 +116,7 @@ requirements = [
] ]
tests_require = [ tests_require = [
"sanic-testing@git+https://github.com/sanic-org/sanic-testing.git@main#egg=sanic-testing>=22.12.0", "sanic-testing>=23.3.0",
"pytest==7.1.*", "pytest==7.1.*",
"coverage", "coverage",
"beautifulsoup4", "beautifulsoup4",

View File

@ -448,7 +448,7 @@ def test_custom_context():
@pytest.mark.parametrize("use", (False, True)) @pytest.mark.parametrize("use", (False, True))
def test_uvloop_config(app: Sanic, monkeypatch, use): def test_uvloop_config(app: Sanic, monkeypatch, use):
@app.get("/test") @app.get("/test", name="test")
def handler(request): def handler(request):
return text("ok") return text("ok")
@ -571,21 +571,6 @@ def test_cannot_run_single_process_and_workers_or_auto_reload(
app.run(single_process=True, **extra) app.run(single_process=True, **extra)
def test_cannot_run_single_process_and_legacy(app: Sanic):
message = "Cannot run single process and legacy mode"
with pytest.raises(RuntimeError, match=message):
app.run(single_process=True, legacy=True)
def test_cannot_run_without_sys_signals_with_workers(app: Sanic):
message = (
"Cannot run Sanic.serve with register_sys_signals=False. "
"Use either Sanic.serve_single or Sanic.serve_legacy."
)
with pytest.raises(RuntimeError, match=message):
app.run(register_sys_signals=False, single_process=False, legacy=False)
def test_default_configure_logging(): def test_default_configure_logging():
with patch("sanic.app.logging") as mock: with patch("sanic.app.logging") as mock:
Sanic("Test") Sanic("Test")

View File

@ -66,3 +66,11 @@ def test_bp_copy(app: Sanic):
_, response = app.test_client.get("/version6/page") _, response = app.test_client.get("/version6/page")
assert "Hello world!" in response.text assert "Hello world!" in response.text
route_names = [route.name for route in app.router.routes]
assert "test_bp_copy.test_bp1.handle_request" in route_names
assert "test_bp_copy.test_bp2.handle_request" in route_names
assert "test_bp_copy.test_bp3.handle_request" in route_names
assert "test_bp_copy.test_bp4.handle_request" in route_names
assert "test_bp_copy.test_bp5.handle_request" in route_names
assert "test_bp_copy.test_bp6.handle_request" in route_names

View File

@ -303,6 +303,10 @@ def test_bp_with_host_list(app: Sanic):
assert response.text == "Hello subdomain!" assert response.text == "Hello subdomain!"
route_names = [r.name for r in app.router.routes]
assert "test_bp_with_host_list.test_bp_host.handler1" in route_names
assert "test_bp_with_host_list.test_bp_host.handler2" in route_names
def test_several_bp_with_host_list(app: Sanic): def test_several_bp_with_host_list(app: Sanic):
bp = Blueprint( bp = Blueprint(

View File

@ -248,9 +248,9 @@ def test_fallback_with_content_type_mismatch_accept(app):
app.router.reset() app.router.reset()
@app.route("/alt1") @app.route("/alt1", name="alt1")
@app.route("/alt2", error_format="text") @app.route("/alt2", error_format="text", name="alt2")
@app.route("/alt3", error_format="html") @app.route("/alt3", error_format="html", name="alt3")
def handler(_): def handler(_):
raise Exception("problem here") raise Exception("problem here")
# Yes, we know this return value is unreachable. This is on purpose. # Yes, we know this return value is unreachable. This is on purpose.

View File

@ -285,9 +285,15 @@ def test_contextual_exception_context(debug):
def fail(): def fail():
raise TeapotError(context={"foo": "bar"}) raise TeapotError(context={"foo": "bar"})
app.post("/coffee/json", error_format="json")(lambda _: fail()) app.post("/coffee/json", error_format="json", name="json")(
app.post("/coffee/html", error_format="html")(lambda _: fail()) lambda _: fail()
app.post("/coffee/text", error_format="text")(lambda _: fail()) )
app.post("/coffee/html", error_format="html", name="html")(
lambda _: fail()
)
app.post("/coffee/text", error_format="text", name="text")(
lambda _: fail()
)
_, response = app.test_client.post("/coffee/json", debug=debug) _, response = app.test_client.post("/coffee/json", debug=debug)
assert response.status == 418 assert response.status == 418
@ -323,9 +329,15 @@ def test_contextual_exception_extra(debug):
def fail(): def fail():
raise TeapotError(extra={"foo": "bar"}) raise TeapotError(extra={"foo": "bar"})
app.post("/coffee/json", error_format="json")(lambda _: fail()) app.post("/coffee/json", error_format="json", name="json")(
app.post("/coffee/html", error_format="html")(lambda _: fail()) lambda _: fail()
app.post("/coffee/text", error_format="text")(lambda _: fail()) )
app.post("/coffee/html", error_format="html", name="html")(
lambda _: fail()
)
app.post("/coffee/text", error_format="text", name="text")(
lambda _: fail()
)
_, response = app.test_client.post("/coffee/json", debug=debug) _, response = app.test_client.post("/coffee/json", debug=debug)
assert response.status == 418 assert response.status == 418

View File

@ -266,20 +266,17 @@ def test_exception_handler_response_was_sent(
assert "Error" in response.text assert "Error" in response.text
def test_warn_on_duplicate( def test_errir_on_duplicate(app: Sanic):
app: Sanic, caplog: LogCaptureFixture, recwarn: WarningsRecorder
):
@app.exception(ServerError) @app.exception(ServerError)
async def exception_handler_1(request, exception): async def exception_handler_1(request, exception):
... ...
@app.exception(ServerError) message = (
async def exception_handler_2(request, exception):
...
assert len(caplog.records) == 1
assert len(recwarn) == 1
assert caplog.records[0].message == (
"Duplicate exception handler definition on: route=__ALL_ROUTES__ and " "Duplicate exception handler definition on: route=__ALL_ROUTES__ and "
"exception=<class 'sanic.exceptions.ServerError'>" "exception=<class 'sanic.exceptions.ServerError'>"
) )
with pytest.raises(ServerError, match=message):
@app.exception(ServerError)
async def exception_handler_2(request, exception):
...

View File

@ -49,96 +49,6 @@ def test_multiprocessing(app):
assert len(process_list) == num_workers + 1 assert len(process_list) == num_workers + 1
@pytest.mark.skipif(
not hasattr(signal, "SIGALRM"),
reason="SIGALRM is not implemented for this platform, we have to come "
"up with another timeout strategy to test these",
)
def test_multiprocessing_legacy(app):
"""Tests that the number of children we produce is correct"""
# Selects a number at random so we can spot check
num_workers = random.choice(range(2, multiprocessing.cpu_count() * 2 + 1))
process_list = set()
@app.after_server_start
async def shutdown(app):
await sleep(2.1)
app.stop()
def stop_on_alarm(*args):
for process in multiprocessing.active_children():
process_list.add(process.pid)
signal.signal(signal.SIGALRM, stop_on_alarm)
signal.alarm(2)
app.run(HOST, 4121, workers=num_workers, debug=True, legacy=True)
assert len(process_list) == num_workers
@pytest.mark.skipif(
not hasattr(signal, "SIGALRM"),
reason="SIGALRM is not implemented for this platform, we have to come "
"up with another timeout strategy to test these",
)
def test_multiprocessing_legacy_sock(app):
"""Tests that the number of children we produce is correct"""
# Selects a number at random so we can spot check
num_workers = random.choice(range(2, multiprocessing.cpu_count() * 2 + 1))
process_list = set()
@app.after_server_start
async def shutdown(app):
await sleep(2.1)
app.stop()
def stop_on_alarm(*args):
for process in multiprocessing.active_children():
process_list.add(process.pid)
signal.signal(signal.SIGALRM, stop_on_alarm)
signal.alarm(2)
sock = configure_socket(
{
"host": HOST,
"port": 4121,
"unix": None,
"backlog": 100,
}
)
app.run(workers=num_workers, debug=True, legacy=True, sock=sock)
sock.close()
assert len(process_list) == num_workers
@pytest.mark.skipif(
not hasattr(signal, "SIGALRM"),
reason="SIGALRM is not implemented for this platform, we have to come "
"up with another timeout strategy to test these",
)
def test_multiprocessing_legacy_unix(app):
"""Tests that the number of children we produce is correct"""
# Selects a number at random so we can spot check
num_workers = random.choice(range(2, multiprocessing.cpu_count() * 2 + 1))
process_list = set()
@app.after_server_start
async def shutdown(app):
await sleep(2.1)
app.stop()
def stop_on_alarm(*args):
for process in multiprocessing.active_children():
process_list.add(process.pid)
signal.signal(signal.SIGALRM, stop_on_alarm)
signal.alarm(2)
app.run(workers=num_workers, debug=True, legacy=True, unix="./test.sock")
assert len(process_list) == num_workers
@pytest.mark.skipif( @pytest.mark.skipif(
not hasattr(signal, "SIGALRM"), not hasattr(signal, "SIGALRM"),
reason="SIGALRM is not implemented for this platform", reason="SIGALRM is not implemented for this platform",

View File

@ -105,11 +105,11 @@ def test_html(app):
return html("<h1>Hello</h1>") return html("<h1>Hello</h1>")
@app.route("/foo") @app.route("/foo")
async def handler(request): async def handler_foo(request):
return html(Foo()) return html(Foo())
@app.route("/bar") @app.route("/bar")
async def handler(request): async def handler_bar(request):
return html(Bar()) return html(Bar())
request, response = app.test_client.get("/") request, response = app.test_client.get("/")
@ -2199,10 +2199,25 @@ def test_safe_method_with_body(app):
assert response.body == b"OK" assert response.body == b"OK"
def test_conflicting_body_methods_overload(app): @pytest.mark.asyncio
async def test_conflicting_body_methods_overload_error(app: Sanic):
@app.put("/") @app.put("/")
@app.put("/p/") @app.put("/p/")
@app.put("/p/<foo>") @app.put("/p/<foo>")
async def put(request, foo=None):
...
with pytest.raises(
ServerError,
match="Duplicate route names detected: test_conflicting_body_methods_overload_error\.put.*",
):
await app._startup()
def test_conflicting_body_methods_overload(app: Sanic):
@app.put("/", name="one")
@app.put("/p/", name="two")
@app.put("/p/<foo>", name="three")
async def put(request, foo=None): async def put(request, foo=None):
return json( return json(
{"name": request.route.name, "body": str(request.body), "foo": foo} {"name": request.route.name, "body": str(request.body), "foo": foo}
@ -2220,21 +2235,21 @@ def test_conflicting_body_methods_overload(app):
_, response = app.test_client.put("/", json=payload) _, response = app.test_client.put("/", json=payload)
assert response.status == 200 assert response.status == 200
assert response.json == { assert response.json == {
"name": "test_conflicting_body_methods_overload.put", "name": "test_conflicting_body_methods_overload.one",
"foo": None, "foo": None,
"body": data, "body": data,
} }
_, response = app.test_client.put("/p", json=payload) _, response = app.test_client.put("/p", json=payload)
assert response.status == 200 assert response.status == 200
assert response.json == { assert response.json == {
"name": "test_conflicting_body_methods_overload.put", "name": "test_conflicting_body_methods_overload.two",
"foo": None, "foo": None,
"body": data, "body": data,
} }
_, response = app.test_client.put("/p/test", json=payload) _, response = app.test_client.put("/p/test", json=payload)
assert response.status == 200 assert response.status == 200
assert response.json == { assert response.json == {
"name": "test_conflicting_body_methods_overload.put", "name": "test_conflicting_body_methods_overload.three",
"foo": "test", "foo": "test",
"body": data, "body": data,
} }
@ -2247,9 +2262,26 @@ def test_conflicting_body_methods_overload(app):
} }
def test_handler_overload(app): @pytest.mark.asyncio
async def test_handler_overload_error(app: Sanic):
@app.get("/long/sub/route/param_a/<param_a:str>/param_b/<param_b:str>") @app.get("/long/sub/route/param_a/<param_a:str>/param_b/<param_b:str>")
@app.post("/long/sub/route/") @app.post("/long/sub/route/")
def handler(request, **kwargs):
...
with pytest.raises(
ServerError,
match="Duplicate route names detected: test_handler_overload_error\.handler.*",
):
await app._startup()
def test_handler_overload(app: Sanic):
@app.get(
"/long/sub/route/param_a/<param_a:str>/param_b/<param_b:str>",
name="one",
)
@app.post("/long/sub/route/", name="two")
def handler(request, **kwargs): def handler(request, **kwargs):
return json(kwargs) return json(kwargs)

View File

@ -12,7 +12,7 @@ from sanic_testing.testing import SanicTestClient
from sanic import Blueprint, Sanic from sanic import Blueprint, Sanic
from sanic.constants import HTTP_METHODS from sanic.constants import HTTP_METHODS
from sanic.exceptions import NotFound, SanicException from sanic.exceptions import NotFound, SanicException, ServerError
from sanic.request import Request from sanic.request import Request
from sanic.response import empty, json, text from sanic.response import empty, json, text
@ -744,8 +744,8 @@ def test_route_duplicate(app):
def test_double_stack_route(app): def test_double_stack_route(app):
@app.route("/test/1") @app.route("/test/1", name="test1")
@app.route("/test/2") @app.route("/test/2", name="test2")
async def handler1(request): async def handler1(request):
return text("OK") return text("OK")
@ -759,8 +759,8 @@ def test_double_stack_route(app):
async def test_websocket_route_asgi(app): async def test_websocket_route_asgi(app):
ev = asyncio.Event() ev = asyncio.Event()
@app.websocket("/test/1") @app.websocket("/test/1", name="test1")
@app.websocket("/test/2") @app.websocket("/test/2", name="test2")
async def handler(request, ws): async def handler(request, ws):
ev.set() ev.set()
@ -1279,7 +1279,7 @@ async def test_added_callable_route_ctx_kwargs(app):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_duplicate_route_deprecation(app): async def test_duplicate_route_error(app):
@app.route("/foo", name="duped") @app.route("/foo", name="duped")
async def handler_foo(request): async def handler_foo(request):
return text("...") return text("...")
@ -1289,9 +1289,7 @@ async def test_duplicate_route_deprecation(app):
return text("...") return text("...")
message = ( message = (
r"\[DEPRECATION v23\.3\] Duplicate route names detected: " "Duplicate route names detected: test_duplicate_route_error.duped."
r"test_duplicate_route_deprecation\.duped\. In the future, "
r"Sanic will enforce uniqueness in route naming\."
) )
with pytest.warns(DeprecationWarning, match=message): with pytest.raises(ServerError, match=message):
await app._startup() await app._startup()

View File

@ -66,8 +66,8 @@ def test_no_register_system_signals_fails(app):
app.listener("after_server_stop")(after) app.listener("after_server_stop")(after)
message = ( message = (
"Cannot run Sanic.serve with register_sys_signals=False. Use " r"Cannot run Sanic\.serve with register_sys_signals=False\. Use "
"either Sanic.serve_single or Sanic.serve_legacy." r"Sanic.serve_single\."
) )
with pytest.raises(RuntimeError, match=message): with pytest.raises(RuntimeError, match=message):
app.prepare(HOST, PORT, register_sys_signals=False) app.prepare(HOST, PORT, register_sys_signals=False)

View File

@ -9,7 +9,7 @@ from time import gmtime, strftime
import pytest import pytest
from sanic import Sanic, text from sanic import Sanic, text
from sanic.exceptions import FileNotFound from sanic.exceptions import FileNotFound, ServerError
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
@ -108,14 +108,9 @@ def test_static_file_pathlib(app, static_file_directory, file_name):
def test_static_file_bytes(app, static_file_directory, file_name): def test_static_file_bytes(app, static_file_directory, file_name):
bsep = os.path.sep.encode("utf-8") bsep = os.path.sep.encode("utf-8")
file_path = static_file_directory.encode("utf-8") + bsep + file_name file_path = static_file_directory.encode("utf-8") + bsep + file_name
message = ( message = "Static file or directory must be a path-like object or string"
"Serving a static directory with a bytes " with pytest.raises(TypeError, match=message):
"string is deprecated and will be removed in v22.9."
)
with pytest.warns(DeprecationWarning, match=message):
app.static("/testing.file", file_path) app.static("/testing.file", file_path)
request, response = app.test_client.get("/testing.file")
assert response.status == 200
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -523,10 +518,26 @@ def test_no_stack_trace_on_not_found(app, static_file_directory, caplog):
assert response.text == "No file: /static/non_existing_file.file" assert response.text == "No file: /static/non_existing_file.file"
def test_multiple_statics(app, static_file_directory): @pytest.mark.asyncio
async def test_multiple_statics_error(app, static_file_directory):
app.static("/file", get_file_path(static_file_directory, "test.file")) app.static("/file", get_file_path(static_file_directory, "test.file"))
app.static("/png", get_file_path(static_file_directory, "python.png")) app.static("/png", get_file_path(static_file_directory, "python.png"))
message = (
r"Duplicate route names detected: test_multiple_statics_error\.static"
)
with pytest.raises(ServerError, match=message):
await app._startup()
def test_multiple_statics(app, static_file_directory):
app.static(
"/file", get_file_path(static_file_directory, "test.file"), name="file"
)
app.static(
"/png", get_file_path(static_file_directory, "python.png"), name="png"
)
_, response = app.test_client.get("/file") _, response = app.test_client.get("/file")
assert response.status == 200 assert response.status == 200
assert response.body == get_file_content( assert response.body == get_file_content(
@ -540,10 +551,22 @@ def test_multiple_statics(app, static_file_directory):
) )
def test_resource_type_default(app, static_file_directory): @pytest.mark.asyncio
async def test_resource_type_default_error(app, static_file_directory):
app.static("/static", static_file_directory) app.static("/static", static_file_directory)
app.static("/file", get_file_path(static_file_directory, "test.file")) app.static("/file", get_file_path(static_file_directory, "test.file"))
message = r"Duplicate route names detected: test_resource_type_default_error\.static"
with pytest.raises(ServerError, match=message):
await app._startup()
def test_resource_type_default(app, static_file_directory):
app.static("/static", static_file_directory, name="static")
app.static(
"/file", get_file_path(static_file_directory, "test.file"), name="file"
)
_, response = app.test_client.get("/static") _, response = app.test_client.get("/static")
assert response.status == 404 assert response.status == 404

View File

@ -72,24 +72,6 @@ def test_not_have_multiplexer_single(app: Sanic):
assert not event.is_set() assert not event.is_set()
def test_not_have_multiplexer_legacy(app: Sanic):
event = Event()
@app.main_process_start
async def setup(app, _):
app.shared_ctx.event = event
@app.after_server_start
def stop(app):
if hasattr(app, "m") and isinstance(app.m, WorkerMultiplexer):
app.shared_ctx.event.set()
app.stop()
app.run(legacy=True)
assert not event.is_set()
def test_ack(worker_state: Dict[str, Any], m: WorkerMultiplexer): def test_ack(worker_state: Dict[str, Any], m: WorkerMultiplexer):
worker_state["Test"] = {"foo": "bar"} worker_state["Test"] = {"foo": "bar"}
m.ack() m.ack()