diff --git a/aiohttp_jinja2/__init__.py b/aiohttp_jinja2/__init__.py index bca973be..2961fd24 100644 --- a/aiohttp_jinja2/__init__.py +++ b/aiohttp_jinja2/__init__.py @@ -7,6 +7,7 @@ Awaitable, Callable, Dict, + Final, Iterable, Mapping, Optional, @@ -22,35 +23,35 @@ from aiohttp.abc import AbstractView if sys.version_info >= (3, 8): - from typing import Protocol + from typing import Protocol, TypedDict else: - from typing_extensions import Protocol + from typing_extensions import Protocol, TypedDict from .helpers import GLOBAL_HELPERS -from .typedefs import Filters +from .typedefs import AppState as AppState, ContextProcessor, Filters __version__ = "1.5" __all__ = ("setup", "get_env", "render_template", "render_string", "template") -APP_CONTEXT_PROCESSORS_KEY = "aiohttp_jinja2_context_processors" -APP_KEY = "aiohttp_jinja2_environment" -REQUEST_CONTEXT_KEY = "aiohttp_jinja2_context" +APP_KEY: Final = "_aiohttp_jinja2_environment" +APP_CONTEXT_PROCESSORS_KEY: Final = "_aiohttp_jinja2_context_processors" +REQUEST_CONTEXT_KEY: Final = "_aiohttp_jinja2_context" _TemplateReturnType = Awaitable[Union[web.StreamResponse, Mapping[str, Any]]] -_SimpleTemplateHandler = Callable[[web.Request], _TemplateReturnType] -_ContextProcessor = Callable[[web.Request], Awaitable[Dict[str, Any]]] +_SimpleTemplateHandler = Callable[[web.Request[_T]], _TemplateReturnType] _T = TypeVar("_T") -_AbstractView = TypeVar("_AbstractView", bound=AbstractView) +_U = TypeVar("_U") +_AbstractView = TypeVar("_AbstractView", bound=AbstractView[Any]) class _TemplateWrapper(Protocol): @overload def __call__( - self, func: _SimpleTemplateHandler - ) -> Callable[[web.Request], Awaitable[web.StreamResponse]]: + self, func: _SimpleTemplateHandler[_T] + ) -> Callable[[web.Request[_T]], Awaitable[web.StreamResponse]]: ... @overload @@ -61,16 +62,16 @@ def __call__( @overload def __call__( - self, func: Callable[[_T, web.Request], _TemplateReturnType] - ) -> Callable[[_T, web.Request], Awaitable[web.StreamResponse]]: + self, func: Callable[[_T, web.Request[_U]], _TemplateReturnType] + ) -> Callable[[_T, web.Request[_U]], Awaitable[web.StreamResponse]]: ... def setup( - app: web.Application, + app: web.Application[AppState], *args: Any, app_key: str = APP_KEY, - context_processors: Iterable[_ContextProcessor] = (), + context_processors: Iterable[ContextProcessor] = (), filters: Optional[Filters] = None, default_helpers: bool = True, **kwargs: Any, @@ -81,9 +82,9 @@ def setup( env.globals.update(GLOBAL_HELPERS) if filters is not None: env.filters.update(filters) - app[app_key] = env + app.state["_aiohttp_jinja2_environment"] = env if context_processors: - app[APP_CONTEXT_PROCESSORS_KEY] = context_processors + app.state["_aiohttp_jinja2_context_processors"] = context_processors app.middlewares.append(context_processors_middleware) env.globals["app"] = app @@ -91,13 +92,13 @@ def setup( return env -def get_env(app: web.Application, *, app_key: str = APP_KEY) -> jinja2.Environment: - return cast(jinja2.Environment, app.get(app_key)) +def get_env(app: web.Application[AppState]) -> jinja2.Environment: + return app.state["_aiohttp_jinja2_environment"] def _render_string( template_name: str, - request: web.Request, + request: web.Request[AppState], context: Mapping[str, Any], app_key: str, ) -> Tuple[jinja2.Template, Mapping[str, Any]]: @@ -128,7 +129,7 @@ def _render_string( def render_string( template_name: str, - request: web.Request, + request: web.Request[AppState], context: Mapping[str, Any], *, app_key: str = APP_KEY, @@ -139,7 +140,7 @@ def render_string( async def render_string_async( template_name: str, - request: web.Request, + request: web.Request[AppState], context: Mapping[str, Any], *, app_key: str = APP_KEY, @@ -163,7 +164,7 @@ def _render_template( def render_template( template_name: str, - request: web.Request, + request: web.Request[AppState], context: Optional[Mapping[str, Any]], *, app_key: str = APP_KEY, @@ -177,7 +178,7 @@ def render_template( async def render_template_async( template_name: str, - request: web.Request, + request: web.Request[AppState], context: Optional[Mapping[str, Any]], *, app_key: str = APP_KEY, @@ -200,8 +201,8 @@ def template( ) -> _TemplateWrapper: @overload def wrapper( - func: _SimpleTemplateHandler, - ) -> Callable[[web.Request], Awaitable[web.StreamResponse]]: + func: _SimpleTemplateHandler[_T], + ) -> Callable[[web.Request[_T]], Awaitable[web.StreamResponse]]: ... @overload @@ -212,8 +213,8 @@ def wrapper( @overload def wrapper( - func: Callable[[_T, web.Request], _TemplateReturnType] - ) -> Callable[[_T, web.Request], Awaitable[web.StreamResponse]]: + func: Callable[[_T, web.Request[_U]], _TemplateReturnType] + ) -> Callable[[_T, web.Request[_U]], Awaitable[web.StreamResponse]]: ... def wrapper( @@ -257,11 +258,10 @@ async def wrapped(*args: Any) -> web.StreamResponse: # type: ignore[misc] @web.middleware -async def context_processors_middleware( - request: web.Request, - handler: Callable[[web.Request], Awaitable[web.StreamResponse]], +async def context_processors_middleware( # type: ignore[misc] + request: web.Request[AppState], + handler: Callable[[web.Request[Any]], Awaitable[web.StreamResponse]], ) -> web.StreamResponse: - if REQUEST_CONTEXT_KEY not in request: request[REQUEST_CONTEXT_KEY] = {} for processor in request.config_dict[APP_CONTEXT_PROCESSORS_KEY]: @@ -269,5 +269,5 @@ async def context_processors_middleware( return await handler(request) -async def request_processor(request: web.Request) -> Dict[str, web.Request]: +async def request_processor(request: web.Request[_T]) -> Dict[str, web.Request[_T]]: return {"request": request} diff --git a/aiohttp_jinja2/helpers.py b/aiohttp_jinja2/helpers.py index ddc99f3d..ab662de0 100644 --- a/aiohttp_jinja2/helpers.py +++ b/aiohttp_jinja2/helpers.py @@ -9,11 +9,13 @@ from aiohttp import web from yarl import URL +from .typedefs import AppState + if sys.version_info >= (3, 8): from typing import TypedDict class _Context(TypedDict, total=False): - app: web.Application + app: web.Application[AppState] else: @@ -70,7 +72,7 @@ def static_url(context: _Context, static_file_path: str) -> str: """ app = context["app"] try: - static_url = app["static_root_url"] + static_url = app.state["static_root_url"] except KeyError: raise RuntimeError( "app does not define a static root url " diff --git a/aiohttp_jinja2/typedefs.py b/aiohttp_jinja2/typedefs.py index 7c78a4d3..79ef580c 100644 --- a/aiohttp_jinja2/typedefs.py +++ b/aiohttp_jinja2/typedefs.py @@ -1,4 +1,26 @@ -from typing import Callable, Iterable, Mapping, Tuple, Union +from typing import ( + Any, + Awaitable, + Callable, + Dict, + Iterable, + Mapping, + Tuple, + TypedDict, + Union, +) +import jinja2 +from aiohttp import web + +ContextProcessor = Callable[[web.Request[Any]], Awaitable[Dict[str, Any]]] Filter = Callable[..., str] Filters = Union[Iterable[Tuple[str, Filter]], Mapping[str, Filter]] + + +class AppState(TypedDict, total=False): + """App config used by aiohttp-jinja2.""" + + _aiohttp_jinja2_context_processors: Iterable[ContextProcessor] + _aiohttp_jinja2_environment: jinja2.Environment + static_root_url: str diff --git a/tests/conftest.py b/tests/conftest.py index e69de29b..9d13c145 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -0,0 +1,6 @@ +from aiohttp.web import Application, Request + +from aiohttp_jinja2 import AppState + +_App = Application[AppState] +_Request = Request[AppState] diff --git a/tests/test_context_processors.py b/tests/test_context_processors.py index 78dcf0bd..4ef2921c 100644 --- a/tests/test_context_processors.py +++ b/tests/test_context_processors.py @@ -5,13 +5,17 @@ import aiohttp_jinja2 +from .conftest import _App, _Request + async def test_context_processors(aiohttp_client): @aiohttp_jinja2.template("tmpl.jinja2") async def func(request): return {"bar": 2} - app = web.Application(middlewares=[aiohttp_jinja2.context_processors_middleware]) + app: _App = web.Application( + middlewares=[aiohttp_jinja2.context_processors_middleware] + ) aiohttp_jinja2.setup( app, loader=jinja2.DictLoader( @@ -19,10 +23,10 @@ async def func(request): ), ) - async def processor(request: web.Request) -> Dict[str, Union[str, int]]: + async def processor(request: _Request) -> Dict[str, Union[str, int]]: return {"foo": 1, "bar": "should be overwriten"} - app["aiohttp_jinja2_context_processors"] = ( + app.state[aiohttp_jinja2.APP_CONTEXT_PROCESSORS_KEY] = ( aiohttp_jinja2.request_processor, processor, ) @@ -42,7 +46,9 @@ async def test_nested_context_processors(aiohttp_client): async def func(request): return {"bar": 2} - subapp = web.Application(middlewares=[aiohttp_jinja2.context_processors_middleware]) + subapp: _App = web.Application( + middlewares=[aiohttp_jinja2.context_processors_middleware] + ) aiohttp_jinja2.setup( subapp, loader=jinja2.DictLoader( @@ -56,20 +62,22 @@ async def func(request): async def subprocessor(request): return {"foo": 1, "bar": "should be overwriten"} - subapp["aiohttp_jinja2_context_processors"] = ( + subapp.state[aiohttp_jinja2.APP_CONTEXT_PROCESSORS_KEY] = ( aiohttp_jinja2.request_processor, subprocessor, ) subapp.router.add_get("/", func) - app = web.Application(middlewares=[aiohttp_jinja2.context_processors_middleware]) + app: _App = web.Application( + middlewares=[aiohttp_jinja2.context_processors_middleware] + ) aiohttp_jinja2.setup(app, loader=jinja2.DictLoader({})) async def processor(request): return {"baz": 5} - app["aiohttp_jinja2_context_processors"] = ( + app.state[aiohttp_jinja2.APP_CONTEXT_PROCESSORS_KEY] = ( aiohttp_jinja2.request_processor, processor, ) @@ -89,7 +97,7 @@ async def test_context_is_response(aiohttp_client): async def func(request): raise web.HTTPForbidden() - app = web.Application() + app: _App = web.Application() aiohttp_jinja2.setup(app, loader=jinja2.DictLoader({"tmpl.jinja2": "template"})) app.router.add_route("GET", "/", func) @@ -107,7 +115,7 @@ async def func(request): async def processor(request): return {"foo": 1, "bar": "should be overwriten"} - app = web.Application() + app: _App = web.Application() aiohttp_jinja2.setup( app, loader=jinja2.DictLoader( @@ -139,7 +147,7 @@ async def func(request): async def processor(request): return {"foo": 1} - app = web.Application() + app: _App = web.Application() aiohttp_jinja2.setup( app, loader=jinja2.DictLoader({"tmpl.jinja2": "foo: {{ foo }}"}), diff --git a/tests/test_jinja_filters.py b/tests/test_jinja_filters.py index 9b716d01..c75f7d86 100644 --- a/tests/test_jinja_filters.py +++ b/tests/test_jinja_filters.py @@ -3,6 +3,8 @@ import aiohttp_jinja2 +from .conftest import _App + async def test_jinja_filters(aiohttp_client): @aiohttp_jinja2.template("tmpl.jinja2") @@ -12,7 +14,7 @@ async def index(request): def add_2(value): return value + 2 - app = web.Application() + app: _App = web.Application() aiohttp_jinja2.setup( app, loader=jinja2.DictLoader({"tmpl.jinja2": "{{ 5|add_2 }}"}), diff --git a/tests/test_jinja_globals.py b/tests/test_jinja_globals.py index 55291825..c009ee86 100644 --- a/tests/test_jinja_globals.py +++ b/tests/test_jinja_globals.py @@ -4,9 +4,11 @@ import aiohttp_jinja2 +from .conftest import _App, _Request + def test_get_env(): - app = web.Application() + app: _App = web.Application() aiohttp_jinja2.setup(app, loader=jinja2.DictLoader({"tmpl.jinja2": "tmpl"})) env = aiohttp_jinja2.get_env(app) @@ -22,7 +24,7 @@ async def index(request): async def other(request): return - app = web.Application() + app: _App = web.Application() aiohttp_jinja2.setup( app, loader=jinja2.DictLoader({"tmpl.jinja2": "{{ url('other', name='John_Doe')}}"}), @@ -43,7 +45,7 @@ async def test_url_with_query(aiohttp_client): async def index(request): return {} - app = web.Application() + app: _App = web.Application() aiohttp_jinja2.setup( app, loader=jinja2.DictLoader( @@ -68,7 +70,7 @@ async def index(request): async def other(request): return - app = web.Application() + app: _App = web.Application() aiohttp_jinja2.setup( app, loader=jinja2.DictLoader({"tmpl.jinja2": "{{ url('other', arg=1)}}"}) ) @@ -98,7 +100,7 @@ async def index(request): async def other(request): return - app = web.Application() + app: _App = web.Application() aiohttp_jinja2.setup( app, loader=jinja2.DictLoader({"tmpl.jinja2": "{{ url('other', arg=True)}}"}) ) @@ -117,7 +119,7 @@ async def index(request): aiohttp_jinja2.render_template("tmpl.jinja2", request, {}) return web.Response() - app = web.Application() + app: _App = web.Application() aiohttp_jinja2.setup( app, default_helpers=False, @@ -136,12 +138,12 @@ async def test_static(aiohttp_client): async def index(request): return {} - app = web.Application() + app: _App = web.Application() aiohttp_jinja2.setup( app, loader=jinja2.DictLoader({"tmpl.jinja2": "{{ static('whatever.js') }}"}) ) - app["static_root_url"] = "/static" + app.state["static_root_url"] = "/static" app.router.add_route("GET", "/", index) client = await aiohttp_client(app) @@ -157,7 +159,7 @@ async def index(request): aiohttp_jinja2.render_template("tmpl.jinja2", request, {}) return web.Response() - app = web.Application() + app: _App = web.Application() aiohttp_jinja2.setup( app, loader=jinja2.DictLoader({"tmpl.jinja2": "{{ static('whatever.js') }}"}) ) diff --git a/tests/test_simple_renderer.py b/tests/test_simple_renderer.py index b3706dcb..c35670a5 100644 --- a/tests/test_simple_renderer.py +++ b/tests/test_simple_renderer.py @@ -7,17 +7,19 @@ import aiohttp_jinja2 +from .conftest import _App, _Request + _T = TypeVar("_T") @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]: + async def func(request: _Request) -> Dict[str, str]: return {"head": "HEAD", "text": "text"} template = "

{{head}}

{{text}}" - app = web.Application() + app: _App = web.Application() aiohttp_jinja2.setup( app, enable_async=enable_async, @@ -42,7 +44,7 @@ async def get(self) -> Dict[str, str]: template = "

{{head}}

{{text}}" - app = web.Application() + app: _App = web.Application() aiohttp_jinja2.setup(app, loader=jinja2.DictLoader({"tmpl.jinja2": template})) app.router.add_route("*", "/", MyView) @@ -66,7 +68,7 @@ async def meth(self, request): handler = Handler() - app = web.Application() + app: _App = web.Application() aiohttp_jinja2.setup(app, loader=jinja2.DictLoader({"tmpl.jinja2": template})) app.router.add_route("*", "/", handler.meth) @@ -87,7 +89,7 @@ async def func(request): template = "

{{head}}

{{text}}" - app = web.Application() + app: _App = web.Application() aiohttp_jinja2.setup(app, loader=jinja2.DictLoader({"tmpl.jinja2": template})) app.router.add_route("*", "/", func) @@ -101,10 +103,10 @@ async def func(request): async def test_render_not_initialized(): - async def func(request: web.Request) -> web.Response: + async def func(request: _Request) -> web.Response: return aiohttp_jinja2.render_template("template", request, {}) - app = web.Application() + app: _App = web.Application() app.router.add_route("GET", "/", func) @@ -128,7 +130,7 @@ async def func(request): template = "

{{head}}

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

{{head}}

{{text}}" - app = web.Application() + app: _App = web.Application() aiohttp_jinja2.setup( app, enable_async=enable_async, @@ -189,7 +191,7 @@ async def func(request): template = "

{{head}}

{{text}}" - app = web.Application() + app: _App = web.Application() aiohttp_jinja2.setup(app, loader=jinja2.DictLoader({"tmpl.jinja2": template})) app.router.add_route("*", "/", func) @@ -204,10 +206,10 @@ async def func(request): async def test_template_not_found(): - async def func(request: web.Request) -> web.Response: + async def func(request: _Request) -> web.Response: return aiohttp_jinja2.render_template("template", request, {}) - app = web.Application() + app: _App = web.Application() aiohttp_jinja2.setup(app, loader=jinja2.DictLoader({})) app.router.add_route("GET", "/", func) @@ -224,10 +226,10 @@ async def func(request: web.Request) -> web.Response: async def test_render_not_mapping(): @aiohttp_jinja2.template("tmpl.jinja2") # type: ignore[arg-type] - async def func(request: web.Request) -> int: + async def func(request: _Request) -> int: return 123 - app = web.Application() + app: _App = web.Application() aiohttp_jinja2.setup(app, loader=jinja2.DictLoader({"tmpl.jinja2": "tmpl"})) app.router.add_route("GET", "/", func) @@ -247,7 +249,7 @@ async def func(request): template = "

{{text}}

" - app = web.Application() + app: _App = web.Application() aiohttp_jinja2.setup(app, loader=jinja2.DictLoader({"tmpl.jinja2": template})) app.router.add_route("GET", "/", func) @@ -265,7 +267,7 @@ async def test_render_default_is_autoescaped(aiohttp_client): async def func(request): return {"text": ""} - app = web.Application() + app: _App = web.Application() aiohttp_jinja2.setup( app, loader=jinja2.DictLoader({"tmpl.jinja2": "{{text}}"}) ) @@ -285,7 +287,7 @@ async def test_render_can_disable_autoescape(aiohttp_client): async def func(request): return {"text": ""} - app = web.Application() + app: _App = web.Application() aiohttp_jinja2.setup( app, loader=jinja2.DictLoader({"tmpl.jinja2": "{{text}}"}), @@ -304,9 +306,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]]: - async def wrapped(request: web.Request) -> _T: + func: Callable[[_Request], Awaitable[_T]] + ) -> Callable[[_Request], Awaitable[_T]]: + async def wrapped(request: _Request) -> _T: with pytest.warns( DeprecationWarning, match="Bare functions are deprecated" ): @@ -316,10 +318,10 @@ async def wrapped(request: web.Request) -> _T: @wrapper # type: ignore[arg-type] # Deprecated functionality @aiohttp_jinja2.template("tmpl.jinja2") - def func(request: web.Request) -> Dict[str, str]: + def func(request: _Request) -> Dict[str, str]: return {"text": "OK"} - app = web.Application() + app: _App = web.Application() aiohttp_jinja2.setup(app, loader=jinja2.DictLoader({"tmpl.jinja2": "{{text}}"})) app.router.add_route("GET", "/", func) @@ -337,7 +339,7 @@ async def test_skip_render_for_response_from_handler(aiohttp_client): async def func(request): return web.Response(text="OK") - app = web.Application() + app: _App = web.Application() aiohttp_jinja2.setup(app, loader=jinja2.DictLoader({"tmpl.jinja2": "{{text}}"})) app.router.add_route("GET", "/", func)