patchdiff 0.3.8__tar.gz → 0.3.9__tar.gz
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.
- {patchdiff-0.3.8 → patchdiff-0.3.9}/.github/workflows/ci.yml +1 -1
- {patchdiff-0.3.8 → patchdiff-0.3.9}/PKG-INFO +1 -1
- {patchdiff-0.3.8 → patchdiff-0.3.9}/patchdiff/produce.py +120 -4
- {patchdiff-0.3.8 → patchdiff-0.3.9}/pyproject.toml +1 -1
- {patchdiff-0.3.8 → patchdiff-0.3.9}/tests/test_apply.py +23 -0
- {patchdiff-0.3.8 → patchdiff-0.3.9}/tests/test_produce_core.py +76 -0
- {patchdiff-0.3.8 → patchdiff-0.3.9}/tests/test_produce_dict.py +126 -0
- {patchdiff-0.3.8 → patchdiff-0.3.9}/tests/test_produce_list.py +157 -0
- {patchdiff-0.3.8 → patchdiff-0.3.9}/tests/test_produce_set.py +145 -0
- {patchdiff-0.3.8 → patchdiff-0.3.9}/.github/workflows/benchmark.yml +0 -0
- {patchdiff-0.3.8 → patchdiff-0.3.9}/.gitignore +0 -0
- {patchdiff-0.3.8 → patchdiff-0.3.9}/README.md +0 -0
- {patchdiff-0.3.8 → patchdiff-0.3.9}/benchmarks/benchmark.py +0 -0
- {patchdiff-0.3.8 → patchdiff-0.3.9}/patchdiff/__init__.py +0 -0
- {patchdiff-0.3.8 → patchdiff-0.3.9}/patchdiff/apply.py +0 -0
- {patchdiff-0.3.8 → patchdiff-0.3.9}/patchdiff/diff.py +0 -0
- {patchdiff-0.3.8 → patchdiff-0.3.9}/patchdiff/pointer.py +0 -0
- {patchdiff-0.3.8 → patchdiff-0.3.9}/patchdiff/serialize.py +0 -0
- {patchdiff-0.3.8 → patchdiff-0.3.9}/patchdiff/types.py +0 -0
- {patchdiff-0.3.8 → patchdiff-0.3.9}/tests/test_diff.py +0 -0
- {patchdiff-0.3.8 → patchdiff-0.3.9}/tests/test_observ_integration.py +0 -0
- {patchdiff-0.3.8 → patchdiff-0.3.9}/tests/test_pointer.py +0 -0
- {patchdiff-0.3.8 → patchdiff-0.3.9}/tests/test_proxy.py +0 -0
- {patchdiff-0.3.8 → patchdiff-0.3.9}/tests/test_serialize.py +0 -0
|
@@ -47,7 +47,7 @@ jobs:
|
|
|
47
47
|
- name: Format
|
|
48
48
|
run: uv run ruff format --check
|
|
49
49
|
- name: Test
|
|
50
|
-
run: uv run pytest -v --cov=patchdiff --cov-report=term-missing
|
|
50
|
+
run: uv run pytest -v --cov=patchdiff --cov-report=term-missing --cov-fail-under=100
|
|
51
51
|
|
|
52
52
|
build:
|
|
53
53
|
name: Build and test wheel
|
|
@@ -98,6 +98,9 @@ class PatchRecorder:
|
|
|
98
98
|
class DictProxy:
|
|
99
99
|
"""Proxy for dict objects that tracks mutations and generates patches."""
|
|
100
100
|
|
|
101
|
+
__slots__ = ("_data", "_path", "_proxies", "_recorder")
|
|
102
|
+
__hash__ = None # dicts are unhashable
|
|
103
|
+
|
|
101
104
|
def __init__(self, data: Dict, recorder: PatchRecorder, path: Pointer):
|
|
102
105
|
self._data = data
|
|
103
106
|
self._recorder = recorder
|
|
@@ -217,6 +220,21 @@ class DictProxy:
|
|
|
217
220
|
del self._proxies[key]
|
|
218
221
|
return key, value
|
|
219
222
|
|
|
223
|
+
def values(self):
|
|
224
|
+
"""Return proxied values so nested mutations are tracked."""
|
|
225
|
+
for key in self._data:
|
|
226
|
+
yield self._wrap(key, self._data[key])
|
|
227
|
+
|
|
228
|
+
def items(self):
|
|
229
|
+
"""Return (key, proxied_value) pairs so nested mutations are tracked."""
|
|
230
|
+
for key in self._data:
|
|
231
|
+
yield key, self._wrap(key, self._data[key])
|
|
232
|
+
|
|
233
|
+
def __ior__(self, other):
|
|
234
|
+
"""Implement |= operator (merge update)."""
|
|
235
|
+
self.update(other)
|
|
236
|
+
return self
|
|
237
|
+
|
|
220
238
|
|
|
221
239
|
# Add simple reader methods to DictProxy
|
|
222
240
|
_add_reader_methods(
|
|
@@ -226,18 +244,32 @@ _add_reader_methods(
|
|
|
226
244
|
"__contains__",
|
|
227
245
|
"__repr__",
|
|
228
246
|
"__iter__",
|
|
247
|
+
# __reversed__ returns keys (not values), so pass-through is fine
|
|
229
248
|
"__reversed__",
|
|
230
249
|
"keys",
|
|
231
|
-
|
|
232
|
-
|
|
250
|
+
# values() and items() are implemented as custom methods above
|
|
251
|
+
# to return proxied nested objects
|
|
233
252
|
"copy",
|
|
253
|
+
"__str__",
|
|
254
|
+
"__format__",
|
|
255
|
+
"__eq__",
|
|
256
|
+
"__ne__",
|
|
257
|
+
"__or__",
|
|
258
|
+
"__ror__",
|
|
234
259
|
],
|
|
235
260
|
)
|
|
261
|
+
# Skipped dict methods:
|
|
262
|
+
# - fromkeys: classmethod, not relevant for proxy instances
|
|
263
|
+
# - __class_getitem__: typing support (dict[str, int]), not relevant for instances
|
|
264
|
+
# - __lt__, __le__, __gt__, __ge__: dicts don't support ordering comparisons
|
|
236
265
|
|
|
237
266
|
|
|
238
267
|
class ListProxy:
|
|
239
268
|
"""Proxy for list objects that tracks mutations and generates patches."""
|
|
240
269
|
|
|
270
|
+
__slots__ = ("_data", "_path", "_proxies", "_recorder")
|
|
271
|
+
__hash__ = None # lists are unhashable
|
|
272
|
+
|
|
241
273
|
def __init__(self, data: List, recorder: PatchRecorder, path: Pointer):
|
|
242
274
|
self._data = data
|
|
243
275
|
self._recorder = recorder
|
|
@@ -466,6 +498,31 @@ class ListProxy:
|
|
|
466
498
|
# Invalidate all proxy caches as positions changed
|
|
467
499
|
self._proxies.clear()
|
|
468
500
|
|
|
501
|
+
def __iter__(self):
|
|
502
|
+
"""Iterate over list elements, wrapping nested structures in proxies."""
|
|
503
|
+
for i in range(len(self._data)):
|
|
504
|
+
yield self._wrap(i, self._data[i])
|
|
505
|
+
|
|
506
|
+
def __reversed__(self):
|
|
507
|
+
"""Iterate in reverse, wrapping nested structures in proxies."""
|
|
508
|
+
for i in range(len(self._data) - 1, -1, -1):
|
|
509
|
+
yield self._wrap(i, self._data[i])
|
|
510
|
+
|
|
511
|
+
def __iadd__(self, other):
|
|
512
|
+
"""Implement += operator (in-place extend)."""
|
|
513
|
+
self.extend(other)
|
|
514
|
+
return self
|
|
515
|
+
|
|
516
|
+
def __imul__(self, n):
|
|
517
|
+
"""Implement *= operator (in-place repeat)."""
|
|
518
|
+
if n <= 0:
|
|
519
|
+
self.clear()
|
|
520
|
+
elif n > 1:
|
|
521
|
+
original = list(self._data)
|
|
522
|
+
for _ in range(n - 1):
|
|
523
|
+
self.extend(original)
|
|
524
|
+
return self
|
|
525
|
+
|
|
469
526
|
|
|
470
527
|
# Add simple reader methods to ListProxy
|
|
471
528
|
_add_reader_methods(
|
|
@@ -474,18 +531,34 @@ _add_reader_methods(
|
|
|
474
531
|
"__len__",
|
|
475
532
|
"__contains__",
|
|
476
533
|
"__repr__",
|
|
477
|
-
|
|
478
|
-
|
|
534
|
+
# __iter__ and __reversed__ are implemented as custom methods above
|
|
535
|
+
# to return proxied nested objects
|
|
479
536
|
"index",
|
|
480
537
|
"count",
|
|
481
538
|
"copy",
|
|
539
|
+
"__str__",
|
|
540
|
+
"__format__",
|
|
541
|
+
"__eq__",
|
|
542
|
+
"__ne__",
|
|
543
|
+
"__lt__",
|
|
544
|
+
"__le__",
|
|
545
|
+
"__gt__",
|
|
546
|
+
"__ge__",
|
|
547
|
+
"__add__",
|
|
548
|
+
"__mul__",
|
|
549
|
+
"__rmul__",
|
|
482
550
|
],
|
|
483
551
|
)
|
|
552
|
+
# Skipped list methods:
|
|
553
|
+
# - __class_getitem__: typing support (list[int]), not relevant for instances
|
|
484
554
|
|
|
485
555
|
|
|
486
556
|
class SetProxy:
|
|
487
557
|
"""Proxy for set objects that tracks mutations and generates patches."""
|
|
488
558
|
|
|
559
|
+
__slots__ = ("_data", "_path", "_recorder")
|
|
560
|
+
__hash__ = None # sets are unhashable
|
|
561
|
+
|
|
489
562
|
def __init__(self, data: Set, recorder: PatchRecorder, path: Pointer):
|
|
490
563
|
self._data = data
|
|
491
564
|
self._recorder = recorder
|
|
@@ -564,6 +637,31 @@ class SetProxy:
|
|
|
564
637
|
self.add(value)
|
|
565
638
|
return self
|
|
566
639
|
|
|
640
|
+
def difference_update(self, *others):
|
|
641
|
+
"""Remove all elements found in others."""
|
|
642
|
+
for other in others:
|
|
643
|
+
for value in other:
|
|
644
|
+
if value in self._data:
|
|
645
|
+
self.remove(value)
|
|
646
|
+
|
|
647
|
+
def intersection_update(self, *others):
|
|
648
|
+
"""Keep only elements found in all others."""
|
|
649
|
+
# Compute the intersection first, then remove what's not in it
|
|
650
|
+
keep = self._data.copy()
|
|
651
|
+
for other in others:
|
|
652
|
+
keep &= set(other)
|
|
653
|
+
values_to_remove = [v for v in self._data if v not in keep]
|
|
654
|
+
for value in values_to_remove:
|
|
655
|
+
self.remove(value)
|
|
656
|
+
|
|
657
|
+
def symmetric_difference_update(self, other):
|
|
658
|
+
"""Update with symmetric difference."""
|
|
659
|
+
for value in other:
|
|
660
|
+
if value in self._data:
|
|
661
|
+
self.remove(value)
|
|
662
|
+
else:
|
|
663
|
+
self.add(value)
|
|
664
|
+
|
|
567
665
|
|
|
568
666
|
# Add simple reader methods to SetProxy
|
|
569
667
|
_add_reader_methods(
|
|
@@ -581,8 +679,26 @@ _add_reader_methods(
|
|
|
581
679
|
"issubset",
|
|
582
680
|
"issuperset",
|
|
583
681
|
"copy",
|
|
682
|
+
"__str__",
|
|
683
|
+
"__format__",
|
|
684
|
+
"__eq__",
|
|
685
|
+
"__ne__",
|
|
686
|
+
"__le__",
|
|
687
|
+
"__lt__",
|
|
688
|
+
"__ge__",
|
|
689
|
+
"__gt__",
|
|
690
|
+
"__or__",
|
|
691
|
+
"__ror__",
|
|
692
|
+
"__and__",
|
|
693
|
+
"__rand__",
|
|
694
|
+
"__sub__",
|
|
695
|
+
"__rsub__",
|
|
696
|
+
"__xor__",
|
|
697
|
+
"__rxor__",
|
|
584
698
|
],
|
|
585
699
|
)
|
|
700
|
+
# Skipped set methods:
|
|
701
|
+
# - __class_getitem__: typing support (set[int]), not relevant for instances
|
|
586
702
|
|
|
587
703
|
|
|
588
704
|
def produce(
|
|
@@ -34,6 +34,29 @@ def test_apply_list():
|
|
|
34
34
|
assert a == d
|
|
35
35
|
|
|
36
36
|
|
|
37
|
+
def test_apply_empty():
|
|
38
|
+
a = {
|
|
39
|
+
"a": [5, 7, 9, {"a", "b", "c"}],
|
|
40
|
+
"b": 6,
|
|
41
|
+
}
|
|
42
|
+
b = {
|
|
43
|
+
"a": [5, 7, 9, {"a", "b", "c"}],
|
|
44
|
+
"b": 6,
|
|
45
|
+
}
|
|
46
|
+
assert a == b
|
|
47
|
+
|
|
48
|
+
ops, rops = diff(a, b)
|
|
49
|
+
|
|
50
|
+
assert not ops
|
|
51
|
+
assert not rops
|
|
52
|
+
|
|
53
|
+
c = apply(a, ops)
|
|
54
|
+
assert c == b
|
|
55
|
+
|
|
56
|
+
d = apply(b, rops)
|
|
57
|
+
assert a == d
|
|
58
|
+
|
|
59
|
+
|
|
37
60
|
def test_add_remove_list():
|
|
38
61
|
a = []
|
|
39
62
|
b = [1]
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import pytest
|
|
4
4
|
|
|
5
5
|
from patchdiff import apply, produce
|
|
6
|
+
from patchdiff.produce import DictProxy, ListProxy, SetProxy
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
def assert_patches_work(base, recipe):
|
|
@@ -793,3 +794,78 @@ def test_cross_level_modifications():
|
|
|
793
794
|
assert result["level1"]["sibling"][0]["a"] == 10
|
|
794
795
|
|
|
795
796
|
assert len(patches) >= 7
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
# -- Proxy API completeness tests --
|
|
800
|
+
# These tests ensure that proxy classes cover all methods of their base types.
|
|
801
|
+
# If a new Python version adds a method to dict/list/set, the corresponding
|
|
802
|
+
# test will fail. To fix it, either:
|
|
803
|
+
# 1. Add the method name to SKIPPED below (if it doesn't need proxying), or
|
|
804
|
+
# 2. Implement it on the proxy class.
|
|
805
|
+
|
|
806
|
+
# Methods inherited from object that are not part of the container API
|
|
807
|
+
_OBJECT_INTERNALS = {
|
|
808
|
+
"__class__",
|
|
809
|
+
"__delattr__",
|
|
810
|
+
"__dir__",
|
|
811
|
+
"__doc__",
|
|
812
|
+
"__getattribute__",
|
|
813
|
+
"__getstate__",
|
|
814
|
+
"__init__",
|
|
815
|
+
"__init_subclass__",
|
|
816
|
+
"__new__",
|
|
817
|
+
"__reduce__",
|
|
818
|
+
"__reduce_ex__",
|
|
819
|
+
"__setattr__",
|
|
820
|
+
"__sizeof__",
|
|
821
|
+
"__subclasshook__",
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
def _unhandled_methods(proxy_cls, base_cls, skipped):
|
|
826
|
+
"""Return methods on base_cls that are missing from proxy_cls and not in skipped."""
|
|
827
|
+
base_methods = set(dir(base_cls)) - _OBJECT_INTERNALS
|
|
828
|
+
proxy_methods = set(dir(proxy_cls)) - _OBJECT_INTERNALS
|
|
829
|
+
return (base_methods - proxy_methods) - set(skipped)
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
# Methods intentionally not implemented on the proxy classes.
|
|
833
|
+
# If a new Python version adds a method to dict/list/set, the test will
|
|
834
|
+
# fail. To fix, either implement the method or add it here.
|
|
835
|
+
_DICT_SKIPPED = {
|
|
836
|
+
"fromkeys", # classmethod, not relevant for proxy instances
|
|
837
|
+
"__class_getitem__", # typing support (dict[str, int])
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
_LIST_SKIPPED = {
|
|
841
|
+
"__class_getitem__", # typing support (list[int])
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
_SET_SKIPPED = {
|
|
845
|
+
"__class_getitem__", # typing support (set[int])
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
class TestProxyApiCompleteness:
|
|
850
|
+
"""Verify proxy classes implement all methods of their base types."""
|
|
851
|
+
|
|
852
|
+
def test_dict_proxy_api_completeness(self):
|
|
853
|
+
unhandled = _unhandled_methods(DictProxy, dict, _DICT_SKIPPED)
|
|
854
|
+
assert not unhandled, (
|
|
855
|
+
f"DictProxy is missing methods from dict: {sorted(unhandled)}. "
|
|
856
|
+
f"Either implement them on DictProxy or add to _DICT_SKIPPED."
|
|
857
|
+
)
|
|
858
|
+
|
|
859
|
+
def test_list_proxy_api_completeness(self):
|
|
860
|
+
unhandled = _unhandled_methods(ListProxy, list, _LIST_SKIPPED)
|
|
861
|
+
assert not unhandled, (
|
|
862
|
+
f"ListProxy is missing methods from list: {sorted(unhandled)}. "
|
|
863
|
+
f"Either implement them on ListProxy or add to _LIST_SKIPPED."
|
|
864
|
+
)
|
|
865
|
+
|
|
866
|
+
def test_set_proxy_api_completeness(self):
|
|
867
|
+
unhandled = _unhandled_methods(SetProxy, set, _SET_SKIPPED)
|
|
868
|
+
assert not unhandled, (
|
|
869
|
+
f"SetProxy is missing methods from set: {sorted(unhandled)}. "
|
|
870
|
+
f"Either implement them on SetProxy or add to _SET_SKIPPED."
|
|
871
|
+
)
|
|
@@ -102,6 +102,23 @@ def test_dict_pop():
|
|
|
102
102
|
assert patches[0]["op"] == "remove"
|
|
103
103
|
|
|
104
104
|
|
|
105
|
+
def test_dict_pop_invalidates_proxy_cache():
|
|
106
|
+
"""Test that pop() invalidates the proxy cache for nested structures."""
|
|
107
|
+
base = {"nested": {"a": 1}, "other": 2}
|
|
108
|
+
|
|
109
|
+
def recipe(draft):
|
|
110
|
+
# Access nested to populate the proxy cache
|
|
111
|
+
_ = draft["nested"]["a"]
|
|
112
|
+
# Pop the key that has a cached proxy
|
|
113
|
+
draft.pop("nested")
|
|
114
|
+
|
|
115
|
+
result, patches, _reverse = produce(base, recipe)
|
|
116
|
+
|
|
117
|
+
assert result == {"other": 2}
|
|
118
|
+
assert len(patches) == 1
|
|
119
|
+
assert patches[0]["op"] == "remove"
|
|
120
|
+
|
|
121
|
+
|
|
105
122
|
def test_dict_update():
|
|
106
123
|
"""Test dict.update() operation."""
|
|
107
124
|
base = {"a": 1}
|
|
@@ -281,6 +298,23 @@ def test_dict_popitem():
|
|
|
281
298
|
assert patches[0]["op"] == "remove"
|
|
282
299
|
|
|
283
300
|
|
|
301
|
+
def test_dict_popitem_invalidates_proxy_cache():
|
|
302
|
+
"""Test that popitem() invalidates the proxy cache for nested structures."""
|
|
303
|
+
base = {"a": {"x": 1}}
|
|
304
|
+
|
|
305
|
+
def recipe(draft):
|
|
306
|
+
# Access nested to populate the proxy cache
|
|
307
|
+
_ = draft["a"]["x"]
|
|
308
|
+
# popitem removes the only key which has a cached proxy
|
|
309
|
+
draft.popitem()
|
|
310
|
+
|
|
311
|
+
result, patches, _reverse = produce(base, recipe)
|
|
312
|
+
|
|
313
|
+
assert result == {}
|
|
314
|
+
assert len(patches) == 1
|
|
315
|
+
assert patches[0]["op"] == "remove"
|
|
316
|
+
|
|
317
|
+
|
|
284
318
|
def test_dict_popitem_empty():
|
|
285
319
|
"""Test popitem() on empty dict raises KeyError."""
|
|
286
320
|
base = {}
|
|
@@ -567,6 +601,98 @@ def test_dict_get_none_implicit():
|
|
|
567
601
|
assert result == {"a": 1}
|
|
568
602
|
|
|
569
603
|
|
|
604
|
+
def test_dict_values_returns_proxied_nested():
|
|
605
|
+
"""Test that values() returns proxied nested objects."""
|
|
606
|
+
base = {"a": {"x": 1}, "b": {"x": 2}}
|
|
607
|
+
|
|
608
|
+
def recipe(draft):
|
|
609
|
+
for v in draft.values():
|
|
610
|
+
v["x"] = 99
|
|
611
|
+
|
|
612
|
+
result, patches, _reverse = produce(base, recipe)
|
|
613
|
+
|
|
614
|
+
assert result == {"a": {"x": 99}, "b": {"x": 99}}
|
|
615
|
+
assert len(patches) == 2
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def test_dict_items_returns_proxied_nested():
|
|
619
|
+
"""Test that items() returns proxied nested objects."""
|
|
620
|
+
base = {"a": {"x": 1}, "b": {"x": 2}}
|
|
621
|
+
|
|
622
|
+
def recipe(draft):
|
|
623
|
+
for k, v in draft.items():
|
|
624
|
+
v["x"] = 99
|
|
625
|
+
|
|
626
|
+
result, patches, _reverse = produce(base, recipe)
|
|
627
|
+
|
|
628
|
+
assert result == {"a": {"x": 99}, "b": {"x": 99}}
|
|
629
|
+
assert len(patches) == 2
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def test_dict_ior_operator():
|
|
633
|
+
"""Test |= operator (merge update) on dict proxy."""
|
|
634
|
+
base = {"a": 1}
|
|
635
|
+
|
|
636
|
+
def recipe(draft):
|
|
637
|
+
draft |= {"b": 2, "c": 3}
|
|
638
|
+
|
|
639
|
+
result, patches, _reverse = produce(base, recipe)
|
|
640
|
+
|
|
641
|
+
assert result == {"a": 1, "b": 2, "c": 3}
|
|
642
|
+
assert len(patches) == 2
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def test_dict_or_operator():
|
|
646
|
+
"""Test | operator (merge) on dict proxy returns new dict."""
|
|
647
|
+
base = {"a": 1}
|
|
648
|
+
|
|
649
|
+
def recipe(draft):
|
|
650
|
+
merged = draft | {"b": 2}
|
|
651
|
+
assert isinstance(merged, dict)
|
|
652
|
+
assert merged == {"a": 1, "b": 2}
|
|
653
|
+
|
|
654
|
+
_result, patches, _reverse = produce(base, recipe)
|
|
655
|
+
|
|
656
|
+
assert patches == [] # No mutations to draft
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def test_dict_eq():
|
|
660
|
+
"""Test __eq__ on dict proxy."""
|
|
661
|
+
base = {"a": 1, "b": 2}
|
|
662
|
+
|
|
663
|
+
def recipe(draft):
|
|
664
|
+
assert draft == {"a": 1, "b": 2}
|
|
665
|
+
assert not (draft == {"a": 1})
|
|
666
|
+
|
|
667
|
+
produce(base, recipe)
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
def test_dict_ne():
|
|
671
|
+
"""Test __ne__ on dict proxy."""
|
|
672
|
+
base = {"a": 1}
|
|
673
|
+
|
|
674
|
+
def recipe(draft):
|
|
675
|
+
assert draft != {"b": 2}
|
|
676
|
+
assert not (draft != {"a": 1})
|
|
677
|
+
|
|
678
|
+
produce(base, recipe)
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def test_dict_bool():
|
|
682
|
+
"""Test __bool__ on dict proxy."""
|
|
683
|
+
base_empty = {}
|
|
684
|
+
base_full = {"a": 1}
|
|
685
|
+
|
|
686
|
+
def recipe_empty(draft):
|
|
687
|
+
assert not draft
|
|
688
|
+
|
|
689
|
+
def recipe_full(draft):
|
|
690
|
+
assert draft
|
|
691
|
+
|
|
692
|
+
produce(base_empty, recipe_empty)
|
|
693
|
+
produce(base_full, recipe_full)
|
|
694
|
+
|
|
695
|
+
|
|
570
696
|
def test_dict_get_none_explicit():
|
|
571
697
|
"""Test get() with explicit None default."""
|
|
572
698
|
base = {"a": 1}
|
|
@@ -982,3 +982,160 @@ def test_list_count_empty_list():
|
|
|
982
982
|
result, _patches, _reverse = produce(base, recipe)
|
|
983
983
|
|
|
984
984
|
assert result == []
|
|
985
|
+
|
|
986
|
+
|
|
987
|
+
def test_list_iter_returns_proxied_nested():
|
|
988
|
+
"""Test that __iter__ returns proxied nested objects."""
|
|
989
|
+
base = [{"x": 1}, {"x": 2}]
|
|
990
|
+
|
|
991
|
+
def recipe(draft):
|
|
992
|
+
for item in draft:
|
|
993
|
+
item["x"] = 99
|
|
994
|
+
|
|
995
|
+
result, patches, _reverse = produce(base, recipe)
|
|
996
|
+
|
|
997
|
+
assert result == [{"x": 99}, {"x": 99}]
|
|
998
|
+
assert len(patches) == 2
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
def test_list_reversed_returns_proxied_nested():
|
|
1002
|
+
"""Test that __reversed__ returns proxied nested objects."""
|
|
1003
|
+
base = [{"x": 1}, {"x": 2}]
|
|
1004
|
+
|
|
1005
|
+
def recipe(draft):
|
|
1006
|
+
for item in reversed(draft):
|
|
1007
|
+
item["x"] = 99
|
|
1008
|
+
|
|
1009
|
+
result, patches, _reverse = produce(base, recipe)
|
|
1010
|
+
|
|
1011
|
+
assert result == [{"x": 99}, {"x": 99}]
|
|
1012
|
+
assert len(patches) == 2
|
|
1013
|
+
|
|
1014
|
+
|
|
1015
|
+
def test_list_iadd_operator():
|
|
1016
|
+
"""Test += operator (in-place add, like extend) on list proxy."""
|
|
1017
|
+
base = [1, 2]
|
|
1018
|
+
|
|
1019
|
+
def recipe(draft):
|
|
1020
|
+
draft += [3, 4]
|
|
1021
|
+
|
|
1022
|
+
result, patches, _reverse = produce(base, recipe)
|
|
1023
|
+
|
|
1024
|
+
assert result == [1, 2, 3, 4]
|
|
1025
|
+
assert len(patches) == 2
|
|
1026
|
+
|
|
1027
|
+
|
|
1028
|
+
def test_list_imul_operator():
|
|
1029
|
+
"""Test *= operator (in-place repeat) on list proxy."""
|
|
1030
|
+
base = [1, 2]
|
|
1031
|
+
|
|
1032
|
+
def recipe(draft):
|
|
1033
|
+
draft *= 3
|
|
1034
|
+
|
|
1035
|
+
result, patches, _reverse = produce(base, recipe)
|
|
1036
|
+
|
|
1037
|
+
assert result == [1, 2, 1, 2, 1, 2]
|
|
1038
|
+
assert len(patches) == 4 # 4 new elements added
|
|
1039
|
+
|
|
1040
|
+
|
|
1041
|
+
def test_list_imul_zero():
|
|
1042
|
+
"""Test *= 0 clears the list."""
|
|
1043
|
+
base = [1, 2, 3]
|
|
1044
|
+
|
|
1045
|
+
def recipe(draft):
|
|
1046
|
+
draft *= 0
|
|
1047
|
+
|
|
1048
|
+
result, patches, _reverse = produce(base, recipe)
|
|
1049
|
+
|
|
1050
|
+
assert result == []
|
|
1051
|
+
assert len(patches) == 3 # 3 elements removed
|
|
1052
|
+
|
|
1053
|
+
|
|
1054
|
+
def test_list_add_operator():
|
|
1055
|
+
"""Test + operator returns new list, not a proxy."""
|
|
1056
|
+
base = [1, 2]
|
|
1057
|
+
|
|
1058
|
+
def recipe(draft):
|
|
1059
|
+
new = draft + [3, 4] # noqa: RUF005
|
|
1060
|
+
assert isinstance(new, list)
|
|
1061
|
+
assert new == [1, 2, 3, 4]
|
|
1062
|
+
|
|
1063
|
+
_result, patches, _reverse = produce(base, recipe)
|
|
1064
|
+
|
|
1065
|
+
assert patches == []
|
|
1066
|
+
|
|
1067
|
+
|
|
1068
|
+
def test_list_mul_operator():
|
|
1069
|
+
"""Test * operator returns new list, not a proxy."""
|
|
1070
|
+
base = [1, 2]
|
|
1071
|
+
|
|
1072
|
+
def recipe(draft):
|
|
1073
|
+
new = draft * 3
|
|
1074
|
+
assert isinstance(new, list)
|
|
1075
|
+
assert new == [1, 2, 1, 2, 1, 2]
|
|
1076
|
+
|
|
1077
|
+
_result, patches, _reverse = produce(base, recipe)
|
|
1078
|
+
|
|
1079
|
+
assert patches == []
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
def test_list_rmul_operator():
|
|
1083
|
+
"""Test reverse * operator (int * list) returns new list."""
|
|
1084
|
+
base = [1, 2]
|
|
1085
|
+
|
|
1086
|
+
def recipe(draft):
|
|
1087
|
+
new = 3 * draft
|
|
1088
|
+
assert isinstance(new, list)
|
|
1089
|
+
assert new == [1, 2, 1, 2, 1, 2]
|
|
1090
|
+
|
|
1091
|
+
_result, patches, _reverse = produce(base, recipe)
|
|
1092
|
+
|
|
1093
|
+
assert patches == []
|
|
1094
|
+
|
|
1095
|
+
|
|
1096
|
+
def test_list_eq():
|
|
1097
|
+
"""Test __eq__ on list proxy."""
|
|
1098
|
+
base = [1, 2, 3]
|
|
1099
|
+
|
|
1100
|
+
def recipe(draft):
|
|
1101
|
+
assert draft == [1, 2, 3]
|
|
1102
|
+
assert not (draft == [1, 2])
|
|
1103
|
+
|
|
1104
|
+
produce(base, recipe)
|
|
1105
|
+
|
|
1106
|
+
|
|
1107
|
+
def test_list_ne():
|
|
1108
|
+
"""Test __ne__ on list proxy."""
|
|
1109
|
+
base = [1, 2, 3]
|
|
1110
|
+
|
|
1111
|
+
def recipe(draft):
|
|
1112
|
+
assert draft != [1, 2]
|
|
1113
|
+
assert not (draft != [1, 2, 3])
|
|
1114
|
+
|
|
1115
|
+
produce(base, recipe)
|
|
1116
|
+
|
|
1117
|
+
|
|
1118
|
+
def test_list_bool():
|
|
1119
|
+
"""Test __bool__ on list proxy."""
|
|
1120
|
+
|
|
1121
|
+
def recipe_empty(draft):
|
|
1122
|
+
assert not draft
|
|
1123
|
+
|
|
1124
|
+
def recipe_full(draft):
|
|
1125
|
+
assert draft
|
|
1126
|
+
|
|
1127
|
+
produce([], recipe_empty)
|
|
1128
|
+
produce([1], recipe_full)
|
|
1129
|
+
|
|
1130
|
+
|
|
1131
|
+
def test_list_lt_le_gt_ge():
|
|
1132
|
+
"""Test comparison operators on list proxy."""
|
|
1133
|
+
base = [1, 2, 3]
|
|
1134
|
+
|
|
1135
|
+
def recipe(draft):
|
|
1136
|
+
assert draft < [1, 2, 4]
|
|
1137
|
+
assert draft <= [1, 2, 3]
|
|
1138
|
+
assert draft > [1, 2, 2]
|
|
1139
|
+
assert draft >= [1, 2, 3]
|
|
1140
|
+
|
|
1141
|
+
produce(base, recipe)
|
|
@@ -535,3 +535,148 @@ def test_set_issuperset_self():
|
|
|
535
535
|
result, _patches, _reverse = produce(base, recipe)
|
|
536
536
|
|
|
537
537
|
assert result == base
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def test_set_difference_update_method():
|
|
541
|
+
"""Test difference_update() method on set proxy."""
|
|
542
|
+
base = {1, 2, 3, 4}
|
|
543
|
+
|
|
544
|
+
def recipe(draft):
|
|
545
|
+
draft.difference_update({2, 4})
|
|
546
|
+
|
|
547
|
+
result, patches, _reverse = produce(base, recipe)
|
|
548
|
+
|
|
549
|
+
assert result == {1, 3}
|
|
550
|
+
assert len(patches) == 2
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def test_set_intersection_update_method():
|
|
554
|
+
"""Test intersection_update() method on set proxy."""
|
|
555
|
+
base = {1, 2, 3, 4}
|
|
556
|
+
|
|
557
|
+
def recipe(draft):
|
|
558
|
+
draft.intersection_update({2, 3, 5})
|
|
559
|
+
|
|
560
|
+
result, patches, _reverse = produce(base, recipe)
|
|
561
|
+
|
|
562
|
+
assert result == {2, 3}
|
|
563
|
+
assert len(patches) == 2 # Removed 1 and 4
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def test_set_symmetric_difference_update_method():
|
|
567
|
+
"""Test symmetric_difference_update() method on set proxy."""
|
|
568
|
+
base = {1, 2, 3}
|
|
569
|
+
|
|
570
|
+
def recipe(draft):
|
|
571
|
+
draft.symmetric_difference_update({2, 3, 4})
|
|
572
|
+
|
|
573
|
+
result, patches, _reverse = produce(base, recipe)
|
|
574
|
+
|
|
575
|
+
assert result == {1, 4}
|
|
576
|
+
assert len(patches) == 3 # Removed 2, 3, added 4
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def test_set_or_operator():
|
|
580
|
+
"""Test | operator (union) returns new set, not a proxy."""
|
|
581
|
+
base = {1, 2, 3}
|
|
582
|
+
|
|
583
|
+
def recipe(draft):
|
|
584
|
+
new = draft | {3, 4, 5}
|
|
585
|
+
assert isinstance(new, set)
|
|
586
|
+
assert new == {1, 2, 3, 4, 5}
|
|
587
|
+
|
|
588
|
+
_result, patches, _reverse = produce(base, recipe)
|
|
589
|
+
|
|
590
|
+
assert patches == []
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def test_set_and_operator():
|
|
594
|
+
"""Test & operator (intersection) returns new set."""
|
|
595
|
+
base = {1, 2, 3}
|
|
596
|
+
|
|
597
|
+
def recipe(draft):
|
|
598
|
+
new = draft & {2, 3, 4}
|
|
599
|
+
assert isinstance(new, set)
|
|
600
|
+
assert new == {2, 3}
|
|
601
|
+
|
|
602
|
+
_result, patches, _reverse = produce(base, recipe)
|
|
603
|
+
|
|
604
|
+
assert patches == []
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def test_set_sub_operator():
|
|
608
|
+
"""Test - operator (difference) returns new set."""
|
|
609
|
+
base = {1, 2, 3}
|
|
610
|
+
|
|
611
|
+
def recipe(draft):
|
|
612
|
+
new = draft - {2, 4}
|
|
613
|
+
assert isinstance(new, set)
|
|
614
|
+
assert new == {1, 3}
|
|
615
|
+
|
|
616
|
+
_result, patches, _reverse = produce(base, recipe)
|
|
617
|
+
|
|
618
|
+
assert patches == []
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def test_set_xor_operator():
|
|
622
|
+
"""Test ^ operator (symmetric difference) returns new set."""
|
|
623
|
+
base = {1, 2, 3}
|
|
624
|
+
|
|
625
|
+
def recipe(draft):
|
|
626
|
+
new = draft ^ {2, 3, 4}
|
|
627
|
+
assert isinstance(new, set)
|
|
628
|
+
assert new == {1, 4}
|
|
629
|
+
|
|
630
|
+
_result, patches, _reverse = produce(base, recipe)
|
|
631
|
+
|
|
632
|
+
assert patches == []
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
def test_set_eq():
|
|
636
|
+
"""Test __eq__ on set proxy."""
|
|
637
|
+
base = {1, 2, 3}
|
|
638
|
+
|
|
639
|
+
def recipe(draft):
|
|
640
|
+
assert draft == {1, 2, 3}
|
|
641
|
+
assert not (draft == {1, 2})
|
|
642
|
+
|
|
643
|
+
produce(base, recipe)
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def test_set_ne():
|
|
647
|
+
"""Test __ne__ on set proxy."""
|
|
648
|
+
base = {1, 2, 3}
|
|
649
|
+
|
|
650
|
+
def recipe(draft):
|
|
651
|
+
assert draft != {1, 2}
|
|
652
|
+
assert not (draft != {1, 2, 3})
|
|
653
|
+
|
|
654
|
+
produce(base, recipe)
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def test_set_bool():
|
|
658
|
+
"""Test __bool__ on set proxy."""
|
|
659
|
+
|
|
660
|
+
def recipe_empty(draft):
|
|
661
|
+
assert not draft
|
|
662
|
+
|
|
663
|
+
def recipe_full(draft):
|
|
664
|
+
assert draft
|
|
665
|
+
|
|
666
|
+
produce(set(), recipe_empty)
|
|
667
|
+
produce({1}, recipe_full)
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
def test_set_le_lt_ge_gt():
|
|
671
|
+
"""Test comparison operators on set proxy (subset/superset)."""
|
|
672
|
+
base = {1, 2, 3}
|
|
673
|
+
|
|
674
|
+
def recipe(draft):
|
|
675
|
+
assert draft <= {1, 2, 3, 4} # subset
|
|
676
|
+
assert draft <= {1, 2, 3} # equal is also <=
|
|
677
|
+
assert draft < {1, 2, 3, 4} # proper subset
|
|
678
|
+
assert not (draft < {1, 2, 3}) # not proper subset of equal
|
|
679
|
+
assert draft >= {1, 2} # superset
|
|
680
|
+
assert draft > {1, 2} # proper superset
|
|
681
|
+
|
|
682
|
+
produce(base, recipe)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|