Skip to content

Commit 354778d

Browse files
committed
Make sync() populate objects with same .id with the same set of fields
* Currently sync() will track objects by their Python address, so if we have `alice1` with just `name` property, and we have `alice2` with the same `id` but only `email` property, after sync() they would only have their `.name` and `.email` properties updated respectively. We are changing sync() to populate them with the same data, so after sync() they would both have `.name` and `.email` attributes and their `.__dict__` would be equal. We do this because of computed backlinks (although I think we can find more motivation for this if we think hard). If `alice1` and `alice2` have the same `.id` and `alice1` was added to the `UserGroup.users` multi link, we want *both* `alice1.groups` and `alice2.groups` computeds to point back to the new user group they are member of. We also want to make it irrelevant if it was `alice1` or `alice2` added to `UserGroup.users` -- after sync() it shouldn't matter as they will both reflect the same data and be completely equivalent (albeit still two distinct Python objects, with distinct link set & multi props & mutable props collections!) * LinkSet / LinkWithPropsSet / TrackedList get `__copy__` and `__deepcopy__` methods
1 parent b887b84 commit 354778d

File tree

7 files changed

+380
-169
lines changed

7 files changed

+380
-169
lines changed

gel/_internal/_qbmodel/_abstract/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@
4040
get_proxy_linkprops,
4141
is_proxy_linked,
4242
copy_or_ref_lprops,
43+
reconcile_link,
44+
reconcile_proxy_link,
4345
)
4446

4547
from ._link_set import (
@@ -184,4 +186,6 @@
184186
"is_generic_type",
185187
"is_proxy_linked",
186188
"maybe_get_protocol_for_py_type",
189+
"reconcile_link",
190+
"reconcile_proxy_link",
187191
)

gel/_internal/_qbmodel/_abstract/_descriptors.py

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@
4141

4242
if TYPE_CHECKING:
4343
import types
44-
from collections.abc import Sequence, Set as AbstractSet
44+
import uuid
45+
46+
from collections.abc import Sequence, Set as AbstractSet, Iterable
47+
4548
from ._link_set import (
4649
AbstractLinkSet,
4750
ComputedLinkSet,
@@ -740,6 +743,63 @@ def copy_or_ref_lprops(lp: _LM_co) -> _LM_co: # type: ignore [misc]
740743
return lp
741744

742745

746+
def reconcile_link(
747+
*,
748+
existing: _MT_co | None,
749+
refetched: _MT_co, # type: ignore [misc]
750+
existing_objects: dict[uuid.UUID, _MT_co | Iterable[_MT_co]],
751+
new_objects: dict[uuid.UUID, _MT_co],
752+
) -> _MT_co:
753+
if existing is not None:
754+
return existing
755+
756+
obj_id: uuid.UUID = ll_getattr(refetched, "id")
757+
758+
if (link_to := existing_objects.get(obj_id)) is not None:
759+
if isinstance(link_to, AbstractGelModel):
760+
return link_to
761+
else:
762+
return next(iter(link_to))
763+
764+
return new_objects[obj_id]
765+
766+
767+
def reconcile_proxy_link(
768+
*,
769+
existing: AbstractGelProxyModel[_MT_co, _LM_co] | None,
770+
refetched: AbstractGelProxyModel[_MT_co, _LM_co],
771+
existing_objects: dict[uuid.UUID, _MT_co | Iterable[_MT_co]],
772+
new_objects: dict[uuid.UUID, _MT_co],
773+
) -> AbstractGelProxyModel[_MT_co, _LM_co]:
774+
if existing is not None:
775+
# `refetched` will have newly refetched __linkprops__,
776+
# so copy them
777+
existing.__gel_replace_linkprops__(
778+
copy_or_ref_lprops(refetched.__linkprops__)
779+
)
780+
return existing
781+
782+
obj_id: uuid.UUID = ll_getattr(refetched.without_linkprops(), "id")
783+
784+
if (_link_to := existing_objects.get(obj_id)) is not None:
785+
if isinstance(_link_to, AbstractGelModel):
786+
link_to = _link_to
787+
else:
788+
link_to = next(iter(_link_to))
789+
else:
790+
link_to = new_objects[obj_id]
791+
792+
# Make sure we create a new proxy model that
793+
# would wrap either an object that already exists
794+
# or a new one just inserted by sync(); copy linkprops
795+
# efficiently.
796+
return refetched.__gel_proxy_construct__(
797+
link_to, # pyright: ignore [reportArgumentType]
798+
copy_or_ref_lprops(refetched.__linkprops__),
799+
linked=True,
800+
)
801+
802+
743803
def proxy_link(
744804
*,
745805
existing: AbstractGelProxyModel[_MT_co, _LM_co] | None,

gel/_internal/_qbmodel/_abstract/_link_set.py

Lines changed: 73 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from gel._internal import _typing_parametric as parametric
1616
from gel._internal._qbmodel._abstract._base import AbstractGelLinkModel
17+
from gel._internal._qbmodel import _abstract
1718
from gel._internal._tracked_list import (
1819
AbstractCollection,
1920
DefaultList,
@@ -74,10 +75,32 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
7475
self._index_snapshot = None
7576
super().__init__(*args, **kwargs)
7677

78+
def __copy__(self) -> Self:
79+
obj = type(self).__new__(type(self))
80+
81+
obj.__gel_overwrite_data__ = self.__gel_overwrite_data__
82+
obj._mode = self._mode
83+
obj._items = list(self._items)
84+
obj._index_snapshot = (
85+
dict(self._index_snapshot) if self._index_snapshot else None
86+
)
87+
obj._tracking_set = (
88+
dict(self._tracking_set) if self._tracking_set else None
89+
)
90+
obj._tracking_index = (
91+
dict(self._tracking_index) if self._tracking_index else None
92+
)
93+
return obj
94+
95+
def __deepcopy__(self, memo: dict[int, Any]) -> Self:
96+
return self.__copy__()
97+
7798
def __gel_reconcile__(
7899
self,
79100
updated: AbstractLinkSet[_MT_co],
80-
existing_objects: dict[uuid.UUID, AbstractGelModel],
101+
existing_objects: dict[
102+
uuid.UUID, AbstractGelModel | Iterable[AbstractGelModel]
103+
],
81104
new_objects: dict[uuid.UUID, AbstractGelModel],
82105
) -> Self:
83106
raise NotImplementedError
@@ -117,10 +140,10 @@ def _is_tracked(self, item: _MT_co) -> bool: # type: ignore [misc]
117140
def _reconcile(
118141
self,
119142
updated: AbstractLinkSet[_MT_co],
120-
existing_objects: dict[uuid.UUID, AbstractGelModel],
143+
existing_objects: dict[
144+
uuid.UUID, AbstractGelModel | Iterable[AbstractGelModel]
145+
],
121146
new_objects: dict[uuid.UUID, AbstractGelModel],
122-
*,
123-
is_computed: bool,
124147
) -> list[_MT_co]:
125148
raise NotImplementedError
126149

@@ -250,6 +273,12 @@ def __repr__(self) -> str:
250273
else:
251274
return repr(self._items)
252275

276+
def __gel_replace_with_empty__(self) -> None:
277+
self._items.clear()
278+
self._index_snapshot = None
279+
self._tracking_set = None
280+
self._tracking_index = None
281+
253282

254283
class ComputedLinkSet(
255284
parametric.SingleParametricType[_MT_co],
@@ -266,10 +295,10 @@ def _pyid(item: _MT_co) -> int: # type: ignore [misc]
266295
def _reconcile(
267296
self,
268297
updated: AbstractLinkSet[_MT_co],
269-
existing_objects: dict[uuid.UUID, AbstractGelModel],
298+
existing_objects: dict[
299+
uuid.UUID, AbstractGelModel | Iterable[AbstractGelModel]
300+
],
270301
new_objects: dict[uuid.UUID, AbstractGelModel],
271-
*,
272-
is_computed: bool,
273302
) -> list[_MT_co]:
274303
# This method is called by sync() when it refetches the link.
275304
#
@@ -319,30 +348,29 @@ def _reconcile(
319348

320349
updated_items: list[_MT_co] = []
321350
for obj in updated:
322-
obj_id: uuid.UUID = obj.id # type: ignore [attr-defined]
323-
324-
try:
325-
# This works because `GelModel` hashes and compares by `.id`
326-
existing = existing_set[obj]
327-
except KeyError:
328-
if is_computed and obj_id in existing_objects:
329-
updated_items.append(existing_objects[obj_id]) # type: ignore [arg-type]
330-
else:
331-
updated_items.append(new_objects[obj_id]) # type: ignore [arg-type]
332-
else:
333-
updated_items.append(existing)
351+
# This works because `GelModel` hashes and compares by `.id`
352+
existing: _MT_co | None = existing_set.get(obj)
353+
354+
updated_items.append(
355+
_abstract.reconcile_link( # type: ignore [misc]
356+
existing=existing,
357+
refetched=obj,
358+
existing_objects=existing_objects,
359+
new_objects=new_objects,
360+
)
361+
)
334362

335363
return updated_items
336364

337365
def __gel_reconcile__( # pyright: ignore [reportIncompatibleMethodOverride]
338366
self,
339367
updated: AbstractLinkSet[_MT_co],
340-
existing_objects: dict[uuid.UUID, AbstractGelModel],
368+
existing_objects: dict[
369+
uuid.UUID, AbstractGelModel | Iterable[AbstractGelModel]
370+
],
341371
new_objects: dict[uuid.UUID, AbstractGelModel],
342372
) -> Self:
343-
new_items = self._reconcile(
344-
updated, existing_objects, new_objects, is_computed=True
345-
)
373+
new_items = self._reconcile(updated, existing_objects, new_objects)
346374
return type(self)(
347375
new_items,
348376
__mode__=self._mode,
@@ -415,10 +443,10 @@ def __gel_basetype_iter__(self) -> Iterator[_BMT_co]: # type: ignore [override]
415443
def _reconcile( # pyright: ignore [reportIncompatibleMethodOverride]
416444
self,
417445
updated: LinkWithPropsSet[_PT_co, _BMT_co], # type: ignore [override]
418-
existing_objects: dict[uuid.UUID, AbstractGelModel],
446+
existing_objects: dict[
447+
uuid.UUID, AbstractGelModel | Iterable[AbstractGelModel]
448+
],
419449
new_objects: dict[uuid.UUID, AbstractGelModel],
420-
*,
421-
is_computed: bool,
422450
) -> list[_PT_co]:
423451
# See comments in `LinkSet._reconcile()` for most implementation
424452
# details.
@@ -432,36 +460,29 @@ def _reconcile( # pyright: ignore [reportIncompatibleMethodOverride]
432460

433461
updated_items: list[_PT_co] = []
434462
for obj in updated:
435-
obj_id: uuid.UUID = obj.id # type: ignore [attr-defined]
436-
437-
try:
438-
existing = existing_set[obj]
439-
except KeyError:
440-
if is_computed and obj_id in existing_objects:
441-
new = existing_objects[obj_id]
442-
else:
443-
new = new_objects[obj_id]
444-
# `obj` will have updated __linkprops__ but the wrapped
445-
# model will be coming from new_objects
446-
obj.__gel_replace_wrapped_model__(new)
447-
updated_items.append(obj)
448-
else:
449-
# updated will have newly refetched __linkprops__, so
450-
# copy them
451-
existing.__gel_replace_linkprops__(obj.__linkprops__)
452-
updated_items.append(existing)
463+
# This works because `GelModel` hashes and compares by `.id`
464+
existing = existing_set.get(obj)
465+
466+
updated_items.append(
467+
_abstract.reconcile_proxy_link(
468+
existing=existing,
469+
refetched=obj,
470+
existing_objects=existing_objects,
471+
new_objects=new_objects,
472+
) # type: ignore [arg-type]
473+
)
453474

454475
return updated_items
455476

456477
def __gel_reconcile__( # pyright: ignore [reportIncompatibleMethodOverride]
457478
self,
458479
updated: LinkWithPropsSet[_PT_co, _BMT_co], # type: ignore [override]
459-
existing_objects: dict[uuid.UUID, AbstractGelModel],
480+
existing_objects: dict[
481+
uuid.UUID, AbstractGelModel | Iterable[AbstractGelModel]
482+
],
460483
new_objects: dict[uuid.UUID, AbstractGelModel],
461484
) -> Self:
462-
new_items = self._reconcile(
463-
updated, existing_objects, new_objects, is_computed=True
464-
)
485+
new_items = self._reconcile(updated, existing_objects, new_objects)
465486
return type(self)(
466487
new_items,
467488
__mode__=self._mode,
@@ -562,12 +583,12 @@ class AbstractMutableLinkSet(
562583
def __gel_reconcile__( # pyright: ignore [reportIncompatibleMethodOverride]
563584
self,
564585
updated: AbstractLinkSet[_MT_co],
565-
existing_objects: dict[uuid.UUID, AbstractGelModel],
586+
existing_objects: dict[
587+
uuid.UUID, AbstractGelModel | Iterable[AbstractGelModel]
588+
],
566589
new_objects: dict[uuid.UUID, AbstractGelModel],
567590
) -> Self:
568-
self._items = self._reconcile(
569-
updated, existing_objects, new_objects, is_computed=False
570-
)
591+
self._items = self._reconcile(updated, existing_objects, new_objects)
571592
# Reset tracking: it will likely be not needed. Typically there
572593
# should be just one `sync()` call with no modifications of anything
573594
# after it.
@@ -595,12 +616,6 @@ def _ensure_snapshot(self) -> None:
595616
assert self._tracking_index is not None
596617
self._index_snapshot = dict(self._tracking_index)
597618

598-
def __gel_replace_with_empty__(self) -> None:
599-
self._items.clear()
600-
self._index_snapshot = None
601-
self._tracking_set = None
602-
self._tracking_index = None
603-
604619
def __gel_get_added__(self) -> list[_MT_co]:
605620
match bool(self._index_snapshot), bool(self._tracking_index):
606621
case True, False:

0 commit comments

Comments
 (0)