diff --git a/.mypy.ini b/.mypy.ini index 9a888ebffff..b3e17b9731a 100644 --- a/.mypy.ini +++ b/.mypy.ini @@ -35,11 +35,5 @@ ignore_missing_imports = True [mypy-gunicorn.*] ignore_missing_imports = True -[mypy-tokio] -ignore_missing_imports = True - -[mypy-uvloop] -ignore_missing_imports = True - [mypy-python_on_whales] ignore_missing_imports = True diff --git a/CHANGES/6594.feature b/CHANGES/6594.feature new file mode 100644 index 00000000000..4edadb07b3a --- /dev/null +++ b/CHANGES/6594.feature @@ -0,0 +1,2 @@ +Exported ``HTTPMove`` which can be used to catch any redirection request +that has a location -- :user:`dreamsorcerer`. diff --git a/CHANGES/7281.removal b/CHANGES/7281.removal new file mode 100644 index 00000000000..ddbf457b175 --- /dev/null +++ b/CHANGES/7281.removal @@ -0,0 +1 @@ +Removed support for unsupported ``tokio`` event loop -- by :user:`Dreamsorcerer` diff --git a/aiohttp/http_websocket.py b/aiohttp/http_websocket.py index cbc547f84ff..f2e348d651b 100644 --- a/aiohttp/http_websocket.py +++ b/aiohttp/http_websocket.py @@ -1,7 +1,6 @@ """WebSocket protocol versions 13 and 8.""" import asyncio -import collections import functools import json import random @@ -10,7 +9,18 @@ import zlib from enum import IntEnum from struct import Struct -from typing import Any, Callable, List, Optional, Pattern, Set, Tuple, Union, cast +from typing import ( + Any, + Callable, + List, + NamedTuple, + Optional, + Pattern, + Set, + Tuple, + Union, + cast, +) from typing_extensions import Final @@ -80,10 +90,12 @@ class WSMsgType(IntEnum): DEFAULT_LIMIT: Final[int] = 2**16 -_WSMessageBase = collections.namedtuple("_WSMessageBase", ["type", "data", "extra"]) - +class WSMessage(NamedTuple): + type: WSMsgType + # To type correctly, this would need some kind of tagged union for each type. + data: Any + extra: Optional[str] -class WSMessage(_WSMessageBase): def json(self, *, loads: Callable[[Any], Any] = json.loads) -> Any: """Return parsed JSON data. diff --git a/aiohttp/pytest_plugin.py b/aiohttp/pytest_plugin.py index d08327e0311..8bbe46f559a 100644 --- a/aiohttp/pytest_plugin.py +++ b/aiohttp/pytest_plugin.py @@ -2,7 +2,7 @@ import contextlib import inspect import warnings -from typing import Any, Awaitable, Callable, Dict, Generator, Optional, Type, Union +from typing import Any, Awaitable, Callable, Dict, Iterator, Optional, Type, Union import pytest @@ -22,14 +22,11 @@ try: import uvloop except ImportError: # pragma: no cover - uvloop = None - -try: - import tokio -except ImportError: # pragma: no cover - tokio = None + uvloop = None # type: ignore[assignment] AiohttpClient = Callable[[Union[Application, BaseTestServer]], Awaitable[TestClient]] +AiohttpRawServer = Callable[[Application], Awaitable[RawTestServer]] +AiohttpServer = Callable[[Application], Awaitable[TestServer]] def pytest_addoption(parser): # type: ignore[no-untyped-def] @@ -43,7 +40,7 @@ def pytest_addoption(parser): # type: ignore[no-untyped-def] "--aiohttp-loop", action="store", default="pyloop", - help="run tests with specific loop: pyloop, uvloop, tokio or all", + help="run tests with specific loop: pyloop, uvloop or all", ) parser.addoption( "--aiohttp-enable-loop-debug", @@ -198,16 +195,14 @@ def pytest_generate_tests(metafunc): # type: ignore[no-untyped-def] return loops = metafunc.config.option.aiohttp_loop + avail_factories: Dict[str, Type[asyncio.AbstractEventLoopPolicy]] avail_factories = {"pyloop": asyncio.DefaultEventLoopPolicy} if uvloop is not None: # pragma: no cover avail_factories["uvloop"] = uvloop.EventLoopPolicy - if tokio is not None: # pragma: no cover - avail_factories["tokio"] = tokio.EventLoopPolicy - if loops == "all": - loops = "pyloop,uvloop?,tokio?" + loops = "pyloop,uvloop?" factories = {} # type: ignore[var-annotated] for name in loops.split(","): @@ -250,13 +245,13 @@ def proactor_loop(): # type: ignore[no-untyped-def] @pytest.fixture -def aiohttp_unused_port(): # type: ignore[no-untyped-def] +def aiohttp_unused_port() -> Callable[[], int]: """Return a port that is unused on the current host.""" return _unused_port @pytest.fixture -def aiohttp_server(loop): # type: ignore[no-untyped-def] +def aiohttp_server(loop: asyncio.AbstractEventLoop) -> Iterator[AiohttpServer]: """Factory to create a TestServer instance, given an app. aiohttp_server(app, **kwargs) @@ -279,7 +274,7 @@ async def finalize() -> None: @pytest.fixture -def aiohttp_raw_server(loop): # type: ignore[no-untyped-def] +def aiohttp_raw_server(loop: asyncio.AbstractEventLoop) -> Iterator[AiohttpRawServer]: """Factory to create a RawTestServer instance, given a web handler. aiohttp_raw_server(handler, **kwargs) @@ -331,7 +326,7 @@ def test_login(aiohttp_client): @pytest.fixture def aiohttp_client( loop: asyncio.AbstractEventLoop, aiohttp_client_cls: Type[TestClient] -) -> Generator[AiohttpClient, None, None]: +) -> Iterator[AiohttpClient]: """Factory to create a TestClient instance. aiohttp_client(app, **kwargs) diff --git a/aiohttp/test_utils.py b/aiohttp/test_utils.py index 652a8d4713d..9e8a533d48b 100644 --- a/aiohttp/test_utils.py +++ b/aiohttp/test_utils.py @@ -36,6 +36,7 @@ from .client_ws import ClientWebSocketResponse from .helpers import _SENTINEL, PY_38, sentinel from .http import HttpVersion, RawRequestMessage +from .typedefs import StrOrURL from .web import ( Application, AppRunner, @@ -148,14 +149,14 @@ async def start_server(self, **kwargs: Any) -> None: async def _make_runner(self, **kwargs: Any) -> BaseRunner: pass - def make_url(self, path: str) -> URL: + def make_url(self, path: StrOrURL) -> URL: assert self._root is not None url = URL(path) if not self.skip_url_asserts: assert not url.is_absolute() return self._root.join(url) else: - return URL(str(self._root) + path) + return URL(str(self._root) + str(path)) @property def started(self) -> bool: @@ -304,16 +305,20 @@ def session(self) -> ClientSession: """ return self._session - def make_url(self, path: str) -> URL: + def make_url(self, path: StrOrURL) -> URL: return self._server.make_url(path) - async def _request(self, method: str, path: str, **kwargs: Any) -> ClientResponse: + async def _request( + self, method: str, path: StrOrURL, **kwargs: Any + ) -> ClientResponse: resp = await self._session.request(method, self.make_url(path), **kwargs) # save it to close later self._responses.append(resp) return resp - def request(self, method: str, path: str, **kwargs: Any) -> _RequestContextManager: + def request( + self, method: str, path: StrOrURL, **kwargs: Any + ) -> _RequestContextManager: """Routes a request to tested http server. The interface is identical to aiohttp.ClientSession.request, @@ -323,35 +328,35 @@ def request(self, method: str, path: str, **kwargs: Any) -> _RequestContextManag """ return _RequestContextManager(self._request(method, path, **kwargs)) - def get(self, path: str, **kwargs: Any) -> _RequestContextManager: + def get(self, path: StrOrURL, **kwargs: Any) -> _RequestContextManager: """Perform an HTTP GET request.""" return _RequestContextManager(self._request(hdrs.METH_GET, path, **kwargs)) - def post(self, path: str, **kwargs: Any) -> _RequestContextManager: + def post(self, path: StrOrURL, **kwargs: Any) -> _RequestContextManager: """Perform an HTTP POST request.""" return _RequestContextManager(self._request(hdrs.METH_POST, path, **kwargs)) - def options(self, path: str, **kwargs: Any) -> _RequestContextManager: + def options(self, path: StrOrURL, **kwargs: Any) -> _RequestContextManager: """Perform an HTTP OPTIONS request.""" return _RequestContextManager(self._request(hdrs.METH_OPTIONS, path, **kwargs)) - def head(self, path: str, **kwargs: Any) -> _RequestContextManager: + def head(self, path: StrOrURL, **kwargs: Any) -> _RequestContextManager: """Perform an HTTP HEAD request.""" return _RequestContextManager(self._request(hdrs.METH_HEAD, path, **kwargs)) - def put(self, path: str, **kwargs: Any) -> _RequestContextManager: + def put(self, path: StrOrURL, **kwargs: Any) -> _RequestContextManager: """Perform an HTTP PUT request.""" return _RequestContextManager(self._request(hdrs.METH_PUT, path, **kwargs)) - def patch(self, path: str, **kwargs: Any) -> _RequestContextManager: + def patch(self, path: StrOrURL, **kwargs: Any) -> _RequestContextManager: """Perform an HTTP PATCH request.""" return _RequestContextManager(self._request(hdrs.METH_PATCH, path, **kwargs)) - def delete(self, path: str, **kwargs: Any) -> _RequestContextManager: + def delete(self, path: StrOrURL, **kwargs: Any) -> _RequestContextManager: """Perform an HTTP PATCH request.""" return _RequestContextManager(self._request(hdrs.METH_DELETE, path, **kwargs)) - def ws_connect(self, path: str, **kwargs: Any) -> _WSRequestContextManager: + def ws_connect(self, path: StrOrURL, **kwargs: Any) -> _WSRequestContextManager: """Initiate websocket connection. The api corresponds to aiohttp.ClientSession.ws_connect. @@ -359,7 +364,9 @@ def ws_connect(self, path: str, **kwargs: Any) -> _WSRequestContextManager: """ return _WSRequestContextManager(self._ws_connect(path, **kwargs)) - async def _ws_connect(self, path: str, **kwargs: Any) -> ClientWebSocketResponse: + async def _ws_connect( + self, path: StrOrURL, **kwargs: Any + ) -> ClientWebSocketResponse: ws = await self._session.ws_connect(self.make_url(path), **kwargs) self._websockets.append(ws) return ws diff --git a/aiohttp/web.py b/aiohttp/web.py index 02c2f55ddb6..71c2089941f 100644 --- a/aiohttp/web.py +++ b/aiohttp/web.py @@ -44,6 +44,7 @@ HTTPLengthRequired, HTTPMethodNotAllowed, HTTPMisdirectedRequest, + HTTPMove, HTTPMovedPermanently, HTTPMultipleChoices, HTTPNetworkAuthenticationRequired, @@ -157,6 +158,7 @@ "HTTPLengthRequired", "HTTPMethodNotAllowed", "HTTPMisdirectedRequest", + "HTTPMove", "HTTPMovedPermanently", "HTTPMultipleChoices", "HTTPNetworkAuthenticationRequired", diff --git a/aiohttp/worker.py b/aiohttp/worker.py index 93cb7544f0b..e573b799279 100644 --- a/aiohttp/worker.py +++ b/aiohttp/worker.py @@ -26,7 +26,7 @@ SSLContext = object # type: ignore[misc,assignment] -__all__ = ("GunicornWebWorker", "GunicornUVLoopWebWorker", "GunicornTokioWebWorker") +__all__ = ("GunicornWebWorker", "GunicornUVLoopWebWorker") class GunicornWebWorker(base.Worker): # type: ignore[misc,no-any-unimported] @@ -185,7 +185,7 @@ def init_signals(self) -> None: # there is no need to reset it. signal.signal(signal.SIGCHLD, signal.SIG_DFL) - def handle_quit(self, sig: int, frame: FrameType) -> None: + def handle_quit(self, sig: int, frame: Optional[FrameType]) -> None: self.alive = False # worker_int callback @@ -194,7 +194,7 @@ def handle_quit(self, sig: int, frame: FrameType) -> None: # wakeup closing process self._notify_waiter_done() - def handle_abort(self, sig: int, frame: FrameType) -> None: + def handle_abort(self, sig: int, frame: Optional[FrameType]) -> None: self.alive = False self.exit_code = 1 self.cfg.worker_abort(self) @@ -243,15 +243,3 @@ def init_process(self) -> None: asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) super().init_process() - - -class GunicornTokioWebWorker(GunicornWebWorker): - def init_process(self) -> None: # pragma: no cover - import tokio - - # Setup tokio policy, so that every - # asyncio.get_event_loop() will create an instance - # of tokio event loop. - asyncio.set_event_loop_policy(tokio.EventLoopPolicy()) - - super().init_process() diff --git a/requirements/base.txt b/requirements/base.txt index 8b90004d551..6fe7459db76 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -10,5 +10,5 @@ cchardet==2.1.7; python_version < "3.10" # Unmaintained: aio-libs/aiohttp#6819 charset-normalizer==2.0.12 frozenlist==1.3.1 gunicorn==20.1.0 -uvloop==0.14.0; platform_system!="Windows" and implementation_name=="cpython" and python_version<"3.9" # MagicStack/uvloop#14 +uvloop==0.17.0; platform_system!="Windows" and implementation_name=="cpython" and python_version<"3.9" # MagicStack/uvloop#14 yarl==1.9.2 diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 290fe99f243..17fe5622a4a 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -243,7 +243,7 @@ uritemplate==4.1.1 # via gidgethub urllib3==1.26.7 # via requests -uvloop==0.14.0 ; platform_system != "Windows" and implementation_name == "cpython" and python_version < "3.9" +uvloop==0.17.0 ; platform_system != "Windows" and implementation_name == "cpython" and python_version < "3.9" # via -r requirements/base.txt virtualenv==20.10.0 # via pre-commit diff --git a/requirements/lint.txt b/requirements/lint.txt index 1bab0a04bd7..1fe526e0c91 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -4,3 +4,4 @@ mypy==0.982; implementation_name=="cpython" pre-commit==2.17.0 pytest==6.2.5 slotscheck==0.8.0 +uvloop==0.17.0; platform_system!="Windows" diff --git a/tests/test_run_app.py b/tests/test_run_app.py index d7ab4b9de4b..18f59b074ad 100644 --- a/tests/test_run_app.py +++ b/tests/test_run_app.py @@ -55,12 +55,6 @@ HAS_IPV6 = False -# tokio event loop does not allow to override attributes -def skip_if_no_dict(loop: Any) -> None: - if not hasattr(loop, "__dict__"): - pytest.skip("can not override loop attributes") - - def skip_if_on_windows() -> None: if platform.system() == "Windows": pytest.skip("the test is not valid for Windows") @@ -68,7 +62,6 @@ def skip_if_on_windows() -> None: @pytest.fixture def patched_loop(loop: Any): - skip_if_no_dict(loop) server = mock.Mock() server.wait_closed = make_mocked_coro(None) loop.create_server = make_mocked_coro(server) diff --git a/tests/test_web_app.py b/tests/test_web_app.py index c1a00548878..11cb34c06ee 100644 --- a/tests/test_web_app.py +++ b/tests/test_web_app.py @@ -1,6 +1,5 @@ -# type: ignore import asyncio -from typing import Any, Iterator +from typing import Any, AsyncIterator, Callable, Iterator, NoReturn from unittest import mock import pytest @@ -36,7 +35,7 @@ async def test_app_register_coro() -> None: app = web.Application() fut = asyncio.get_event_loop().create_future() - async def cb(app): + async def cb(app: web.Application) -> None: await asyncio.sleep(0.001) fut.set_result(123) @@ -58,7 +57,7 @@ async def test_on_shutdown() -> None: app = web.Application() called = False - async def on_shutdown(app_param): + async def on_shutdown(app_param: web.Application) -> None: nonlocal called assert app is app_param called = True @@ -76,21 +75,21 @@ async def test_on_startup() -> None: long_running2_called = False all_long_running_called = False - async def long_running1(app_param): + async def long_running1(app_param: web.Application) -> None: nonlocal long_running1_called assert app is app_param long_running1_called = True - async def long_running2(app_param): + async def long_running2(app_param: web.Application) -> None: nonlocal long_running2_called assert app is app_param long_running2_called = True - async def on_startup_all_long_running(app_param): + async def on_startup_all_long_running(app_param: web.Application) -> None: nonlocal all_long_running_called assert app is app_param all_long_running_called = True - return await asyncio.gather(long_running1(app_param), long_running2(app_param)) + await asyncio.gather(long_running1(app_param), long_running2(app_param)) app.on_startup.append(on_startup_all_long_running) app.freeze() @@ -117,8 +116,8 @@ def test_appkey_repr_concrete() -> None: "", # pytest-xdist "", ) - key = web.AppKey("key", web.Request) - assert repr(key) in ( + key2 = web.AppKey("key", web.Request) + assert repr(key2) in ( # pytest-xdist: "", "", @@ -186,7 +185,7 @@ def test_app_run_middlewares() -> None: root.freeze() assert root._run_middlewares is False - async def middleware(request, handler: Handler): + async def middleware(request: web.Request, handler: Handler) -> web.StreamResponse: return await handler(request) root = web.Application(middlewares=[middleware]) @@ -214,22 +213,22 @@ def test_subapp_pre_frozen_after_adding() -> None: def test_app_inheritance() -> None: with pytest.raises(TypeError): - class A(web.Application): + class A(web.Application): # type: ignore[misc] pass def test_app_custom_attr() -> None: app = web.Application() with pytest.raises(AttributeError): - app.custom = None + app.custom = None # type: ignore[attr-defined] async def test_cleanup_ctx() -> None: app = web.Application() out = [] - def f(num): - async def inner(app): + def f(num: int) -> Callable[[web.Application], AsyncIterator[None]]: + async def inner(app: web.Application) -> AsyncIterator[None]: out.append("pre_" + str(num)) yield None out.append("post_" + str(num)) @@ -251,8 +250,10 @@ async def test_cleanup_ctx_exception_on_startup() -> None: exc = Exception("fail") - def f(num, fail=False): - async def inner(app): + def f( + num: int, fail: bool = False + ) -> Callable[[web.Application], AsyncIterator[None]]: + async def inner(app: web.Application) -> AsyncIterator[None]: out.append("pre_" + str(num)) if fail: raise exc @@ -279,8 +280,10 @@ async def test_cleanup_ctx_exception_on_cleanup() -> None: exc = Exception("fail") - def f(num, fail=False): - async def inner(app): + def f( + num: int, fail: bool = False + ) -> Callable[[web.Application], AsyncIterator[None]]: + async def inner(app: web.Application) -> AsyncIterator[None]: out.append("pre_" + str(num)) yield None out.append("post_" + str(num)) @@ -305,13 +308,13 @@ async def test_cleanup_ctx_cleanup_after_exception() -> None: app = web.Application() ctx_state = None - async def success_ctx(app): + async def success_ctx(app: web.Application) -> AsyncIterator[None]: nonlocal ctx_state ctx_state = "START" yield ctx_state = "CLEAN" - async def fail_ctx(app): + async def fail_ctx(app: web.Application) -> AsyncIterator[NoReturn]: raise Exception() yield @@ -331,8 +334,10 @@ async def test_cleanup_ctx_exception_on_cleanup_multiple() -> None: app = web.Application() out = [] - def f(num, fail=False): - async def inner(app): + def f( + num: int, fail: bool = False + ) -> Callable[[web.Application], AsyncIterator[None]]: + async def inner(app: web.Application) -> AsyncIterator[None]: out.append("pre_" + str(num)) yield None out.append("post_" + str(num)) @@ -360,8 +365,8 @@ async def test_cleanup_ctx_multiple_yields() -> None: app = web.Application() out = [] - def f(num): - async def inner(app): + def f(num: int) -> Callable[[web.Application], AsyncIterator[None]]: + async def inner(app: web.Application) -> AsyncIterator[None]: out.append("pre_" + str(num)) yield None out.append("post_" + str(num)) @@ -383,7 +388,7 @@ async def test_subapp_chained_config_dict_visibility(aiohttp_client: Any) -> Non key1 = web.AppKey("key1", str) key2 = web.AppKey("key2", str) - async def main_handler(request): + async def main_handler(request: web.Request) -> web.Response: assert request.config_dict[key1] == "val1" assert key2 not in request.config_dict return web.Response(status=200) @@ -392,7 +397,7 @@ async def main_handler(request): root[key1] = "val1" root.add_routes([web.get("/", main_handler)]) - async def sub_handler(request): + async def sub_handler(request: web.Request) -> web.Response: assert request.config_dict[key1] == "val1" assert request.config_dict[key2] == "val2" return web.Response(status=201) @@ -413,7 +418,7 @@ async def sub_handler(request): async def test_subapp_chained_config_dict_overriding(aiohttp_client: Any) -> None: key = web.AppKey("key", str) - async def main_handler(request): + async def main_handler(request: web.Request) -> web.Response: assert request.config_dict[key] == "val1" return web.Response(status=200) @@ -421,7 +426,7 @@ async def main_handler(request): root[key] = "val1" root.add_routes([web.get("/", main_handler)]) - async def sub_handler(request): + async def sub_handler(request: web.Request) -> web.Response: assert request.config_dict[key] == "val2" return web.Response(status=201) @@ -445,7 +450,7 @@ async def test_subapp_on_startup(aiohttp_client: Any) -> None: startup_called = False - async def on_startup(app): + async def on_startup(app: web.Application) -> None: nonlocal startup_called startup_called = True app[startup] = True @@ -455,7 +460,7 @@ async def on_startup(app): ctx_pre_called = False ctx_post_called = False - async def cleanup_ctx(app): + async def cleanup_ctx(app: web.Application) -> AsyncIterator[None]: nonlocal ctx_pre_called, ctx_post_called ctx_pre_called = True app[cleanup] = True @@ -466,7 +471,7 @@ async def cleanup_ctx(app): shutdown_called = False - async def on_shutdown(app): + async def on_shutdown(app: web.Application) -> None: nonlocal shutdown_called shutdown_called = True @@ -474,7 +479,7 @@ async def on_shutdown(app): cleanup_called = False - async def on_cleanup(app): + async def on_cleanup(app: web.Application) -> None: nonlocal cleanup_called cleanup_called = True @@ -528,9 +533,9 @@ def test_app_iter() -> None: def test_app_forbid_nonslot_attr() -> None: app = web.Application() with pytest.raises(AttributeError): - app.unknow_attr + app.unknow_attr # type: ignore[attr-defined] with pytest.raises(AttributeError): - app.unknow_attr = 1 + app.unknow_attr = 1 # type: ignore[attr-defined] def test_forbid_changing_frozen_app() -> None: diff --git a/tests/test_web_exceptions.py b/tests/test_web_exceptions.py index de3b0da4b8a..2c9e2d32d2e 100644 --- a/tests/test_web_exceptions.py +++ b/tests/test_web_exceptions.py @@ -1,13 +1,13 @@ -# type: ignore import collections import pickle from traceback import format_exception -from typing import Any +from typing import Mapping, NoReturn import pytest from yarl import URL from aiohttp import web +from aiohttp.pytest_plugin import AiohttpClient def test_all_http_exceptions_exported() -> None: @@ -23,7 +23,8 @@ def test_all_http_exceptions_exported() -> None: async def test_ctor() -> None: resp = web.HTTPOk() assert resp.text == "200: OK" - assert resp.headers == {"Content-Type": "text/plain"} + compare: Mapping[str, str] = {"Content-Type": "text/plain"} + assert resp.headers == compare assert resp.reason == "OK" assert resp.status == 200 assert bool(resp) @@ -32,7 +33,8 @@ async def test_ctor() -> None: async def test_ctor_with_headers() -> None: resp = web.HTTPOk(headers={"X-Custom": "value"}) assert resp.text == "200: OK" - assert resp.headers == {"Content-Type": "text/plain", "X-Custom": "value"} + compare: Mapping[str, str] = {"Content-Type": "text/plain", "X-Custom": "value"} + assert resp.headers == compare assert resp.reason == "OK" assert resp.status == 200 @@ -40,7 +42,8 @@ async def test_ctor_with_headers() -> None: async def test_ctor_content_type() -> None: resp = web.HTTPOk(text="text", content_type="custom") assert resp.text == "text" - assert resp.headers == {"Content-Type": "custom"} + compare: Mapping[str, str] = {"Content-Type": "custom"} + assert resp.headers == compare assert resp.reason == "OK" assert resp.status == 200 assert bool(resp) @@ -53,7 +56,8 @@ async def test_ctor_content_type_without_text() -> None: ): resp = web.HTTPResetContent(content_type="custom") assert resp.text is None - assert resp.headers == {"Content-Type": "custom"} + compare: Mapping[str, str] = {"Content-Type": "custom"} + assert resp.headers == compare assert resp.reason == "Reset Content" assert resp.status == 205 assert bool(resp) @@ -67,7 +71,8 @@ async def test_ctor_text_for_empty_body() -> None: ): resp = web.HTTPResetContent(text="text") assert resp.text == "text" - assert resp.headers == {"Content-Type": "text/plain"} + compare: Mapping[str, str] = {"Content-Type": "text/plain"} + assert resp.headers == compare assert resp.reason == "Reset Content" assert resp.status == 205 @@ -139,7 +144,8 @@ def test_ctor_all(self) -> None: content_type="custom", ) assert resp.text == "text" - assert resp.headers == {"X-Custom": "value", "Content-Type": "custom"} + compare: Mapping[str, str] = {"X-Custom": "value", "Content-Type": "custom"} + assert resp.headers == compare assert resp.reason == "Done" assert resp.status == 200 @@ -150,7 +156,7 @@ def test_pickle(self) -> None: text="text", content_type="custom", ) - resp.foo = "bar" + resp.foo = "bar" # type: ignore[attr-defined] for proto in range(2, pickle.HIGHEST_PROTOCOL + 1): pickled = pickle.dumps(resp, proto) resp2 = pickle.loads(pickled) @@ -160,8 +166,8 @@ def test_pickle(self) -> None: assert resp2.status == 200 assert resp2.foo == "bar" - async def test_app(self, aiohttp_client: Any) -> None: - async def handler(request): + async def test_app(self, aiohttp_client: AiohttpClient) -> None: + async def handler(request: web.Request) -> NoReturn: raise web.HTTPOk() app = web.Application() @@ -189,7 +195,7 @@ def test_empty_location(self) -> None: with pytest.raises(ValueError): web.HTTPFound(location="") with pytest.raises(ValueError): - web.HTTPFound(location=None) + web.HTTPFound(location=None) # type: ignore[arg-type] def test_location_CRLF(self) -> None: exc = web.HTTPFound(location="/redirect\r\n") @@ -203,7 +209,7 @@ def test_pickle(self) -> None: text="text", content_type="custom", ) - resp.foo = "bar" + resp.foo = "bar" # type: ignore[attr-defined] for proto in range(2, pickle.HIGHEST_PROTOCOL + 1): pickled = pickle.dumps(resp, proto) resp2 = pickle.loads(pickled) @@ -214,8 +220,8 @@ def test_pickle(self) -> None: assert resp2.status == 302 assert resp2.foo == "bar" - async def test_app(self, aiohttp_client: Any) -> None: - async def handler(request): + async def test_app(self, aiohttp_client: AiohttpClient) -> None: + async def handler(request: web.Request) -> NoReturn: raise web.HTTPFound(location="/redirect") app = web.Application() @@ -242,11 +248,12 @@ async def test_ctor(self) -> None: assert resp.method == "GET" assert resp.allowed_methods == {"POST", "PUT"} assert resp.text == "text" - assert resp.headers == { + compare: Mapping[str, str] = { "X-Custom": "value", "Content-Type": "custom", "Allow": "POST,PUT", } + assert resp.headers == compare assert resp.reason == "Unsupported" assert resp.status == 405 @@ -259,7 +266,7 @@ def test_pickle(self) -> None: text="text", content_type="custom", ) - resp.foo = "bar" + resp.foo = "bar" # type: ignore[attr-defined] for proto in range(2, pickle.HIGHEST_PROTOCOL + 1): pickled = pickle.dumps(resp, proto) resp2 = pickle.loads(pickled) @@ -283,7 +290,8 @@ def test_ctor(self) -> None: assert resp.text == ( "Maximum request body size 100 exceeded, " "actual body size 123" ) - assert resp.headers == {"X-Custom": "value", "Content-Type": "text/plain"} + compare: Mapping[str, str] = {"X-Custom": "value", "Content-Type": "text/plain"} + assert resp.headers == compare assert resp.reason == "Too large" assert resp.status == 413 @@ -291,7 +299,7 @@ def test_pickle(self) -> None: resp = web.HTTPRequestEntityTooLarge( 100, actual_size=123, headers={"X-Custom": "value"}, reason="Too large" ) - resp.foo = "bar" + resp.foo = "bar" # type: ignore[attr-defined] for proto in range(2, pickle.HIGHEST_PROTOCOL + 1): pickled = pickle.dumps(resp, proto) resp2 = pickle.loads(pickled) @@ -313,11 +321,12 @@ def test_ctor(self) -> None: ) assert resp.link == URL("http://warning.or.kr/") assert resp.text == "text" - assert resp.headers == { + compare: Mapping[str, str] = { "X-Custom": "value", "Content-Type": "custom", "Link": '; rel="blocked-by"', } + assert resp.headers == compare assert resp.reason == "Zaprescheno" assert resp.status == 451 @@ -329,7 +338,7 @@ def test_pickle(self) -> None: text="text", content_type="custom", ) - resp.foo = "bar" + resp.foo = "bar" # type: ignore[attr-defined] for proto in range(2, pickle.HIGHEST_PROTOCOL + 1): pickled = pickle.dumps(resp, proto) resp2 = pickle.loads(pickled) diff --git a/tests/test_web_request_handler.py b/tests/test_web_request_handler.py index 005cfaaecb7..06f99be76c0 100644 --- a/tests/test_web_request_handler.py +++ b/tests/test_web_request_handler.py @@ -1,12 +1,10 @@ -# type: ignore -from typing import Any from unittest import mock from aiohttp import web from aiohttp.test_utils import make_mocked_coro -async def serve(request: Any): +async def serve(request: web.BaseRequest) -> web.Response: return web.Response() @@ -16,8 +14,8 @@ async def test_repr() -> None: assert "" == repr(handler) - handler.transport = object() - assert "" == repr(handler) + with mock.patch.object(handler, "transport", autospec=True): + assert "" == repr(handler) async def test_connections() -> None: @@ -26,10 +24,10 @@ async def test_connections() -> None: handler = object() transport = object() - manager.connection_made(handler, transport) + manager.connection_made(handler, transport) # type: ignore[arg-type] assert manager.connections == [handler] - manager.connection_lost(handler, None) + manager.connection_lost(handler, None) # type: ignore[arg-type] assert manager.connections == [] diff --git a/tests/test_web_urldispatcher.py b/tests/test_web_urldispatcher.py index ce2ec3cc77f..299da051554 100644 --- a/tests/test_web_urldispatcher.py +++ b/tests/test_web_urldispatcher.py @@ -1,7 +1,6 @@ -# type: ignore import asyncio import pathlib -from typing import Any +from typing import Optional from unittest import mock from unittest.mock import MagicMock @@ -9,6 +8,7 @@ import yarl from aiohttp import web +from aiohttp.pytest_plugin import AiohttpClient from aiohttp.web_urldispatcher import SystemRoute @@ -41,12 +41,12 @@ ], ) async def test_access_root_of_static_handler( - tmp_path: Any, - aiohttp_client: Any, - show_index: Any, - status: Any, - prefix: Any, - data: Any, + tmp_path: pathlib.Path, + aiohttp_client: AiohttpClient, + show_index: bool, + status: int, + prefix: str, + data: Optional[bytes], ) -> None: # Tests the operation of static file server. # Try to access the root of static file server, and make @@ -79,7 +79,9 @@ async def test_access_root_of_static_handler( assert read_ == data -async def test_follow_symlink(tmp_path: Any, aiohttp_client: Any) -> None: +async def test_follow_symlink( + tmp_path: pathlib.Path, aiohttp_client: AiohttpClient +) -> None: # Tests the access to a symlink, in static folder data = "hello world" @@ -113,7 +115,11 @@ async def test_follow_symlink(tmp_path: Any, aiohttp_client: Any) -> None: ], ) async def test_access_to_the_file_with_spaces( - tmp_path: Any, aiohttp_client: Any, dir_name: Any, filename: Any, data: Any + tmp_path: pathlib.Path, + aiohttp_client: AiohttpClient, + dir_name: str, + filename: str, + data: str, ) -> None: # Checks operation of static files with spaces @@ -138,7 +144,9 @@ async def test_access_to_the_file_with_spaces( await r.release() -async def test_access_non_existing_resource(tmp_path: Any, aiohttp_client: Any) -> None: +async def test_access_non_existing_resource( + tmp_path: pathlib.Path, aiohttp_client: AiohttpClient +) -> None: # Tests accessing non-existing resource # Try to access a non-exiting resource and make sure that 404 HTTP status # returned. @@ -162,12 +170,12 @@ async def test_access_non_existing_resource(tmp_path: Any, aiohttp_client: Any) ], ) async def test_url_escaping( - aiohttp_client: Any, registered_path: Any, request_url: Any + aiohttp_client: AiohttpClient, registered_path: str, request_url: str ) -> None: # Tests accessing a resource with app = web.Application() - async def handler(request): + async def handler(request: web.Request) -> web.Response: return web.Response() app.router.add_get(registered_path, handler) @@ -182,7 +190,7 @@ async def test_handler_metadata_persistence() -> None: # router. app = web.Application() - async def async_handler(request): + async def async_handler(request: web.Request) -> web.Response: """Doc""" return web.Response() @@ -193,7 +201,9 @@ async def async_handler(request): assert route.handler.__doc__ == "Doc" -async def test_unauthorized_folder_access(tmp_path: Any, aiohttp_client: Any) -> None: +async def test_unauthorized_folder_access( + tmp_path: pathlib.Path, aiohttp_client: AiohttpClient +) -> None: # Tests the unauthorized access to a folder of static file server. # Try to list a folder content of static file server when server does not # have permissions to do so for the folder. @@ -218,7 +228,9 @@ async def test_unauthorized_folder_access(tmp_path: Any, aiohttp_client: Any) -> assert r.status == 403 -async def test_access_symlink_loop(tmp_path: Any, aiohttp_client: Any) -> None: +async def test_access_symlink_loop( + tmp_path: pathlib.Path, aiohttp_client: AiohttpClient +) -> None: # Tests the access to a looped symlink, which could not be resolved. my_dir_path = tmp_path / "my_symlink" pathlib.Path(str(my_dir_path)).symlink_to(str(my_dir_path), True) @@ -234,7 +246,9 @@ async def test_access_symlink_loop(tmp_path: Any, aiohttp_client: Any) -> None: assert r.status == 404 -async def test_access_special_resource(tmp_path: Any, aiohttp_client: Any) -> None: +async def test_access_special_resource( + tmp_path: pathlib.Path, aiohttp_client: AiohttpClient +) -> None: # Tests the access to a resource that is neither a file nor a directory. # Checks that if a special resource is accessed (f.e. named pipe or UNIX # domain socket) then 404 HTTP status returned. @@ -261,7 +275,9 @@ async def test_access_special_resource(tmp_path: Any, aiohttp_client: Any) -> No assert r.status == 403 -async def test_static_head(tmp_path: Any, aiohttp_client: Any) -> None: +async def test_static_head( + tmp_path: pathlib.Path, aiohttp_client: AiohttpClient +) -> None: # Test HEAD on static route my_file_path = tmp_path / "test.txt" with my_file_path.open("wb") as fw: @@ -299,11 +315,11 @@ def test_system_route() -> None: assert "test" == route.reason -async def test_allow_head(aiohttp_client: Any) -> None: +async def test_allow_head(aiohttp_client: AiohttpClient) -> None: # Test allow_head on routes. app = web.Application() - async def handler(_): + async def handler(request: web.Request) -> web.Response: return web.Response() app.router.add_get("/a", handler, name="a") @@ -334,12 +350,12 @@ async def handler(_): "/{a}", ], ) -def test_reuse_last_added_resource(path: Any) -> None: +def test_reuse_last_added_resource(path: str) -> None: # Test that adding a route with the same name and path of the last added # resource doesn't create a new resource. app = web.Application() - async def handler(request): + async def handler(request: web.Request) -> web.Response: return web.Response() app.router.add_get(path, handler, name="a") @@ -351,27 +367,29 @@ async def handler(request): def test_resource_raw_match() -> None: app = web.Application() - async def handler(request): + async def handler(request: web.Request) -> web.Response: return web.Response() route = app.router.add_get("/a", handler, name="a") + assert route.resource is not None assert route.resource.raw_match("/a") route = app.router.add_get("/{b}", handler, name="b") + assert route.resource is not None assert route.resource.raw_match("/{b}") resource = app.router.add_static("/static", ".") assert not resource.raw_match("/static") -async def test_add_view(aiohttp_client: Any) -> None: +async def test_add_view(aiohttp_client: AiohttpClient) -> None: app = web.Application() class MyView(web.View): - async def get(self): + async def get(self) -> web.Response: return web.Response() - async def post(self): + async def post(self) -> web.Response: return web.Response() app.router.add_view("/a", MyView) @@ -391,15 +409,15 @@ async def post(self): await r.release() -async def test_decorate_view(aiohttp_client: Any) -> None: +async def test_decorate_view(aiohttp_client: AiohttpClient) -> None: routes = web.RouteTableDef() @routes.view("/a") class MyView(web.View): - async def get(self): + async def get(self) -> web.Response: return web.Response() - async def post(self): + async def post(self) -> web.Response: return web.Response() app = web.Application() @@ -420,14 +438,14 @@ async def post(self): await r.release() -async def test_web_view(aiohttp_client: Any) -> None: +async def test_web_view(aiohttp_client: AiohttpClient) -> None: app = web.Application() class MyView(web.View): - async def get(self): + async def get(self) -> web.Response: return web.Response() - async def post(self): + async def post(self) -> web.Response: return web.Response() app.router.add_routes([web.view("/a", MyView)]) @@ -447,7 +465,9 @@ async def post(self): await r.release() -async def test_static_absolute_url(aiohttp_client: Any, tmp_path: Any) -> None: +async def test_static_absolute_url( + aiohttp_client: AiohttpClient, tmp_path: pathlib.Path +) -> None: # requested url is an absolute name like # /static/\\machine_name\c$ or /static/D:\path # where the static dir is totally different @@ -461,11 +481,13 @@ async def test_static_absolute_url(aiohttp_client: Any, tmp_path: Any) -> None: assert resp.status == 403 -async def test_for_issue_5250(aiohttp_client: Any, tmp_path: Any) -> None: +async def test_for_issue_5250( + aiohttp_client: AiohttpClient, tmp_path: pathlib.Path +) -> None: app = web.Application() app.router.add_static("/foo", tmp_path) - async def get_foobar(request): + async def get_foobar(request: web.Request) -> web.Response: return web.Response(body="success!") app.router.add_get("/foobar", get_foobar) @@ -490,14 +512,14 @@ async def get_foobar(request): ids=("urldecoded_route", "urldecoded_route_with_regex", "urlencoded_route"), ) async def test_decoded_url_match( - aiohttp_client, - route_definition, - urlencoded_path, - expected_http_resp_status, + aiohttp_client: AiohttpClient, + route_definition: str, + urlencoded_path: str, + expected_http_resp_status: int, ) -> None: app = web.Application() - async def handler(_): + async def handler(request: web.Request) -> web.Response: return web.Response() app.router.add_get(route_definition, handler) diff --git a/tests/test_worker.py b/tests/test_worker.py index 5f973179228..34e0234a113 100644 --- a/tests/test_worker.py +++ b/tests/test_worker.py @@ -1,39 +1,37 @@ -# type: ignore # Tests for aiohttp/worker.py import asyncio import os import socket import ssl +from typing import TYPE_CHECKING, Callable, Dict, Optional from unittest import mock import pytest +from _pytest.fixtures import SubRequest from aiohttp import web -base_worker = pytest.importorskip("aiohttp.worker") +if TYPE_CHECKING: + from aiohttp import worker as base_worker +else: + base_worker = pytest.importorskip("aiohttp.worker") try: import uvloop except ImportError: - uvloop = None + uvloop = None # type: ignore[assignment] WRONG_LOG_FORMAT = '%a "%{Referrer}i" %(h)s %(l)s %s' ACCEPTABLE_LOG_FORMAT = '%a "%{Referrer}i" %s' -# tokio event loop does not allow to override attributes -def skip_if_no_dict(loop): - if not hasattr(loop, "__dict__"): - pytest.skip("can not override loop attributes") - - class BaseTestWorker: - def __init__(self): - self.servers = {} + def __init__(self) -> None: + self.servers: Dict[object, object] = {} self.exit_code = 0 - self._notify_waiter = None + self._notify_waiter: Optional[asyncio.Future[bool]] = None self.cfg = mock.Mock() self.cfg.graceful_timeout = 100 self.pid = "pid" @@ -54,14 +52,16 @@ class UvloopWorker(BaseTestWorker, base_worker.GunicornUVLoopWebWorker): @pytest.fixture(params=PARAMS) -def worker(request, loop): +def worker( + request: SubRequest, loop: asyncio.AbstractEventLoop +) -> base_worker.GunicornWebWorker: asyncio.set_event_loop(loop) ret = request.param() ret.notify = mock.Mock() - return ret + return ret # type: ignore[no-any-return] -def test_init_process(worker) -> None: +def test_init_process(worker: base_worker.GunicornWebWorker) -> None: with mock.patch("aiohttp.worker.asyncio") as m_asyncio: try: worker.init_process() @@ -72,7 +72,9 @@ def test_init_process(worker) -> None: assert m_asyncio.set_event_loop.called -def test_run(worker, loop) -> None: +def test_run( + worker: base_worker.GunicornWebWorker, loop: asyncio.AbstractEventLoop +) -> None: worker.log = mock.Mock() worker.cfg = mock.Mock() worker.cfg.access_log_format = ACCEPTABLE_LOG_FORMAT @@ -86,7 +88,9 @@ def test_run(worker, loop) -> None: assert loop.is_closed() -def test_run_async_factory(worker, loop) -> None: +def test_run_async_factory( + worker: base_worker.GunicornWebWorker, loop: asyncio.AbstractEventLoop +) -> None: worker.log = mock.Mock() worker.cfg = mock.Mock() worker.cfg.access_log_format = ACCEPTABLE_LOG_FORMAT @@ -94,8 +98,8 @@ def test_run_async_factory(worker, loop) -> None: worker.sockets = [] app = worker.wsgi - async def make_app(): - return app + async def make_app() -> web.Application: + return app # type: ignore[no-any-return] worker.wsgi = make_app @@ -107,7 +111,9 @@ async def make_app(): assert loop.is_closed() -def test_run_not_app(worker, loop) -> None: +def test_run_not_app( + worker: base_worker.GunicornWebWorker, loop: asyncio.AbstractEventLoop +) -> None: worker.log = mock.Mock() worker.cfg = mock.Mock() worker.cfg.access_log_format = ACCEPTABLE_LOG_FORMAT @@ -121,32 +127,24 @@ def test_run_not_app(worker, loop) -> None: assert loop.is_closed() -def test_handle_quit(worker, loop) -> None: - worker.loop = mock.Mock() - worker.handle_quit(object(), object()) - assert not worker.alive - assert worker.exit_code == 0 - worker.loop.call_later.asset_called_with(0.1, worker._notify_waiter_done) - - -def test_handle_abort(worker) -> None: +def test_handle_abort(worker: base_worker.GunicornWebWorker) -> None: with mock.patch("aiohttp.worker.sys") as m_sys: - worker.handle_abort(object(), object()) + worker.handle_abort(0, None) assert not worker.alive assert worker.exit_code == 1 m_sys.exit.assert_called_with(1) -def test__wait_next_notify(worker) -> None: - worker.loop = mock.Mock() - worker._notify_waiter_done = mock.Mock() - fut = worker._wait_next_notify() +def test__wait_next_notify(worker: base_worker.GunicornWebWorker) -> None: + worker.loop = mloop = mock.create_autospec(asyncio.AbstractEventLoop) + with mock.patch.object(worker, "_notify_waiter_done", autospec=True): + fut = worker._wait_next_notify() - assert worker._notify_waiter == fut - worker.loop.call_later.assert_called_with(1.0, worker._notify_waiter_done, fut) + assert worker._notify_waiter == fut + mloop.call_later.assert_called_with(1.0, worker._notify_waiter_done, fut) -def test__notify_waiter_done(worker) -> None: +def test__notify_waiter_done(worker: base_worker.GunicornWebWorker) -> None: worker._notify_waiter = None worker._notify_waiter_done() assert worker._notify_waiter is None @@ -159,7 +157,9 @@ def test__notify_waiter_done(worker) -> None: waiter.set_result.assert_called_with(True) -def test__notify_waiter_done_explicit_waiter(worker) -> None: +def test__notify_waiter_done_explicit_waiter( + worker: base_worker.GunicornWebWorker, +) -> None: worker._notify_waiter = None assert worker._notify_waiter is None @@ -173,7 +173,7 @@ def test__notify_waiter_done_explicit_waiter(worker) -> None: assert not waiter2.set_result.called -def test_init_signals(worker) -> None: +def test_init_signals(worker: base_worker.GunicornWebWorker) -> None: worker.loop = mock.Mock() worker.init_signals() assert worker.loop.add_signal_handler.called @@ -189,19 +189,23 @@ def test_init_signals(worker) -> None: ), ], ) -def test__get_valid_log_format_ok(worker, source, result) -> None: +def test__get_valid_log_format_ok( + worker: base_worker.GunicornWebWorker, source: str, result: str +) -> None: assert result == worker._get_valid_log_format(source) -def test__get_valid_log_format_exc(worker) -> None: +def test__get_valid_log_format_exc(worker: base_worker.GunicornWebWorker) -> None: with pytest.raises(ValueError) as exc: worker._get_valid_log_format(WRONG_LOG_FORMAT) assert "%(name)s" in str(exc.value) -async def test__run_ok_parent_changed(worker, loop, aiohttp_unused_port) -> None: - skip_if_no_dict(loop) - +async def test__run_ok_parent_changed( + worker: base_worker.GunicornWebWorker, + loop: asyncio.AbstractEventLoop, + aiohttp_unused_port: Callable[[], int], +) -> None: worker.ppid = 0 worker.alive = True sock = socket.socket() @@ -220,9 +224,11 @@ async def test__run_ok_parent_changed(worker, loop, aiohttp_unused_port) -> None worker.log.info.assert_called_with("Parent changed, shutting down: %s", worker) -async def test__run_exc(worker, loop, aiohttp_unused_port) -> None: - skip_if_no_dict(loop) - +async def test__run_exc( + worker: base_worker.GunicornWebWorker, + loop: asyncio.AbstractEventLoop, + aiohttp_unused_port: Callable[[], int], +) -> None: worker.ppid = os.getppid() worker.alive = True sock = socket.socket() @@ -235,9 +241,10 @@ async def test__run_exc(worker, loop, aiohttp_unused_port) -> None: worker.cfg.max_requests = 0 worker.cfg.is_ssl = False - def raiser(): + def raiser() -> None: waiter = worker._notify_waiter worker.alive = False + assert waiter is not None waiter.set_exception(RuntimeError()) loop.call_later(0.1, raiser) @@ -247,8 +254,8 @@ def raiser(): def test__create_ssl_context_without_certs_and_ciphers( - worker, - tls_certificate_pem_path, + worker: base_worker.GunicornWebWorker, + tls_certificate_pem_path: str, ) -> None: worker.cfg.ssl_version = ssl.PROTOCOL_TLS_CLIENT worker.cfg.cert_reqs = ssl.CERT_OPTIONAL @@ -261,8 +268,8 @@ def test__create_ssl_context_without_certs_and_ciphers( def test__create_ssl_context_with_ciphers( - worker, - tls_certificate_pem_path, + worker: base_worker.GunicornWebWorker, + tls_certificate_pem_path: str, ) -> None: worker.cfg.ssl_version = ssl.PROTOCOL_TLS_CLIENT worker.cfg.cert_reqs = ssl.CERT_OPTIONAL @@ -275,9 +282,9 @@ def test__create_ssl_context_with_ciphers( def test__create_ssl_context_with_ca_certs( - worker, - tls_ca_certificate_pem_path, - tls_certificate_pem_path, + worker: base_worker.GunicornWebWorker, + tls_ca_certificate_pem_path: str, + tls_certificate_pem_path: str, ) -> None: worker.cfg.ssl_version = ssl.PROTOCOL_TLS_CLIENT worker.cfg.cert_reqs = ssl.CERT_OPTIONAL