flet 0.70.0.dev5776__py3-none-any.whl → 0.70.0.dev6145__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of flet might be problematic. Click here for more details.
- flet/__init__.py +32 -4
- flet/components/__init__.py +0 -0
- flet/components/component.py +346 -0
- flet/components/component_decorator.py +24 -0
- flet/components/component_owned.py +22 -0
- flet/components/hooks/__init__.py +0 -0
- flet/components/hooks/hook.py +12 -0
- flet/components/hooks/use_callback.py +28 -0
- flet/components/hooks/use_context.py +91 -0
- flet/components/hooks/use_effect.py +104 -0
- flet/components/hooks/use_memo.py +52 -0
- flet/components/hooks/use_state.py +58 -0
- flet/components/memo.py +34 -0
- flet/components/observable.py +269 -0
- flet/components/public_utils.py +10 -0
- flet/components/utils.py +85 -0
- flet/controls/base_control.py +34 -10
- flet/controls/base_page.py +44 -40
- flet/controls/context.py +22 -1
- flet/controls/control_event.py +19 -2
- flet/controls/core/column.py +5 -0
- flet/controls/core/drag_target.py +17 -8
- flet/controls/core/row.py +5 -0
- flet/controls/core/view.py +6 -6
- flet/controls/cupertino/cupertino_icons.py +1 -1
- flet/controls/id_counter.py +24 -0
- flet/controls/material/divider.py +6 -0
- flet/controls/material/icons.py +1 -1
- flet/controls/material/textfield.py +10 -1
- flet/controls/material/vertical_divider.py +6 -0
- flet/controls/object_patch.py +434 -197
- flet/controls/page.py +203 -84
- flet/controls/services/haptic_feedback.py +0 -3
- flet/controls/services/shake_detector.py +0 -3
- flet/messaging/flet_socket_server.py +13 -6
- flet/messaging/session.py +103 -10
- flet/{controls/session_storage.py → messaging/session_store.py} +2 -2
- flet/version.py +1 -1
- {flet-0.70.0.dev5776.dist-info → flet-0.70.0.dev6145.dist-info}/METADATA +4 -5
- {flet-0.70.0.dev5776.dist-info → flet-0.70.0.dev6145.dist-info}/RECORD +43 -30
- flet/controls/cache.py +0 -87
- flet/controls/control_id.py +0 -22
- flet/controls/core/state_view.py +0 -60
- {flet-0.70.0.dev5776.dist-info → flet-0.70.0.dev6145.dist-info}/WHEEL +0 -0
- {flet-0.70.0.dev5776.dist-info → flet-0.70.0.dev6145.dist-info}/entry_points.txt +0 -0
- {flet-0.70.0.dev5776.dist-info → flet-0.70.0.dev6145.dist-info}/top_level.txt +0 -0
flet/controls/object_patch.py
CHANGED
|
@@ -30,11 +30,16 @@
|
|
|
30
30
|
#
|
|
31
31
|
|
|
32
32
|
import dataclasses
|
|
33
|
+
import logging
|
|
33
34
|
import weakref
|
|
34
35
|
from enum import Enum
|
|
36
|
+
from typing import Any, Optional
|
|
35
37
|
|
|
36
38
|
from flet.controls.keys import Key
|
|
37
39
|
|
|
40
|
+
logger = logging.getLogger("flet_object_patch")
|
|
41
|
+
logger.setLevel(logging.INFO)
|
|
42
|
+
|
|
38
43
|
_ST_ADD = 0
|
|
39
44
|
_ST_REMOVE = 1
|
|
40
45
|
|
|
@@ -228,13 +233,16 @@ class ObjectPatch:
|
|
|
228
233
|
src,
|
|
229
234
|
dst,
|
|
230
235
|
control_cls,
|
|
236
|
+
parent: Any = None,
|
|
237
|
+
path: Optional[list[Any]] = None,
|
|
238
|
+
frozen: bool = False,
|
|
231
239
|
):
|
|
232
240
|
builder = DiffBuilder(
|
|
233
241
|
src,
|
|
234
242
|
dst,
|
|
235
243
|
control_cls=control_cls,
|
|
236
244
|
)
|
|
237
|
-
builder._compare_values(
|
|
245
|
+
builder._compare_values(parent, path or [], None, src, dst, frozen=frozen)
|
|
238
246
|
|
|
239
247
|
ops = list(builder.execute())
|
|
240
248
|
added = list(builder.get_added_controls())
|
|
@@ -429,119 +437,155 @@ class DiffBuilder:
|
|
|
429
437
|
yield curr[2].operation
|
|
430
438
|
curr = curr[1]
|
|
431
439
|
|
|
440
|
+
def _index_key(self, item, item_key, path):
|
|
441
|
+
"""
|
|
442
|
+
Return the composite key used to pair add/remove
|
|
443
|
+
(by control key if present).
|
|
444
|
+
"""
|
|
445
|
+
return item_key if item_key is not None else item
|
|
446
|
+
|
|
447
|
+
def _maybe_compare_dataclasses(self, parent, path, src, dst, frozen):
|
|
448
|
+
"""
|
|
449
|
+
Compare dataclasses only when both are dataclasses and
|
|
450
|
+
identity/"frozen" rules allow it.
|
|
451
|
+
"""
|
|
452
|
+
if (
|
|
453
|
+
dataclasses.is_dataclass(src)
|
|
454
|
+
and dataclasses.is_dataclass(dst)
|
|
455
|
+
and ((not frozen and src is dst) or (frozen and src is not dst))
|
|
456
|
+
):
|
|
457
|
+
self._compare_dataclasses(parent, path, src, dst, frozen)
|
|
458
|
+
|
|
459
|
+
def _affected_is_list(self, op):
|
|
460
|
+
"""
|
|
461
|
+
Return True if the item the op affects lives in a list in the destination doc.
|
|
462
|
+
We must not rely on op.key’s type because PatchOperation coerces to int.
|
|
463
|
+
"""
|
|
464
|
+
container = op.to_last(self.dst_doc)
|
|
465
|
+
return isinstance(container, list)
|
|
466
|
+
|
|
467
|
+
def _emit_move(self, from_loc, to_loc):
|
|
468
|
+
"""Create and insert a move operation."""
|
|
469
|
+
self.insert(MoveOperation({"op": "move", "from": from_loc, "path": to_loc}))
|
|
470
|
+
|
|
432
471
|
def _item_added(self, parent, path, key, item, item_key=None, frozen=False):
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
472
|
+
logger.debug(
|
|
473
|
+
f"_item_added: path={path} key={key} item={item} item_key={item_key}"
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
index_key = self._index_key(item, item_key, path)
|
|
477
|
+
paired_idx = self.take_index(index_key, _ST_REMOVE)
|
|
478
|
+
|
|
479
|
+
# A matching 'remove' exists: it's a move (or an in-place update)
|
|
480
|
+
if paired_idx is not None:
|
|
481
|
+
rem_op: RemoveOperation = paired_idx[2] # the earlier remove
|
|
482
|
+
src = rem_op.operation["value"]
|
|
442
483
|
dst = item
|
|
443
484
|
|
|
485
|
+
# undo bookkeeping for the removed dataclass (it’s coming back)
|
|
444
486
|
self._undo_dataclass_removed(src)
|
|
445
487
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
488
|
+
# If the affected sequence is a list, adjust later op indices after removal
|
|
489
|
+
if isinstance(rem_op.key, int) and isinstance(key, int):
|
|
490
|
+
for v in self.iter_from(paired_idx):
|
|
491
|
+
rem_op.key = v._on_undo_remove(rem_op.path, rem_op.key)
|
|
492
|
+
|
|
493
|
+
# Drop the paired remove from the chain: we’re going to emit either
|
|
494
|
+
# compares or a move
|
|
495
|
+
self.remove(paired_idx)
|
|
496
|
+
|
|
497
|
+
src_loc = rem_op.location
|
|
498
|
+
dst_loc = _path_join(path, key)
|
|
499
|
+
|
|
500
|
+
# Compare first (for dataclasses), anchored at source or dest depending
|
|
501
|
+
# on whether we move
|
|
502
|
+
if src_loc != dst_loc:
|
|
503
|
+
# Compare on the source location before we move (matches your tests’
|
|
504
|
+
# expectations)
|
|
505
|
+
self._maybe_compare_dataclasses(parent, src_loc, src, dst, frozen)
|
|
506
|
+
# Then emit the move
|
|
507
|
+
self._emit_move(src_loc, dst_loc)
|
|
508
|
+
return
|
|
509
|
+
else:
|
|
510
|
+
# No move, just in-place updates
|
|
511
|
+
self._maybe_compare_dataclasses(parent, dst_loc, src, dst, frozen)
|
|
512
|
+
return
|
|
458
513
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
{
|
|
467
|
-
"op": "move",
|
|
468
|
-
"from": op.location,
|
|
469
|
-
"path": _path_join(path, key),
|
|
470
|
-
}
|
|
471
|
-
)
|
|
472
|
-
self.insert(new_op)
|
|
473
|
-
else:
|
|
474
|
-
new_op = AddOperation(
|
|
475
|
-
{
|
|
476
|
-
"op": "add",
|
|
477
|
-
"path": _path_join(path, key),
|
|
478
|
-
"value": item,
|
|
479
|
-
}
|
|
480
|
-
)
|
|
481
|
-
new_index = self.insert(new_op)
|
|
482
|
-
self.store_index(index_key, new_index, _ST_ADD)
|
|
483
|
-
self._dataclass_added(item, parent, frozen)
|
|
514
|
+
# No matching remove: this is a plain add
|
|
515
|
+
add_op = AddOperation(
|
|
516
|
+
{"op": "add", "path": _path_join(path, key), "value": item}
|
|
517
|
+
)
|
|
518
|
+
add_idx = self.insert(add_op)
|
|
519
|
+
self.store_index(index_key, add_idx, _ST_ADD)
|
|
520
|
+
self._dataclass_added(item, parent, frozen)
|
|
484
521
|
|
|
485
522
|
def _item_removed(self, path, key, item, item_key=None, frozen=False):
|
|
486
|
-
|
|
487
|
-
|
|
523
|
+
logger.debug(
|
|
524
|
+
f"_item_removed: path={path} key={key} item={item} item_key={item_key}"
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
index_key = self._index_key(item, item_key, path)
|
|
528
|
+
rem_op = RemoveOperation(
|
|
488
529
|
{"op": "remove", "path": _path_join(path, key), "value": item}
|
|
489
530
|
)
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
# compare moved item
|
|
531
|
+
rem_idx = self.insert(rem_op)
|
|
532
|
+
|
|
533
|
+
paired_idx = self.take_index(index_key, _ST_ADD)
|
|
534
|
+
|
|
535
|
+
# A matching 'add' exists: it's a move (or an in-place update)
|
|
536
|
+
if paired_idx is not None:
|
|
537
|
+
add_op: AddOperation = paired_idx[2] # the earlier add
|
|
498
538
|
src = item
|
|
499
|
-
dst =
|
|
539
|
+
dst = add_op.operation["value"]
|
|
500
540
|
|
|
541
|
+
# undo bookkeeping for the added dataclass (it’s being consumed by the move)
|
|
501
542
|
self._undo_dataclass_added(dst)
|
|
502
543
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
544
|
+
# If the added op affects a list, adjust later ops after that add
|
|
545
|
+
if self._affected_is_list(add_op):
|
|
546
|
+
for v in self.iter_from(paired_idx):
|
|
547
|
+
add_op.key = v._on_undo_add(add_op.path, add_op.key)
|
|
548
|
+
|
|
549
|
+
src_loc = rem_op.location
|
|
550
|
+
dst_loc = add_op.location
|
|
551
|
+
|
|
552
|
+
# The earlier add no longer stands on its own
|
|
553
|
+
self.remove(paired_idx)
|
|
554
|
+
|
|
555
|
+
if src_loc != dst_loc:
|
|
556
|
+
# If we’re moving, compare anchored at the source BEFORE the move
|
|
557
|
+
# (matches your tests)
|
|
558
|
+
self._maybe_compare_dataclasses(
|
|
559
|
+
dst.parent if hasattr(dst, "parent") else None,
|
|
560
|
+
src_loc,
|
|
511
561
|
src,
|
|
512
562
|
dst,
|
|
513
563
|
frozen,
|
|
514
564
|
)
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
# for numeric string dict keys while the intention is to only handle lists.
|
|
519
|
-
# So we do an explicit check on the item affected by the op instead.
|
|
520
|
-
added_item = op.to_last(self.dst_doc)
|
|
521
|
-
if isinstance(added_item, list):
|
|
522
|
-
for v in self.iter_from(index):
|
|
523
|
-
op.key = v._on_undo_add(op.path, op.key)
|
|
524
|
-
|
|
525
|
-
self.remove(index)
|
|
526
|
-
if new_op.location != op.location:
|
|
527
|
-
new_op = MoveOperation(
|
|
528
|
-
{
|
|
529
|
-
"op": "move",
|
|
530
|
-
"from": new_op.location,
|
|
531
|
-
"path": op.location,
|
|
532
|
-
}
|
|
565
|
+
# Turn the just-inserted remove into a move (reuse node)
|
|
566
|
+
rem_idx[2] = MoveOperation(
|
|
567
|
+
{"op": "move", "from": src_loc, "path": dst_loc}
|
|
533
568
|
)
|
|
534
|
-
|
|
535
|
-
|
|
569
|
+
return
|
|
536
570
|
else:
|
|
537
|
-
|
|
571
|
+
# No move after all; drop the remove from the chain (pair consumed)
|
|
572
|
+
self.remove(rem_idx)
|
|
573
|
+
# In-place updates only
|
|
574
|
+
self._maybe_compare_dataclasses(
|
|
575
|
+
dst.parent if hasattr(dst, "parent") else None,
|
|
576
|
+
_path_join(add_op.path, add_op.key),
|
|
577
|
+
src,
|
|
578
|
+
dst,
|
|
579
|
+
frozen,
|
|
580
|
+
)
|
|
581
|
+
return
|
|
538
582
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
583
|
+
# No matching add: keep the remove and remember this dataclass
|
|
584
|
+
self.store_index(index_key, rem_idx, _ST_REMOVE)
|
|
585
|
+
self._dataclass_removed(item)
|
|
542
586
|
|
|
543
587
|
def _item_replaced(self, path, key, item):
|
|
544
|
-
|
|
588
|
+
logger.debug("_item_replaced: %s %s %s:", path, key, item)
|
|
545
589
|
self.insert(
|
|
546
590
|
ReplaceOperation(
|
|
547
591
|
{
|
|
@@ -553,7 +597,7 @@ class DiffBuilder:
|
|
|
553
597
|
)
|
|
554
598
|
|
|
555
599
|
def _compare_dicts(self, parent, path, src, dst, frozen):
|
|
556
|
-
|
|
600
|
+
logger.debug("\n_compare_dicts: %s %s %s", path, src, dst)
|
|
557
601
|
|
|
558
602
|
src_keys = set(src.keys())
|
|
559
603
|
dst_keys = set(dst.keys())
|
|
@@ -570,106 +614,246 @@ class DiffBuilder:
|
|
|
570
614
|
self._compare_values(parent, path, key, src[key], dst[key], frozen)
|
|
571
615
|
|
|
572
616
|
def _compare_lists(self, parent, path, src, dst, frozen):
|
|
573
|
-
|
|
617
|
+
logger.debug(f"\n_compare_lists: {path} {src} {dst}")
|
|
574
618
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
for key in range(max_len):
|
|
579
|
-
if key < min_len:
|
|
580
|
-
old, new = src[key], dst[key]
|
|
581
|
-
# print("\n\nCOMPARE LIST ITEM:", key, "\n\nOLD:", old, "\n\nNEW:", new)
|
|
619
|
+
# ----- helper: get keys quickly -----
|
|
620
|
+
def k(obj):
|
|
621
|
+
return get_control_key(obj)
|
|
582
622
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
elif isinstance(old, list) and isinstance(new, list):
|
|
587
|
-
self._compare_lists(parent, _path_join(path, key), old, new, frozen)
|
|
623
|
+
src_keys = [k(item) for item in src]
|
|
624
|
+
dst_keys = [k(item) for item in dst]
|
|
588
625
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
626
|
+
# Use keyed algorithm only when every element provides a key on both sides
|
|
627
|
+
all_keyed = (
|
|
628
|
+
src_keys
|
|
629
|
+
and dst_keys
|
|
630
|
+
and all(key is not None for key in src_keys)
|
|
631
|
+
and all(key is not None for key in dst_keys)
|
|
632
|
+
)
|
|
633
|
+
# print("list info", path, len(src_keys), len(dst_keys), all_keyed)
|
|
634
|
+
if not all_keyed:
|
|
635
|
+
# fall back to your existing element-wise logic
|
|
636
|
+
len_src, len_dst = len(src), len(dst)
|
|
637
|
+
max_len = max(len_src, len_dst)
|
|
638
|
+
min_len = min(len_src, len_dst)
|
|
639
|
+
for key in range(max_len):
|
|
640
|
+
if key < min_len:
|
|
641
|
+
old, new = src[key], dst[key]
|
|
642
|
+
logger.debug(
|
|
643
|
+
f"\n\nCOMPARE LIST ITEM: {key}\n\nOLD: {old}\n\nNEW: {new}"
|
|
594
644
|
)
|
|
595
645
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
if (not frozen and old is new) or (
|
|
600
|
-
frozen
|
|
601
|
-
and old is not new # not a cached control tree
|
|
602
|
-
and type(old) is type(new) # iteams are of the same type
|
|
603
|
-
and (
|
|
604
|
-
old_control_key is None
|
|
605
|
-
or new_control_key is None
|
|
606
|
-
or old_control_key == new_control_key
|
|
607
|
-
) # same list key or both None
|
|
608
|
-
):
|
|
609
|
-
# print("\n\ncompare list dataclasses:", new)
|
|
610
|
-
self._compare_dataclasses(
|
|
646
|
+
if isinstance(old, dict) and isinstance(new, dict):
|
|
647
|
+
self._compare_dicts(
|
|
611
648
|
parent, _path_join(path, key), old, new, frozen
|
|
612
649
|
)
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
# old,
|
|
618
|
-
# "\n\nNEW:",
|
|
619
|
-
# new,
|
|
620
|
-
# )
|
|
621
|
-
self._item_removed(
|
|
622
|
-
path,
|
|
623
|
-
key,
|
|
624
|
-
old,
|
|
625
|
-
item_key=(old_control_key, path)
|
|
626
|
-
if old_control_key is not None
|
|
627
|
-
else old,
|
|
628
|
-
frozen=frozen,
|
|
650
|
+
|
|
651
|
+
elif isinstance(old, list) and isinstance(new, list):
|
|
652
|
+
self._compare_lists(
|
|
653
|
+
parent, _path_join(path, key), old, new, frozen
|
|
629
654
|
)
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
frozen=frozen,
|
|
655
|
+
|
|
656
|
+
elif dataclasses.is_dataclass(old) and dataclasses.is_dataclass(
|
|
657
|
+
new
|
|
658
|
+
):
|
|
659
|
+
frozen_local = (
|
|
660
|
+
(old is not None and hasattr(old, "_frozen"))
|
|
661
|
+
or (new is not None and hasattr(new, "_frozen"))
|
|
662
|
+
or frozen
|
|
639
663
|
)
|
|
664
|
+
old_control_key = k(old)
|
|
665
|
+
new_control_key = k(new)
|
|
666
|
+
if (not frozen_local and old is new) or (
|
|
667
|
+
frozen_local
|
|
668
|
+
and old is not new
|
|
669
|
+
and type(old) is type(new)
|
|
670
|
+
and (
|
|
671
|
+
old_control_key is None
|
|
672
|
+
or new_control_key is None
|
|
673
|
+
or old_control_key == new_control_key
|
|
674
|
+
)
|
|
675
|
+
):
|
|
676
|
+
self._compare_dataclasses(
|
|
677
|
+
parent, _path_join(path, key), old, new, frozen_local
|
|
678
|
+
)
|
|
679
|
+
else:
|
|
680
|
+
self._item_removed(path, key, old, frozen=frozen)
|
|
681
|
+
self._item_added(parent, path, key, new, frozen=frozen)
|
|
682
|
+
|
|
683
|
+
elif type(old) is not type(new) or old != new:
|
|
684
|
+
self._item_removed(path, key, old, frozen=frozen)
|
|
685
|
+
self._item_added(parent, path, key, new, frozen=frozen)
|
|
686
|
+
|
|
687
|
+
elif len_src > len_dst:
|
|
688
|
+
control_key = k(src[key])
|
|
689
|
+
self._item_removed(
|
|
690
|
+
path,
|
|
691
|
+
len_dst,
|
|
692
|
+
src[key],
|
|
693
|
+
item_key=(control_key, path)
|
|
694
|
+
if control_key is not None
|
|
695
|
+
else src[key],
|
|
696
|
+
frozen=frozen,
|
|
697
|
+
)
|
|
698
|
+
else:
|
|
699
|
+
control_key = k(dst[key])
|
|
700
|
+
self._item_added(
|
|
701
|
+
parent,
|
|
702
|
+
path,
|
|
703
|
+
key,
|
|
704
|
+
dst[key],
|
|
705
|
+
item_key=(control_key, path)
|
|
706
|
+
if control_key is not None
|
|
707
|
+
else dst[key],
|
|
708
|
+
frozen=frozen,
|
|
709
|
+
)
|
|
710
|
+
return
|
|
640
711
|
|
|
712
|
+
if src_keys == dst_keys:
|
|
713
|
+
# print("keyed fast path", path, len(src))
|
|
714
|
+
# Keys are identical and in the same order: treat as positional diff
|
|
715
|
+
for idx, (old, new) in enumerate(zip(src, dst)):
|
|
716
|
+
if isinstance(old, dict) and isinstance(new, dict):
|
|
717
|
+
self._compare_dicts(parent, _path_join(path, idx), old, new, frozen)
|
|
718
|
+
elif isinstance(old, list) and isinstance(new, list):
|
|
719
|
+
self._compare_lists(parent, _path_join(path, idx), old, new, frozen)
|
|
720
|
+
elif dataclasses.is_dataclass(old) and dataclasses.is_dataclass(new):
|
|
721
|
+
self._compare_dataclasses(
|
|
722
|
+
parent, _path_join(path, idx), old, new, frozen
|
|
723
|
+
)
|
|
641
724
|
elif type(old) is not type(new) or old != new:
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
725
|
+
self._item_replaced(path, idx, new)
|
|
726
|
+
return
|
|
727
|
+
|
|
728
|
+
# -------- Keyed, React-style reconciliation --------
|
|
729
|
+
# We’ll mutate a working copy of the old list so emitted indices are “live”.
|
|
730
|
+
work = list(src)
|
|
731
|
+
work_keys = src_keys[:]
|
|
732
|
+
# Map key -> current index in `work`
|
|
733
|
+
pos = {key: i for i, key in enumerate(work_keys)}
|
|
734
|
+
dst_keys = dst_keys # renamed for clarity
|
|
735
|
+
new_index_by_key = {key: i for i, key in enumerate(dst_keys)}
|
|
736
|
+
new_keys_set = set(new_index_by_key.keys())
|
|
737
|
+
|
|
738
|
+
def _reindex(start_idx: int) -> None:
|
|
739
|
+
for j in range(start_idx, len(work_keys)):
|
|
740
|
+
pos[work_keys[j]] = j
|
|
741
|
+
|
|
742
|
+
def _remove_from_work(idx: int):
|
|
743
|
+
removed_item = work.pop(idx)
|
|
744
|
+
removed_key = work_keys.pop(idx)
|
|
745
|
+
pos.pop(removed_key, None)
|
|
746
|
+
if idx < len(work_keys):
|
|
747
|
+
_reindex(idx)
|
|
748
|
+
return removed_item, removed_key
|
|
749
|
+
|
|
750
|
+
def _insert_into_work(idx: int, item, key):
|
|
751
|
+
work.insert(idx, item)
|
|
752
|
+
work_keys.insert(idx, key)
|
|
753
|
+
pos[key] = idx
|
|
754
|
+
if idx + 1 <= len(work_keys):
|
|
755
|
+
_reindex(idx + 1)
|
|
756
|
+
|
|
757
|
+
def emit_replace_at(idx, old_item, new_item):
|
|
758
|
+
# For dataclasses we delegate to your existing field diff.
|
|
759
|
+
if (
|
|
760
|
+
dataclasses.is_dataclass(old_item)
|
|
761
|
+
and dataclasses.is_dataclass(new_item)
|
|
762
|
+
and old_item is not new_item
|
|
763
|
+
):
|
|
764
|
+
# Force field-wise compare even if different instances:
|
|
765
|
+
self._compare_dataclasses(
|
|
766
|
+
parent, _path_join(path, idx), old_item, new_item, True
|
|
656
767
|
)
|
|
768
|
+
elif type(old_item) is not type(new_item) or old_item != new_item:
|
|
769
|
+
self._item_replaced(path, idx, new_item)
|
|
770
|
+
|
|
771
|
+
# Scan forward through desired new order.
|
|
772
|
+
i = 0
|
|
773
|
+
while i < len(dst):
|
|
774
|
+
target_key = dst_keys[i]
|
|
775
|
+
|
|
776
|
+
# First, delete any items currently at position i that should NOT be here:
|
|
777
|
+
# - keys that don't exist anymore
|
|
778
|
+
# - or keys that exist but must appear BEFORE i in the new order
|
|
779
|
+
# (they are out of place here)
|
|
780
|
+
while i < len(work):
|
|
781
|
+
cur_key = work_keys[i]
|
|
782
|
+
if cur_key not in new_keys_set:
|
|
783
|
+
# remove disappearing item at i
|
|
784
|
+
self._item_removed(
|
|
785
|
+
path, i, work[i], item_key=(cur_key, path), frozen=True
|
|
786
|
+
)
|
|
787
|
+
_remove_from_work(i)
|
|
788
|
+
continue
|
|
789
|
+
desired_pos = new_index_by_key[cur_key]
|
|
790
|
+
if desired_pos < i:
|
|
791
|
+
# this item belongs earlier; it should have already been moved
|
|
792
|
+
# before.
|
|
793
|
+
# remove it here; it will be re-inserted (moved) where needed.
|
|
794
|
+
self._item_removed(
|
|
795
|
+
path, i, work[i], item_key=(cur_key, path), frozen=True
|
|
796
|
+
)
|
|
797
|
+
_remove_from_work(i)
|
|
798
|
+
continue
|
|
799
|
+
break # current slot is ok to fill with target
|
|
800
|
+
|
|
801
|
+
if target_key in pos:
|
|
802
|
+
cur_idx = pos[target_key]
|
|
803
|
+
old_item = work[cur_idx]
|
|
804
|
+
new_item = dst[i]
|
|
805
|
+
|
|
806
|
+
if cur_idx == i:
|
|
807
|
+
# in place: just emit updates
|
|
808
|
+
emit_replace_at(i, old_item, new_item)
|
|
809
|
+
else:
|
|
810
|
+
# move from cur_idx -> i
|
|
811
|
+
# Emit updates anchored at SOURCE (cur_idx) before the move
|
|
812
|
+
# (matches your tests)
|
|
813
|
+
emit_replace_at(cur_idx, old_item, new_item)
|
|
814
|
+
|
|
815
|
+
# Emit move
|
|
816
|
+
move_op = MoveOperation(
|
|
817
|
+
{
|
|
818
|
+
"op": "move",
|
|
819
|
+
"from": _path_join(path, cur_idx),
|
|
820
|
+
"path": _path_join(path, i),
|
|
821
|
+
}
|
|
822
|
+
)
|
|
823
|
+
self.insert(move_op)
|
|
657
824
|
|
|
825
|
+
# Apply the move in our working model
|
|
826
|
+
moved_item, _ = _remove_from_work(cur_idx)
|
|
827
|
+
insert_idx = i if cur_idx >= i else i
|
|
828
|
+
_insert_into_work(insert_idx, moved_item, target_key)
|
|
658
829
|
else:
|
|
659
|
-
|
|
830
|
+
# brand-new key: add at i
|
|
660
831
|
self._item_added(
|
|
661
832
|
parent,
|
|
662
833
|
path,
|
|
663
|
-
|
|
664
|
-
dst[
|
|
665
|
-
item_key=(
|
|
666
|
-
|
|
667
|
-
else dst[key],
|
|
668
|
-
frozen=frozen,
|
|
834
|
+
i,
|
|
835
|
+
dst[i],
|
|
836
|
+
item_key=(target_key, path),
|
|
837
|
+
frozen=True,
|
|
669
838
|
)
|
|
839
|
+
_insert_into_work(i, dst[i], target_key)
|
|
840
|
+
|
|
841
|
+
i += 1
|
|
842
|
+
|
|
843
|
+
# Finally, remove any trailing leftovers (present in old but not in new)
|
|
844
|
+
# We remove from the end so indices stay valid.
|
|
845
|
+
j = len(work) - 1
|
|
846
|
+
while j >= 0:
|
|
847
|
+
key_j = work_keys[j]
|
|
848
|
+
if key_j not in new_keys_set:
|
|
849
|
+
self._item_removed(
|
|
850
|
+
path, j, work[j], item_key=(key_j, path), frozen=True
|
|
851
|
+
)
|
|
852
|
+
_remove_from_work(j)
|
|
853
|
+
j -= 1
|
|
670
854
|
|
|
671
855
|
def _compare_dataclasses(self, parent, path, src, dst, frozen):
|
|
672
|
-
|
|
856
|
+
logger.debug(f"\n_compare_dataclasses: {path} \n\n{src}\n{dst}\n")
|
|
673
857
|
|
|
674
858
|
if (
|
|
675
859
|
self.control_cls
|
|
@@ -681,18 +865,16 @@ class DiffBuilder:
|
|
|
681
865
|
|
|
682
866
|
if self.control_cls and isinstance(dst, self.control_cls):
|
|
683
867
|
if frozen and hasattr(src, "_i"):
|
|
684
|
-
dst.
|
|
868
|
+
dst._migrate_state(src)
|
|
685
869
|
if not hasattr(dst, "_initialized"):
|
|
686
870
|
orig_frozen = getattr(dst, "_frozen", None)
|
|
687
871
|
if orig_frozen is not None:
|
|
688
872
|
del dst._frozen
|
|
689
873
|
dst.build()
|
|
690
|
-
dst.before_update()
|
|
691
874
|
if orig_frozen is not None:
|
|
692
875
|
object.__setattr__(dst, "_frozen", orig_frozen)
|
|
693
876
|
object.__setattr__(dst, "_initialized", True)
|
|
694
|
-
|
|
695
|
-
dst.before_update()
|
|
877
|
+
dst._before_update_safe()
|
|
696
878
|
|
|
697
879
|
if not frozen:
|
|
698
880
|
# in-place comparison
|
|
@@ -708,10 +890,17 @@ class DiffBuilder:
|
|
|
708
890
|
old = change[0]
|
|
709
891
|
new = change[1]
|
|
710
892
|
|
|
711
|
-
|
|
893
|
+
logger.debug("\n\n_compare_values:changes %s %s", old, new)
|
|
712
894
|
|
|
713
895
|
self._compare_values(dst, path, field_name, old, new, frozen)
|
|
714
896
|
|
|
897
|
+
if field_name in prev_lists:
|
|
898
|
+
del prev_lists[field_name]
|
|
899
|
+
if field_name in prev_dicts:
|
|
900
|
+
del prev_dicts[field_name]
|
|
901
|
+
if field_name in prev_classes:
|
|
902
|
+
del prev_classes[field_name]
|
|
903
|
+
|
|
715
904
|
# update prev value
|
|
716
905
|
if isinstance(new, list):
|
|
717
906
|
new = new[:]
|
|
@@ -729,8 +918,11 @@ class DiffBuilder:
|
|
|
729
918
|
del prev_lists[field_name]
|
|
730
919
|
continue
|
|
731
920
|
new = getattr(dst, field_name)
|
|
921
|
+
if not isinstance(new, list):
|
|
922
|
+
del prev_lists[field_name]
|
|
923
|
+
else:
|
|
924
|
+
prev_lists[field_name] = new[:]
|
|
732
925
|
self._compare_values(dst, path, field_name, old, new, frozen)
|
|
733
|
-
prev_lists[field_name] = new[:]
|
|
734
926
|
|
|
735
927
|
# compare dicts
|
|
736
928
|
for field_name, old in list(prev_dicts.items()):
|
|
@@ -739,8 +931,11 @@ class DiffBuilder:
|
|
|
739
931
|
del prev_dicts[field_name]
|
|
740
932
|
continue
|
|
741
933
|
new = getattr(dst, field_name)
|
|
934
|
+
if not isinstance(new, dict):
|
|
935
|
+
del prev_dicts[field_name]
|
|
936
|
+
else:
|
|
937
|
+
prev_dicts[field_name] = new.copy()
|
|
742
938
|
self._compare_values(dst, path, field_name, old, new, frozen)
|
|
743
|
-
prev_dicts[field_name] = new.copy()
|
|
744
939
|
|
|
745
940
|
# compare dataclasses
|
|
746
941
|
for field_name, old in list(prev_classes.items()):
|
|
@@ -749,20 +944,21 @@ class DiffBuilder:
|
|
|
749
944
|
del prev_classes[field_name]
|
|
750
945
|
continue
|
|
751
946
|
new = getattr(dst, field_name)
|
|
947
|
+
if not dataclasses.is_dataclass(new):
|
|
948
|
+
del prev_classes[field_name]
|
|
949
|
+
else:
|
|
950
|
+
prev_classes[field_name] = new
|
|
752
951
|
self._compare_values(dst, path, field_name, old, new, frozen)
|
|
753
|
-
prev_classes[field_name] = new
|
|
754
952
|
|
|
755
953
|
changes.clear()
|
|
756
954
|
else:
|
|
757
955
|
# frozen comparison
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
# parent,
|
|
765
|
-
# )
|
|
956
|
+
logger.debug(
|
|
957
|
+
"\nfrozen dataclass compare:%s\n\n dst:%s\n\n parent:%s",
|
|
958
|
+
src,
|
|
959
|
+
dst,
|
|
960
|
+
parent,
|
|
961
|
+
)
|
|
766
962
|
for field in dataclasses.fields(dst):
|
|
767
963
|
if "skip" not in field.metadata:
|
|
768
964
|
old = getattr(src, field.name)
|
|
@@ -775,7 +971,14 @@ class DiffBuilder:
|
|
|
775
971
|
self._dataclass_added(dst, parent, frozen)
|
|
776
972
|
|
|
777
973
|
def _compare_values(self, parent, path, key, src, dst, frozen):
|
|
778
|
-
|
|
974
|
+
logger.debug(
|
|
975
|
+
"\n_compare_values: %s %s (Frozen: %s)\n\n%s\n%s\n",
|
|
976
|
+
path,
|
|
977
|
+
key,
|
|
978
|
+
frozen,
|
|
979
|
+
src,
|
|
980
|
+
dst,
|
|
981
|
+
)
|
|
779
982
|
|
|
780
983
|
if isinstance(src, dict) and isinstance(dst, dict):
|
|
781
984
|
self._compare_dicts(parent, _path_join(path, key), src, dst, frozen)
|
|
@@ -795,7 +998,9 @@ class DiffBuilder:
|
|
|
795
998
|
or frozen
|
|
796
999
|
)
|
|
797
1000
|
|
|
798
|
-
|
|
1001
|
+
logger.debug(
|
|
1002
|
+
"\n_compare_values:dataclasses (Frozen: %s) %s %s", frozen, src, dst
|
|
1003
|
+
)
|
|
799
1004
|
|
|
800
1005
|
if (not frozen and src is dst) or (
|
|
801
1006
|
frozen and src is not dst and type(src) is type(dst)
|
|
@@ -811,20 +1016,50 @@ class DiffBuilder:
|
|
|
811
1016
|
self._dataclass_added(dst, parent, frozen)
|
|
812
1017
|
|
|
813
1018
|
elif type(src) is not type(dst) or src != dst:
|
|
1019
|
+
logger.debug(
|
|
1020
|
+
"\n_compare_values:replaced %s %s %s\n\n%s %s",
|
|
1021
|
+
path,
|
|
1022
|
+
key,
|
|
1023
|
+
src,
|
|
1024
|
+
dst,
|
|
1025
|
+
frozen,
|
|
1026
|
+
)
|
|
814
1027
|
self._item_replaced(path, key, dst)
|
|
815
1028
|
self._dataclass_removed(src)
|
|
816
1029
|
self._dataclass_added(dst, parent, frozen)
|
|
817
1030
|
|
|
1031
|
+
if not frozen:
|
|
1032
|
+
prev_lists = getattr(dst, "__prev_lists", {})
|
|
1033
|
+
prev_dicts = getattr(dst, "__prev_dicts", {})
|
|
1034
|
+
prev_classes = getattr(dst, "__prev_classes", {})
|
|
1035
|
+
|
|
1036
|
+
if isinstance(src, list) and key in prev_lists:
|
|
1037
|
+
del prev_lists[key]
|
|
1038
|
+
if isinstance(src, dict) and key in prev_dicts:
|
|
1039
|
+
del prev_dicts[key]
|
|
1040
|
+
if dataclasses.is_dataclass(src) and key in prev_classes:
|
|
1041
|
+
del prev_classes[key]
|
|
1042
|
+
if isinstance(dst, list) and key is not None:
|
|
1043
|
+
prev_lists[key] = dst[:]
|
|
1044
|
+
if isinstance(dst, dict) and key is not None:
|
|
1045
|
+
prev_dicts[key] = dst.copy()
|
|
1046
|
+
if dataclasses.is_dataclass(dst) and key is not None:
|
|
1047
|
+
prev_classes[key] = dst
|
|
1048
|
+
|
|
818
1049
|
def _dataclass_added(self, item, parent, frozen):
|
|
1050
|
+
logger.debug("\n\nDataclass added: %s %s %s", item, parent, frozen)
|
|
819
1051
|
if dataclasses.is_dataclass(item):
|
|
820
1052
|
if parent:
|
|
1053
|
+
logger.debug("\n\nAdding parent %s to item: %s", parent, item)
|
|
821
1054
|
if parent is item:
|
|
822
1055
|
raise Exception(f"Parent is the same as item: {item}")
|
|
823
1056
|
item._parent = weakref.ref(parent)
|
|
1057
|
+
else:
|
|
1058
|
+
logger.debug("\n\nSkip adding parent to item: %s", item)
|
|
824
1059
|
if frozen:
|
|
825
1060
|
item._frozen = frozen
|
|
826
1061
|
|
|
827
|
-
|
|
1062
|
+
logger.debug("\n_dataclass_added: %s", self._get_dataclass_key(item))
|
|
828
1063
|
self._added_dataclasses[self._get_dataclass_key(item)] = item
|
|
829
1064
|
|
|
830
1065
|
elif isinstance(item, dict):
|
|
@@ -836,12 +1071,11 @@ class DiffBuilder:
|
|
|
836
1071
|
self._dataclass_added(v, parent, frozen)
|
|
837
1072
|
|
|
838
1073
|
def _undo_dataclass_added(self, item):
|
|
839
|
-
# print("\n_undo_dataclass_added:", self._get_dataclass_key(item))
|
|
840
1074
|
self._added_dataclasses.pop(self._get_dataclass_key(item), None)
|
|
841
1075
|
|
|
842
1076
|
def _dataclass_removed(self, item):
|
|
1077
|
+
logger.debug("\n\nDataclass removed: %s", item)
|
|
843
1078
|
if dataclasses.is_dataclass(item):
|
|
844
|
-
# print("\n_dataclass_removed:", self._get_dataclass_key(item))
|
|
845
1079
|
self._removed_dataclasses[self._get_dataclass_key(item)] = item
|
|
846
1080
|
|
|
847
1081
|
elif isinstance(item, dict):
|
|
@@ -854,7 +1088,6 @@ class DiffBuilder:
|
|
|
854
1088
|
|
|
855
1089
|
def _undo_dataclass_removed(self, item):
|
|
856
1090
|
if dataclasses.is_dataclass(item):
|
|
857
|
-
# print("\n_undo_dataclass_removed:", self._get_dataclass_key(item))
|
|
858
1091
|
self._removed_dataclasses.pop(self._get_dataclass_key(item), None)
|
|
859
1092
|
|
|
860
1093
|
def _get_dataclass_key(self, item):
|
|
@@ -866,7 +1099,9 @@ class DiffBuilder:
|
|
|
866
1099
|
|
|
867
1100
|
def _configure_dataclass(self, item, parent, frozen, configure_setattr_only=False):
|
|
868
1101
|
if dataclasses.is_dataclass(item):
|
|
869
|
-
|
|
1102
|
+
logger.debug(
|
|
1103
|
+
"\n_configure_dataclass: %s %s %s", item, frozen, configure_setattr_only
|
|
1104
|
+
)
|
|
870
1105
|
|
|
871
1106
|
if parent:
|
|
872
1107
|
if parent is item:
|
|
@@ -897,12 +1132,14 @@ class DiffBuilder:
|
|
|
897
1132
|
value if not name.startswith("on_") else value is not None
|
|
898
1133
|
)
|
|
899
1134
|
if old_value != new_value:
|
|
900
|
-
#
|
|
1135
|
+
# logger.debug(
|
|
901
1136
|
# f"\n\nset_attr: {obj.__class__.__name__}.{name} = "
|
|
902
1137
|
# f"{new_value}, old: {old_value}"
|
|
903
1138
|
# )
|
|
904
1139
|
changes = getattr(obj, "__changes")
|
|
905
1140
|
changes[name] = (old_value, new_value)
|
|
1141
|
+
if hasattr(obj, "_notify"):
|
|
1142
|
+
obj._notify(name, new_value)
|
|
906
1143
|
object.__setattr__(obj, name, value)
|
|
907
1144
|
|
|
908
1145
|
item.__class__.__setattr__ = control_setattr # type: ignore
|
|
@@ -910,7 +1147,7 @@ class DiffBuilder:
|
|
|
910
1147
|
if self.control_cls and isinstance(item, self.control_cls):
|
|
911
1148
|
if not configure_setattr_only:
|
|
912
1149
|
item.build()
|
|
913
|
-
item.
|
|
1150
|
+
item._before_update_safe()
|
|
914
1151
|
object.__setattr__(item, "_initialized", True)
|
|
915
1152
|
yield item
|
|
916
1153
|
|