Skip to content

Commit 1a1bb6b

Browse files
authored
Merge pull request #43 from eadwinCode/generic_updates
feat: support for generic class
2 parents 99edde1 + 63daca4 commit 1a1bb6b

File tree

7 files changed

+62
-20
lines changed

7 files changed

+62
-20
lines changed

README.md

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Starting version `0.13.4`, Ninja schema will support both v1 and v2 of pydantic
1616
Python >= 3.8
1717
django >= 3
1818
pydantic >= 1.6
19-
19+
2020
**Key features:**
2121
- **Custom Field Support**: Ninja Schema converts django model to native pydantic types which gives you quick field validation out of the box. eg Enums, email, IPAddress, URLs, JSON, etc
2222
- **Field Validator**: Fields can be validated with **model_validator** just like pydantic **[validator](https://pydantic-docs.helpmanual.io/usage/validators/)** or **[root_validator](https://pydantic-docs.helpmanual.io/usage/validators/)**.
@@ -63,7 +63,6 @@ class CreateUserSchema(ModelSchema):
6363
## `from_orm(cls, obj: Any)`
6464
You can generate a schema instance from your django model instance
6565
```Python
66-
from typings import Optional
6766
from django.contrib.auth import get_user_model
6867
from ninja_schema import ModelSchema, model_validator
6968

@@ -90,15 +89,14 @@ print(schema.json(indent=2)
9089
}
9190
```
9291

93-
## `apply(self, model_instance, **kwargs)`
92+
## `apply_to_model(self, model_instance, **kwargs)`
9493
You can transfer data from your ModelSchema to Django Model instance using the `apply` function.
95-
The `apply` function uses Pydantic model `.dict` function, `dict` function filtering that can be passed as `kwargs` to the `.apply` function.
94+
The `apply_to_model` function uses Pydantic model `.dict` function, `dict` function filtering that can be passed as `kwargs` to the `.apply` function.
9695

9796
For more info, visit [Pydantic model export](https://pydantic-docs.helpmanual.io/usage/exporting_models/)
9897
```Python
99-
from typings import Optional
10098
from django.contrib.auth import get_user_model
101-
from ninja_schema import ModelSchema, model_validator
99+
from ninja_schema import ModelSchema
102100

103101
UserModel = get_user_model()
104102
new_user = UserModel.objects.create_user(username='eadwin', email='[email protected]', password='password')
@@ -111,7 +109,7 @@ class UpdateUserSchema(ModelSchema):
111109
optional = ['username'] # `username` is now optional
112110

113111
schema = UpdateUserSchema(first_name='Emeka', last_name='Okoro')
114-
schema.apply(new_user, exclude_none=True)
112+
schema.apply_to_model(new_user, exclude_none=True)
115113

116114
assert new_user.first_name == 'Emeka' # True
117115
assert new_user.username == 'eadwin' # True
@@ -269,4 +267,3 @@ print(UserSchema.schema())
269267
}
270268
}
271269
```
272-

ninja_schema/orm/model_schema.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -320,11 +320,12 @@ def __new__(
320320
name: str,
321321
bases: tuple,
322322
namespace: dict,
323+
**kwargs: Any,
323324
):
324325
if bases == (SchemaBaseModel,) or not namespace.get(
325326
"Config", namespace.get("model_config")
326327
):
327-
return super().__new__(mcs, name, bases, namespace)
328+
return super().__new__(mcs, name, bases, namespace, **kwargs)
328329

329330
config = namespace.get("Config")
330331
if not config:
@@ -407,16 +408,20 @@ def __new__(
407408

408409
field_values[field_name] = (python_type, pydantic_field)
409410
if IS_PYDANTIC_V1:
410-
cls = super().__new__(mcs, name, bases, namespace)
411+
cls = super().__new__(mcs, name, bases, namespace, **kwargs)
411412
return update_class_missing_fields(
412413
cls,
413414
list(bases),
414415
compute_field_annotations(namespace, **field_values),
415416
)
416417
return super().__new__(
417-
mcs, name, bases, compute_field_annotations(namespace, **field_values)
418+
mcs,
419+
name,
420+
bases,
421+
compute_field_annotations(namespace, **field_values),
422+
**kwargs,
418423
)
419-
return super().__new__(mcs, name, bases, namespace)
424+
return super().__new__(mcs, name, bases, namespace, **kwargs)
420425

421426

422427
class SchemaBaseModel(SchemaMixins, BaseModel):

ninja_schema/orm/schema_registry.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@ def register_model(self, model: Type[Model], schema: Type["ModelSchema"]) -> Non
3131
from ninja_schema.orm.model_schema import ModelSchema
3232

3333
assert is_valid_class(schema) and issubclass(schema, (ModelSchema,)), (
34-
"Only Schema can be" 'registered, received "{}"'.format(schema.__name__)
34+
'Only Schema can beregistered, received "{}"'.format(schema.__name__)
35+
)
36+
assert is_valid_django_model(model), (
37+
"Only Django Models are allowed. {}".format(model.__name__)
3538
)
36-
assert is_valid_django_model(
37-
model
38-
), "Only Django Models are allowed. {}".format(model.__name__)
3939
# TODO: register model as module_name.model_name
4040
self.register_schema(model, schema)
4141

ninja_schema/orm/utils/converter.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@
3939

4040
def assert_valid_name(name: str) -> None:
4141
"""Helper to assert that provided names are valid."""
42-
assert COMPILED_NAME_PATTERN.match(
43-
name
44-
), 'Names must match /{}/ but "{}" does not.'.format(NAME_PATTERN, name)
42+
assert COMPILED_NAME_PATTERN.match(name), (
43+
'Names must match /{}/ but "{}" does not.'.format(NAME_PATTERN, name)
44+
)
4545

4646

4747
def convert_choice_name(name: str) -> str:

ninja_schema/py.typed

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

tests/test_v1_pydantic/test_custom_fields.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class Config:
3333
"title": "SemesterEnum",
3434
"description": "An enumeration.",
3535
"enum": ["1", "2", "3"],
36-
"type": "string"
36+
"type": "string",
3737
}
3838
},
3939
}

tests/test_v2_pydantic/test_model_schema.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import json
2+
import typing as t
23

34
import pydantic
45
import pytest
6+
from django.db.models import Model as DjangoModel
57

68
from ninja_schema import ModelSchema, SchemaFactory, model_validator
79
from ninja_schema.errors import ConfigError
810
from ninja_schema.pydanticutils import IS_PYDANTIC_V1
911
from tests.models import Event
1012

13+
T = t.TypeVar("T", bound=DjangoModel)
14+
1115

1216
class TestModelSchema:
1317
@pytest.mark.skipif(IS_PYDANTIC_V1, reason="requires pydantic == 2.1.x")
@@ -514,3 +518,38 @@ def validate_title(cls, value):
514518

515519
event = EventWithNewModelConfig(start_date="2021-06-12", title="PyConf 2021")
516520
assert "value cleaned" in event.title
521+
522+
@pytest.mark.django_db
523+
@pytest.mark.skipif(IS_PYDANTIC_V1, reason="requires pydantic == 2.1.x")
524+
def test_schema_with_mixin_generic_class(self):
525+
"""
526+
Test that a schema with a generic mixin class works correctly.
527+
"""
528+
529+
class GenericMixin(t.Generic[T]):
530+
def save(self, instance: t.Optional[T] = None) -> T:
531+
"""
532+
Save the model instance and return it.
533+
"""
534+
if instance:
535+
self.apply_to_model(instance, **self.dict())
536+
instance.save()
537+
return instance
538+
539+
instance = self.Config.model(**self.dict())
540+
instance.save()
541+
return instance
542+
543+
class BaseModelSchema(ModelSchema, GenericMixin[T]): ...
544+
545+
class EventGenericSchema(BaseModelSchema[Event]):
546+
class Config:
547+
model = Event
548+
include = ("title",)
549+
550+
event = EventGenericSchema(title="PyConf 2021")
551+
assert event.title == "PyConf 2021"
552+
553+
instance_event = event.save()
554+
assert isinstance(instance_event, Event)
555+
assert instance_event.title == "PyConf 2021"

0 commit comments

Comments
 (0)