Skip to content

Commit

Permalink
feat: deprecate update in favor of model_update
Browse files Browse the repository at this point in the history
  • Loading branch information
art049 committed Dec 13, 2023
1 parent f25935a commit 8e29920
Show file tree
Hide file tree
Showing 9 changed files with 79 additions and 42 deletions.
4 changes: 2 additions & 2 deletions docs/engine.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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") }}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
4 changes: 2 additions & 2 deletions docs/examples_src/engine/sync/patch_multiple_fields_dict.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from odmantic import SyncEngine, Model
from odmantic import Model, SyncEngine


class Player(Model):
Expand All @@ -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')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from pydantic import BaseModel

from odmantic import SyncEngine, Model
from odmantic import Model, SyncEngine


class Player(Model):
Expand All @@ -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')
Expand Down
2 changes: 1 addition & 1 deletion docs/examples_src/usage_fastapi/example_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 3 additions & 3 deletions docs/usage_fastapi.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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`.
Expand Down Expand Up @@ -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.
- <del>Implement a Model.update method to update the model fields from a dictionnary or from
- <del>Implement a Model.model_update method to update the model fields from a dictionnary or from
a Pydantic schema.</del>
- Automatically generate CRUD endpoints directly from an ODMantic Model.
55 changes: 45 additions & 10 deletions odmantic/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]],
Expand All @@ -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.
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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]],
Expand All @@ -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)
Expand All @@ -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,
Expand Down
42 changes: 22 additions & 20 deletions tests/unit/test_model_logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand All @@ -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
Expand All @@ -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():
Expand All @@ -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():
Expand All @@ -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


Expand All @@ -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


Expand All @@ -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


Expand All @@ -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

Expand All @@ -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)


Expand All @@ -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__

Expand All @@ -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__

Expand All @@ -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(
Expand All @@ -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"


Expand All @@ -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(
Expand All @@ -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"

0 comments on commit 8e29920

Please sign in to comment.