From cf57c42376d15364928b6e6679a0e34f65882a6f Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Sat, 31 Oct 2020 16:57:26 +0000 Subject: [PATCH 01/25] Expand typing to tests. --- .mypy.ini | 24 ++++++++++++++++++++++++ aiohttp_jinja2/__init__.py | 6 ++++-- aiohttp_jinja2/helpers.py | 19 ++++++++++++------- tests/__init__.py | 0 tests/test_context_processors.py | 4 +++- tests/test_simple_renderer.py | 18 +++++++++++------- 6 files changed, 54 insertions(+), 17 deletions(-) create mode 100644 .mypy.ini create mode 100644 tests/__init__.py diff --git a/.mypy.ini b/.mypy.ini new file mode 100644 index 00000000..02ae0955 --- /dev/null +++ b/.mypy.ini @@ -0,0 +1,24 @@ +[mypy] +files = aiohttp_jinja2, tests +check_untyped_defs = True +follow_imports_for_stubs = True +disallow_any_decorated = True +disallow_any_generics = True +disallow_incomplete_defs = True +disallow_subclassing_any = True +disallow_untyped_calls = True +disallow_untyped_decorators = True +disallow_untyped_defs = True +implicit_reexport = False +no_implicit_optional = True +show_error_codes = True +strict_equality = True +warn_incomplete_stub = True +warn_redundant_casts = True +warn_unreachable = True +warn_unused_ignores = True +disallow_any_unimported = True +warn_return_any = True + +[mypy-tests.*] +disallow_untyped_defs = False diff --git a/aiohttp_jinja2/__init__.py b/aiohttp_jinja2/__init__.py index 162a80c5..bfaff055 100644 --- a/aiohttp_jinja2/__init__.py +++ b/aiohttp_jinja2/__init__.py @@ -43,12 +43,14 @@ _SimpleTemplateHandler, _MethodTemplateHandler, _ViewTemplateHandler ] +_ContextProcessor = Callable[[web.Request], Awaitable[Dict[str, Any]]] + def setup( app: web.Application, *args: Any, app_key: str = APP_KEY, - context_processors: Iterable[Callable[[web.Request], Dict[str, Any]]] = (), + context_processors: Iterable[_ContextProcessor] = (), filters: Optional[Filters] = None, default_helpers: bool = True, **kwargs: Any, @@ -109,7 +111,7 @@ def render_string( def render_template( template_name: str, request: web.Request, - context: Mapping[str, Any], + context: Optional[Mapping[str, Any]], *, app_key: str = APP_KEY, encoding: str = "utf-8", diff --git a/aiohttp_jinja2/helpers.py b/aiohttp_jinja2/helpers.py index e9c70262..62f79f26 100644 --- a/aiohttp_jinja2/helpers.py +++ b/aiohttp_jinja2/helpers.py @@ -2,27 +2,32 @@ useful context functions, see http://jinja.pocoo.org/docs/dev/api/#jinja2.contextfunction """ -from typing import Any, Dict, cast +from typing import Any, Dict, TypedDict, Union, cast import jinja2 from aiohttp import web from yarl import URL +class _Context(TypedDict, total=False): + app: web.Application + + @jinja2.contextfunction -def url_for(context: Dict[str, Any], __route_name: str, **parts: Any) -> URL: +def url_for(context: _Context, __route_name: str, **parts: Union[str, int]) -> URL: """Filter for generating urls. Usage: {{ url('the-view-name') }} might become "/path/to/view" or {{ url('item-details', id=123, query={'active': 'true'}) }} might become "/items/1?active=true". """ - app = cast(web.Application, context["app"]) + app = context["app"] query = None if "query_" in parts: - query = parts.pop("query_") + query = str(parts.pop("query_")) + parts_clean: Dict[str, str] = {} for key in parts: val = parts[key] if isinstance(val, str): @@ -37,16 +42,16 @@ def url_for(context: Dict[str, Any], __route_name: str, **parts: Any) -> URL: "argument value should be str or int, " "got {} -> [{}] {!r}".format(key, type(val), val) ) - parts[key] = val + parts_clean[key] = val - url = app.router[__route_name].url_for(**parts) + url = app.router[__route_name].url_for(**parts_clean) if query: url = url.with_query(query) return url @jinja2.contextfunction -def static_url(context: Dict[str, Any], static_file_path: str) -> str: +def static_url(context: _Context, static_file_path: str) -> str: """Filter for generating urls for static files. NOTE: you'll need diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_context_processors.py b/tests/test_context_processors.py index fab36b7d..78dcf0bd 100644 --- a/tests/test_context_processors.py +++ b/tests/test_context_processors.py @@ -1,3 +1,5 @@ +from typing import Dict, Union + import jinja2 from aiohttp import web @@ -17,7 +19,7 @@ async def func(request): ), ) - async def processor(request): + async def processor(request: web.Request) -> Dict[str, Union[str, int]]: return {"foo": 1, "bar": "should be overwriten"} app["aiohttp_jinja2_context_processors"] = ( diff --git a/tests/test_simple_renderer.py b/tests/test_simple_renderer.py index ecb34dc3..62ec28b7 100644 --- a/tests/test_simple_renderer.py +++ b/tests/test_simple_renderer.py @@ -1,3 +1,5 @@ +from typing import Awaitable, Callable, Dict, TypeVar + import jinja2 import pytest from aiohttp import web @@ -5,10 +7,12 @@ import aiohttp_jinja2 +_T = TypeVar("_T") + async def test_func(aiohttp_client): @aiohttp_jinja2.template("tmpl.jinja2") - async def func(request): + async def func(request: web.Request) -> Dict[str, str]: return {"head": "HEAD", "text": "text"} template = "

{{head}}

{{text}}" @@ -28,7 +32,7 @@ async def func(request): async def test_render_class_based_view(aiohttp_client): class MyView(web.View): @aiohttp_jinja2.template("tmpl.jinja2") - async def get(self): + async def get(self) -> Dict[str, str]: return {"head": "HEAD", "text": "text"} template = "

{{head}}

{{text}}" @@ -92,7 +96,7 @@ async def func(request): async def test_render_not_initialized(): - async def func(request): + async def func(request: web.Request) -> web.Response: return aiohttp_jinja2.render_template("template", request, {}) app = web.Application() @@ -178,7 +182,7 @@ async def func(request): async def test_template_not_found(): - async def func(request): + async def func(request: web.Request) -> web.Response: return aiohttp_jinja2.render_template("template", request, {}) app = web.Application() @@ -277,8 +281,8 @@ async def func(request): async def test_render_bare_funcs_deprecated(aiohttp_client): - def wrapper(func): - async def wrapped(request): + def wrapper(func: Callable[[web.Request], Awaitable[_T]]) -> Callable[[web.Request], Awaitable[_T]]: + async def wrapped(request: web.Request) -> _T: with pytest.warns( DeprecationWarning, match="Bare functions are deprecated" ): @@ -288,7 +292,7 @@ async def wrapped(request): @wrapper @aiohttp_jinja2.template("tmpl.jinja2") - def func(request): + def func(request: web.Request) -> Dict[str, str]: return {"text": "OK"} app = web.Application() From b4a83bac30719dbce985dccebfc4f2335ef48787 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Sat, 31 Oct 2020 17:16:03 +0000 Subject: [PATCH 02/25] Line wrapping --- tests/test_simple_renderer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_simple_renderer.py b/tests/test_simple_renderer.py index 62ec28b7..e79b8323 100644 --- a/tests/test_simple_renderer.py +++ b/tests/test_simple_renderer.py @@ -281,7 +281,9 @@ async def func(request): async def test_render_bare_funcs_deprecated(aiohttp_client): - def wrapper(func: Callable[[web.Request], Awaitable[_T]]) -> Callable[[web.Request], Awaitable[_T]]: + def wrapper( + func: Callable[[web.Request], Awaitable[_T]] + ) -> Callable[[web.Request], Awaitable[_T]]: async def wrapped(request: web.Request) -> _T: with pytest.warns( DeprecationWarning, match="Bare functions are deprecated" From bbe473248ca33e9c871fc0d338b6f238ebe097dc Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Sat, 31 Oct 2020 17:28:25 +0000 Subject: [PATCH 03/25] Unused imports --- aiohttp_jinja2/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiohttp_jinja2/helpers.py b/aiohttp_jinja2/helpers.py index 62f79f26..3ada7d86 100644 --- a/aiohttp_jinja2/helpers.py +++ b/aiohttp_jinja2/helpers.py @@ -2,7 +2,7 @@ useful context functions, see http://jinja.pocoo.org/docs/dev/api/#jinja2.contextfunction """ -from typing import Any, Dict, TypedDict, Union, cast +from typing import Dict, TypedDict, Union import jinja2 from aiohttp import web From 790070d369756115504caa9cae79845b71ddfd98 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Sat, 31 Oct 2020 17:39:10 +0000 Subject: [PATCH 04/25] TypedDict only for 3.8+ --- aiohttp_jinja2/helpers.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/aiohttp_jinja2/helpers.py b/aiohttp_jinja2/helpers.py index 3ada7d86..782792be 100644 --- a/aiohttp_jinja2/helpers.py +++ b/aiohttp_jinja2/helpers.py @@ -2,15 +2,20 @@ useful context functions, see http://jinja.pocoo.org/docs/dev/api/#jinja2.contextfunction """ -from typing import Dict, TypedDict, Union +import sys +from typing import Any, Dict, Union import jinja2 from aiohttp import web from yarl import URL -class _Context(TypedDict, total=False): - app: web.Application +if sys.version_info >= (3, 8): + from typing import TypedDict + class _Context(TypedDict, total=False): + app: web.Application +else: + _Context = Dict[str, Any] @jinja2.contextfunction From df416b2b5a6b57a3a941ab0ce643b73c0aaa4286 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Sat, 31 Oct 2020 17:41:21 +0000 Subject: [PATCH 05/25] Spacing --- aiohttp_jinja2/helpers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/aiohttp_jinja2/helpers.py b/aiohttp_jinja2/helpers.py index 782792be..7d2eb9fc 100644 --- a/aiohttp_jinja2/helpers.py +++ b/aiohttp_jinja2/helpers.py @@ -9,11 +9,13 @@ from aiohttp import web from yarl import URL - if sys.version_info >= (3, 8): from typing import TypedDict + class _Context(TypedDict, total=False): app: web.Application + + else: _Context = Dict[str, Any] From 6f431f5cbd649dc9ea8f033588cdacd283cf3fbf Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Sat, 31 Oct 2020 17:43:51 +0000 Subject: [PATCH 06/25] ...Github... --- aiohttp_jinja2/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiohttp_jinja2/helpers.py b/aiohttp_jinja2/helpers.py index 7d2eb9fc..7b7e8d49 100644 --- a/aiohttp_jinja2/helpers.py +++ b/aiohttp_jinja2/helpers.py @@ -11,7 +11,7 @@ if sys.version_info >= (3, 8): from typing import TypedDict - + class _Context(TypedDict, total=False): app: web.Application From fdcfe9e88640694cf5c2296f985be4b007cd0d52 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Tue, 3 Nov 2020 19:51:09 +0000 Subject: [PATCH 07/25] Make query_ a proper keyword argument. --- aiohttp_jinja2/helpers.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/aiohttp_jinja2/helpers.py b/aiohttp_jinja2/helpers.py index 7b7e8d49..a10d300d 100644 --- a/aiohttp_jinja2/helpers.py +++ b/aiohttp_jinja2/helpers.py @@ -3,7 +3,7 @@ http://jinja.pocoo.org/docs/dev/api/#jinja2.contextfunction """ import sys -from typing import Any, Dict, Union +from typing import Any, Dict, Optional, Union import jinja2 from aiohttp import web @@ -21,7 +21,7 @@ class _Context(TypedDict, total=False): @jinja2.contextfunction -def url_for(context: _Context, __route_name: str, **parts: Union[str, int]) -> URL: +def url_for(context: _Context, __route_name: str, query_: Optional[Dict[str, str]] = None, **parts: Union[str, int]) -> URL: """Filter for generating urls. Usage: {{ url('the-view-name') }} might become "/path/to/view" or @@ -30,10 +30,6 @@ def url_for(context: _Context, __route_name: str, **parts: Union[str, int]) -> U """ app = context["app"] - query = None - if "query_" in parts: - query = str(parts.pop("query_")) - parts_clean: Dict[str, str] = {} for key in parts: val = parts[key] @@ -52,8 +48,8 @@ def url_for(context: _Context, __route_name: str, **parts: Union[str, int]) -> U parts_clean[key] = val url = app.router[__route_name].url_for(**parts_clean) - if query: - url = url.with_query(query) + if query_: + url = url.with_query(query_) return url From d8445d657ff72b4d2bfa19f7afd1b68f01525664 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Tue, 3 Nov 2020 19:55:39 +0000 Subject: [PATCH 08/25] Wrapping --- aiohttp_jinja2/helpers.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/aiohttp_jinja2/helpers.py b/aiohttp_jinja2/helpers.py index a10d300d..0fd51870 100644 --- a/aiohttp_jinja2/helpers.py +++ b/aiohttp_jinja2/helpers.py @@ -21,7 +21,12 @@ class _Context(TypedDict, total=False): @jinja2.contextfunction -def url_for(context: _Context, __route_name: str, query_: Optional[Dict[str, str]] = None, **parts: Union[str, int]) -> URL: +def url_for( + context: _Context, + __route_name: str, + query_: Optional[Dict[str, str]] = None, + **parts: Union[str, int] +) -> URL: """Filter for generating urls. Usage: {{ url('the-view-name') }} might become "/path/to/view" or From 10e16f4c72509bd2d2ad7317d398b05121569213 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Tue, 3 Nov 2020 22:02:16 +0000 Subject: [PATCH 09/25] Fix typing of wrapper. --- aiohttp_jinja2/__init__.py | 26 ++++++++++++++++---------- tests/test_simple_renderer.py | 2 +- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/aiohttp_jinja2/__init__.py b/aiohttp_jinja2/__init__.py index bfaff055..ed48fb91 100644 --- a/aiohttp_jinja2/__init__.py +++ b/aiohttp_jinja2/__init__.py @@ -9,6 +9,7 @@ Iterable, Mapping, Optional, + Protocol, Type, Union, cast, @@ -31,10 +32,6 @@ APP_KEY = "aiohttp_jinja2_environment" REQUEST_CONTEXT_KEY = "aiohttp_jinja2_context" -_SimpleHandler = Callable[[web.Request], Awaitable[web.StreamResponse]] -_MethodHandler = Callable[[Any, web.Request], Awaitable[web.StreamResponse]] -_ViewHandler = Callable[[Type[AbstractView]], Awaitable[web.StreamResponse]] -_HandlerType = Union[_SimpleHandler, _MethodHandler, _ViewHandler] _TemplateReturnType = Awaitable[Union[web.StreamResponse, Mapping[str, Any]]] _SimpleTemplateHandler = Callable[[web.Request], _TemplateReturnType] _MethodTemplateHandler = Callable[[Any, web.Request], _TemplateReturnType] @@ -46,6 +43,15 @@ _ContextProcessor = Callable[[web.Request], Awaitable[Dict[str, Any]]] +class _TemplateWrapped(Protocol): + @overload + async def __call__(self, request: web.Request, /) -> web.StreamResponse: ... + @overload + async def __call__(self, view: AbstractView, /) -> web.StreamResponse: ... + @overload + async def __call__(self, _self: Any, request: web.Request, /) -> web.StreamResponse: ... + + def setup( app: web.Application, *args: Any, @@ -133,18 +139,18 @@ def template( app_key: str = APP_KEY, encoding: str = "utf-8", status: int = 200, -) -> Callable[[_TemplateHandler], _HandlerType]: - def wrapper(func: _TemplateHandler) -> _HandlerType: +) -> Callable[[_TemplateHandler], _TemplateWrapped]: + def wrapper(func: _TemplateHandler) -> _TemplateWrapped: @overload - async def wrapped(request: web.Request) -> web.StreamResponse: + async def wrapped(request: web.Request, /) -> web.StreamResponse: ... @overload - async def wrapped(view: AbstractView) -> web.StreamResponse: + async def wrapped(view: AbstractView, /) -> web.StreamResponse: ... @overload - async def wrapped(_self: Any, request: web.Request) -> web.StreamResponse: + async def wrapped(_self: Any, request: web.Request, /) -> web.StreamResponse: ... @functools.wraps(func) @@ -153,7 +159,7 @@ async def wrapped(*args: Any) -> web.StreamResponse: coro = func else: warnings.warn( - "Bare functions are deprecated, " "use async ones", + "Bare functions are deprecated, use async ones", DeprecationWarning, ) coro = asyncio.coroutine(func) diff --git a/tests/test_simple_renderer.py b/tests/test_simple_renderer.py index e79b8323..774ed6e4 100644 --- a/tests/test_simple_renderer.py +++ b/tests/test_simple_renderer.py @@ -292,7 +292,7 @@ async def wrapped(request: web.Request) -> _T: return wrapped - @wrapper + @wrapper # type: ignore[arg-type] # Deprecated functionality @aiohttp_jinja2.template("tmpl.jinja2") def func(request: web.Request) -> Dict[str, str]: return {"text": "OK"} From 2d8feab19cde59cc99a6fd5333b673e74d51a3a0 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Tue, 3 Nov 2020 22:05:30 +0000 Subject: [PATCH 10/25] Wrapping --- aiohttp_jinja2/__init__.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/aiohttp_jinja2/__init__.py b/aiohttp_jinja2/__init__.py index ed48fb91..72021a3c 100644 --- a/aiohttp_jinja2/__init__.py +++ b/aiohttp_jinja2/__init__.py @@ -45,11 +45,16 @@ class _TemplateWrapped(Protocol): @overload - async def __call__(self, request: web.Request, /) -> web.StreamResponse: ... + async def __call__(self, request: web.Request, /) -> web.StreamResponse: + ... + @overload - async def __call__(self, view: AbstractView, /) -> web.StreamResponse: ... + async def __call__(self, view: AbstractView, /) -> web.StreamResponse: + ... + @overload - async def __call__(self, _self: Any, request: web.Request, /) -> web.StreamResponse: ... + async def __call__(self, _self: Any, request: web.Request, /) -> web.StreamResponse: + ... def setup( From 3f439a2ee22b6d86159be1a8178b02116162e576 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Tue, 3 Nov 2020 22:06:38 +0000 Subject: [PATCH 11/25] Unused import. --- aiohttp_jinja2/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/aiohttp_jinja2/__init__.py b/aiohttp_jinja2/__init__.py index 72021a3c..b534b9cc 100644 --- a/aiohttp_jinja2/__init__.py +++ b/aiohttp_jinja2/__init__.py @@ -10,7 +10,6 @@ Mapping, Optional, Protocol, - Type, Union, cast, overload, From 81481c3fc77ac614d5e4a3c91d4c0df41617aa91 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Tue, 3 Nov 2020 22:24:23 +0000 Subject: [PATCH 12/25] Revert positional parameters. --- aiohttp_jinja2/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/aiohttp_jinja2/__init__.py b/aiohttp_jinja2/__init__.py index b534b9cc..903f4f52 100644 --- a/aiohttp_jinja2/__init__.py +++ b/aiohttp_jinja2/__init__.py @@ -44,15 +44,15 @@ class _TemplateWrapped(Protocol): @overload - async def __call__(self, request: web.Request, /) -> web.StreamResponse: + async def __call__(self, request: web.Request) -> web.StreamResponse: ... @overload - async def __call__(self, view: AbstractView, /) -> web.StreamResponse: + async def __call__(self, view: AbstractView) -> web.StreamResponse: ... @overload - async def __call__(self, _self: Any, request: web.Request, /) -> web.StreamResponse: + async def __call__(self, _self: Any, request: web.Request) -> web.StreamResponse: ... @@ -146,15 +146,15 @@ def template( ) -> Callable[[_TemplateHandler], _TemplateWrapped]: def wrapper(func: _TemplateHandler) -> _TemplateWrapped: @overload - async def wrapped(request: web.Request, /) -> web.StreamResponse: + async def wrapped(request: web.Request) -> web.StreamResponse: ... @overload - async def wrapped(view: AbstractView, /) -> web.StreamResponse: + async def wrapped(view: AbstractView) -> web.StreamResponse: ... @overload - async def wrapped(_self: Any, request: web.Request, /) -> web.StreamResponse: + async def wrapped(_self: Any, request: web.Request) -> web.StreamResponse: ... @functools.wraps(func) From 8b639da0b0ee985b594e41743b725dca88280f6d Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Tue, 3 Nov 2020 22:32:07 +0000 Subject: [PATCH 13/25] Protcol only in Python 3.8+ --- aiohttp_jinja2/__init__.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/aiohttp_jinja2/__init__.py b/aiohttp_jinja2/__init__.py index 903f4f52..b86508dc 100644 --- a/aiohttp_jinja2/__init__.py +++ b/aiohttp_jinja2/__init__.py @@ -1,5 +1,6 @@ import asyncio import functools +import sys import warnings from typing import ( Any, @@ -9,7 +10,6 @@ Iterable, Mapping, Optional, - Protocol, Union, cast, overload, @@ -41,19 +41,25 @@ _ContextProcessor = Callable[[web.Request], Awaitable[Dict[str, Any]]] +if sys.version_info >= (3, 8): + from typing import Protocol -class _TemplateWrapped(Protocol): - @overload - async def __call__(self, request: web.Request) -> web.StreamResponse: - ... + class _TemplateWrapped(Protocol): + @overload + async def __call__(self, request: web.Request) -> web.StreamResponse: + ... + + @overload + async def __call__(self, view: AbstractView) -> web.StreamResponse: + ... + + @overload + async def __call__(self, _self: Any, request: web.Request) -> web.StreamResponse: + ... - @overload - async def __call__(self, view: AbstractView) -> web.StreamResponse: - ... - @overload - async def __call__(self, _self: Any, request: web.Request) -> web.StreamResponse: - ... +else: + _TemplateWrapped = Callable[[Any, ...], web.StreamResponse] def setup( From 6f3637a6d05fea75fb8b82bcc14636d5d941cb6b Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Tue, 3 Nov 2020 22:35:01 +0000 Subject: [PATCH 14/25] Wrapping --- aiohttp_jinja2/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/aiohttp_jinja2/__init__.py b/aiohttp_jinja2/__init__.py index b86508dc..e54a6459 100644 --- a/aiohttp_jinja2/__init__.py +++ b/aiohttp_jinja2/__init__.py @@ -54,7 +54,9 @@ async def __call__(self, view: AbstractView) -> web.StreamResponse: ... @overload - async def __call__(self, _self: Any, request: web.Request) -> web.StreamResponse: + async def __call__( + self, _self: Any, request: web.Request + ) -> web.StreamResponse: ... From 3b1178f1dc33335af66df450a34f6bd7ebe15b47 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Tue, 3 Nov 2020 22:43:33 +0000 Subject: [PATCH 15/25] Fix fallback. --- aiohttp_jinja2/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiohttp_jinja2/__init__.py b/aiohttp_jinja2/__init__.py index e54a6459..785bd025 100644 --- a/aiohttp_jinja2/__init__.py +++ b/aiohttp_jinja2/__init__.py @@ -61,7 +61,7 @@ async def __call__( else: - _TemplateWrapped = Callable[[Any, ...], web.StreamResponse] + _TemplateWrapped = Callable[[...], web.StreamResponse] def setup( From 9ab806532411b7791cb322492c681b6f04e18cd6 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Tue, 3 Nov 2020 22:46:16 +0000 Subject: [PATCH 16/25] Fix fallback. --- aiohttp_jinja2/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiohttp_jinja2/__init__.py b/aiohttp_jinja2/__init__.py index 785bd025..5d0117fe 100644 --- a/aiohttp_jinja2/__init__.py +++ b/aiohttp_jinja2/__init__.py @@ -61,7 +61,7 @@ async def __call__( else: - _TemplateWrapped = Callable[[...], web.StreamResponse] + _TemplateWrapped = Callable[..., web.StreamResponse] def setup( From b920fd22f8d523fbf85d98c25171fc0f2678bfee Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Sat, 7 Nov 2020 00:00:32 +0000 Subject: [PATCH 17/25] Fix typing of wrapper. --- aiohttp_jinja2/__init__.py | 40 +++++++++++++++++++---------------- tests/test_simple_renderer.py | 4 ++-- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/aiohttp_jinja2/__init__.py b/aiohttp_jinja2/__init__.py index 5d0117fe..941783ce 100644 --- a/aiohttp_jinja2/__init__.py +++ b/aiohttp_jinja2/__init__.py @@ -10,6 +10,7 @@ Iterable, Mapping, Optional, + TypeVar, Union, cast, overload, @@ -41,27 +42,30 @@ _ContextProcessor = Callable[[web.Request], Awaitable[Dict[str, Any]]] +_T = TypeVar("_T") +_AbstractView = TypeVar("_AbstractView", bound=AbstractView) + if sys.version_info >= (3, 8): from typing import Protocol - class _TemplateWrapped(Protocol): + class _TemplateWrapper(Protocol): @overload - async def __call__(self, request: web.Request) -> web.StreamResponse: + def __call__(self, func: _SimpleTemplateHandler) -> Callable[[web.Request], Awaitable[web.StreamResponse]]: ... @overload - async def __call__(self, view: AbstractView) -> web.StreamResponse: + def __call__(self, func: Callable[[_AbstractView], _TemplateReturnType]) -> Callable[[_AbstractView], Awaitable[web.StreamResponse]]: ... @overload - async def __call__( - self, _self: Any, request: web.Request - ) -> web.StreamResponse: + def __call__(self, func: Callable[[_T, web.Request], _TemplateReturnType]) -> Callable[[_T, web.Request], Awaitable[web.StreamResponse]]: ... else: - _TemplateWrapped = Callable[..., web.StreamResponse] + _TemplateHandler = Callable[..., _TemplateReturnType] + _WebHandler = Callable[..., Awaitable[web.StreamResponse]] + _TemplateWrapper = Callable[[_TemplateHandler], _WebHandler] def setup( @@ -151,20 +155,20 @@ def template( app_key: str = APP_KEY, encoding: str = "utf-8", status: int = 200, -) -> Callable[[_TemplateHandler], _TemplateWrapped]: - def wrapper(func: _TemplateHandler) -> _TemplateWrapped: - @overload - async def wrapped(request: web.Request) -> web.StreamResponse: - ... +) -> _TemplateWrapper: + @overload + def wrapper(func: _SimpleTemplateHandler) -> Callable[[web.Request], Awaitable[web.StreamResponse]]: + ... - @overload - async def wrapped(view: AbstractView) -> web.StreamResponse: - ... + @overload + def wrapper(func: Callable[[_AbstractView], _TemplateReturnType]) -> Callable[[_AbstractView], Awaitable[web.StreamResponse]]: + ... - @overload - async def wrapped(_self: Any, request: web.Request) -> web.StreamResponse: - ... + @overload + def wrapper(func: Callable[[_T, web.Request], _TemplateReturnType]) -> Callable[[_T, web.Request], Awaitable[web.StreamResponse]]: + ... + def wrapper(func: Callable[..., _TemplateReturnType]) -> Callable[..., Awaitable[web.StreamResponse]]: @functools.wraps(func) async def wrapped(*args: Any) -> web.StreamResponse: if asyncio.iscoroutinefunction(func): diff --git a/tests/test_simple_renderer.py b/tests/test_simple_renderer.py index 774ed6e4..ee7bf996 100644 --- a/tests/test_simple_renderer.py +++ b/tests/test_simple_renderer.py @@ -201,8 +201,8 @@ async def func(request: web.Request) -> web.Response: async def test_render_not_mapping(): - @aiohttp_jinja2.template("tmpl.jinja2") - async def func(request): + @aiohttp_jinja2.template("tmpl.jinja2") # type: ignore[arg-type] + async def func(request: web.Request) -> int: return 123 app = web.Application() From 1728574030138a9616488a658e5e7c82e9274e42 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Sat, 7 Nov 2020 00:07:01 +0000 Subject: [PATCH 18/25] Fix typing of wrapper. --- aiohttp_jinja2/__init__.py | 40 +++++++++++++++++++---------------- tests/test_simple_renderer.py | 4 ++-- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/aiohttp_jinja2/__init__.py b/aiohttp_jinja2/__init__.py index 5d0117fe..941783ce 100644 --- a/aiohttp_jinja2/__init__.py +++ b/aiohttp_jinja2/__init__.py @@ -10,6 +10,7 @@ Iterable, Mapping, Optional, + TypeVar, Union, cast, overload, @@ -41,27 +42,30 @@ _ContextProcessor = Callable[[web.Request], Awaitable[Dict[str, Any]]] +_T = TypeVar("_T") +_AbstractView = TypeVar("_AbstractView", bound=AbstractView) + if sys.version_info >= (3, 8): from typing import Protocol - class _TemplateWrapped(Protocol): + class _TemplateWrapper(Protocol): @overload - async def __call__(self, request: web.Request) -> web.StreamResponse: + def __call__(self, func: _SimpleTemplateHandler) -> Callable[[web.Request], Awaitable[web.StreamResponse]]: ... @overload - async def __call__(self, view: AbstractView) -> web.StreamResponse: + def __call__(self, func: Callable[[_AbstractView], _TemplateReturnType]) -> Callable[[_AbstractView], Awaitable[web.StreamResponse]]: ... @overload - async def __call__( - self, _self: Any, request: web.Request - ) -> web.StreamResponse: + def __call__(self, func: Callable[[_T, web.Request], _TemplateReturnType]) -> Callable[[_T, web.Request], Awaitable[web.StreamResponse]]: ... else: - _TemplateWrapped = Callable[..., web.StreamResponse] + _TemplateHandler = Callable[..., _TemplateReturnType] + _WebHandler = Callable[..., Awaitable[web.StreamResponse]] + _TemplateWrapper = Callable[[_TemplateHandler], _WebHandler] def setup( @@ -151,20 +155,20 @@ def template( app_key: str = APP_KEY, encoding: str = "utf-8", status: int = 200, -) -> Callable[[_TemplateHandler], _TemplateWrapped]: - def wrapper(func: _TemplateHandler) -> _TemplateWrapped: - @overload - async def wrapped(request: web.Request) -> web.StreamResponse: - ... +) -> _TemplateWrapper: + @overload + def wrapper(func: _SimpleTemplateHandler) -> Callable[[web.Request], Awaitable[web.StreamResponse]]: + ... - @overload - async def wrapped(view: AbstractView) -> web.StreamResponse: - ... + @overload + def wrapper(func: Callable[[_AbstractView], _TemplateReturnType]) -> Callable[[_AbstractView], Awaitable[web.StreamResponse]]: + ... - @overload - async def wrapped(_self: Any, request: web.Request) -> web.StreamResponse: - ... + @overload + def wrapper(func: Callable[[_T, web.Request], _TemplateReturnType]) -> Callable[[_T, web.Request], Awaitable[web.StreamResponse]]: + ... + def wrapper(func: Callable[..., _TemplateReturnType]) -> Callable[..., Awaitable[web.StreamResponse]]: @functools.wraps(func) async def wrapped(*args: Any) -> web.StreamResponse: if asyncio.iscoroutinefunction(func): diff --git a/tests/test_simple_renderer.py b/tests/test_simple_renderer.py index 774ed6e4..ee7bf996 100644 --- a/tests/test_simple_renderer.py +++ b/tests/test_simple_renderer.py @@ -201,8 +201,8 @@ async def func(request: web.Request) -> web.Response: async def test_render_not_mapping(): - @aiohttp_jinja2.template("tmpl.jinja2") - async def func(request): + @aiohttp_jinja2.template("tmpl.jinja2") # type: ignore[arg-type] + async def func(request: web.Request) -> int: return 123 app = web.Application() From 13aea24b8d7fda9d5e4ce2c64683e1dce1515d24 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Sat, 7 Nov 2020 00:32:20 +0000 Subject: [PATCH 19/25] Line wrapping. --- aiohttp_jinja2/__init__.py | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/aiohttp_jinja2/__init__.py b/aiohttp_jinja2/__init__.py index 941783ce..bfab5868 100644 --- a/aiohttp_jinja2/__init__.py +++ b/aiohttp_jinja2/__init__.py @@ -34,12 +34,6 @@ _TemplateReturnType = Awaitable[Union[web.StreamResponse, Mapping[str, Any]]] _SimpleTemplateHandler = Callable[[web.Request], _TemplateReturnType] -_MethodTemplateHandler = Callable[[Any, web.Request], _TemplateReturnType] -_ViewTemplateHandler = Callable[[AbstractView], _TemplateReturnType] -_TemplateHandler = Union[ - _SimpleTemplateHandler, _MethodTemplateHandler, _ViewTemplateHandler -] - _ContextProcessor = Callable[[web.Request], Awaitable[Dict[str, Any]]] _T = TypeVar("_T") @@ -50,15 +44,21 @@ class _TemplateWrapper(Protocol): @overload - def __call__(self, func: _SimpleTemplateHandler) -> Callable[[web.Request], Awaitable[web.StreamResponse]]: + def __call__( + self, func: _SimpleTemplateHandler + ) -> Callable[[web.Request], Awaitable[web.StreamResponse]]: ... @overload - def __call__(self, func: Callable[[_AbstractView], _TemplateReturnType]) -> Callable[[_AbstractView], Awaitable[web.StreamResponse]]: + def __call__( + self, func: Callable[[_AbstractView], _TemplateReturnType] + ) -> Callable[[_AbstractView], Awaitable[web.StreamResponse]]: ... @overload - def __call__(self, func: Callable[[_T, web.Request], _TemplateReturnType]) -> Callable[[_T, web.Request], Awaitable[web.StreamResponse]]: + def __call__( + self, func: Callable[[_T, web.Request], _TemplateReturnType] + ) -> Callable[[_T, web.Request], Awaitable[web.StreamResponse]]: ... @@ -157,18 +157,26 @@ def template( status: int = 200, ) -> _TemplateWrapper: @overload - def wrapper(func: _SimpleTemplateHandler) -> Callable[[web.Request], Awaitable[web.StreamResponse]]: + def wrapper( + func: _SimpleTemplateHandler + ) -> Callable[[web.Request], Awaitable[web.StreamResponse]]: ... @overload - def wrapper(func: Callable[[_AbstractView], _TemplateReturnType]) -> Callable[[_AbstractView], Awaitable[web.StreamResponse]]: + def wrapper( + func: Callable[[_AbstractView], _TemplateReturnType] + ) -> Callable[[_AbstractView], Awaitable[web.StreamResponse]]: ... @overload - def wrapper(func: Callable[[_T, web.Request], _TemplateReturnType]) -> Callable[[_T, web.Request], Awaitable[web.StreamResponse]]: + def wrapper( + func: Callable[[_T, web.Request], _TemplateReturnType] + ) -> Callable[[_T, web.Request], Awaitable[web.StreamResponse]]: ... - def wrapper(func: Callable[..., _TemplateReturnType]) -> Callable[..., Awaitable[web.StreamResponse]]: + def wrapper( + func: Callable[..., _TemplateReturnType] + ) -> Callable[..., Awaitable[web.StreamResponse]]: @functools.wraps(func) async def wrapped(*args: Any) -> web.StreamResponse: if asyncio.iscoroutinefunction(func): From 1e8206db0cb16ffe36e1b1f91aabfcbcb5cb536b Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Sat, 7 Nov 2020 00:36:28 +0000 Subject: [PATCH 20/25] Comma. --- aiohttp_jinja2/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiohttp_jinja2/__init__.py b/aiohttp_jinja2/__init__.py index bfab5868..b8a1db7f 100644 --- a/aiohttp_jinja2/__init__.py +++ b/aiohttp_jinja2/__init__.py @@ -158,7 +158,7 @@ def template( ) -> _TemplateWrapper: @overload def wrapper( - func: _SimpleTemplateHandler + func: _SimpleTemplateHandler, ) -> Callable[[web.Request], Awaitable[web.StreamResponse]]: ... From f4f7c2a6a388e7be1c832b02130f760d09571306 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Sat, 7 Nov 2020 14:04:06 +0000 Subject: [PATCH 21/25] Add async rendering. --- aiohttp_jinja2/__init__.py | 81 ++++++++++++++++++++++++++++++-------- 1 file changed, 65 insertions(+), 16 deletions(-) diff --git a/aiohttp_jinja2/__init__.py b/aiohttp_jinja2/__init__.py index b8a1db7f..4ac52a8b 100644 --- a/aiohttp_jinja2/__init__.py +++ b/aiohttp_jinja2/__init__.py @@ -10,6 +10,7 @@ Iterable, Mapping, Optional, + Tuple, TypeVar, Union, cast, @@ -97,13 +98,12 @@ def get_env(app: web.Application, *, app_key: str = APP_KEY) -> jinja2.Environme return cast(jinja2.Environment, app.get(app_key)) -def render_string( +def _render_string( template_name: str, request: web.Request, context: Mapping[str, Any], - *, - app_key: str = APP_KEY, -) -> str: + app_key: str, +) -> Tuple[jinja2.Template, Mapping[str, Any]]: env = request.config_dict.get(app_key) if env is None: text = ( @@ -126,26 +126,69 @@ def render_string( raise web.HTTPInternalServerError(reason=text, text=text) if request.get(REQUEST_CONTEXT_KEY): context = dict(request[REQUEST_CONTEXT_KEY], **context) - text = template.render(context) - return text + return template, context -def render_template( +def render_string( template_name: str, request: web.Request, - context: Optional[Mapping[str, Any]], + context: Mapping[str, Any], *, app_key: str = APP_KEY, - encoding: str = "utf-8", - status: int = 200, -) -> web.Response: +) -> str: + template, context = _render_string(template_name, request, context, app_key) + return template.render(context) + + +async def render_string_async( + template_name: str, + request: web.Request, + context: Mapping[str, Any], + *, + app_key: str = APP_KEY, +) -> str: + template, context = _render_string(template_name, request, context, app_key) + return await template.render_async(context) + + +def _render_template( + context: Optional[Mapping[str, Any]], + encoding: str, + status: int, +) -> Tuple[web.Response, Mapping[str, Any]]: response = web.Response(status=status) if context is None: context = {} - text = render_string(template_name, request, context, app_key=app_key) response.content_type = "text/html" response.charset = encoding - response.text = text + return response, context + + +def render_template( + template_name: str, + request: web.Request, + context: Optional[Mapping[str, Any]], + *, + app_key: str = APP_KEY, + encoding: str = "utf-8", + status: int = 200, +) -> web.Response: + response, context = _render_template(context, encoding, status) + response.text = render_string(template_name, request, context, app_key=app_key) + return response + + +async def render_template_async( + template_name: str, + request: web.Request, + context: Optional[Mapping[str, Any]], + *, + app_key: str = APP_KEY, + encoding: str = "utf-8", + status: int = 200, +) -> web.Response: + response, context = _render_template(context, encoding, status) + response.text = await render_string_async(template_name, request, context, app_key=app_key) return response @@ -197,9 +240,15 @@ async def wrapped(*args: Any) -> web.StreamResponse: else: request = args[-1] - response = render_template( - template_name, request, context, app_key=app_key, encoding=encoding - ) + env = request.config_dict.get(app_key) + if env and env.is_async: + response = await render_template_async( + template_name, request, context, app_key=app_key, encoding=encoding + ) + else: + response = render_template( + template_name, request, context, app_key=app_key, encoding=encoding + ) response.set_status(status) return response From 9636c4ab55ed231df0142011032df82d472247d3 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Sat, 7 Nov 2020 14:23:51 +0000 Subject: [PATCH 22/25] Add tests for async. --- tests/test_simple_renderer.py | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/tests/test_simple_renderer.py b/tests/test_simple_renderer.py index ee7bf996..9ac6f653 100644 --- a/tests/test_simple_renderer.py +++ b/tests/test_simple_renderer.py @@ -10,14 +10,15 @@ _T = TypeVar("_T") -async def test_func(aiohttp_client): +@pytest.mark.parametrize("enable_async", (False, True)) +async def test_func(aiohttp_client, enable_async): @aiohttp_jinja2.template("tmpl.jinja2") async def func(request: web.Request) -> Dict[str, str]: return {"head": "HEAD", "text": "text"} template = "

{{head}}

{{text}}" app = web.Application() - aiohttp_jinja2.setup(app, loader=jinja2.DictLoader({"tmpl.jinja2": template})) + aiohttp_jinja2.setup(app, enable_async=enable_async, loader=jinja2.DictLoader({"tmpl.jinja2": template})) app.router.add_route("*", "/", func) @@ -137,16 +138,11 @@ async def func(request): assert "

HEAD

text" == txt -async def test_render_template(aiohttp_client): - async def func(request): - return aiohttp_jinja2.render_template( - "tmpl.jinja2", request, {"head": "HEAD", "text": "text"} - ) - +async def _test_render_template(func, aiohttp_client, enable_async): template = "

{{head}}

{{text}}" app = web.Application() - aiohttp_jinja2.setup(app, loader=jinja2.DictLoader({"tmpl.jinja2": template})) + aiohttp_jinja2.setup(app, enable_async=enable_async, loader=jinja2.DictLoader({"tmpl.jinja2": template})) app.router.add_route("*", "/", func) @@ -159,6 +155,24 @@ async def func(request): assert "

HEAD

text" == txt +async def test_render_template(aiohttp_client): + async def func(request): + return aiohttp_jinja2.render_template( + "tmpl.jinja2", request, {"head": "HEAD", "text": "text"} + ) + + await _test_render_template(func, aiohttp_client, enable_async=False) + + +async def test_render_template_async(aiohttp_client): + async def func(request): + return await aiohttp_jinja2.render_template_async( + "tmpl.jinja2", request, {"head": "HEAD", "text": "text"} + ) + + await _test_render_template(func, aiohttp_client, enable_async=True) + + async def test_render_template_custom_status(aiohttp_client): async def func(request): return aiohttp_jinja2.render_template( From f3b112a30f788c88c928018b4f95aa4a3131f029 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Sat, 7 Nov 2020 14:34:16 +0000 Subject: [PATCH 23/25] Line wrapping. --- aiohttp_jinja2/__init__.py | 4 +++- tests/test_simple_renderer.py | 12 ++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/aiohttp_jinja2/__init__.py b/aiohttp_jinja2/__init__.py index 4ac52a8b..c70c528c 100644 --- a/aiohttp_jinja2/__init__.py +++ b/aiohttp_jinja2/__init__.py @@ -188,7 +188,9 @@ async def render_template_async( status: int = 200, ) -> web.Response: response, context = _render_template(context, encoding, status) - response.text = await render_string_async(template_name, request, context, app_key=app_key) + response.text = await render_string_async( + template_name, request, context, app_key=app_key + ) return response diff --git a/tests/test_simple_renderer.py b/tests/test_simple_renderer.py index 9ac6f653..548cdb10 100644 --- a/tests/test_simple_renderer.py +++ b/tests/test_simple_renderer.py @@ -18,7 +18,11 @@ async def func(request: web.Request) -> Dict[str, str]: template = "

{{head}}

{{text}}" app = web.Application() - aiohttp_jinja2.setup(app, enable_async=enable_async, loader=jinja2.DictLoader({"tmpl.jinja2": template})) + aiohttp_jinja2.setup( + app, + enable_async=enable_async, + loader=jinja2.DictLoader({"tmpl.jinja2": template}) + ) app.router.add_route("*", "/", func) @@ -142,7 +146,11 @@ async def _test_render_template(func, aiohttp_client, enable_async): template = "

{{head}}

{{text}}" app = web.Application() - aiohttp_jinja2.setup(app, enable_async=enable_async, loader=jinja2.DictLoader({"tmpl.jinja2": template})) + aiohttp_jinja2.setup( + app, + enable_async=enable_async, + loader=jinja2.DictLoader({"tmpl.jinja2": template}) + ) app.router.add_route("*", "/", func) From 0756023a8cf185f465a388d964a82a228692a85e Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Sat, 7 Nov 2020 14:36:16 +0000 Subject: [PATCH 24/25] Comma. --- tests/test_simple_renderer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_simple_renderer.py b/tests/test_simple_renderer.py index 548cdb10..b3706dcb 100644 --- a/tests/test_simple_renderer.py +++ b/tests/test_simple_renderer.py @@ -21,7 +21,7 @@ async def func(request: web.Request) -> Dict[str, str]: aiohttp_jinja2.setup( app, enable_async=enable_async, - loader=jinja2.DictLoader({"tmpl.jinja2": template}) + loader=jinja2.DictLoader({"tmpl.jinja2": template}), ) app.router.add_route("*", "/", func) @@ -149,7 +149,7 @@ async def _test_render_template(func, aiohttp_client, enable_async): aiohttp_jinja2.setup( app, enable_async=enable_async, - loader=jinja2.DictLoader({"tmpl.jinja2": template}) + loader=jinja2.DictLoader({"tmpl.jinja2": template}), ) app.router.add_route("*", "/", func) From 61295a2c951a0bfca482aab3fe67b126f9ed2997 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Sun, 8 Nov 2020 12:28:23 +0000 Subject: [PATCH 25/25] Fix mypy. --- .mypy.ini | 2 ++ aiohttp_jinja2/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.mypy.ini b/.mypy.ini index 02ae0955..65dc26f3 100644 --- a/.mypy.ini +++ b/.mypy.ini @@ -21,4 +21,6 @@ disallow_any_unimported = True warn_return_any = True [mypy-tests.*] +disallow_any_decorated = False +disallow_untyped_calls = False disallow_untyped_defs = False diff --git a/aiohttp_jinja2/__init__.py b/aiohttp_jinja2/__init__.py index 6c791eb6..c70c528c 100644 --- a/aiohttp_jinja2/__init__.py +++ b/aiohttp_jinja2/__init__.py @@ -132,7 +132,7 @@ def _render_string( def render_string( template_name: str, request: web.Request, - context: Optional[Mapping[str, Any]], + context: Mapping[str, Any], *, app_key: str = APP_KEY, ) -> str: