From 8e2992050a38fe46d37ebf800e8a6be94a21aa1f Mon Sep 17 00:00:00 2001 From: Arthur Pastel Date: Tue, 12 Dec 2023 21:04:59 +0100 Subject: [PATCH] feat: deprecate update in favor of model_update --- docs/engine.md | 4 +- .../async/patch_multiple_fields_dict.py | 2 +- .../async/patch_multiple_fields_pydantic.py | 2 +- .../engine/sync/patch_multiple_fields_dict.py | 4 +- .../sync/patch_multiple_fields_pydantic.py | 4 +- .../usage_fastapi/example_update.py | 2 +- docs/usage_fastapi.md | 6 +- odmantic/model.py | 55 +++++++++++++++---- tests/unit/test_model_logic.py | 42 +++++++------- 9 files changed, 79 insertions(+), 42 deletions(-) diff --git a/docs/engine.md b/docs/engine.md index 7bd5bbe4..a0f73c51 100644 --- a/docs/engine.md +++ b/docs/engine.md @@ -196,7 +196,7 @@ saving the instance. ### Patching multiple fields at once The easiest way to change multiple fields at once is to use the -[Model.update][odmantic.model._BaseODMModel.update] method. This method will take either a +[Model.model_update][odmantic.model._BaseODMModel.model_update] method. This method will take either a Pydantic object or a dictionary and update the matching fields of the instance. #### From a Pydantic Model @@ -222,7 +222,7 @@ Directly changing the primary field value as explained above is not possible and a `NotImplementedError` exception will be raised if you try to do so. The easiest way to change an instance primary field is to perform a local copy of the -instance using the [Model.copy][odmantic.model._BaseODMModel.copy] method. +instance using the [Model.copy][odmantic.model._BaseODMModel.model_copy] method. {{ async_sync_snippet("engine", "primary_key_update.py", hl_lines="18 20 22") }} diff --git a/docs/examples_src/engine/async/patch_multiple_fields_dict.py b/docs/examples_src/engine/async/patch_multiple_fields_dict.py index 58daa51e..02f15ed6 100644 --- a/docs/examples_src/engine/async/patch_multiple_fields_dict.py +++ b/docs/examples_src/engine/async/patch_multiple_fields_dict.py @@ -15,7 +15,7 @@ class Player(Model): # Create the patch dictionary containing the new values patch_object = {"name": "TheLittleOne", "game": "Starcraft II"} # Update the local instance -player_tlo.update(patch_object) +player_tlo.model_update(patch_object) print(repr(player_tlo)) #> Player(id=ObjectId(...), name='TheLittleOne', game='Starcraft II') diff --git a/docs/examples_src/engine/async/patch_multiple_fields_pydantic.py b/docs/examples_src/engine/async/patch_multiple_fields_pydantic.py index 9f30884c..b60511c8 100644 --- a/docs/examples_src/engine/async/patch_multiple_fields_pydantic.py +++ b/docs/examples_src/engine/async/patch_multiple_fields_pydantic.py @@ -24,7 +24,7 @@ class PatchPlayerSchema(BaseModel): # Create the patch object containing the new values patch_object = PatchPlayerSchema(name="TheLittleOne", game="Starcraft II") # Apply the patch to the instance -player_tlo.update(patch_object) +player_tlo.model_update(patch_object) print(repr(player_tlo)) #> Player(id=ObjectId(...), name='TheLittleOne', game='Starcraft II') diff --git a/docs/examples_src/engine/sync/patch_multiple_fields_dict.py b/docs/examples_src/engine/sync/patch_multiple_fields_dict.py index ecdb37b9..5d14d7f8 100644 --- a/docs/examples_src/engine/sync/patch_multiple_fields_dict.py +++ b/docs/examples_src/engine/sync/patch_multiple_fields_dict.py @@ -1,4 +1,4 @@ -from odmantic import SyncEngine, Model +from odmantic import Model, SyncEngine class Player(Model): @@ -15,7 +15,7 @@ class Player(Model): # Create the patch dictionary containing the new values patch_object = {"name": "TheLittleOne", "game": "Starcraft II"} # Update the local instance -player_tlo.update(patch_object) +player_tlo.model_update(patch_object) print(repr(player_tlo)) #> Player(id=ObjectId(...), name='TheLittleOne', game='Starcraft II') diff --git a/docs/examples_src/engine/sync/patch_multiple_fields_pydantic.py b/docs/examples_src/engine/sync/patch_multiple_fields_pydantic.py index 02540fb8..098baaf7 100644 --- a/docs/examples_src/engine/sync/patch_multiple_fields_pydantic.py +++ b/docs/examples_src/engine/sync/patch_multiple_fields_pydantic.py @@ -1,6 +1,6 @@ from pydantic import BaseModel -from odmantic import SyncEngine, Model +from odmantic import Model, SyncEngine class Player(Model): @@ -24,7 +24,7 @@ class PatchPlayerSchema(BaseModel): # Create the patch object containing the new values patch_object = PatchPlayerSchema(name="TheLittleOne", game="Starcraft II") # Apply the patch to the instance -player_tlo.update(patch_object) +player_tlo.model_update(patch_object) print(repr(player_tlo)) #> Player(id=ObjectId(...), name='TheLittleOne', game='Starcraft II') diff --git a/docs/examples_src/usage_fastapi/example_update.py b/docs/examples_src/usage_fastapi/example_update.py index 102be46f..a823bad5 100644 --- a/docs/examples_src/usage_fastapi/example_update.py +++ b/docs/examples_src/usage_fastapi/example_update.py @@ -34,6 +34,6 @@ async def update_tree_by_id(id: ObjectId, patch: TreePatchSchema): tree = await engine.find_one(Tree, Tree.id == id) if tree is None: raise HTTPException(404) - tree.update(patch) + tree.model_update(patch) await engine.save(tree) return tree diff --git a/docs/usage_fastapi.md b/docs/usage_fastapi.md index facede02..14c9ecc0 100644 --- a/docs/usage_fastapi.md +++ b/docs/usage_fastapi.md @@ -674,7 +674,7 @@ as a path parameter and the `TreePatchSchema` as the request body parameter. After all the parameters have been validated properly and the appropriate instance have been gathered, we can apply the modifications to the local instance using the -[Model.update][odmantic.model._BaseODMModel.update] method. By default, the update +[Model.model_update][odmantic.model._BaseODMModel.model_update] method. By default, the update method will replace each field values in the instance with the ones explicitely set in the patch object. Thus, the fields containing the None default values are not gonna be changed in the instance. @@ -714,7 +714,7 @@ We can then finish by saving and returning the updated tree. (More details: [pydantic #1223](https://github.com/samuelcolvin/pydantic/issues/1223#issuecomment-594632324){:target=blank_}, [pydantic: Required fields](https://pydantic-docs.helpmanual.io/usage/models/#required-fields){:target=blank_}) - By default [Model.update][odmantic.model._BaseODMModel.update], will not apply + By default [Model.model_update][odmantic.model._BaseODMModel.model_update], will not apply values from unset (not explicitely populated) fields. Since we don't want to allow explicitely set `None` values in the example, we used fields defined as `#!python c: int = None`. @@ -795,6 +795,6 @@ Some ideas that should arrive soon: document is not found an exception will be raised directly. - Implement the equivalent of MongoDB insert method to be able to create document without overwriting existing ones. -- Implement a Model.update method to update the model fields from a dictionnary or from +- Implement a Model.model_update method to update the model fields from a dictionnary or from a Pydantic schema. - Automatically generate CRUD endpoints directly from an ODMantic Model. diff --git a/odmantic/model.py b/odmantic/model.py index 91b44c79..a1238d33 100644 --- a/odmantic/model.py +++ b/odmantic/model.py @@ -617,6 +617,9 @@ def _post_copy_update(self: BaseT) -> None: value = getattr(self, field_name) value._post_copy_update() + @deprecated( + "update is deprecated, please use model_update instead", + ) def update( self, patch_object: Union[BaseModel, Dict[str, Any]], @@ -626,6 +629,25 @@ def update( exclude_unset: bool = True, exclude_defaults: bool = False, exclude_none: bool = False, + ) -> None: + self.model_update( + patch_object, + include=include, + exclude=exclude, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) + + def model_update( + self, + patch_object: Union[BaseModel, Dict[str, Any]], + *, + include: "IncEx" = None, + exclude: "IncEx" = None, + exclude_unset: bool = True, + exclude_defaults: bool = False, + exclude_none: bool = False, ) -> None: """Update instance fields from a Pydantic model or a dictionary. @@ -690,15 +712,6 @@ def __setattr__(self, name: str, value: Any) -> None: "doc is deprecated, please use model_dump_doc instead", ) def doc(self, include: Optional["AbstractSetIntStr"] = None) -> Dict[str, Any]: - """Generate a document representation of the instance (as a dictionary). - - Args: - include: field that should be included; if None, every fields will be - included - - Returns: - the document associated to the instance - """ return self.model_dump_doc(include=include) def model_dump_doc( @@ -954,6 +967,9 @@ def __indexes__(cls) -> Tuple[Union[ODMBaseIndex, pymongo.IndexModel], ...]: ) return tuple(indexes) + @deprecated( + "update is deprecated, please use model_update instead", + ) def update( self, patch_object: Union[BaseModel, Dict[str, Any]], @@ -963,6 +979,25 @@ def update( exclude_unset: bool = True, exclude_defaults: bool = False, exclude_none: bool = False, + ) -> None: + return self.model_update( + patch_object, + include=include, + exclude=exclude, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) + + def model_update( + self, + patch_object: Union[BaseModel, Dict[str, Any]], + *, + include: "IncEx" = None, + exclude: "IncEx" = None, + exclude_unset: bool = True, + exclude_defaults: bool = False, + exclude_none: bool = False, ) -> None: is_primary_field_in_patch = ( isinstance(patch_object, BaseModel) @@ -981,7 +1016,7 @@ def update( "Updating the primary key is not supported. " "See the copy method if you want to modify the primary field." ) - return super().update( + return super().model_update( patch_object, include=include, exclude=exclude, diff --git a/tests/unit/test_model_logic.py b/tests/unit/test_model_logic.py index cdd99cab..65995ea0 100644 --- a/tests/unit/test_model_logic.py +++ b/tests/unit/test_model_logic.py @@ -488,28 +488,28 @@ class Update(BaseModel): first_name: str update_obj = Update(first_name=UPDATED_NAME) - instance_to_update.update(update_obj) + instance_to_update.model_update(update_obj) assert instance_to_update.first_name == UPDATED_NAME assert instance_to_update.last_name == INITIAL_LAST_NAME def test_update_dictionary(instance_to_update): update_obj = {"first_name": UPDATED_NAME} - instance_to_update.update(update_obj) + instance_to_update.model_update(update_obj) assert instance_to_update.first_name == UPDATED_NAME assert instance_to_update.last_name == INITIAL_LAST_NAME def test_update_include(instance_to_update): update_obj = {"first_name": UPDATED_NAME} - instance_to_update.update(update_obj, include=set()) + instance_to_update.model_update(update_obj, include=set()) assert instance_to_update.first_name == INITIAL_FIRST_NAME assert instance_to_update.last_name == INITIAL_LAST_NAME def test_update_exclude(instance_to_update): update_obj = {"first_name": UPDATED_NAME} - instance_to_update.update(update_obj, exclude={"first_name"}) + instance_to_update.model_update(update_obj, exclude={"first_name"}) assert instance_to_update.first_name == INITIAL_FIRST_NAME assert instance_to_update.last_name == INITIAL_LAST_NAME @@ -520,7 +520,7 @@ class Update(BaseModel): last_name: Optional[str] update_obj = Update(first_name=UPDATED_NAME, last_name=None) - instance_to_update.update(update_obj, exclude_unset=False, exclude_none=True) + instance_to_update.model_update(update_obj, exclude_unset=False, exclude_none=True) assert instance_to_update.first_name == UPDATED_NAME assert instance_to_update.last_name == INITIAL_LAST_NAME @@ -533,13 +533,15 @@ class Update(BaseModel): last_name: str = UPDATED_NAME update_obj = Update() - instance_to_update.update(update_obj, exclude_unset=False, exclude_defaults=True) + instance_to_update.model_update( + update_obj, exclude_unset=False, exclude_defaults=True + ) assert instance_to_update == initial_instance def test_update_exclude_over_include(instance_to_update): update_obj = {"first_name": UPDATED_NAME} - instance_to_update.update( + instance_to_update.model_update( update_obj, include={"first_name"}, exclude={"first_name"} ) assert instance_to_update.first_name == INITIAL_FIRST_NAME @@ -553,7 +555,7 @@ class M(Model): instance = M(f=12) update_obj = {"f": "aaa"} with pytest.raises(ValidationError): - instance.update(update_obj) + instance.model_update(update_obj) def test_update_model_undue_update_fields(): @@ -562,7 +564,7 @@ class M(Model): instance = M(f=12) update_obj = {"not_in_model": "aaa"} - instance.update(update_obj) + instance.model_update(update_obj) def test_update_pydantic_unset_update_fields(): @@ -576,7 +578,7 @@ class M(Model): instance = M(f=0) update_obj = P() - instance.update(update_obj) + instance.model_update(update_obj) assert instance.f != UPDATEED_VALUE @@ -591,7 +593,7 @@ class M(Model): instance = M(f=0) update_obj = P() - instance.update(update_obj, exclude_unset=False) + instance.model_update(update_obj, exclude_unset=False) assert instance.f == UPDATEED_VALUE @@ -600,7 +602,7 @@ class E(EmbeddedModel): f: int instance = E(f=12) - instance.update({"f": 15}) + instance.model_update({"f": 15}) assert instance.f == 15 @@ -615,7 +617,7 @@ class M(Model): r1 = R(f=1) instance = M(r=r0) - instance.update({"r": r1}) + instance.model_update({"r": r1}) assert instance.r.f == r1.f assert instance.r == r1 @@ -626,7 +628,7 @@ class M(Model): instance = M(f=12) update_obj = {"f": "12"} - instance.update(update_obj) + instance.model_update(update_obj) assert isinstance(instance.f, int) @@ -644,7 +646,7 @@ def set_area(cls, v): r = Rectangle(width=1, height=1) assert r.area == 1 r.__fields_modified__.clear() - r.update({"width": 5}) + r.model_update({"width": 5}) assert r.area == 5 assert "area" in r.__fields_modified__ @@ -666,7 +668,7 @@ def set_area(cls, v): r = Rectangle(width=1, height=1) assert r.area == 1 r.__fields_modified__.clear() - r.update({"width": 5}) + r.model_update({"width": 5}) assert r.area == 5 assert "area" in r.__fields_modified__ @@ -678,7 +680,7 @@ class M(Model): m = M(alternate_id=0, f=0) with pytest.raises(ValueError, match="Updating the primary key is not supported"): - m.update({"alternate_id": 1}) + m.model_update({"alternate_id": 1}) @pytest.mark.parametrize( @@ -695,7 +697,7 @@ class M(Model): f: int m = M(alternate_id=0, f=0) - m.update({"alternate_id": 1}, **update_kwargs) + m.model_update({"alternate_id": 1}, **update_kwargs) assert m.f == 0 and m.alternate_id == 0, "instance should be unchanged" @@ -710,7 +712,7 @@ class UpdateObject(BaseModel): alternate_id: int with pytest.raises(ValueError, match="Updating the primary key is not supported"): - m.update(UpdateObject(alternate_id=1)) + m.model_update(UpdateObject(alternate_id=1)) @pytest.mark.parametrize( @@ -730,5 +732,5 @@ class UpdateObject(BaseModel): alternate_id: int m = M(alternate_id=0, f=0) - m.update(UpdateObject(alternate_id=1), **update_kwargs) + m.model_update(UpdateObject(alternate_id=1), **update_kwargs) assert m.f == 0 and m.alternate_id == 0, "instance should be unchanged"