diff --git a/README.md b/README.md index 89d3e30..79d96f9 100644 --- a/README.md +++ b/README.md @@ -587,7 +587,7 @@ metadata.drop_all(engine) * `create(**kwargs): -> Model` * `get(*args, **kwargs): -> Model` * `get_or_none(*args, **kwargs): -> Optional[Model]` -* `get_or_create(*args, **kwargs) -> Model` +* `get_or_create(_defaults: Optional[Dict[str, Any]] = None, *args, **kwargs) -> Tuple[Model, bool]` * `first(*args, **kwargs): -> Model` * `update(each: bool = False, **kwargs) -> int` * `update_or_create(**kwargs) -> Model` diff --git a/docs/api/query-set/query-set.md b/docs/api/query-set/query-set.md index 36a6721..1439dd4 100644 --- a/docs/api/query-set/query-set.md +++ b/docs/api/query-set/query-set.md @@ -753,7 +753,7 @@ below. #### get\_or\_create ```python - | async get_or_create(*args: Any, **kwargs: Any) -> "T" + | async get_or_create(_defaults: Optional[Dict[str, Any]] = None, *args: Any, **kwargs: Any) -> Tuple["T", bool] ``` Combination of create and get methods. diff --git a/docs/api/relations/queryset-proxy.md b/docs/api/relations/queryset-proxy.md index f36f8f4..def1958 100644 --- a/docs/api/relations/queryset-proxy.md +++ b/docs/api/relations/queryset-proxy.md @@ -404,7 +404,7 @@ each=True flag to affect whole table. #### get\_or\_create ```python - | async get_or_create(*args: Any, **kwargs: Any) -> "T" + | async get_or_create(_defaults: Optional[Dict[str, Any]] = None, *args: Any, **kwargs: Any) -> Tuple["T", bool] ``` Combination of create and get methods. diff --git a/docs/index.md b/docs/index.md index 740c158..fa6ce96 100644 --- a/docs/index.md +++ b/docs/index.md @@ -596,7 +596,7 @@ metadata.drop_all(engine) * `create(**kwargs): -> Model` * `get(*args, **kwargs): -> Model` * `get_or_none(*args, **kwargs): -> Optional[Model]` -* `get_or_create(*args, **kwargs) -> Model` +* `get_or_create(_defaults: Optional[Dict[str, Any]] = None, *args, **kwargs) -> Tuple[Model, bool]` * `first(*args, **kwargs): -> Model` * `update(each: bool = False, **kwargs) -> int` * `update_or_create(**kwargs) -> Model` diff --git a/docs/queries/create.md b/docs/queries/create.md index 560ccae..76b5d2e 100644 --- a/docs/queries/create.md +++ b/docs/queries/create.md @@ -3,7 +3,7 @@ Following methods allow you to insert data into the database. * `create(**kwargs) -> Model` -* `get_or_create(**kwargs) -> Model` +* `get_or_create(_defaults: Optional[Dict[str, Any]] = None, **kwargs) -> Tuple[Model, bool]` * `update_or_create(**kwargs) -> Model` * `bulk_create(objects: List[Model]) -> None` @@ -16,7 +16,7 @@ Following methods allow you to insert data into the database. * `QuerysetProxy` * `QuerysetProxy.create(**kwargs)` method - * `QuerysetProxy.get_or_create(**kwargs)` method + * `QuerysetProxy.get_or_create(_defaults: Optional[Dict[str, Any]] = None, **kwargs)` method * `QuerysetProxy.update_or_create(**kwargs)` method ## create @@ -56,12 +56,15 @@ await malibu.save() ## get_or_create -`get_or_create(**kwargs) -> Model` +`get_or_create(_defaults: Optional[Dict[str, Any]] = None, **kwargs) -> Tuple[Model, bool]` Combination of create and get methods. Tries to get a row meeting the criteria and if `NoMatch` exception is raised it creates -a new one with given kwargs. +a new one with given kwargs and _defaults. + +When `_defaults` dictionary is provided the values set in `_defaults` will **always** be set, including overwriting explicitly provided values. +i.e. `get_or_create(_defaults: {"title": "I win"}, title="never used")` will always use "I win" as title whether you provide your own value in kwargs or not. ```python class Album(ormar.Model): @@ -72,12 +75,17 @@ class Album(ormar.Model): id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) + year: int = ormar.Integer() ``` ```python -album = await Album.objects.get_or_create(name='The Cat') +album, created = await Album.objects.get_or_create(name='The Cat', _defaults={"year": 1999}) +assert created is True +assert album.name == "The Cat" +assert album.year == 1999 # object is created as it does not exist -album2 = await Album.objects.get_or_create(name='The Cat') +album2, created = await Album.objects.get_or_create(name='The Cat') +assert created is False assert album == album2 # return True as the same db row is returned ``` @@ -182,4 +190,4 @@ from other side of the relation. [models-save]: ../models/methods.md#save [models-upsert]: ../models/methods.md#upsert [models-save-related]: ../models/methods.md#save_related -[querysetproxy]: ../relations/queryset-proxy.md \ No newline at end of file +[querysetproxy]: ../relations/queryset-proxy.md diff --git a/docs/queries/filter-and-sort.md b/docs/queries/filter-and-sort.md index 1f778d9..ac7aee9 100644 --- a/docs/queries/filter-and-sort.md +++ b/docs/queries/filter-and-sort.md @@ -6,7 +6,7 @@ You can use following methods to filter the data (sql where clause). * `exclude(*args, **kwargs) -> QuerySet` * `get(*args, **kwargs) -> Model` * `get_or_none(*args, **kwargs) -> Optional[Model]` -* `get_or_create(*args, **kwargs) -> Model` +* `get_or_create(_defaults: Optional[Dict[str, Any]] = None, *args, **kwargs) -> Tuple[Model, bool]` * `all(*args, **kwargs) -> List[Optional[Model]]` @@ -15,7 +15,7 @@ You can use following methods to filter the data (sql where clause). * `QuerysetProxy.exclude(*args, **kwargs)` method * `QuerysetProxy.get(*args, **kwargs)` method * `QuerysetProxy.get_or_none(*args, **kwargs)` method - * `QuerysetProxy.get_or_create(*args, **kwargs)` method + * `QuerysetProxy.get_or_create(_defaults: Optional[Dict[str, Any]] = None, *args, **kwargs)` method * `QuerysetProxy.all(*args, **kwargs)` method And following methods to sort the data (sql order by clause). @@ -598,7 +598,7 @@ Exact equivalent of get described above but instead of raising the exception ret ## get_or_create -`get_or_create(*args, **kwargs) -> Model` +`get_or_create(_defaults: Optional[Dict[str, Any]] = None, *args, **kwargs) -> Tuple[Model, bool]` Combination of create and get methods. diff --git a/docs/queries/index.md b/docs/queries/index.md index 9389b97..8d8381c 100644 --- a/docs/queries/index.md +++ b/docs/queries/index.md @@ -24,7 +24,7 @@ To read more about any specific section or function please refer to the details ###[Insert data into database](./create.md) * `create(**kwargs) -> Model` -* `get_or_create(**kwargs) -> Model` +* `get_or_create(_defaults: Optional[Dict[str, Any]] = None, **kwargs) -> Tuple[Model, bool]` * `update_or_create(**kwargs) -> Model` * `bulk_create(objects: List[Model]) -> None` @@ -37,7 +37,7 @@ To read more about any specific section or function please refer to the details * `QuerysetProxy` * `QuerysetProxy.create(**kwargs)` method - * `QuerysetProxy.get_or_create(**kwargs)` method + * `QuerysetProxy.get_or_create(_defaults: Optional[Dict[str, Any]] = None, **kwargs)` method * `QuerysetProxy.update_or_create(**kwargs)` method !!!tip @@ -47,7 +47,7 @@ To read more about any specific section or function please refer to the details * `get(**kwargs) -> Model` * `get_or_none(**kwargs) -> Optional[Model]` -* `get_or_create(**kwargs) -> Model` +* `get_or_create(_defaults: Optional[Dict[str, Any]] = None, **kwargs) -> Tuple[Model, bool]` * `first() -> Model` * `all(**kwargs) -> List[Optional[Model]]` @@ -59,7 +59,7 @@ To read more about any specific section or function please refer to the details * `QuerysetProxy` * `QuerysetProxy.get(**kwargs)` method * `QuerysetProxy.get_or_none(**kwargs)` method - * `QuerysetProxy.get_or_create(**kwargs)` method + * `QuerysetProxy.get_or_create(_defaults: Optional[Dict[str, Any]] = None, **kwargs)` method * `QuerysetProxy.first()` method * `QuerysetProxy.all(**kwargs)` method @@ -140,7 +140,7 @@ Instead of ormar models return raw data in form list of dictionaries or tuples. * `order_by(columns:Union[List, str]) -> QuerySet` * `get(**kwargs) -> Model` * `get_or_none(**kwargs) -> Optional[Model]` -* `get_or_create(**kwargs) -> Model` +* `get_or_create(_defaults: Optional[Dict[str, Any]] = None, **kwargs) -> Tuple[Model, bool]` * `all(**kwargs) -> List[Optional[Model]]` @@ -150,7 +150,7 @@ Instead of ormar models return raw data in form list of dictionaries or tuples. * `QuerysetProxy.order_by(columns:Union[List, str])` method * `QuerysetProxy.get(**kwargs)` method * `QuerysetProxy.get_or_none(**kwargs)` method - * `QuerysetProxy.get_or_create(**kwargs)` method + * `QuerysetProxy.get_or_create(_defaults: Optional[Dict[str, Any]] = None, **kwargs)` method * `QuerysetProxy.all(**kwargs)` method !!!tip diff --git a/docs/queries/read.md b/docs/queries/read.md index 17972b5..9fa6794 100644 --- a/docs/queries/read.md +++ b/docs/queries/read.md @@ -3,7 +3,7 @@ Following methods allow you to load data from the database. * `get(*args, **kwargs) -> Model` -* `get_or_create(*args, **kwargs) -> Model` +* `get_or_create(_defaults: Optional[Dict[str, Any]] = None, *args, **kwargs) -> Tuple[Model, bool]` * `first(*args, **kwargs) -> Model` * `all(*args, **kwargs) -> List[Optional[Model]]` @@ -14,7 +14,7 @@ Following methods allow you to load data from the database. * `QuerysetProxy` * `QuerysetProxy.get(*args, **kwargs)` method - * `QuerysetProxy.get_or_create(*args, **kwargs)` method + * `QuerysetProxy.get_or_create(_defaults: Optional[Dict[str, Any]] = None, *args, **kwargs)` method * `QuerysetProxy.first(*args, **kwargs)` method * `QuerysetProxy.all(*args, **kwargs)` method @@ -64,12 +64,12 @@ Exact equivalent of get described above but instead of raising the exception ret ## get_or_create -`get_or_create(*args, **kwargs) -> Model` +`get_or_create(_defaults: Optional[Dict[str, Any]] = None, *args, **kwargs) -> Tuple[Model, bool]` Combination of create and get methods. Tries to get a row meeting the criteria and if `NoMatch` exception is raised it creates -a new one with given kwargs. +a new one with given kwargs and _defaults. ```python class Album(ormar.Model): @@ -80,12 +80,17 @@ class Album(ormar.Model): id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) + year: int = ormar.Integer() ``` ```python -album = await Album.objects.get_or_create(name='The Cat') +album, created = await Album.objects.get_or_create(name='The Cat', _defaults={"year": 1999}) +assert created is True +assert album.name == "The Cat" +assert album.year == 1999 # object is created as it does not exist -album2 = await Album.objects.get_or_create(name='The Cat') +album2, created = await Album.objects.get_or_create(name='The Cat') +assert created is False assert album == album2 # return True as the same db row is returned ``` diff --git a/docs/relations/queryset-proxy.md b/docs/relations/queryset-proxy.md index 0150913..4d167b6 100644 --- a/docs/relations/queryset-proxy.md +++ b/docs/relations/queryset-proxy.md @@ -56,9 +56,9 @@ assert post.categories[0] == news ### get_or_create -`get_or_create(**kwargs) -> Model` +`get_or_create(_defaults: Optional[Dict[str, Any]] = None, **kwargs) -> Tuple[Model, bool]` -Tries to get a row meeting the criteria and if NoMatch exception is raised it creates a new one with given kwargs. +Tries to get a row meeting the criteria and if NoMatch exception is raised it creates a new one with given kwargs and _defaults. !!!tip Read more in queries documentation [get_or_create][get_or_create] @@ -129,7 +129,7 @@ await post.categories.create( ### get_or_create -`get_or_create(**kwargs) -> Model` +`get_or_create(_defaults: Optional[Dict[str, Any]] = None, **kwargs) -> Tuple[Model, bool]` Tries to get a row meeting the criteria and if NoMatch exception is raised it creates a new one with given kwargs. diff --git a/ormar/protocols/queryset_protocol.py b/ormar/protocols/queryset_protocol.py index fe58ca1..a2c1d58 100644 --- a/ormar/protocols/queryset_protocol.py +++ b/ormar/protocols/queryset_protocol.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, Optional, Sequence, Set, TYPE_CHECKING, Union +from typing import Any, Dict, List, Optional, Sequence, Set, TYPE_CHECKING, Tuple, Union try: from typing import Protocol @@ -55,7 +55,11 @@ class QuerySetProtocol(Protocol): # pragma: nocover async def update(self, each: bool = False, **kwargs: Any) -> int: ... - async def get_or_create(self, **kwargs: Any) -> "Model": + async def get_or_create( + self, + _defaults: Optional[Dict[str, Any]] = None, + **kwargs: Any, + ) -> Tuple["Model", bool]: ... async def update_or_create(self, **kwargs: Any) -> "Model": diff --git a/ormar/queryset/queryset.py b/ormar/queryset/queryset.py index 0f3af12..542881f 100644 --- a/ormar/queryset/queryset.py +++ b/ormar/queryset/queryset.py @@ -981,26 +981,34 @@ class QuerySet(Generic[T]): self.check_single_result_rows_count(processed_rows) return processed_rows[0] # type: ignore - async def get_or_create(self, *args: Any, **kwargs: Any) -> "T": + async def get_or_create( + self, + _defaults: Optional[Dict[str, Any]] = None, + *args: Any, + **kwargs: Any, + ) -> Tuple["T", bool]: """ Combination of create and get methods. Tries to get a row meeting the criteria for kwargs and if `NoMatch` exception is raised - it creates a new one with given kwargs. + it creates a new one with given kwargs and _defaults. Passing a criteria is actually calling filter(*args, **kwargs) method described below. :param kwargs: fields names and proper value types :type kwargs: Any - :return: returned or created Model - :rtype: Model + :param _defaults: default values for creating object + :type _defaults: Optional[Dict[str, Any]] + :return: model instance and a boolean + :rtype: Tuple("T", bool) """ try: - return await self.get(*args, **kwargs) + return await self.get(*args, **kwargs), False except NoMatch: - return await self.create(**kwargs) + _defaults = _defaults or {} + return await self.create(**{**kwargs, **_defaults}), True async def update_or_create(self, **kwargs: Any) -> "T": """ diff --git a/ormar/relations/querysetproxy.py b/ormar/relations/querysetproxy.py index 791d950..fad3e2d 100644 --- a/ormar/relations/querysetproxy.py +++ b/ormar/relations/querysetproxy.py @@ -9,6 +9,7 @@ from typing import ( # noqa: I100, I201 Sequence, Set, TYPE_CHECKING, + Tuple, Type, TypeVar, Union, @@ -488,23 +489,31 @@ class QuerysetProxy(Generic[T]): ) return len(children) - async def get_or_create(self, *args: Any, **kwargs: Any) -> "T": + async def get_or_create( + self, + _defaults: Optional[Dict[str, Any]] = None, + *args: Any, + **kwargs: Any, + ) -> Tuple["T", bool]: """ Combination of create and get methods. Tries to get a row meeting the criteria fro kwargs and if `NoMatch` exception is raised - it creates a new one with given kwargs. + it creates a new one with given kwargs and _defaults. :param kwargs: fields names and proper value types :type kwargs: Any - :return: returned or created Model - :rtype: Model + :param _defaults: default values for creating object + :type _defaults: Optional[Dict[str, Any]] + :return: model instance and a boolean + :rtype: Tuple("T", bool) """ try: - return await self.get(*args, **kwargs) - except ormar.NoMatch: - return await self.create(**kwargs) + return await self.get(*args, **kwargs), False + except NoMatch: + _defaults = _defaults or {} + return await self.create(**{**kwargs, **_defaults}), True async def update_or_create(self, **kwargs: Any) -> "T": """ diff --git a/tests/test_fastapi/test_m2m_forwardref.py b/tests/test_fastapi/test_m2m_forwardref.py index bfccaef..c75791a 100644 --- a/tests/test_fastapi/test_m2m_forwardref.py +++ b/tests/test_fastapi/test_m2m_forwardref.py @@ -102,7 +102,7 @@ def test_payload(): "native_name": "Thailand", } resp = client.post("/", json=payload, headers={"application-type": "json"}) - print(resp.content) + # print(resp.content) assert resp.status_code == 201 resp_country = Country(**resp.json()) diff --git a/tests/test_model_definition/test_aliases.py b/tests/test_model_definition/test_aliases.py index bb6b40f..d403304 100644 --- a/tests/test_model_definition/test_aliases.py +++ b/tests/test_model_definition/test_aliases.py @@ -157,15 +157,17 @@ async def test_bulk_operations_and_fields(): async def test_working_with_aliases_get_or_create(): async with database: async with database.transaction(force_rollback=True): - artist = await Artist.objects.get_or_create( + artist, created = await Artist.objects.get_or_create( first_name="Teddy", last_name="Bear", born_year=2020 ) assert artist.pk is not None + assert created is True - artist2 = await Artist.objects.get_or_create( + artist2, created = await Artist.objects.get_or_create( first_name="Teddy", last_name="Bear", born_year=2020 ) assert artist == artist2 + assert created is False art3 = artist2.dict() art3["born_year"] = 2019 diff --git a/tests/test_model_definition/test_save_status.py b/tests/test_model_definition/test_save_status.py index 9762810..b104072 100644 --- a/tests/test_model_definition/test_save_status.py +++ b/tests/test_model_definition/test_save_status.py @@ -195,12 +195,18 @@ async def test_queryset_methods(): comps = await Company.objects.all() assert [comp.saved for comp in comps] - comp2 = await Company.objects.get_or_create(name="Banzai_new", founded=2001) + comp2, created = await Company.objects.get_or_create( + name="Banzai_new", founded=2001 + ) assert comp2.saved + assert created is True - comp3 = await Company.objects.get_or_create(name="Banzai", founded=1988) + comp3, created = await Company.objects.get_or_create( + name="Banzai", founded=1988 + ) assert comp3.saved assert comp3.pk == comp.pk + assert created is False update_dict = comp.dict() update_dict["founded"] = 2010 diff --git a/tests/test_queries/test_queryproxy_on_m2m_models.py b/tests/test_queries/test_queryproxy_on_m2m_models.py index a91c4f8..b9ac07e 100644 --- a/tests/test_queries/test_queryproxy_on_m2m_models.py +++ b/tests/test_queries/test_queryproxy_on_m2m_models.py @@ -102,20 +102,25 @@ async def test_queryset_methods(): await post.categories.add(news) await post.categories.add(breaking) - category = await post.categories.get_or_create(name="News") + category, created = await post.categories.get_or_create(name="News") assert category == news assert len(post.categories) == 1 + assert created is False - category = await post.categories.get_or_create(name="Breaking News") + category, created = await post.categories.get_or_create( + name="Breaking News" + ) assert category != breaking assert category.pk is not None assert len(post.categories) == 2 + assert created is True await post.categories.update_or_create(pk=category.pk, name="Urgent News") assert len(post.categories) == 2 - cat = await post.categories.get_or_create(name="Urgent News") + cat, created = await post.categories.get_or_create(name="Urgent News") assert cat.pk == category.pk assert len(post.categories) == 1 + assert created is False await post.categories.remove(cat) await cat.delete() diff --git a/tests/test_queries/test_queryset_level_methods.py b/tests/test_queries/test_queryset_level_methods.py index 7176010..fa9a6fb 100644 --- a/tests/test_queries/test_queryset_level_methods.py +++ b/tests/test_queries/test_queryset_level_methods.py @@ -166,17 +166,18 @@ async def test_delete_and_update(): @pytest.mark.asyncio async def test_get_or_create(): async with database: - tom = await Book.objects.get_or_create( + tom, created = await Book.objects.get_or_create( title="Volume I", author="Anonymous", genre="Fiction" ) assert await Book.objects.count() == 1 + assert created is True - assert ( - await Book.objects.get_or_create( - title="Volume I", author="Anonymous", genre="Fiction" - ) - == tom + second_tom, created = await Book.objects.get_or_create( + title="Volume I", author="Anonymous", genre="Fiction" ) + + assert second_tom.pk == tom.pk + assert created is False assert await Book.objects.count() == 1 assert await Book.objects.create( @@ -188,6 +189,42 @@ async def test_get_or_create(): ) +@pytest.mark.asyncio +async def test_get_or_create_with_defaults(): + async with database: + book, created = await Book.objects.get_or_create( + title="Nice book", _defaults={"author": "Mojix", "genre": "Historic"} + ) + assert created is True + assert book.author == "Mojix" + assert book.title == "Nice book" + assert book.genre == "Historic" + + book2, created = await Book.objects.get_or_create( + author="Mojix", _defaults={"title": "Book2"} + ) + assert created is False + assert book2 == book + assert book2.title == "Nice book" + assert book2.author == "Mojix" + assert book2.genre == "Historic" + assert await Book.objects.count() == 1 + + book, created = await Book.objects.get_or_create( + title="doesn't exist", + _defaults={"title": "overwritten", "author": "Mojix", "genre": "Historic"}, + ) + assert created is True + assert book.title == "overwritten" + + book2, created = await Book.objects.get_or_create( + title="overwritten", _defaults={"title": "doesn't work"} + ) + assert created is False + assert book2.title == "overwritten" + assert book2 == book + + @pytest.mark.asyncio async def test_update_or_create(): async with database: diff --git a/tests/test_queries/test_reverse_fk_queryset.py b/tests/test_queries/test_reverse_fk_queryset.py index 80193bc..25f54a0 100644 --- a/tests/test_queries/test_reverse_fk_queryset.py +++ b/tests/test_queries/test_reverse_fk_queryset.py @@ -87,22 +87,26 @@ async def test_quering_by_reverse_fk(): assert await album.tracks.exists() assert await album.tracks.count() == 3 - track = await album.tracks.get_or_create( + track, created = await album.tracks.get_or_create( title="The Bird", position=1, play_count=30 ) assert track == track1 + assert created is False assert len(album.tracks) == 1 - track = await album.tracks.get_or_create( - title="The Bird2", position=4, play_count=5 + track, created = await album.tracks.get_or_create( + title="The Bird2", _defaults={"position": 4, "play_count": 5} ) assert track != track1 + assert created is True assert track.pk is not None + assert track.position == 4 and track.play_count == 5 assert len(album.tracks) == 2 await album.tracks.update_or_create(pk=track.pk, play_count=50) assert len(album.tracks) == 2 - track = await album.tracks.get_or_create(title="The Bird2") + track, created = await album.tracks.get_or_create(title="The Bird2") + assert created is False assert track.play_count == 50 assert len(album.tracks) == 1