Skip to content

Commit

Permalink
Merge pull request #172 from WinterFramework/page-inheritance
Browse files Browse the repository at this point in the history
Support Page class inheritance
  • Loading branch information
pristupa authored Jul 15, 2020
2 parents 37f8ae1 + 17625fa commit 31781e4
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 10 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
## [4.1.0] - 2020-07-15

### New features
- winter.web now supports Page-inherited classes. Extra fields are put to meta during serialization.

## [4.0.0] - 2020-07-14

Expand Down
35 changes: 34 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ class NotFoundExceptionHandler(winter.web.ExceptionHandler):
todo_list: List[str] = []


@winter.controller
@winter.web.controller
@winter.route('todo/')
class TodoController:
@winter.route_post('')
Expand Down Expand Up @@ -132,3 +132,36 @@ class TodoController:
def _build_todo_dto(self, todo_index: int):
return TodoDTO(todo_index=todo_index, todo=todo_list[todo_index])
```


## Extending Page class
```python
import winter
import winter.web
from dataclasses import dataclass
from winter.data.pagination import Page
from winter.data.pagination import PagePosition
from typing import TypeVar
from typing import Generic


T = TypeVar('T')

@dataclass(frozen=True)
class CustomPage(Page, Generic[T]):
extra_field: str # The field will go to meta JSON response field


@winter.web.controller
class ExampleController:
@winter.route_get('/')
def create_todo(self, page_position: PagePosition) -> CustomPage[int]:
return CustomPage(
# Standard Page fields
total_count=3,
items=[1, 2, 3],
position=page_position,
# Custom fields
extra_field=456,
)
```
13 changes: 11 additions & 2 deletions tests/controllers/simple_controller.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from http import HTTPStatus

import dataclasses
from dataclasses import dataclass
from rest_framework.request import Request
from rest_framework.response import Response

Expand All @@ -10,11 +10,16 @@
from winter.data.pagination import PagePosition


@dataclasses.dataclass
@dataclass
class Dataclass:
number: int


@dataclass(frozen=True)
class CustomPage(Page[int]):
extra: int


@winter.controller
@winter.route('winter-simple/')
class SimpleController:
Expand All @@ -28,6 +33,10 @@ def page_response(self, page_position: PagePosition) -> Page[Dataclass]:
items = [Dataclass(1)]
return Page(10, items, page_position)

@winter.route_get('custom-page-response/')
def custom_page_response(self, page_position: PagePosition) -> CustomPage:
return CustomPage(total_count=10, items=[1, 2], position=page_position, extra=456)

@winter.route_get('get-response-entity/')
@winter.response_status(HTTPStatus.ACCEPTED)
def return_response_entity(self) -> ResponseEntity[Dataclass]:
Expand Down
29 changes: 25 additions & 4 deletions tests/schema/test_type_inspection.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import datetime
import decimal
import uuid
from dataclasses import dataclass
from enum import Enum
from enum import IntEnum
from typing import Generic
from typing import List
from typing import NewType
from typing import Optional
from typing import TypeVar

import dataclasses
import pytest
from drf_yasg import openapi

from winter.core.utils import TypeWrapper
from winter.data.pagination import Page
from winter_openapi import inspect_enum_class
from winter_openapi import InspectorNotFound
from winter_openapi import TypeInfo
from winter_openapi import inspect_enum_class
from winter_openapi import inspect_type


Expand All @@ -27,7 +29,7 @@ class IntegerValueEnum(Enum):
TestType = NewType('TestType', int)


@dataclasses.dataclass
@dataclass
class NestedDataclass:
nested_number: int

Expand All @@ -36,7 +38,7 @@ class Id(int):
pass


@dataclasses.dataclass
@dataclass
class Dataclass:
nested: NestedDataclass

Expand All @@ -60,6 +62,14 @@ class DataclassWrapper(TypeWrapper):
pass


CustomPageItem = TypeVar('CustomPageItem')


@dataclass(frozen=True)
class CustomPage(Page, Generic[CustomPageItem]):
extra: str


@pytest.mark.parametrize('type_hint, expected_type_info', [
(TestType, TypeInfo(openapi.TYPE_INTEGER)),
(Id, TypeInfo(openapi.TYPE_INTEGER)),
Expand Down Expand Up @@ -97,6 +107,17 @@ class DataclassWrapper(TypeWrapper):
'nested_number': TypeInfo(openapi.TYPE_INTEGER),
})),
})),
(CustomPage[int], TypeInfo(openapi.TYPE_OBJECT, properties={
'meta': TypeInfo(openapi.TYPE_OBJECT, properties={
'total_count': TypeInfo(openapi.TYPE_INTEGER),
'limit': TypeInfo(openapi.TYPE_INTEGER, nullable=True),
'offset': TypeInfo(openapi.TYPE_INTEGER, nullable=True),
'previous': TypeInfo(openapi.TYPE_STRING, openapi.FORMAT_URI, nullable=True),
'next': TypeInfo(openapi.TYPE_STRING, openapi.FORMAT_URI, nullable=True),
'extra': TypeInfo(openapi.TYPE_STRING),
}),
'objects': TypeInfo(openapi.TYPE_ARRAY, child=TypeInfo(openapi.TYPE_INTEGER)),
})),
(DataclassWrapper[NestedDataclass], TypeInfo(openapi.TYPE_OBJECT, properties={
'nested_number': TypeInfo(openapi.TYPE_INTEGER),
})),
Expand Down
28 changes: 28 additions & 0 deletions tests/test_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,34 @@ def test_page_response():
assert response.json() == expected_body


def test_custom_page_response():
client = APIClient()
user = AuthorizedUser()
client.force_authenticate(user)
expected_body = {
'objects': [1, 2],
'meta': {
'limit': 2,
'offset': 2,
'next': 'http://testserver/winter-simple/custom-page-response/?limit=2&offset=4',
'previous': 'http://testserver/winter-simple/custom-page-response/?limit=2',
'total_count': 10,
'extra': 456,
},
}
request_data = {
'limit': 2,
'offset': 2,
}

# Act
response = client.get('/winter-simple/custom-page-response/', data=request_data)

# Assert
assert response.status_code == HTTPStatus.OK, response.content
assert response.json() == expected_body


def test_no_authentication_controller():
client = APIClient()
response = client.get('/winter-no-auth/')
Expand Down
2 changes: 1 addition & 1 deletion winter/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '4.0.0'
__version__ = '4.1.0'
6 changes: 6 additions & 0 deletions winter/web/pagination/page_processor.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import Dict

import dataclasses
from rest_framework.request import Request as DRFRequest

from winter.data.pagination import Page
Expand All @@ -11,13 +12,18 @@
class PageProcessor(IOutputProcessor):

def process_output(self, output: Page, request: DRFRequest) -> Dict:
extra_fields = set(dataclasses.fields(output)) - set(dataclasses.fields(Page))
return {
'meta': {
'total_count': output.total_count,
'limit': output.position.limit,
'offset': output.position.offset,
'previous': get_previous_page_url(output, request),
'next': get_next_page_url(output, request),
**{
extra_field.name: getattr(output, extra_field.name)
for extra_field in extra_fields
},
},
'objects': output.items,
}
8 changes: 7 additions & 1 deletion winter_openapi/page_inspector.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import dataclasses
from typing import List

from drf_yasg import openapi

from winter.data.pagination import Page
from .type_inspection import TypeInfo
from .type_inspection import inspect_type


def inspect_page(hint_class) -> TypeInfo:
args = getattr(hint_class, '__args__', None)

child_class = args[0] if args else str
extra_fields = set(dataclasses.fields(hint_class.__origin__)) - set(dataclasses.fields(Page))

return TypeInfo(openapi.TYPE_OBJECT, properties={
'meta': TypeInfo(openapi.TYPE_OBJECT, properties={
Expand All @@ -18,6 +20,10 @@ def inspect_page(hint_class) -> TypeInfo:
'offset': TypeInfo(openapi.TYPE_INTEGER, nullable=True),
'previous': TypeInfo(openapi.TYPE_STRING, openapi.FORMAT_URI, nullable=True),
'next': TypeInfo(openapi.TYPE_STRING, openapi.FORMAT_URI, nullable=True),
**{
extra_field.name: inspect_type(extra_field.type)
for extra_field in extra_fields
},
}),
'objects': inspect_type(List[child_class]),
})

0 comments on commit 31781e4

Please sign in to comment.