diff --git a/docs/models/methods.md b/docs/models/methods.md index e9cfe72..ff83eb1 100644 --- a/docs/models/methods.md +++ b/docs/models/methods.md @@ -17,6 +17,273 @@ especially `dict()` and `json()` methods that can also accept `exclude`, `includ To read more check [pydantic][pydantic] documentation +## dict + +`dict` is a method inherited from `pydantic`, yet `ormar` adds its own parameters and has some nuances when working with default values, +therefore it's listed here for clarity. + +`dict` as the name suggests export data from model tree to dictionary. + +Explanation of dict parameters: + +### include (`ormar` modifed) + +`include: Union[Set, Dict] = None` + +Set or dictionary of field names to include in returned dictionary. + +Note that `pydantic` has an uncommon pattern of including/ excluding fields in lists (so also nested models) by an index. +And if you want to exclude the field in all children you need to pass a `__all__` key to dictionary. + +You cannot exclude nested models in `Set`s in `pydantic` but you can in `ormar` +(by adding double underscore on relation name i.e. to exclude name of category for a book you cen use `exclude={"book__category__name"}`) + +`ormar` does not support by index exclusion/ inclusions and accepts a simplified and more user-friendly notation. + +To check how you can include/exclude fields, including nested fields check out [fields](../queries/select-columns.md#fields) section that has an explanation and a lot of samples. + +!!!note + The fact that in `ormar` you can exclude nested models in sets, you can exclude from a whole model tree in `response_model_exclude` and `response_model_include` in fastapi! + +### exclude (`ormar` modified) + +`exclude: Union[Set, Dict] = None` + +Set or dictionary of field names to exclude in returned dictionary. + +Note that `pydantic` has an uncommon pattern of including/ excluding fields in lists (so also nested models) by an index. +And if you want to exclude the field in all children you need to pass a `__all__` key to dictionary. + +You cannot exclude nested models in `Set`s in `pydantic` but you can in `ormar` +(by adding double underscore on relation name i.e. to exclude name of category for a book you cen use `exclude={"book__category__name"}`) + +`ormar` does not support by index exclusion/ inclusions and accepts a simplified and more user-friendly notation. + +To check how you can include/exclude fields, including nested fields check out [fields](../queries/select-columns.md#fields) section that has an explanation and a lot of samples. + +!!!note + The fact that in `ormar` you can exclude nested models in sets, you can exclude from a whole model tree in `response_model_exclude` and `response_model_include` in fastapi! + +### exclude_unset + +`exclude_unset: bool = False` + +Flag indicates whether fields which were not explicitly set when creating the model should be excluded from the returned dictionary. + +!!!warning + Note that after you save data into database each field has its own value -> either provided by you, default, or `None`. + + That means that when you load the data from database, **all** fields are set, and this flag basically stop working! + +```python +class Category(ormar.Model): + class Meta: + tablename = "categories" + metadata = metadata + database = database + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100, default="Test") + visibility: bool = ormar.Boolean(default=True) + + +class Item(ormar.Model): + class Meta: + tablename = "items" + metadata = metadata + database = database + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + price: float = ormar.Float(default=9.99) + categories: List[Category] = ormar.ManyToMany(Category) + +category = Category(name="Test 2") +assert category.dict() == {'id': None, 'items': [], 'name': 'Test 2', + 'visibility': True} +assert category.dict(exclude_unset=True) == {'items': [], 'name': 'Test 2'} + +await category.save() +category2 = await Category.objects.get() +assert category2.dict() == {'id': 1, 'items': [], 'name': 'Test 2', + 'visibility': True} +# NOTE how after loading from db all fields are set explicitly +# as this is what happens when you populate a model from db +assert category2.dict(exclude_unset=True) == {'id': 1, 'items': [], + 'name': 'Test 2', 'visibility': True} +``` + +### exclude_defaults + +`exclude_defaults: bool = False` + +Flag indicates are equal to their default values (whether set or otherwise) should be excluded from the returned dictionary + +```python +class Category(ormar.Model): + class Meta: + tablename = "categories" + metadata = metadata + database = database + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100, default="Test") + visibility: bool = ormar.Boolean(default=True) + +class Item(ormar.Model): + class Meta: + tablename = "items" + metadata = metadata + database = database + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + price: float = ormar.Float(default=9.99) + categories: List[Category] = ormar.ManyToMany(Category) + +category = Category() +# note that Integer pk is by default autoincrement so optional +assert category.dict() == {'id': None, 'items': [], 'name': 'Test', 'visibility': True} +assert category.dict(exclude_defaults=True) == {'items': []} + +# save and reload the data +await category.save() +category2 = await Category.objects.get() + +assert category2.dict() == {'id': 1, 'items': [], 'name': 'Test', 'visibility': True} +assert category2.dict(exclude_defaults=True) == {'id': 1, 'items': []} +``` + +### exclude_none + +`exclude_none: bool = False` + +Flag indicates whether fields which are equal to `None` should be excluded from the returned dictionary. + +```python +class Category(ormar.Model): + class Meta: + tablename = "categories" + metadata = metadata + database = database + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100, default="Test", nullable=True) + visibility: bool = ormar.Boolean(default=True) + + +class Item(ormar.Model): + class Meta: + tablename = "items" + metadata = metadata + database = database + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + price: float = ormar.Float(default=9.99) + categories: List[Category] = ormar.ManyToMany(Category) + + +category = Category(name=None) +assert category.dict() == {'id': None, 'items': [], 'name': None, + 'visibility': True} +# note the id is not set yet so None and excluded +assert category.dict(exclude_none=True) == {'items': [], 'visibility': True} + +await category.save() +category2 = await Category.objects.get() +assert category2.dict() == {'id': 1, 'items': [], 'name': None, + 'visibility': True} +assert category2.dict(exclude_none=True) == {'id': 1, 'items': [], + 'visibility': True} + +``` + +### exclude_primary_keys (`ormar` only) + +`exclude_primary_keys: bool = False` + +Setting flag to `True` will exclude all primary key columns in a tree, including nested models. + +```python +class Item(ormar.Model): + class Meta: + tablename = "items" + metadata = metadata + database = database + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + +item1 = Item(id=1, name="Test Item") +assert item1.dict() == {"id": 1, "name": "Test Item"} +assert item1.dict(exclude_primary_keys=True) == {"name": "Test Item"} +``` + +### exclude_through_models (`ormar` only) + +`exclude_through_models: bool = False` + +`Through` models are auto added for every `ManyToMany` relation, and they hold additional parameters on linking model/table. + +Setting the `exclude_through_models=True` will exclude all through models, including Through models of submodels. + +```python +class Category(ormar.Model): + class Meta: + tablename = "categories" + metadata = metadata + database = database + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + + +class Item(ormar.Model): + class Meta: + tablename = "items" + metadata = metadata + database = database + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + categories: List[Category] = ormar.ManyToMany(Category) + +# tree defining the models +item_dict = { + "name": "test", + "categories": [{"name": "test cat"}, {"name": "test cat2"}], + } +# save whole tree +await Item(**item_dict).save_related(follow=True, save_all=True) + +# get the saved values +item = await Item.objects.select_related("categories").get() + +# by default you can see the through models (itemcategory) +assert item.dict() == {'id': 1, 'name': 'test', + 'categories': [ + {'id': 1, 'name': 'test cat', + 'itemcategory': {'id': 1, 'category': None, 'item': None}}, + {'id': 2, 'name': 'test cat2', + 'itemcategory': {'id': 2, 'category': None, 'item': None}} + ]} + +# you can exclude those fields/ models +assert item.dict(exclude_through_models=True) == { + 'id': 1, 'name': 'test', + 'categories': [ + {'id': 1, 'name': 'test cat'}, + {'id': 2, 'name': 'test cat2'} + ]} +``` + +## json + +`json()` has exactly the same parameters as `dict()` so check above. + +Of course the end result is a string with json representation and not a dictionary. + ## load By default when you query a table without prefetching related models, the ormar will still construct diff --git a/docs/releases.md b/docs/releases.md index b62f7bb..dae266c 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -2,14 +2,18 @@ ## ✨ Features -* Add `exclude_primary_keys` flag to `dict()` method that allows to exclude all primary key columns in the resulting dictionaru. -* Add `exclude_through_models` flag to `dict()` that allows excluding all through models from `ManyToMany` relations +* Add `exclude_primary_keys` flag to `dict()` method that allows to exclude all primary key columns in the resulting dictionaru. [#164](https://github.com/collerek/ormar/issues/164) +* Add `exclude_through_models` flag to `dict()` that allows excluding all through models from `ManyToMany` relations [#164](https://github.com/collerek/ormar/issues/164) ## 🐛 Fixes -* Remove default `None` option for `max_length` for `LargeBinary` field +* Remove default `None` option for `max_length` for `LargeBinary` field [#186](https://github.com/collerek/ormar/issues/186) * Remove default `None` option for `max_length` for `String` field +## 💬 Other + +* Provide a guide and samples of `dict()` parameters in the [docs](https://collerek.github.io/ormar/models/methods/) + # 0.10.6 ## ✨ Features diff --git a/ormar/models/newbasemodel.py b/ormar/models/newbasemodel.py index b12cc77..408ce02 100644 --- a/ormar/models/newbasemodel.py +++ b/ormar/models/newbasemodel.py @@ -1,4 +1,5 @@ import sys +import warnings from typing import ( AbstractSet, Any, @@ -774,6 +775,50 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass return dict_instance + def json( # type: ignore # noqa A003 + self, + *, + include: Union[Set, Dict] = None, + exclude: Union[Set, Dict] = None, + by_alias: bool = False, + skip_defaults: bool = None, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + encoder: Optional[Callable[[Any], Any]] = None, + exclude_primary_keys: bool = False, + exclude_through_models: bool = False, + **dumps_kwargs: Any, + ) -> str: + """ + Generate a JSON representation of the model, `include` and `exclude` + arguments as per `dict()`. + + `encoder` is an optional function to supply as `default` to json.dumps(), + other arguments as per `json.dumps()`. + """ + if skip_defaults is not None: # pragma: no cover + warnings.warn( + f'{self.__class__.__name__}.json(): "skip_defaults" is deprecated ' + f'and replaced by "exclude_unset"', + DeprecationWarning, + ) + exclude_unset = skip_defaults + encoder = cast(Callable[[Any], Any], encoder or self.__json_encoder__) + data = self.dict( + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + exclude_primary_keys=exclude_primary_keys, + exclude_through_models=exclude_through_models, + ) + if self.__custom_root_type__: # pragma: no cover + data = data["__root__"] + return self.__config__.json_dumps(data, default=encoder, **dumps_kwargs) + def update_from_dict(self, value_dict: Dict) -> "NewBaseModel": """ Updates self with values of fields passed in the dictionary. diff --git a/ormar/relations/querysetproxy.py b/ormar/relations/querysetproxy.py index f9a0806..bbe28bf 100644 --- a/ormar/relations/querysetproxy.py +++ b/ormar/relations/querysetproxy.py @@ -282,6 +282,9 @@ class QuerysetProxy(Generic[T]): Actual call delegated to QuerySet. + Passing args and/or kwargs is a shortcut and equals to calling + `filter(*args, **kwargs).first()`. + List of related models is cleared before the call. :param kwargs: @@ -300,7 +303,8 @@ class QuerysetProxy(Generic[T]): If no criteria set it will return the last row in db sorted by pk. - Passing a criteria is actually calling filter(**kwargs) method described below. + Passing args and/or kwargs is a shortcut and equals to calling + `filter(*args, **kwargs).get_or_none()`. If not match is found None will be returned. @@ -324,7 +328,8 @@ class QuerysetProxy(Generic[T]): If no criteria set it will return the last row in db sorted by pk. - Passing a criteria is actually calling filter(**kwargs) method described below. + Passing args and/or kwargs is a shortcut and equals to calling + `filter(*args, **kwargs).get()`. Actual call delegated to QuerySet. @@ -346,7 +351,8 @@ class QuerysetProxy(Generic[T]): """ Returns all rows from a database for given model for set filter options. - Passing kwargs is a shortcut and equals to calling `filter(**kwrags).all()`. + Passing args and/or kwargs is a shortcut and equals to calling + `filter(*args, **kwargs).all()`. If there are no rows meeting the criteria an empty list is returned. diff --git a/tests/test_exclude_include_dict/test_pydantic_dict_params.py b/tests/test_exclude_include_dict/test_pydantic_dict_params.py new file mode 100644 index 0000000..a6c33c9 --- /dev/null +++ b/tests/test_exclude_include_dict/test_pydantic_dict_params.py @@ -0,0 +1,97 @@ +from typing import List + +import databases +import pytest +import sqlalchemy + +import ormar +from tests.settings import DATABASE_URL + +metadata = sqlalchemy.MetaData() +database = databases.Database(DATABASE_URL, force_rollback=True) + + +class Category(ormar.Model): + class Meta: + tablename = "categories" + metadata = metadata + database = database + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100, default="Test", nullable=True) + visibility: bool = ormar.Boolean(default=True) + + +class Item(ormar.Model): + class Meta: + tablename = "items" + metadata = metadata + database = database + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + price: float = ormar.Float(default=9.99) + categories: List[Category] = ormar.ManyToMany(Category) + + +@pytest.fixture(autouse=True, scope="module") +def create_test_database(): + engine = sqlalchemy.create_engine(DATABASE_URL) + metadata.create_all(engine) + yield + metadata.drop_all(engine) + + +@pytest.mark.asyncio +async def test_exclude_default(): + async with database: + category = Category() + assert category.dict() == { + "id": None, + "items": [], + "name": "Test", + "visibility": True, + } + assert category.dict(exclude_defaults=True) == {"items": []} + + await category.save() + category2 = await Category.objects.get() + assert category2.dict() == { + "id": 1, + "items": [], + "name": "Test", + "visibility": True, + } + assert category2.dict(exclude_defaults=True) == {"id": 1, "items": []} + assert category2.json(exclude_defaults=True) == '{"id": 1, "items": []}' + + +@pytest.mark.asyncio +async def test_exclude_none(): + async with database: + category = Category(name=None) + assert category.dict() == { + "id": None, + "items": [], + "name": None, + "visibility": True, + } + assert category.dict(exclude_none=True) == {"items": [], "visibility": True} + + await category.save() + category2 = await Category.objects.get() + assert category2.dict() == { + "id": 1, + "items": [], + "name": None, + "visibility": True, + } + assert category2.dict(exclude_none=True) == { + "id": 1, + "items": [], + "visibility": True, + } + assert ( + category2.json(exclude_none=True) + == '{"id": 1, "visibility": true, "items": []}' + )