Merge pull request #16 from Maillol/view-compatibility
Support subclassing PydanticViews
This commit is contained in:
commit
911bcbc2cd
@ -1,5 +1,5 @@
|
|||||||
from .view import PydanticView
|
from .view import PydanticView
|
||||||
|
|
||||||
__version__ = "1.9.0"
|
__version__ = "1.9.1"
|
||||||
|
|
||||||
__all__ = ("PydanticView", "__version__")
|
__all__ = ("PydanticView", "__version__")
|
||||||
|
@ -305,7 +305,10 @@ class Components:
|
|||||||
|
|
||||||
class OpenApiSpec3:
|
class OpenApiSpec3:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._spec = {"openapi": "3.0.0", "info": {"version": "1.0.0", "title": "Aiohttp pydantic application"}}
|
self._spec = {
|
||||||
|
"openapi": "3.0.0",
|
||||||
|
"info": {"version": "1.0.0", "title": "Aiohttp pydantic application"},
|
||||||
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def info(self) -> Info:
|
def info(self) -> Info:
|
||||||
|
@ -147,7 +147,11 @@ def _add_http_method_to_oas(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def generate_oas(apps: List[Application], version_spec: Optional[str] = None, title_spec: Optional[str] = None) -> dict:
|
def generate_oas(
|
||||||
|
apps: List[Application],
|
||||||
|
version_spec: Optional[str] = None,
|
||||||
|
title_spec: Optional[str] = None,
|
||||||
|
) -> dict:
|
||||||
"""
|
"""
|
||||||
Generate and return Open Api Specification from PydanticView in application.
|
Generate and return Open Api Specification from PydanticView in application.
|
||||||
"""
|
"""
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from functools import update_wrapper
|
from functools import update_wrapper
|
||||||
from inspect import iscoroutinefunction
|
from inspect import iscoroutinefunction
|
||||||
from typing import Any, Callable, Generator, Iterable
|
from typing import Any, Callable, Generator, Iterable, Set, ClassVar
|
||||||
|
import warnings
|
||||||
|
|
||||||
from aiohttp.abc import AbstractView
|
from aiohttp.abc import AbstractView
|
||||||
from aiohttp.hdrs import METH_ALL
|
from aiohttp.hdrs import METH_ALL
|
||||||
@ -24,30 +25,46 @@ class PydanticView(AbstractView):
|
|||||||
An AIOHTTP View that validate request using function annotations.
|
An AIOHTTP View that validate request using function annotations.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Allowed HTTP methods; overridden when subclassed.
|
||||||
|
allowed_methods: ClassVar[Set[str]] = {}
|
||||||
|
|
||||||
async def _iter(self) -> StreamResponse:
|
async def _iter(self) -> StreamResponse:
|
||||||
method = getattr(self, self.request.method.lower(), None)
|
if (method_name := self.request.method) not in self.allowed_methods:
|
||||||
resp = await method()
|
self._raise_allowed_methods()
|
||||||
return resp
|
return await getattr(self, method_name.lower())()
|
||||||
|
|
||||||
def __await__(self) -> Generator[Any, None, StreamResponse]:
|
def __await__(self) -> Generator[Any, None, StreamResponse]:
|
||||||
return self._iter().__await__()
|
return self._iter().__await__()
|
||||||
|
|
||||||
def __init_subclass__(cls, **kwargs):
|
def __init_subclass__(cls, **kwargs) -> None:
|
||||||
|
"""Define allowed methods and decorate handlers.
|
||||||
|
|
||||||
|
Handlers are decorated if and only if they directly bound on the PydanticView class or
|
||||||
|
PydanticView subclass. This prevents that methods are decorated multiple times and that method
|
||||||
|
defined in aiohttp.View parent class is decorated.
|
||||||
|
"""
|
||||||
|
|
||||||
cls.allowed_methods = {
|
cls.allowed_methods = {
|
||||||
meth_name for meth_name in METH_ALL if hasattr(cls, meth_name.lower())
|
meth_name for meth_name in METH_ALL if hasattr(cls, meth_name.lower())
|
||||||
}
|
}
|
||||||
|
|
||||||
for meth_name in METH_ALL:
|
for meth_name in METH_ALL:
|
||||||
if meth_name not in cls.allowed_methods:
|
if meth_name.lower() in vars(cls):
|
||||||
setattr(cls, meth_name.lower(), cls.raise_not_allowed)
|
|
||||||
else:
|
|
||||||
handler = getattr(cls, meth_name.lower())
|
handler = getattr(cls, meth_name.lower())
|
||||||
decorated_handler = inject_params(handler, cls.parse_func_signature)
|
decorated_handler = inject_params(handler, cls.parse_func_signature)
|
||||||
setattr(cls, meth_name.lower(), decorated_handler)
|
setattr(cls, meth_name.lower(), decorated_handler)
|
||||||
|
|
||||||
async def raise_not_allowed(self):
|
def _raise_allowed_methods(self) -> None:
|
||||||
raise HTTPMethodNotAllowed(self.request.method, self.allowed_methods)
|
raise HTTPMethodNotAllowed(self.request.method, self.allowed_methods)
|
||||||
|
|
||||||
|
def raise_not_allowed(self) -> None:
|
||||||
|
warnings.warn(
|
||||||
|
"PydanticView.raise_not_allowed is deprecated and renamed _raise_allowed_methods",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
self._raise_allowed_methods()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_func_signature(func: Callable) -> Iterable[AbstractInjector]:
|
def parse_func_signature(func: Callable) -> Iterable[AbstractInjector]:
|
||||||
path_args, body_args, qs_args, header_args, defaults = _parse_func_signature(
|
path_args, body_args, qs_args, header_args, defaults = _parse_func_signature(
|
||||||
|
71
tests/test_inheritance.py
Normal file
71
tests/test_inheritance.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from aiohttp_pydantic import PydanticView
|
||||||
|
from aiohttp.web import View
|
||||||
|
|
||||||
|
|
||||||
|
def count_wrappers(obj: Any) -> int:
|
||||||
|
"""Count the number of times that an object is wrapped."""
|
||||||
|
i = 0
|
||||||
|
while i < 10:
|
||||||
|
try:
|
||||||
|
obj = obj.__wrapped__
|
||||||
|
except AttributeError:
|
||||||
|
return i
|
||||||
|
else:
|
||||||
|
i += 1
|
||||||
|
raise RuntimeError("Too many wrappers")
|
||||||
|
|
||||||
|
|
||||||
|
class AiohttpViewParent(View):
|
||||||
|
async def put(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PydanticViewParent(PydanticView):
|
||||||
|
async def get(self, id: int, /):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_allowed_methods_get_decorated_exactly_once():
|
||||||
|
class ChildView(PydanticViewParent):
|
||||||
|
async def post(self, id: int, /):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class SubChildView(ChildView):
|
||||||
|
async def get(self, id: int, /):
|
||||||
|
return super().get(id)
|
||||||
|
|
||||||
|
assert count_wrappers(ChildView.post) == 1
|
||||||
|
assert count_wrappers(ChildView.get) == 1
|
||||||
|
assert count_wrappers(SubChildView.post) == 1
|
||||||
|
assert count_wrappers(SubChildView.get) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_methods_inherited_from_aiohttp_view_should_not_be_decorated():
|
||||||
|
class ChildView(AiohttpViewParent, PydanticView):
|
||||||
|
async def post(self, id: int, /):
|
||||||
|
pass
|
||||||
|
|
||||||
|
assert count_wrappers(ChildView.put) == 0
|
||||||
|
assert count_wrappers(ChildView.post) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_allowed_methods_are_set_correctly():
|
||||||
|
class ChildView(AiohttpViewParent, PydanticView):
|
||||||
|
async def post(self, id: int, /):
|
||||||
|
pass
|
||||||
|
|
||||||
|
assert ChildView.allowed_methods == {"POST", "PUT"}
|
||||||
|
|
||||||
|
class ChildView(PydanticViewParent):
|
||||||
|
async def post(self, id: int, /):
|
||||||
|
pass
|
||||||
|
|
||||||
|
assert ChildView.allowed_methods == {"POST", "GET"}
|
||||||
|
|
||||||
|
class ChildView(AiohttpViewParent, PydanticViewParent):
|
||||||
|
async def post(self, id: int, /):
|
||||||
|
pass
|
||||||
|
|
||||||
|
assert ChildView.allowed_methods == {"POST", "PUT", "GET"}
|
@ -22,7 +22,7 @@ def test_show_oas_of_app(cmd_line):
|
|||||||
args.func(args)
|
args.func(args)
|
||||||
|
|
||||||
expected = dedent(
|
expected = dedent(
|
||||||
"""
|
"""
|
||||||
{
|
{
|
||||||
"info": {
|
"info": {
|
||||||
"title": "Aiohttp pydantic application",
|
"title": "Aiohttp pydantic application",
|
||||||
@ -73,7 +73,7 @@ def test_show_oas_of_sub_app(cmd_line):
|
|||||||
args.output = StringIO()
|
args.output = StringIO()
|
||||||
args.func(args)
|
args.func(args)
|
||||||
expected = dedent(
|
expected = dedent(
|
||||||
"""
|
"""
|
||||||
{
|
{
|
||||||
"info": {
|
"info": {
|
||||||
"title": "Aiohttp pydantic application",
|
"title": "Aiohttp pydantic application",
|
||||||
|
@ -37,7 +37,10 @@ def test_info_version():
|
|||||||
assert oas.info.version == "1.0.0"
|
assert oas.info.version == "1.0.0"
|
||||||
oas.info.version = "3.14"
|
oas.info.version = "3.14"
|
||||||
assert oas.info.version == "3.14"
|
assert oas.info.version == "3.14"
|
||||||
assert oas.spec == {"info": {"version": "3.14", "title": "Aiohttp pydantic application"}, "openapi": "3.0.0"}
|
assert oas.spec == {
|
||||||
|
"info": {"version": "3.14", "title": "Aiohttp pydantic application"},
|
||||||
|
"openapi": "3.0.0",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_info_terms_of_service():
|
def test_info_terms_of_service():
|
||||||
|
@ -318,22 +318,32 @@ async def test_simple_type_route_should_have_get_method(generated_oas):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def test_generated_view_info_default():
|
async def test_generated_view_info_default():
|
||||||
apps = (web.Application(),)
|
apps = (web.Application(),)
|
||||||
spec = generate_oas(apps)
|
spec = generate_oas(apps)
|
||||||
|
|
||||||
assert spec == {'info': {'title': 'Aiohttp pydantic application', 'version': '1.0.0'}, 'openapi': '3.0.0'}
|
assert spec == {
|
||||||
|
"info": {"title": "Aiohttp pydantic application", "version": "1.0.0"},
|
||||||
|
"openapi": "3.0.0",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async def test_generated_view_info_as_version():
|
async def test_generated_view_info_as_version():
|
||||||
apps = (web.Application(),)
|
apps = (web.Application(),)
|
||||||
spec = generate_oas(apps, version_spec="test version")
|
spec = generate_oas(apps, version_spec="test version")
|
||||||
|
|
||||||
assert spec == {'info': {'title': 'Aiohttp pydantic application', 'version': 'test version'}, 'openapi': '3.0.0'}
|
assert spec == {
|
||||||
|
"info": {"title": "Aiohttp pydantic application", "version": "test version"},
|
||||||
|
"openapi": "3.0.0",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async def test_generated_view_info_as_title():
|
async def test_generated_view_info_as_title():
|
||||||
apps = (web.Application(),)
|
apps = (web.Application(),)
|
||||||
spec = generate_oas(apps, title_spec="test title")
|
spec = generate_oas(apps, title_spec="test title")
|
||||||
|
|
||||||
assert spec == {'info': {'title': 'test title', 'version': '1.0.0'}, 'openapi': '3.0.0'}
|
assert spec == {
|
||||||
|
"info": {"title": "test title", "version": "1.0.0"},
|
||||||
|
"openapi": "3.0.0",
|
||||||
|
}
|
||||||
|
@ -11,7 +11,7 @@ class ArticleView(PydanticView):
|
|||||||
with_comments: bool,
|
with_comments: bool,
|
||||||
age: Optional[int] = None,
|
age: Optional[int] = None,
|
||||||
nb_items: int = 7,
|
nb_items: int = 7,
|
||||||
tags: List[str] = Field(default_factory=list)
|
tags: List[str] = Field(default_factory=list),
|
||||||
):
|
):
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
{
|
{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user