From d680af3709d664db31f97bad9c5d6546d7a0ca15 Mon Sep 17 00:00:00 2001 From: Adam Hopkins Date: Sun, 26 Mar 2023 15:24:08 +0300 Subject: [PATCH] v23.3 Deprecation Removal (#2717) --- sanic/app.py | 19 ++--- sanic/blueprints.py | 13 ++++ sanic/cli/app.py | 40 +--------- sanic/cli/arguments.py | 31 -------- sanic/handlers/error.py | 12 +-- sanic/mixins/startup.py | 87 +--------------------- sanic/mixins/static.py | 18 ++--- sanic/request/types.py | 12 +-- sanic/router.py | 13 +++- sanic/server/__init__.py | 4 +- sanic/server/legacy.py | 123 ------------------------------- sanic/server/runners.py | 98 +----------------------- sanic/server/websockets/impl.py | 11 +-- setup.py | 2 +- tests/test_app.py | 17 +---- tests/test_blueprint_copy.py | 8 ++ tests/test_blueprints.py | 4 + tests/test_errorpages.py | 6 +- tests/test_exceptions.py | 24 ++++-- tests/test_exceptions_handler.py | 17 ++--- tests/test_multiprocessing.py | 90 ---------------------- tests/test_requests.py | 46 ++++++++++-- tests/test_routes.py | 18 ++--- tests/test_signal_handlers.py | 4 +- tests/test_static.py | 43 ++++++++--- tests/worker/test_multiplexer.py | 18 ----- 26 files changed, 180 insertions(+), 598 deletions(-) delete mode 100644 sanic/server/legacy.py diff --git a/sanic/app.py b/sanic/app.py index 9efc53ef..83fbacae 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -64,12 +64,7 @@ from sanic.exceptions import ( from sanic.handlers import ErrorHandler from sanic.helpers import Default, _default from sanic.http import Stage -from sanic.log import ( - LOGGING_CONFIG_DEFAULTS, - deprecation, - error_logger, - logger, -) +from sanic.log import LOGGING_CONFIG_DEFAULTS, error_logger, logger from sanic.middleware import Middleware, MiddlewareLocation from sanic.mixins.listeners import ListenerEvent from sanic.mixins.startup import StartupMixin @@ -1584,17 +1579,19 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta): self.signalize(self.config.TOUCHUP) self.finalize() - route_names = [route.name for route in self.router.routes] + route_names = [route.extra.ident for route in self.router.routes] duplicates = { name for name in route_names if route_names.count(name) > 1 } if duplicates: names = ", ".join(duplicates) - deprecation( - f"Duplicate route names detected: {names}. In the future, " - "Sanic will enforce uniqueness in route naming.", - 23.3, + message = ( + f"Duplicate route names detected: {names}. You should rename " + "one or more of them explicitly by using the `name` param, " + "or changing the implicit name derived from the class and " + "function name. For more details, please see ___." ) + raise ServerError(message) Sanic._check_uvloop_conflict() diff --git a/sanic/blueprints.py b/sanic/blueprints.py index 8050e1f6..508e25ac 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -93,6 +93,7 @@ class Blueprint(BaseSanic): "_future_listeners", "_future_exceptions", "_future_signals", + "copied_from", "ctx", "exceptions", "host", @@ -118,6 +119,7 @@ class Blueprint(BaseSanic): ): super().__init__(name=name) self.reset() + self.copied_from = "" self.ctx = SimpleNamespace() self.host = host self.strict_slashes = strict_slashes @@ -213,6 +215,7 @@ class Blueprint(BaseSanic): self.reset() new_bp = deepcopy(self) new_bp.name = name + new_bp.copied_from = self.name if not isinstance(url_prefix, Default): new_bp.url_prefix = url_prefix @@ -352,6 +355,16 @@ class Blueprint(BaseSanic): registered.add(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 = ( routes.extend if isinstance(route, list) else routes.append ) diff --git a/sanic/cli/app.py b/sanic/cli/app.py index 7f71d45d..07d02c7e 100644 --- a/sanic/cli/app.py +++ b/sanic/cli/app.py @@ -1,4 +1,3 @@ -import logging import os import shutil import sys @@ -6,7 +5,7 @@ import sys from argparse import Namespace from functools import partial from textwrap import indent -from typing import List, Union, cast +from typing import List, Union from sanic.app import Sanic 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.inspector import make_inspector_parser 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 @@ -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 ) - if self.args.inspect or self.args.inspect_raw or self.args.trigger: - self._inspector_legacy(app_loader) - return - try: app = self._get_app(app_loader) 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) if self.args.single: serve = Sanic.serve_single - elif self.args.legacy: - serve = Sanic.serve_legacy else: serve = partial(Sanic.serve, app_loader=app_loader) 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): args = sys.argv[2:] 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) sys.exit(1) - if self.args.inspect or self.args.inspect_raw: - logging.disable(logging.CRITICAL) def _get_app(self, app_loader: AppLoader): try: @@ -251,7 +216,6 @@ Or, a path to a directory to run as a simple HTTP server: "workers": self.args.workers, "auto_tls": self.args.auto_tls, "single_process": self.args.single, - "legacy": self.args.legacy, } for maybe_arg in ("auto_reload", "dev"): diff --git a/sanic/cli/arguments.py b/sanic/cli/arguments.py index e7fadb1d..c4d64089 100644 --- a/sanic/cli/arguments.py +++ b/sanic/cli/arguments.py @@ -93,32 +93,6 @@ class ApplicationGroup(Group): "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): @@ -247,11 +221,6 @@ class WorkerGroup(Group): action="store_true", 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( "--access-logs", dest="access_log", diff --git a/sanic/handlers/error.py b/sanic/handlers/error.py index e5a14de6..d01f8ba1 100644 --- a/sanic/handlers/error.py +++ b/sanic/handlers/error.py @@ -3,7 +3,8 @@ from __future__ import annotations from typing import Dict, List, Optional, Tuple, Type 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.response import text @@ -43,16 +44,11 @@ class ErrorHandler: if name is None: name = "__ALL_ROUTES__" - error_logger.warning( + message = ( f"Duplicate exception handler definition on: route={name} " f"and exception={exc}" ) - deprecation( - "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, - ) + raise ServerError(message) self.cached_handlers[key] = handler def add(self, exception, handler, route_names: Optional[List[str]] = None): diff --git a/sanic/mixins/startup.py b/sanic/mixins/startup.py index b5fc1f30..e7ddd3ac 100644 --- a/sanic/mixins/startup.py +++ b/sanic/mixins/startup.py @@ -47,17 +47,16 @@ from sanic.helpers import Default, _default from sanic.http.constants import HTTP from sanic.http.tls import get_ssl_context, process_to_context 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.server import Signal as ServerSignal from sanic.server import try_use_uvloop from sanic.server.async_server import AsyncioServer from sanic.server.events import trigger_events -from sanic.server.legacy import watchdog from sanic.server.loop import try_windows_loop from sanic.server.protocols.http_protocol import HttpProtocol 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.worker.loader import AppLoader from sanic.worker.manager import WorkerManager @@ -135,7 +134,6 @@ class StartupMixin(metaclass=SanicMeta): motd_display: Optional[Dict[str, str]] = None, auto_tls: bool = False, single_process: bool = False, - legacy: bool = False, ) -> None: """ Run the HTTP Server and listen until keyboard interrupt or term @@ -197,13 +195,10 @@ class StartupMixin(metaclass=SanicMeta): motd_display=motd_display, auto_tls=auto_tls, single_process=single_process, - legacy=legacy, ) if single_process: serve = self.__class__.serve_single - elif legacy: - serve = self.__class__.serve_legacy else: serve = self.__class__.serve serve(primary=self) # type: ignore @@ -235,7 +230,6 @@ class StartupMixin(metaclass=SanicMeta): coffee: bool = False, auto_tls: bool = False, single_process: bool = False, - legacy: bool = False, ) -> None: if version == 3 and self.state.server_info: raise RuntimeError( @@ -264,13 +258,10 @@ class StartupMixin(metaclass=SanicMeta): "or auto-reload" ) - if single_process and legacy: - raise RuntimeError("Cannot run single process and legacy mode") - - if register_sys_signals is False and not (single_process or legacy): + if register_sys_signals is False and not single_process: raise RuntimeError( "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: @@ -956,76 +947,6 @@ class StartupMixin(metaclass=SanicMeta): cls._cleanup_env_vars() 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( self, primary: Sanic, diff --git a/sanic/mixins/static.py b/sanic/mixins/static.py index d9ca6200..bcffbc82 100644 --- a/sanic/mixins/static.py +++ b/sanic/mixins/static.py @@ -3,7 +3,7 @@ from functools import partial, wraps from mimetypes import guess_type from os import PathLike, path 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 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.handlers import ContentRangeHandler 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.models.futures import FutureStatic from sanic.request import Request @@ -31,7 +31,7 @@ class StaticMixin(BaseMixin, metaclass=SanicMeta): def static( self, uri: str, - file_or_directory: Union[PathLike, str, bytes], + file_or_directory: Union[PathLike, str], pattern: str = r"/?.+", use_modified_since: bool = True, 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}" ) - if isinstance(file_or_directory, bytes): - deprecation( - "Serving a static directory with a bytes string is " - "deprecated and will be removed in v22.9.", - 22.9, + try: + file_or_directory = Path(file_or_directory) + except TypeError: + raise TypeError( + "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): raise ValueError( diff --git a/sanic/request/types.py b/sanic/request/types.py index 106ba510..2e075e88 100644 --- a/sanic/request/types.py +++ b/sanic/request/types.py @@ -55,7 +55,7 @@ from sanic.headers import ( parse_xforwarded, ) 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.response import BaseHTTPResponse, HTTPResponse @@ -205,16 +205,6 @@ class Request: def generate_id(*_): 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 def stream_id(self): """ diff --git a/sanic/router.py b/sanic/router.py index 26d5eff6..a469fd21 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -44,7 +44,9 @@ class Router(BaseRouter): raise MethodNotAllowed( f"Method {method} not allowed for URL {path}", method=method, - allowed_methods=e.allowed_methods, + allowed_methods=tuple(e.allowed_methods) + if e.allowed_methods + else None, ) from None @lru_cache(maxsize=ROUTER_CACHE_SIZE) @@ -133,7 +135,16 @@ class Router(BaseRouter): if 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.extra.ident = ident route.extra.ignore_body = ignore_body route.extra.stream = stream route.extra.hosts = hosts diff --git a/sanic/server/__init__.py b/sanic/server/__init__.py index 116bd05c..a6b1a98c 100644 --- a/sanic/server/__init__.py +++ b/sanic/server/__init__.py @@ -2,7 +2,7 @@ from sanic.models.server_types import ConnInfo, Signal from sanic.server.async_server import AsyncioServer from sanic.server.loop import try_use_uvloop 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__ = ( @@ -11,7 +11,5 @@ __all__ = ( "HttpProtocol", "Signal", "serve", - "serve_multiple", - "serve_single", "try_use_uvloop", ) diff --git a/sanic/server/legacy.py b/sanic/server/legacy.py deleted file mode 100644 index a8018ce2..00000000 --- a/sanic/server/legacy.py +++ /dev/null @@ -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() diff --git a/sanic/server/runners.py b/sanic/server/runners.py index c7066742..f56168f9 100644 --- a/sanic/server/runners.py +++ b/sanic/server/runners.py @@ -9,19 +9,17 @@ from sanic.config import Config from sanic.exceptions import ServerError from sanic.http.constants import HTTP from sanic.http.tls import get_ssl_context -from sanic.server.events import trigger_events if TYPE_CHECKING: from sanic.app import Sanic import asyncio -import multiprocessing import os import socket 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 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.server.async_server import AsyncioServer from sanic.server.protocols.http_protocol import Http3Protocol, HttpProtocol -from sanic.server.socket import ( - bind_socket, - bind_unix_socket, - remove_unix_socket, -) +from sanic.server.socket import bind_unix_socket, remove_unix_socket 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( protocol: Type[asyncio.Protocol], config: Config ) -> Dict[str, Union[int, float]]: diff --git a/sanic/server/websockets/impl.py b/sanic/server/websockets/impl.py index 2125faa7..c76ebc04 100644 --- a/sanic/server/websockets/impl.py +++ b/sanic/server/websockets/impl.py @@ -29,7 +29,7 @@ except ImportError: # websockets >= 11.0 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 ...exceptions import ServerError, WebsocketClosed @@ -99,15 +99,6 @@ class WebsocketImplProtocol: def subprotocol(self): 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): if not self.can_pause: return False diff --git a/setup.py b/setup.py index 82e351fc..e0170f52 100644 --- a/setup.py +++ b/setup.py @@ -116,7 +116,7 @@ requirements = [ ] 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.*", "coverage", "beautifulsoup4", diff --git a/tests/test_app.py b/tests/test_app.py index af302661..b670ef45 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -448,7 +448,7 @@ def test_custom_context(): @pytest.mark.parametrize("use", (False, True)) def test_uvloop_config(app: Sanic, monkeypatch, use): - @app.get("/test") + @app.get("/test", name="test") def handler(request): return text("ok") @@ -571,21 +571,6 @@ def test_cannot_run_single_process_and_workers_or_auto_reload( 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(): with patch("sanic.app.logging") as mock: Sanic("Test") diff --git a/tests/test_blueprint_copy.py b/tests/test_blueprint_copy.py index ca8cd67e..387cb8d2 100644 --- a/tests/test_blueprint_copy.py +++ b/tests/test_blueprint_copy.py @@ -66,3 +66,11 @@ def test_bp_copy(app: Sanic): _, response = app.test_client.get("/version6/page") 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 diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index cf4e7a3d..be49a50f 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -303,6 +303,10 @@ def test_bp_with_host_list(app: Sanic): 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): bp = Blueprint( diff --git a/tests/test_errorpages.py b/tests/test_errorpages.py index 40bdfcf8..d2c1fc7b 100644 --- a/tests/test_errorpages.py +++ b/tests/test_errorpages.py @@ -248,9 +248,9 @@ def test_fallback_with_content_type_mismatch_accept(app): app.router.reset() - @app.route("/alt1") - @app.route("/alt2", error_format="text") - @app.route("/alt3", error_format="html") + @app.route("/alt1", name="alt1") + @app.route("/alt2", error_format="text", name="alt2") + @app.route("/alt3", error_format="html", name="alt3") def handler(_): raise Exception("problem here") # Yes, we know this return value is unreachable. This is on purpose. diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 0fe51f8c..29c4e45c 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -285,9 +285,15 @@ def test_contextual_exception_context(debug): def fail(): raise TeapotError(context={"foo": "bar"}) - app.post("/coffee/json", error_format="json")(lambda _: fail()) - app.post("/coffee/html", error_format="html")(lambda _: fail()) - app.post("/coffee/text", error_format="text")(lambda _: fail()) + app.post("/coffee/json", error_format="json", name="json")( + 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) assert response.status == 418 @@ -323,9 +329,15 @@ def test_contextual_exception_extra(debug): def fail(): raise TeapotError(extra={"foo": "bar"}) - app.post("/coffee/json", error_format="json")(lambda _: fail()) - app.post("/coffee/html", error_format="html")(lambda _: fail()) - app.post("/coffee/text", error_format="text")(lambda _: fail()) + app.post("/coffee/json", error_format="json", name="json")( + 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) assert response.status == 418 diff --git a/tests/test_exceptions_handler.py b/tests/test_exceptions_handler.py index 0c2ce40e..9c211287 100644 --- a/tests/test_exceptions_handler.py +++ b/tests/test_exceptions_handler.py @@ -266,20 +266,17 @@ def test_exception_handler_response_was_sent( assert "Error" in response.text -def test_warn_on_duplicate( - app: Sanic, caplog: LogCaptureFixture, recwarn: WarningsRecorder -): +def test_errir_on_duplicate(app: Sanic): @app.exception(ServerError) async def exception_handler_1(request, exception): ... - @app.exception(ServerError) - async def exception_handler_2(request, exception): - ... - - assert len(caplog.records) == 1 - assert len(recwarn) == 1 - assert caplog.records[0].message == ( + message = ( "Duplicate exception handler definition on: route=__ALL_ROUTES__ and " "exception=" ) + with pytest.raises(ServerError, match=message): + + @app.exception(ServerError) + async def exception_handler_2(request, exception): + ... diff --git a/tests/test_multiprocessing.py b/tests/test_multiprocessing.py index 6333cf9b..6e5569d1 100644 --- a/tests/test_multiprocessing.py +++ b/tests/test_multiprocessing.py @@ -49,96 +49,6 @@ def test_multiprocessing(app): 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( not hasattr(signal, "SIGALRM"), reason="SIGALRM is not implemented for this platform", diff --git a/tests/test_requests.py b/tests/test_requests.py index cc81e70d..4a995d4c 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -105,11 +105,11 @@ def test_html(app): return html("

Hello

") @app.route("/foo") - async def handler(request): + async def handler_foo(request): return html(Foo()) @app.route("/bar") - async def handler(request): + async def handler_bar(request): return html(Bar()) request, response = app.test_client.get("/") @@ -2199,10 +2199,25 @@ def test_safe_method_with_body(app): 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("/p/") @app.put("/p/") + 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/", name="three") async def put(request, foo=None): return json( {"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) assert response.status == 200 assert response.json == { - "name": "test_conflicting_body_methods_overload.put", + "name": "test_conflicting_body_methods_overload.one", "foo": None, "body": data, } _, response = app.test_client.put("/p", json=payload) assert response.status == 200 assert response.json == { - "name": "test_conflicting_body_methods_overload.put", + "name": "test_conflicting_body_methods_overload.two", "foo": None, "body": data, } _, response = app.test_client.put("/p/test", json=payload) assert response.status == 200 assert response.json == { - "name": "test_conflicting_body_methods_overload.put", + "name": "test_conflicting_body_methods_overload.three", "foo": "test", "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_b/") @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_b/", + name="one", + ) + @app.post("/long/sub/route/", name="two") def handler(request, **kwargs): return json(kwargs) diff --git a/tests/test_routes.py b/tests/test_routes.py index 06714644..8d8fecd4 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -12,7 +12,7 @@ from sanic_testing.testing import SanicTestClient from sanic import Blueprint, Sanic 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.response import empty, json, text @@ -744,8 +744,8 @@ def test_route_duplicate(app): def test_double_stack_route(app): - @app.route("/test/1") - @app.route("/test/2") + @app.route("/test/1", name="test1") + @app.route("/test/2", name="test2") async def handler1(request): return text("OK") @@ -759,8 +759,8 @@ def test_double_stack_route(app): async def test_websocket_route_asgi(app): ev = asyncio.Event() - @app.websocket("/test/1") - @app.websocket("/test/2") + @app.websocket("/test/1", name="test1") + @app.websocket("/test/2", name="test2") async def handler(request, ws): ev.set() @@ -1279,7 +1279,7 @@ async def test_added_callable_route_ctx_kwargs(app): @pytest.mark.asyncio -async def test_duplicate_route_deprecation(app): +async def test_duplicate_route_error(app): @app.route("/foo", name="duped") async def handler_foo(request): return text("...") @@ -1289,9 +1289,7 @@ async def test_duplicate_route_deprecation(app): return text("...") message = ( - r"\[DEPRECATION v23\.3\] Duplicate route names detected: " - r"test_duplicate_route_deprecation\.duped\. In the future, " - r"Sanic will enforce uniqueness in route naming\." + "Duplicate route names detected: test_duplicate_route_error.duped." ) - with pytest.warns(DeprecationWarning, match=message): + with pytest.raises(ServerError, match=message): await app._startup() diff --git a/tests/test_signal_handlers.py b/tests/test_signal_handlers.py index 4611b09a..53eff83f 100644 --- a/tests/test_signal_handlers.py +++ b/tests/test_signal_handlers.py @@ -66,8 +66,8 @@ def test_no_register_system_signals_fails(app): app.listener("after_server_stop")(after) message = ( - "Cannot run Sanic.serve with register_sys_signals=False. Use " - "either Sanic.serve_single or Sanic.serve_legacy." + r"Cannot run Sanic\.serve with register_sys_signals=False\. Use " + r"Sanic.serve_single\." ) with pytest.raises(RuntimeError, match=message): app.prepare(HOST, PORT, register_sys_signals=False) diff --git a/tests/test_static.py b/tests/test_static.py index d39d7708..fe2927bf 100644 --- a/tests/test_static.py +++ b/tests/test_static.py @@ -9,7 +9,7 @@ from time import gmtime, strftime import pytest from sanic import Sanic, text -from sanic.exceptions import FileNotFound +from sanic.exceptions import FileNotFound, ServerError @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): bsep = os.path.sep.encode("utf-8") file_path = static_file_directory.encode("utf-8") + bsep + file_name - message = ( - "Serving a static directory with a bytes " - "string is deprecated and will be removed in v22.9." - ) - with pytest.warns(DeprecationWarning, match=message): + message = "Static file or directory must be a path-like object or string" + with pytest.raises(TypeError, match=message): app.static("/testing.file", file_path) - request, response = app.test_client.get("/testing.file") - assert response.status == 200 @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" -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("/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") assert response.status == 200 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("/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") assert response.status == 404 diff --git a/tests/worker/test_multiplexer.py b/tests/worker/test_multiplexer.py index 88072cb7..8195b094 100644 --- a/tests/worker/test_multiplexer.py +++ b/tests/worker/test_multiplexer.py @@ -72,24 +72,6 @@ def test_not_have_multiplexer_single(app: Sanic): 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): worker_state["Test"] = {"foo": "bar"} m.ack()