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.
Files changed (24) hide show
  1. {patchdiff-0.3.8 → patchdiff-0.3.9}/.github/workflows/ci.yml +1 -1
  2. {patchdiff-0.3.8 → patchdiff-0.3.9}/PKG-INFO +1 -1
  3. {patchdiff-0.3.8 → patchdiff-0.3.9}/patchdiff/produce.py +120 -4
  4. {patchdiff-0.3.8 → patchdiff-0.3.9}/pyproject.toml +1 -1
  5. {patchdiff-0.3.8 → patchdiff-0.3.9}/tests/test_apply.py +23 -0
  6. {patchdiff-0.3.8 → patchdiff-0.3.9}/tests/test_produce_core.py +76 -0
  7. {patchdiff-0.3.8 → patchdiff-0.3.9}/tests/test_produce_dict.py +126 -0
  8. {patchdiff-0.3.8 → patchdiff-0.3.9}/tests/test_produce_list.py +157 -0
  9. {patchdiff-0.3.8 → patchdiff-0.3.9}/tests/test_produce_set.py +145 -0
  10. {patchdiff-0.3.8 → patchdiff-0.3.9}/.github/workflows/benchmark.yml +0 -0
  11. {patchdiff-0.3.8 → patchdiff-0.3.9}/.gitignore +0 -0
  12. {patchdiff-0.3.8 → patchdiff-0.3.9}/README.md +0 -0
  13. {patchdiff-0.3.8 → patchdiff-0.3.9}/benchmarks/benchmark.py +0 -0
  14. {patchdiff-0.3.8 → patchdiff-0.3.9}/patchdiff/__init__.py +0 -0
  15. {patchdiff-0.3.8 → patchdiff-0.3.9}/patchdiff/apply.py +0 -0
  16. {patchdiff-0.3.8 → patchdiff-0.3.9}/patchdiff/diff.py +0 -0
  17. {patchdiff-0.3.8 → patchdiff-0.3.9}/patchdiff/pointer.py +0 -0
  18. {patchdiff-0.3.8 → patchdiff-0.3.9}/patchdiff/serialize.py +0 -0
  19. {patchdiff-0.3.8 → patchdiff-0.3.9}/patchdiff/types.py +0 -0
  20. {patchdiff-0.3.8 → patchdiff-0.3.9}/tests/test_diff.py +0 -0
  21. {patchdiff-0.3.8 → patchdiff-0.3.9}/tests/test_observ_integration.py +0 -0
  22. {patchdiff-0.3.8 → patchdiff-0.3.9}/tests/test_pointer.py +0 -0
  23. {patchdiff-0.3.8 → patchdiff-0.3.9}/tests/test_proxy.py +0 -0
  24. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patchdiff
3
- Version: 0.3.8
3
+ Version: 0.3.9
4
4
  Summary: MIT
5
5
  Project-URL: Homepage, https://github.com/fork-tongue/patchdiff
6
6
  Author-email: Korijn van Golen <korijn@gmail.com>, Berend Klein Haneveld <berendkleinhaneveld@gmail.com>
@@ -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
- "values",
232
- "items",
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
- "__iter__",
478
- "__reversed__",
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(
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "patchdiff"
3
- version = "0.3.8"
3
+ version = "0.3.9"
4
4
  description = "MIT"
5
5
  authors = [
6
6
  { name = "Korijn van Golen", email = "korijn@gmail.com" },
@@ -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