Skip to content

Commit

Permalink
Collect validators + class fields in MetaModelSerializer
Browse files Browse the repository at this point in the history
  • Loading branch information
AliRn76 committed Jan 25, 2024
1 parent 1d33ca0 commit 398d3b9
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 16 deletions.
27 changes: 12 additions & 15 deletions example/model_serializer_example.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
from pydantic import Field
from pydantic import field_validator

from panther import Panther, status
from panther.app import API
from panther.db import Model
from panther.request import Request
from panther.response import Response
from panther.serializer import ModelSerializer


Expand All @@ -16,22 +13,22 @@ class User(Model):


class UserSerializer(ModelSerializer):
age: int = Field(default=20)
is_male: bool

class Meta:
model = User
fields = ['username', 'first_name', 'last_name']
required_fields = ['first_name']

@field_validator('username', mode='before')
def validate_username(cls, username):
print(f'{username=}')
return username

@API(input_model=UserSerializer)
async def model_serializer_example(request: Request):
return Response(data=request.validated_data, status_code=status.HTTP_202_ACCEPTED)


url_routing = {
'': model_serializer_example,
}

app = Panther(__name__, configs=__name__, urls=url_routing)
def ok(self):
print('ok')


# print(UserSerializer(username='alirn', first_name='Ali', last_name='RnRn'))
print(UserSerializer(username='alirn', first_name='Ali', last_name='RnRn', is_male=1))
# print(UserSerializer.validate_username)
21 changes: 20 additions & 1 deletion panther/serializer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import typing

from pydantic import create_model
from pydantic.fields import FieldInfo
from pydantic_core._pydantic_core import PydanticUndefined

from panther.db import Model
Expand Down Expand Up @@ -59,8 +60,26 @@ def __new__(
raise AttributeError(msg) from None
field_definitions[required][1].default = PydanticUndefined

# Collect and Override `Class Fields`
for key, value in namespace.get('__annotations__', {}).items():
field_info = namespace.pop(key, FieldInfo(required=True))
field_info.annotation = value
field_definitions[key] = (value, field_info)

# Collect `Validators`
validators = {}
for key, value in namespace.items():
if key in ['__module__', '__qualname__', '__annotations__', 'Meta']:
continue

validators[key] = value

# Create model
return create_model(__model_name=cls_name, **field_definitions)
return create_model(
__model_name=cls_name,
__validators__=validators,
**field_definitions
)


class ModelSerializer(metaclass=MetaModelSerializer):
Expand Down
69 changes: 69 additions & 0 deletions tests/test_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from unittest import TestCase

from pydantic import Field
from pydantic import field_validator

from panther import Panther
from panther.app import API
Expand Down Expand Up @@ -36,6 +37,26 @@ class Meta:
required_fields = ['author', 'pages_count']


class WithValidatorsSerializer(ModelSerializer):
class Meta:
model = Book
fields = ['name', 'author', 'pages_count']
required_fields = ['author', 'pages_count']

@field_validator('name', 'author', 'pages_count')
def validate(cls, field):
return 'validated'


class WithClassFieldsSerializer(ModelSerializer):
age: int = Field(10)

class Meta:
model = Book
fields = ['name', 'author', 'pages_count']
required_fields = ['author', 'pages_count']


@API(input_model=NotRequiredFieldsSerializer)
async def not_required(request: Request):
return request.validated_data
Expand All @@ -51,10 +72,22 @@ async def only_required(request: Request):
return request.validated_data


@API(input_model=WithValidatorsSerializer)
async def with_validators(request: Request):
return request.validated_data


@API(input_model=WithClassFieldsSerializer)
async def with_class_fields(request: Request):
return request.validated_data


urls = {
'not-required': not_required,
'required': required,
'only-required': only_required,
'with-validators': with_validators,
'class-fields': with_class_fields,
}


Expand All @@ -73,6 +106,8 @@ def setUpClass(cls) -> None:
def tearDown(self) -> None:
Path(self.DB_PATH).unlink(missing_ok=True)

# # # Class Usage

def test_not_required_fields_empty_response(self):
payload = {}
res = self.client.post('not-required', payload=payload)
Expand Down Expand Up @@ -120,6 +155,40 @@ def test_only_required_fields_success(self):
assert res.status_code == 200
assert res.data == {'name': 'how to code', 'author': 'ali', 'pages_count': 12}

def test_with_validators(self):
payload = {
'name': 'how to code',
'author': 'ali',
'pages_count': '12'
}
res = self.client.post('with-validators', payload=payload)
assert res.status_code == 200
assert res.data == {'name': 'validated', 'author': 'validated', 'pages_count': 'validated'}

def test_with_class_fields_success(self):
# Test Default Value
payload1 = {
'name': 'how to code',
'author': 'ali',
'pages_count': '12'
}
res = self.client.post('class-fields', payload=payload1)
assert res.status_code == 200
assert res.data == {'name': 'how to code', 'author': 'ali', 'pages_count': 12, 'age': 10}

# Test Validation
payload2 = {
'name': 'how to code',
'author': 'ali',
'pages_count': '12',
'age': 30
}
res = self.client.post('class-fields', payload=payload2)
assert res.status_code == 200
assert res.data == {'name': 'how to code', 'author': 'ali', 'pages_count': 12, 'age': 30}

# # # Class Definition

def test_define_class_without_meta(self):
try:
class Serializer0(ModelSerializer):
Expand Down

0 comments on commit 398d3b9

Please sign in to comment.