diff --git a/docs/docs/release_notes.md b/docs/docs/release_notes.md index b18739e..a58000f 100644 --- a/docs/docs/release_notes.md +++ b/docs/docs/release_notes.md @@ -1,3 +1,6 @@ +### 3.9.0 +- Change the style of `ModelSerializer` usage + ### 3.8.2 - Add `content-type = application/json` header in raise response of `__call__` diff --git a/docs/docs/serializer.md b/docs/docs/serializer.md index fbad8b4..a04769e 100644 --- a/docs/docs/serializer.md +++ b/docs/docs/serializer.md @@ -5,68 +5,114 @@ You can write your `serializer` in 2 style: Write a normal `pydantic` class and use it as serializer: ```python - from pydantic import BaseModel - from pydantic import Field - - from panther.app import API - from panther.request import Request - from panther.response import Response - - - class UserSerializer(BaseModel): - username: str - password: str - first_name: str = Field(default='', min_length=2) - last_name: str = Field(default='', min_length=4) - - - @API(input_model=UserSerializer) - async def serializer_example(request: Request): - return Response(data=request.validated_data) + from pydantic import BaseModel + from pydantic import Field + + from panther.app import API + from panther.request import Request + from panther.response import Response + + + class UserSerializer(BaseModel): + username: str + password: str + first_name: str = Field(default='', min_length=2) + last_name: str = Field(default='', min_length=4) + + + @API(input_model=UserSerializer) + async def serializer_example(request: Request): + return Response(data=request.validated_data) ``` ## Style 2 (Model Serializer) -Use panther `ModelSerializer` to write your serializer which will use your `model` fields as its fields, and you can say which fields are `required` +### Simple Usage + +Use panther `ModelSerializer` to write your serializer which will use your `model` to create fields. ```python - from pydantic import Field - - from panther import 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 - - - class User(Model): - username: str - password: str - first_name: str = Field(default='', min_length=2) - last_name: str = Field(default='', min_length=4) - - - class UserModelSerializer(metaclass=ModelSerializer, model=User): - fields = ['username', 'first_name', 'last_name'] - required_fields = ['first_name'] - - - @API(input_model=UserModelSerializer) - async def model_serializer_example(request: Request): - return Response(data=request.validated_data, status_code=status.HTTP_202_ACCEPTED) + from pydantic import Field + + from panther import 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 + + + class User(Model): + username: str + password: str + first_name: str = Field(default='', min_length=2) + last_name: str = Field(default='', min_length=4) + + + class UserModelSerializer(ModelSerializer): + class Config: + model = User + fields = ['username', 'first_name', 'last_name'] + required_fields = ['first_name'] + + + @API(input_model=UserModelSerializer) + async def model_serializer_example(request: Request): + return Response(data=request.validated_data, status_code=status.HTTP_202_ACCEPTED) ``` -## Notes -1. In the example above `UserModelSerializer` only accepts the values of `fields` attribute +### Notes +1. In the example above, `ModelSerializer` will look up for the value of `Config.fields` in the `User.fields` and use their `type` and `value` for the `validation`. +2. `Config.model` and `Config.fields` are `required` when you are using `ModelSerializer`. +3. If you want to use `Config.required_fields`, you have to put its value in `Config.fields` too. + + + +### Complex Usage + +You can use `pydantic.BaseModel` features in `ModelSerializer` too. + + ```python + from pydantic import Field, field_validator, ConfigDict -2. In default the `UserModelSerializer.fields` are same as `User.fields` but you can change their default and make them required with `required_fields` attribute + from panther import 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 + + + class User(Model): + username: str + password: str + first_name: str = Field(default='', min_length=2) + last_name: str = Field(default='', min_length=4) -3. If you want to use `required_fields` you have to put them in `fields` too. + class UserModelSerializer(ModelSerializer): + model_config = ConfigDict(str_to_upper=True) + age: int = Field(default=20) + is_male: bool + username: str + + class Config: + model = User + fields = ['username', 'first_name', 'last_name'] + required_fields = ['first_name'] -4. `fields` attribute is `required` when you are using `ModelSerializer` as `metaclass` + @field_validator('username') + def validate_username(cls, username): + print(f'{username=}') + return username -5. `model=` is required when you are using `ModelSerializer` as `metaclass` + + @API(input_model=UserModelSerializer) + async def model_serializer_example(request: Request): + return Response(data=request.validated_data, status_code=status.HTTP_202_ACCEPTED) + ``` -6. You have to use `ModelSerializer` as `metaclass` (not as a parent) +### Notes +1. You can add custom `fields` in `pydantic style` +2. You can add `model_config` as `attribute` and also in the `Config` +3. You can use `@field_validator` and other `validators` of `pydantic`. + -7. Panther is going to create a `pydantic` model as your `UserModelSerializer` in the startup \ No newline at end of file diff --git a/example/model_serializer_example.py b/example/model_serializer_example.py index ece1ff0..3aa9788 100644 --- a/example/model_serializer_example.py +++ b/example/model_serializer_example.py @@ -1,10 +1,6 @@ -from pydantic import Field +from pydantic import Field, field_validator, ConfigDict -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 @@ -15,18 +11,31 @@ class User(Model): last_name: str = Field(default='', min_length=4) -class UserSerializer(metaclass=ModelSerializer, model=User): - fields = ['username', 'first_name', 'last_name'] - # required_fields = ['first_name'] +class UserSerializer(ModelSerializer): + """ + Hello this is doc + """ + model_config = ConfigDict(str_to_upper=False) # Has more priority + age: int = Field(default=20) + is_male: bool + username: str + class Config: + str_to_upper = True + model = User + fields = ['username', 'first_name', 'last_name'] + required_fields = ['first_name'] -@API(input_model=UserSerializer) -async def model_serializer_example(request: Request): - return Response(data=request.validated_data, status_code=status.HTTP_202_ACCEPTED) + @field_validator('username', mode='before') + def validate_username(cls, username): + print(f'{username=}') + return username + def create(self) -> type[Model]: + print('UserSerializer.create()') + return super().create() -url_routing = { - '': model_serializer_example, -} -app = Panther(__name__, configs=__name__, urls=url_routing) +serialized = UserSerializer(username='alirn', first_name='Ali', last_name='RnRn', is_male=1) +print(serialized) +# serialized.create() diff --git a/panther/__init__.py b/panther/__init__.py index 7e0370d..076aad5 100644 --- a/panther/__init__.py +++ b/panther/__init__.py @@ -1,6 +1,6 @@ from panther.main import Panther # noqa: F401 -__version__ = '3.8.2' +__version__ = '3.9.0' def version(): diff --git a/panther/cli/template.py b/panther/cli/template.py index a9dce2b..0265df9 100644 --- a/panther/cli/template.py +++ b/panther/cli/template.py @@ -99,6 +99,7 @@ async def info_api(request: Request): TEMPLATE = { 'app': { + '__init__.py': '', 'apis.py': apis_py, 'models.py': models_py, 'serializers.py': serializers_py, @@ -106,6 +107,7 @@ async def info_api(request: Request): 'urls.py': app_urls_py, }, 'core': { + '__init__.py': '', 'configs.py': configs_py, 'urls.py': urls_py, }, @@ -161,6 +163,7 @@ async def info_api(request: Request): """ SINGLE_FILE_TEMPLATE = { + '__init__.py': '', 'main.py': single_main_py, '.env': env, '.gitignore': git_ignore, diff --git a/panther/serializer.py b/panther/serializer.py index 9948e0b..567687e 100644 --- a/panther/serializer.py +++ b/panther/serializer.py @@ -1,47 +1,122 @@ -from pydantic import create_model +import typing + +from pydantic import create_model, BaseModel +from pydantic.fields import FieldInfo from pydantic_core._pydantic_core import PydanticUndefined +from panther.db import Model + + +class MetaModelSerializer: + KNOWN_CONFIGS = ['model', 'fields', 'required_fields'] + + def __new__( + cls, + cls_name: str, + bases: tuple[type[typing.Any], ...], + namespace: dict[str, typing.Any], + **kwargs + ): + if cls_name == 'ModelSerializer': + cls.model_serializer = type(cls_name, (), namespace) + return super().__new__(cls) + + # 1. Initial Check + cls.check_config(cls_name=cls_name, namespace=namespace) + config = namespace.pop('Config') + + # 2. Collect `Fields` + field_definitions = cls.collect_fields(config=config, namespace=namespace) + + # 3. Collect `pydantic.model_config` + model_config = cls.collect_model_config(config=config, namespace=namespace) + namespace |= {'model_config': model_config} + + # 4. Create a serializer + return create_model( + __model_name=cls_name, + __module__=namespace['__module__'], + __validators__=namespace, + __base__=(cls.model_serializer, BaseModel), + __doc__=namespace.get('__doc__'), + model=(typing.ClassVar, config.model), + **field_definitions + ) -class ModelSerializer: - def __new__(cls, *args, model=None, **kwargs): - # Check `metaclass` - if len(args) == 0: - address = f'{cls.__module__}.{cls.__name__}' - msg = f"you should not inherit the 'ModelSerializer', you should use it as 'metaclass' -> {address}" - raise TypeError(msg) + @classmethod + def check_config(cls, cls_name: str, namespace: dict) -> None: + module = namespace['__module__'] + address = f'{module}.{cls_name}' - model_name = args[0] - data = args[2] - address = f'{data["__module__"]}.{model_name}' + # Check `Config` + if (config := namespace.get('Config')) is None: + msg = f'`class Config` is required in {address}.' + raise AttributeError(msg) from None # Check `model` - if model is None: - msg = f"'model' required while using 'ModelSerializer' metaclass -> {address}" - raise AttributeError(msg) - # Check `fields` - if 'fields' not in data: - msg = f"'fields' required while using 'ModelSerializer' metaclass. -> {address}" + if (model := getattr(config, 'model', None)) is None: + msg = f'`{cls_name}.Config.model` is required.' raise AttributeError(msg) from None - model_fields = model.model_fields - field_definitions = {} + # Check `model` type + try: + if not issubclass(model, Model): + msg = f'`{cls_name}.Config.model` is not subclass of `panther.db.Model`.' + raise AttributeError(msg) from None + except TypeError: + msg = f'`{cls_name}.Config.model` is not subclass of `panther.db.Model`.' + raise AttributeError(msg) from None - # Collect `fields` - for field_name in data['fields']: - if field_name not in model_fields: - msg = f"'{field_name}' is not in '{model.__name__}' -> {address}" + # Check `fields` + if (fields := getattr(config, 'fields', None)) is None: + msg = f'`{cls_name}.Config.fields` is required.' + raise AttributeError(msg) from None + + for field_name in fields: + if field_name not in model.model_fields: + msg = f'`{cls_name}.Config.fields.{field_name}` is not valid.' raise AttributeError(msg) from None - field_definitions[field_name] = (model_fields[field_name].annotation, model_fields[field_name]) - # Change `required_fields - for required in data.get('required_fields', []): - if required not in field_definitions: - msg = f"'{required}' is in 'required_fields' but not in 'fields' -> {address}" + # Check `required_fields` + if not hasattr(config, 'required_fields'): + config.required_fields = [] + + for required in config.required_fields: + if required not in config.fields: + msg = f'`{cls_name}.Config.required_fields.{required}` should be in `Config.fields` too.' raise AttributeError(msg) from None + + @classmethod + def collect_fields(cls, config: typing.Callable, namespace: dict) -> dict: + field_definitions = {} + + # Define `fields` + for field_name in config.fields: + field_definitions[field_name] = ( + config.model.model_fields[field_name].annotation, + config.model.model_fields[field_name] + ) + + # Apply `required_fields` + for required in config.required_fields: field_definitions[required][1].default = PydanticUndefined - # Create Model - return create_model( - __model_name=model_name, - **field_definitions - ) + # Collect and Override `Class Fields` + for key, value in namespace.pop('__annotations__', {}).items(): + field_info = namespace.pop(key, FieldInfo(required=True)) + field_info.annotation = value + field_definitions[key] = (value, field_info) + + return field_definitions + + @classmethod + def collect_model_config(cls, config: typing.Callable, namespace: dict) -> dict: + return { + attr: getattr(config, attr) for attr in dir(config) + if not attr.startswith('__') and attr not in cls.KNOWN_CONFIGS + } | namespace.pop('model_config', {}) + + +class ModelSerializer(metaclass=MetaModelSerializer): + def create(self) -> type[Model]: + return self.model.insert_one(self.model_dump()) diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py deleted file mode 100644 index 194c360..0000000 --- a/tests/test_model_serializer.py +++ /dev/null @@ -1,170 +0,0 @@ -from pathlib import Path -from unittest import TestCase - -from pydantic import Field - -from panther import Panther -from panther.app import API -from panther.db import Model -from panther.request import Request -from panther.serializer import ModelSerializer -from panther.test import APIClient - - -class Book(Model): - name: str - author: str = Field('default_author') - pages_count: int = Field(0) - - -class NotRequiredFieldsSerializer(metaclass=ModelSerializer, model=Book): - fields = ['author', 'pages_count'] - - -class RequiredFieldsSerializer(metaclass=ModelSerializer, model=Book): - fields = ['name', 'author', 'pages_count'] - - -class OnlyRequiredFieldsSerializer(metaclass=ModelSerializer, 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 - - -@API(input_model=RequiredFieldsSerializer) -async def required(request: Request): - return request.validated_data - - -@API(input_model=OnlyRequiredFieldsSerializer) -async def only_required(request: Request): - return request.validated_data - - -urls = { - 'not-required': not_required, - 'required': required, - 'only-required': only_required, -} - - -class TestModelSerializer(TestCase): - DB_PATH = 'test.pdb' - - @classmethod - def setUpClass(cls) -> None: - global MIDDLEWARES - MIDDLEWARES = [ - ('panther.middlewares.db.DatabaseMiddleware', {'url': f'pantherdb://{cls.DB_PATH}'}), - ] - app = Panther(__name__, configs=__name__, urls=urls) - cls.client = APIClient(app=app) - - def tearDown(self) -> None: - Path(self.DB_PATH).unlink(missing_ok=True) - - def test_not_required_fields_empty_response(self): - payload = {} - res = self.client.post('not-required', payload=payload) - assert res.status_code == 200 - assert res.data == {'author': 'default_author', 'pages_count': 0} - - def test_not_required_fields_full_response(self): - payload = { - 'author': 'ali', - 'pages_count': '12' - } - res = self.client.post('not-required', payload=payload) - assert res.status_code == 200 - assert res.data == {'author': 'ali', 'pages_count': 12} - - def test_required_fields_error(self): - payload = {} - res = self.client.post('required', payload=payload) - assert res.status_code == 400 - assert res.data == {'name': 'Field required'} - - def test_required_fields_success(self): - payload = { - 'name': 'how to code', - 'author': 'ali', - 'pages_count': '12' - } - res = self.client.post('required', payload=payload) - assert res.status_code == 200 - assert res.data == {'name': 'how to code', 'author': 'ali', 'pages_count': 12} - - def test_only_required_fields_error(self): - payload = {} - res = self.client.post('only-required', payload=payload) - assert res.status_code == 400 - assert res.data == {'name': 'Field required', 'author': 'Field required', 'pages_count': 'Field required'} - - def test_only_required_fields_success(self): - payload = { - 'name': 'how to code', - 'author': 'ali', - 'pages_count': '12' - } - res = self.client.post('only-required', payload=payload) - assert res.status_code == 200 - assert res.data == {'name': 'how to code', 'author': 'ali', 'pages_count': 12} - - def test_define_class_without_fields(self): - try: - class Serializer1(metaclass=ModelSerializer, model=Book): - pass - except Exception as e: - assert isinstance(e, AttributeError) - assert e.args[0] == "'fields' required while using 'ModelSerializer' metaclass. -> tests.test_model_serializer.Serializer1" - else: - assert False - - def test_define_class_with_invalid_fields(self): - try: - class Serializer2(metaclass=ModelSerializer, model=Book): - fields = ['ok', 'no'] - except Exception as e: - assert isinstance(e, AttributeError) - assert e.args[0] == "'ok' is not in 'Book' -> tests.test_model_serializer.Serializer2" - else: - assert False - - def test_define_class_with_invalid_required_fields(self): - try: - class Serializer3(metaclass=ModelSerializer, model=Book): - fields = ['name', 'author'] - required_fields = ['pages_count'] - except Exception as e: - assert isinstance(e, AttributeError) - assert e.args[0] == "'pages_count' is in 'required_fields' but not in 'fields' -> tests.test_model_serializer.Serializer3" - else: - assert False - - def test_define_class_without_model(self): - try: - class Serializer4(metaclass=ModelSerializer): - fields = ['name', 'author'] - required_fields = ['pages_count'] - except Exception as e: - assert isinstance(e, AttributeError) - assert e.args[0] == "'model' required while using 'ModelSerializer' metaclass -> tests.test_model_serializer.Serializer4" - else: - assert False - - def test_define_class_without_metaclass(self): - class Serializer5(ModelSerializer): - fields = ['name', 'author'] - required_fields = ['pages_count'] - - try: - Serializer5(name='alice', author='bob') - except Exception as e: - assert isinstance(e, TypeError) - assert e.args[0] == "you should not inherit the 'ModelSerializer', you should use it as 'metaclass' -> tests.test_model_serializer.Serializer5" - else: - assert False diff --git a/tests/test_serializer.py b/tests/test_serializer.py new file mode 100644 index 0000000..e5b397d --- /dev/null +++ b/tests/test_serializer.py @@ -0,0 +1,317 @@ +from pathlib import Path +from unittest import TestCase + +from pydantic import Field, ConfigDict +from pydantic import field_validator + +from panther import Panther +from panther.app import API +from panther.db import Model +from panther.request import Request +from panther.serializer import ModelSerializer +from panther.test import APIClient + + +class Book(Model): + name: str + author: str = Field('default_author') + pages_count: int = Field(0) + + +class NotRequiredFieldsSerializer(ModelSerializer): + class Config: + model = Book + fields = ['author', 'pages_count'] + + +class RequiredFieldsSerializer(ModelSerializer): + class Config: + model = Book + fields = ['name', 'author', 'pages_count'] + + +class OnlyRequiredFieldsSerializer(ModelSerializer): + class Config: + model = Book + fields = ['name', 'author', 'pages_count'] + required_fields = ['author', 'pages_count'] + + +class WithValidatorsSerializer(ModelSerializer): + class Config: + 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 Config: + 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 + + +@API(input_model=RequiredFieldsSerializer) +async def required(request: Request): + return request.validated_data + + +@API(input_model=OnlyRequiredFieldsSerializer) +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, +} + + +class TestModelSerializer(TestCase): + DB_PATH = 'test.pdb' + + @classmethod + def setUpClass(cls) -> None: + global MIDDLEWARES + MIDDLEWARES = [ + ('panther.middlewares.db.DatabaseMiddleware', {'url': f'pantherdb://{cls.DB_PATH}'}), + ] + app = Panther(__name__, configs=__name__, urls=urls) + cls.client = APIClient(app=app) + + 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) + assert res.status_code == 200 + assert res.data == {'author': 'default_author', 'pages_count': 0} + + def test_not_required_fields_full_response(self): + payload = { + 'author': 'ali', + 'pages_count': '12' + } + res = self.client.post('not-required', payload=payload) + assert res.status_code == 200 + assert res.data == {'author': 'ali', 'pages_count': 12} + + def test_required_fields_error(self): + payload = {} + res = self.client.post('required', payload=payload) + assert res.status_code == 400 + assert res.data == {'name': 'Field required'} + + def test_required_fields_success(self): + payload = { + 'name': 'how to code', + 'author': 'ali', + 'pages_count': '12' + } + res = self.client.post('required', payload=payload) + assert res.status_code == 200 + assert res.data == {'name': 'how to code', 'author': 'ali', 'pages_count': 12} + + def test_only_required_fields_error(self): + payload = {} + res = self.client.post('only-required', payload=payload) + assert res.status_code == 400 + assert res.data == {'name': 'Field required', 'author': 'Field required', 'pages_count': 'Field required'} + + def test_only_required_fields_success(self): + payload = { + 'name': 'how to code', + 'author': 'ali', + 'pages_count': '12' + } + res = self.client.post('only-required', payload=payload) + 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): + pass + except Exception as e: + assert isinstance(e, AttributeError) + assert e.args[0] == '`class Config` is required in tests.test_serializer.Serializer0.' + else: + assert False + + def test_define_class_without_model(self): + try: + class Serializer1(ModelSerializer): + class Config: + pass + except Exception as e: + assert isinstance(e, AttributeError) + assert e.args[0] == '`Serializer1.Config.model` is required.' + else: + assert False + + def test_define_class_without_fields(self): + try: + class Serializer2(ModelSerializer): + class Config: + model = Book + except Exception as e: + assert isinstance(e, AttributeError) + assert e.args[0] == '`Serializer2.Config.fields` is required.' + else: + assert False + + def test_define_class_with_invalid_fields(self): + try: + class Serializer3(ModelSerializer): + class Config: + model = Book + fields = ['ok', 'no'] + except Exception as e: + assert isinstance(e, AttributeError) + assert e.args[0] == '`Serializer3.Config.fields.ok` is not valid.' + else: + assert False + + def test_define_class_with_invalid_required_fields(self): + try: + class Serializer4(ModelSerializer): + class Config: + model = Book + fields = ['name', 'author'] + required_fields = ['pages_count'] + except Exception as e: + assert isinstance(e, AttributeError) + assert e.args[0] == '`Serializer4.Config.required_fields.pages_count` should be in `Config.fields` too.' + else: + assert False + + def test_define_class_with_invalid_model(self): + try: + class Serializer5(ModelSerializer): + class Config: + model = ModelSerializer + fields = ['name', 'author', 'pages_count'] + except Exception as e: + assert isinstance(e, AttributeError) + assert e.args[0] == '`Serializer5.Config.model` is not subclass of `panther.db.Model`.' + else: + assert False + + # # # Serializer Usage + def test_with_simple_model_config(self): + class Serializer(ModelSerializer): + model_config = ConfigDict(str_to_upper=True) + + class Config: + model = Book + fields = ['name', 'author', 'pages_count'] + + serialized = Serializer(name='book', author='AliRn', pages_count='12') + assert serialized.name == 'BOOK' + assert serialized.author == 'ALIRN' + assert serialized.pages_count == 12 + + def test_with_inner_model_config(self): + class Serializer(ModelSerializer): + class Config: + str_to_upper = True + model = Book + fields = ['name', 'author', 'pages_count'] + + serialized = Serializer(name='book', author='AliRn', pages_count='12') + assert serialized.name == 'BOOK' + assert serialized.author == 'ALIRN' + assert serialized.pages_count == 12 + + def test_with_dual_model_config(self): + class Serializer(ModelSerializer): + model_config = ConfigDict(str_to_upper=False) + + class Config: + str_to_upper = True + model = Book + fields = ['name', 'author', 'pages_count'] + + serialized = Serializer(name='book', author='AliRn', pages_count='12') + assert serialized.name == 'book' + assert serialized.author == 'AliRn' + assert serialized.pages_count == 12 + + def test_serializer_doc(self): + class Serializer1(ModelSerializer): + """Hello I'm Doc""" + class Config: + model = Book + fields = ['name', 'author', 'pages_count'] + + serialized = Serializer1(name='book', author='AliRn', pages_count='12') + assert serialized.__doc__ == 'Hello I\'m Doc' + + class Serializer2(ModelSerializer): + class Config: + model = Book + fields = ['name', 'author', 'pages_count'] + + serialized = Serializer2(name='book', author='AliRn', pages_count='12') + assert serialized.__doc__ is None