Skip to content
This repository has been archived by the owner on Jul 18, 2024. It is now read-only.

Fix custom field support and add include_from_annotations. #54

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,4 +222,36 @@ print(OrderUserSchema.from_orm(user).json(ident=4))
"id": 1,
"email": ""
}
```
```


### Include from annotations

By default, a Schema without Config.include or Config.exclude defined will include all fields of the Config.model class.

If you want to limit included fields to the annotations of the Schema without defining Config.include, use `Config.include = "__annotations__"`.


```python
class ProfileSchema(ModelSchema):
website: str

class Config:
model = Profile
include = "__annotations__"

assert ProfileSchema.schema() == {
"title": "ProfileSchema",
"description": "A user's profile.",
"type": "object",
"properties": {
"website": {
"title": "Website",
"type": "string"
}
},
"required": [
"website"
]
}
```
30 changes: 20 additions & 10 deletions djantic/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ def default(self, obj): # pragma: nocover
return super().default(obj)


def get_field_name(field) -> str:
if issubclass(field.__class__, ForeignObjectRel) and not issubclass(field.__class__, OneToOneRel):
return getattr(field, "related_name", None) or f"{field.name}_set"
else:
return getattr(field, "name", field)


class ModelSchemaMetaclass(ModelMetaclass):
@no_type_check
def __new__(
Expand All @@ -49,7 +56,7 @@ def __new__(
raise ConfigError(
f"{exc} (Is `Config` class defined?)"
)

include = getattr(config, "include", None)
exclude = getattr(config, "exclude", None)

Expand All @@ -68,17 +75,18 @@ def __new__(
f"{exc} (Is `Config.model` a valid Django model class?)"
)

if include is None and exclude is None:
cls.__config__.include = [f.name for f in fields]
if include == '__annotations__':
include = list(annotations.keys())
cls.__config__.include = include
elif include is None and exclude is None:
include = list(annotations.keys()) + [get_field_name(f) for f in fields]
cls.__config__.include = include

field_values = {}
_seen = set()

for field in chain(fields, annotations.copy()):
if issubclass(field.__class__, ForeignObjectRel) and not issubclass(field.__class__, OneToOneRel):
field_name = getattr(field, "related_name", None) or f"{field.name}_set"
else:
field_name = getattr(field, "name", field)
field_name = get_field_name(field)

if (
field_name in _seen
Expand Down Expand Up @@ -114,6 +122,8 @@ def __new__(

cls.__doc__ = namespace.get("__doc__", config.model.__doc__)
cls.__fields__ = {}
cls.__alias_map__ = {getattr(model_field[1], 'alias', None) or field_name: field_name
for field_name, model_field in field_values.items()}
model_schema = create_model(
name, __base__=cls, __module__=cls.__module__, **field_values
)
Expand All @@ -129,14 +139,14 @@ def __init__(self, obj: Any, schema_class):
self.schema_class = schema_class

def get(self, key: Any, default: Any = None) -> Any:
alias = self.schema_class.__alias_map__[key]
outer_type_ = self.schema_class.__fields__[alias].outer_type_
if "__" in key:
# Allow double underscores aliases: `first_name: str = Field(alias="user__first_name")`
keys_map = key.split("__")
attr = reduce(lambda a, b: getattr(a, b, default), keys_map, self._obj)
outer_type_ = self.schema_class.__fields__["user"].outer_type_
else:
attr = getattr(self._obj, key)
outer_type_ = self.schema_class.__fields__[key].outer_type_
attr = getattr(self._obj, key, None)

is_manager = issubclass(attr.__class__, Manager)

Expand Down
22 changes: 21 additions & 1 deletion tests/test_multiple_level_relations.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@

from decimal import Decimal
from typing import List
from typing import List, Optional

import pytest
from pydantic import validator
from testapp.order import Order, OrderItem, OrderItemDetail, OrderUser, OrderUserFactory, OrderUserProfile

from djantic import ModelSchema
Expand Down Expand Up @@ -34,9 +35,23 @@ class Config:
class OrderUserSchema(ModelSchema):
orders: List[OrderSchema]
profile: OrderUserProfileSchema
user_cache: Optional[dict]

class Config:
model = OrderUser
include = ('id',
'first_name',
'last_name',
'email',
'profile',
'orders',
'user_cache')

@validator('user_cache', pre=True, always=True)
def get_user_cache(cls, _):
return {
'has_order': True
}

user = OrderUserFactory.create()

Expand All @@ -45,6 +60,7 @@ class Config:
'first_name': '',
'last_name': None,
'email': '',
'user_cache': {'has_order': True},
'profile': {
'id': 1,
'address': '',
Expand Down Expand Up @@ -195,6 +211,10 @@ class Config:
"description": "email",
"maxLength": 254,
"type": "string"
},
"user_cache": {
"title": "User Cache",
"type": "object"
}
},
"required": [
Expand Down
29 changes: 29 additions & 0 deletions tests/test_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -380,3 +380,32 @@ class Config:
]
}"""
assert ConfigurationSchema.schema_json(indent=2) == expected


@pytest.mark.django_db
def test_include_from_annotations():
"""
Test include="__annotations__" config.
"""

class ProfileSchema(ModelSchema):
website: str

class Config:
model = Profile
include = "__annotations__"

assert ProfileSchema.schema() == {
"title": "ProfileSchema",
"description": "A user's profile.",
"type": "object",
"properties": {
"website": {
"title": "Website",
"type": "string"
}
},
"required": [
"website"
]
}