1import copy
2import pickle
3
4from sqlalchemy import event
5from sqlalchemy import ForeignKey
6from sqlalchemy import func
7from sqlalchemy import Integer
8from sqlalchemy import String
9from sqlalchemy import testing
10from sqlalchemy import util
11from sqlalchemy.ext.mutable import MutableComposite
12from sqlalchemy.ext.mutable import MutableDict
13from sqlalchemy.ext.mutable import MutableList
14from sqlalchemy.ext.mutable import MutableSet
15from sqlalchemy.orm import attributes
16from sqlalchemy.orm import column_property
17from sqlalchemy.orm import composite
18from sqlalchemy.orm import declarative_base
19from sqlalchemy.orm.instrumentation import ClassManager
20from sqlalchemy.orm.mapper import Mapper
21from sqlalchemy.testing import assert_raises
22from sqlalchemy.testing import assert_raises_message
23from sqlalchemy.testing import eq_
24from sqlalchemy.testing import fixtures
25from sqlalchemy.testing import is_
26from sqlalchemy.testing import mock
27from sqlalchemy.testing.fixtures import fixture_session
28from sqlalchemy.testing.schema import Column
29from sqlalchemy.testing.schema import Table
30from sqlalchemy.testing.util import picklers
31from sqlalchemy.types import PickleType
32from sqlalchemy.types import TypeDecorator
33from sqlalchemy.types import VARCHAR
34
35
36class Foo(fixtures.BasicEntity):
37    pass
38
39
40class SubFoo(Foo):
41    pass
42
43
44class FooWithEq(object):
45    def __init__(self, **kw):
46        for k in kw:
47            setattr(self, k, kw[k])
48
49    def __hash__(self):
50        return hash(self.id)
51
52    def __eq__(self, other):
53        return self.id == other.id
54
55
56class FooWNoHash(fixtures.BasicEntity):
57    __hash__ = None
58
59
60class Point(MutableComposite):
61    def __init__(self, x, y):
62        self.x = x
63        self.y = y
64
65    def __setattr__(self, key, value):
66        object.__setattr__(self, key, value)
67        self.changed()
68
69    def __composite_values__(self):
70        return self.x, self.y
71
72    def __getstate__(self):
73        return self.x, self.y
74
75    def __setstate__(self, state):
76        self.x, self.y = state
77
78    def __eq__(self, other):
79        return (
80            isinstance(other, Point)
81            and other.x == self.x
82            and other.y == self.y
83        )
84
85
86class MyPoint(Point):
87    @classmethod
88    def coerce(cls, key, value):
89        if isinstance(value, tuple):
90            value = Point(*value)
91        return value
92
93
94class _MutableDictTestFixture(object):
95    @classmethod
96    def _type_fixture(cls):
97        return MutableDict
98
99    def teardown_test(self):
100        # clear out mapper events
101        Mapper.dispatch._clear()
102        ClassManager.dispatch._clear()
103
104
105class _MutableDictTestBase(_MutableDictTestFixture):
106    run_define_tables = "each"
107
108    def setup_mappers(cls):
109        foo = cls.tables.foo
110
111        cls.mapper_registry.map_imperatively(Foo, foo)
112
113    def test_coerce_none(self):
114        sess = fixture_session()
115        f1 = Foo(data=None)
116        sess.add(f1)
117        sess.commit()
118        eq_(f1.data, None)
119
120    def test_coerce_raise(self):
121        assert_raises_message(
122            ValueError,
123            "Attribute 'data' does not accept objects of type",
124            Foo,
125            data=set([1, 2, 3]),
126        )
127
128    def test_in_place_mutation(self):
129        sess = fixture_session()
130
131        f1 = Foo(data={"a": "b"})
132        sess.add(f1)
133        sess.commit()
134
135        f1.data["a"] = "c"
136        sess.commit()
137
138        eq_(f1.data, {"a": "c"})
139
140    def test_modified_event(self):
141        canary = mock.Mock()
142        event.listen(Foo.data, "modified", canary)
143
144        f1 = Foo(data={"a": "b"})
145        f1.data["a"] = "c"
146
147        eq_(
148            canary.mock_calls,
149            [
150                mock.call(
151                    f1, attributes.Event(Foo.data.impl, attributes.OP_MODIFIED)
152                )
153            ],
154        )
155
156    def test_clear(self):
157        sess = fixture_session()
158
159        f1 = Foo(data={"a": "b"})
160        sess.add(f1)
161        sess.commit()
162
163        f1.data.clear()
164        sess.commit()
165
166        eq_(f1.data, {})
167
168    def test_update(self):
169        sess = fixture_session()
170
171        f1 = Foo(data={"a": "b"})
172        sess.add(f1)
173        sess.commit()
174
175        f1.data.update({"a": "z"})
176        sess.commit()
177
178        eq_(f1.data, {"a": "z"})
179
180    def test_pop(self):
181        sess = fixture_session()
182
183        f1 = Foo(data={"a": "b", "c": "d"})
184        sess.add(f1)
185        sess.commit()
186
187        eq_(f1.data.pop("a"), "b")
188        sess.commit()
189
190        assert_raises(KeyError, f1.data.pop, "g")
191
192        eq_(f1.data, {"c": "d"})
193
194    def test_pop_default(self):
195        sess = fixture_session()
196
197        f1 = Foo(data={"a": "b", "c": "d"})
198        sess.add(f1)
199        sess.commit()
200
201        eq_(f1.data.pop("a", "q"), "b")
202        eq_(f1.data.pop("a", "q"), "q")
203        sess.commit()
204
205        eq_(f1.data, {"c": "d"})
206
207    def test_popitem(self):
208        sess = fixture_session()
209
210        orig = {"a": "b", "c": "d"}
211
212        # the orig dict remains unchanged when we assign,
213        # but just making this future-proof
214        data = dict(orig)
215        f1 = Foo(data=data)
216        sess.add(f1)
217        sess.commit()
218
219        k, v = f1.data.popitem()
220        assert k in ("a", "c")
221        orig.pop(k)
222
223        sess.commit()
224
225        eq_(f1.data, orig)
226
227    def test_setdefault(self):
228        sess = fixture_session()
229
230        f1 = Foo(data={"a": "b"})
231        sess.add(f1)
232        sess.commit()
233
234        eq_(f1.data.setdefault("c", "d"), "d")
235        sess.commit()
236
237        eq_(f1.data, {"a": "b", "c": "d"})
238
239        eq_(f1.data.setdefault("c", "q"), "d")
240        sess.commit()
241
242        eq_(f1.data, {"a": "b", "c": "d"})
243
244    def test_replace(self):
245        sess = fixture_session()
246        f1 = Foo(data={"a": "b"})
247        sess.add(f1)
248        sess.flush()
249
250        f1.data = {"b": "c"}
251        sess.commit()
252        eq_(f1.data, {"b": "c"})
253
254    def test_replace_itself_still_ok(self):
255        sess = fixture_session()
256        f1 = Foo(data={"a": "b"})
257        sess.add(f1)
258        sess.flush()
259
260        f1.data = f1.data
261        f1.data["b"] = "c"
262        sess.commit()
263        eq_(f1.data, {"a": "b", "b": "c"})
264
265    def test_pickle_parent(self):
266        sess = fixture_session()
267
268        f1 = Foo(data={"a": "b"})
269        sess.add(f1)
270        sess.commit()
271        f1.data
272        sess.close()
273
274        for loads, dumps in picklers():
275            sess = fixture_session()
276            f2 = loads(dumps(f1))
277            sess.add(f2)
278            f2.data["a"] = "c"
279            assert f2 in sess.dirty
280
281    def test_unrelated_flush(self):
282        sess = fixture_session()
283        f1 = Foo(data={"a": "b"}, unrelated_data="unrelated")
284        sess.add(f1)
285        sess.flush()
286        f1.unrelated_data = "unrelated 2"
287        sess.flush()
288        f1.data["a"] = "c"
289        sess.commit()
290        eq_(f1.data["a"], "c")
291
292    def _test_non_mutable(self):
293        sess = fixture_session()
294
295        f1 = Foo(non_mutable_data={"a": "b"})
296        sess.add(f1)
297        sess.commit()
298
299        f1.non_mutable_data["a"] = "c"
300        sess.commit()
301
302        eq_(f1.non_mutable_data, {"a": "b"})
303
304    def test_copy(self):
305        f1 = Foo(data={"a": "b"})
306        f1.data = copy.copy(f1.data)
307        eq_(f1.data, {"a": "b"})
308
309    def test_deepcopy(self):
310        f1 = Foo(data={"a": "b"})
311        f1.data = copy.deepcopy(f1.data)
312        eq_(f1.data, {"a": "b"})
313
314
315class _MutableListTestFixture(object):
316    @classmethod
317    def _type_fixture(cls):
318        return MutableList
319
320    def teardown_test(self):
321        # clear out mapper events
322        Mapper.dispatch._clear()
323        ClassManager.dispatch._clear()
324
325
326class _MutableListTestBase(_MutableListTestFixture):
327    run_define_tables = "each"
328
329    def setup_mappers(cls):
330        foo = cls.tables.foo
331
332        cls.mapper_registry.map_imperatively(Foo, foo)
333
334    def test_coerce_none(self):
335        sess = fixture_session()
336        f1 = Foo(data=None)
337        sess.add(f1)
338        sess.commit()
339        eq_(f1.data, None)
340
341    def test_coerce_raise(self):
342        assert_raises_message(
343            ValueError,
344            "Attribute 'data' does not accept objects of type",
345            Foo,
346            data=set([1, 2, 3]),
347        )
348
349    def test_in_place_mutation(self):
350        sess = fixture_session()
351
352        f1 = Foo(data=[1, 2])
353        sess.add(f1)
354        sess.commit()
355
356        f1.data[0] = 3
357        sess.commit()
358
359        eq_(f1.data, [3, 2])
360
361    def test_in_place_slice_mutation(self):
362        sess = fixture_session()
363
364        f1 = Foo(data=[1, 2, 3, 4])
365        sess.add(f1)
366        sess.commit()
367
368        f1.data[1:3] = 5, 6
369        sess.commit()
370
371        eq_(f1.data, [1, 5, 6, 4])
372
373    def test_del_slice(self):
374        sess = fixture_session()
375
376        f1 = Foo(data=[1, 2, 3, 4])
377        sess.add(f1)
378        sess.commit()
379
380        del f1.data[1:3]
381        sess.commit()
382
383        eq_(f1.data, [1, 4])
384
385    def test_clear(self):
386        if not hasattr(list, "clear"):
387            # py2 list doesn't have 'clear'
388            return
389        sess = fixture_session()
390
391        f1 = Foo(data=[1, 2])
392        sess.add(f1)
393        sess.commit()
394
395        f1.data.clear()
396        sess.commit()
397
398        eq_(f1.data, [])
399
400    def test_pop(self):
401        sess = fixture_session()
402
403        f1 = Foo(data=[1, 2, 3])
404        sess.add(f1)
405        sess.commit()
406
407        eq_(f1.data.pop(), 3)
408        eq_(f1.data.pop(0), 1)
409        sess.commit()
410
411        assert_raises(IndexError, f1.data.pop, 5)
412
413        eq_(f1.data, [2])
414
415    def test_append(self):
416        sess = fixture_session()
417
418        f1 = Foo(data=[1, 2])
419        sess.add(f1)
420        sess.commit()
421
422        f1.data.append(5)
423        sess.commit()
424
425        eq_(f1.data, [1, 2, 5])
426
427    def test_extend(self):
428        sess = fixture_session()
429
430        f1 = Foo(data=[1, 2])
431        sess.add(f1)
432        sess.commit()
433
434        f1.data.extend([5])
435        sess.commit()
436
437        eq_(f1.data, [1, 2, 5])
438
439    def test_operator_extend(self):
440        sess = fixture_session()
441
442        f1 = Foo(data=[1, 2])
443        sess.add(f1)
444        sess.commit()
445
446        f1.data += [5]
447        sess.commit()
448
449        eq_(f1.data, [1, 2, 5])
450
451    def test_insert(self):
452        sess = fixture_session()
453
454        f1 = Foo(data=[1, 2])
455        sess.add(f1)
456        sess.commit()
457
458        f1.data.insert(1, 5)
459        sess.commit()
460
461        eq_(f1.data, [1, 5, 2])
462
463    def test_remove(self):
464        sess = fixture_session()
465
466        f1 = Foo(data=[1, 2, 3])
467        sess.add(f1)
468        sess.commit()
469
470        f1.data.remove(2)
471        sess.commit()
472
473        eq_(f1.data, [1, 3])
474
475    def test_sort(self):
476        sess = fixture_session()
477
478        f1 = Foo(data=[1, 3, 2])
479        sess.add(f1)
480        sess.commit()
481
482        f1.data.sort()
483        sess.commit()
484
485        eq_(f1.data, [1, 2, 3])
486
487    def test_sort_w_key(self):
488        sess = fixture_session()
489
490        f1 = Foo(data=[1, 3, 2])
491        sess.add(f1)
492        sess.commit()
493
494        f1.data.sort(key=lambda elem: -1 * elem)
495        sess.commit()
496
497        eq_(f1.data, [3, 2, 1])
498
499    def test_sort_w_reverse_kwarg(self):
500        sess = fixture_session()
501
502        f1 = Foo(data=[1, 3, 2])
503        sess.add(f1)
504        sess.commit()
505
506        f1.data.sort(reverse=True)
507        sess.commit()
508
509        eq_(f1.data, [3, 2, 1])
510
511    def test_reverse(self):
512        sess = fixture_session()
513
514        f1 = Foo(data=[1, 3, 2])
515        sess.add(f1)
516        sess.commit()
517
518        f1.data.reverse()
519        sess.commit()
520
521        eq_(f1.data, [2, 3, 1])
522
523    def test_pickle_parent(self):
524        sess = fixture_session()
525
526        f1 = Foo(data=[1, 2])
527        sess.add(f1)
528        sess.commit()
529        f1.data
530        sess.close()
531
532        for loads, dumps in picklers():
533            sess = fixture_session()
534            f2 = loads(dumps(f1))
535            sess.add(f2)
536            f2.data[0] = 3
537            assert f2 in sess.dirty
538
539    def test_unrelated_flush(self):
540        sess = fixture_session()
541        f1 = Foo(data=[1, 2], unrelated_data="unrelated")
542        sess.add(f1)
543        sess.flush()
544        f1.unrelated_data = "unrelated 2"
545        sess.flush()
546        f1.data[0] = 3
547        sess.commit()
548        eq_(f1.data[0], 3)
549
550    def test_copy(self):
551        f1 = Foo(data=[1, 2])
552        f1.data = copy.copy(f1.data)
553        eq_(f1.data, [1, 2])
554
555    def test_deepcopy(self):
556        f1 = Foo(data=[1, 2])
557        f1.data = copy.deepcopy(f1.data)
558        eq_(f1.data, [1, 2])
559
560    def test_legacy_pickle_loads(self):
561        # due to an inconsistency between pickle and copy, we have to change
562        # MutableList to implement a __reduce_ex__ method.   Which means we
563        # have to make sure all the old pickle formats are still
564        # deserializable since these can be used for persistence. these pickles
565        # were all generated using a MutableList that has only __getstate__ and
566        # __setstate__.
567
568        # f1 = Foo(data=[1, 2])
569        # pickles = [
570        #    dumps(f1.data)
571        #    for loads, dumps in picklers()
572        # ]
573        # print(repr(pickles))
574        # return
575
576        if util.py3k:
577            pickles = [
578                b"\x80\x04\x95<\x00\x00\x00\x00\x00\x00\x00\x8c\x16"
579                b"sqlalchemy.ext.mutable\x94\x8c\x0bMutableList\x94\x93\x94)"
580                b"\x81\x94(K\x01K\x02e]\x94(K\x01K\x02eb.",
581                b"ccopy_reg\n_reconstructor\np0\n(csqlalchemy.ext.mutable\n"
582                b"MutableList\np1\nc__builtin__\nlist\np2\n(lp3\nI1\naI2\n"
583                b"atp4\nRp5\n(lp6\nI1\naI2\nab.",
584                b"ccopy_reg\n_reconstructor\nq\x00(csqlalchemy.ext.mutable\n"
585                b"MutableList\nq\x01c__builtin__\nlist\nq\x02]q\x03(K\x01K"
586                b"\x02etq\x04Rq\x05]q\x06(K\x01K\x02eb.",
587                b"\x80\x02csqlalchemy.ext.mutable\nMutableList\nq\x00)\x81q"
588                b"\x01(K\x01K\x02e]q\x02(K\x01K\x02eb.",
589                b"\x80\x03csqlalchemy.ext.mutable\nMutableList\nq\x00)\x81q"
590                b"\x01(K\x01K\x02e]q\x02(K\x01K\x02eb.",
591                b"\x80\x04\x95<\x00\x00\x00\x00\x00\x00\x00\x8c\x16"
592                b"sqlalchemy.ext.mutable\x94\x8c\x0bMutableList\x94\x93\x94)"
593                b"\x81\x94(K\x01K\x02e]\x94(K\x01K\x02eb.",
594            ]
595        else:
596            pickles = [
597                "\x80\x02csqlalchemy.ext.mutable\nMutableList\nq\x00]q\x01"
598                "(K\x01K\x02e\x85q\x02Rq\x03.",
599                "\x80\x02csqlalchemy.ext.mutable\nMutableList"
600                "\nq\x00]q\x01(K\x01K\x02e\x85q\x02Rq\x03.",
601                "csqlalchemy.ext.mutable\nMutableList\np0\n"
602                "((lp1\nI1\naI2\natp2\nRp3\n.",
603                "csqlalchemy.ext.mutable\nMutableList\nq\x00(]"
604                "q\x01(K\x01K\x02etq\x02Rq\x03.",
605                "\x80\x02csqlalchemy.ext.mutable\nMutableList"
606                "\nq\x01]q\x02(K\x01K\x02e\x85Rq\x03.",
607                "\x80\x02csqlalchemy.ext.mutable\nMutableList\n"
608                "q\x01]q\x02(K\x01K\x02e\x85Rq\x03.",
609                "csqlalchemy.ext.mutable\nMutableList\np1\n"
610                "((lp2\nI1\naI2\natRp3\n.",
611                "csqlalchemy.ext.mutable\nMutableList\nq\x01"
612                "(]q\x02(K\x01K\x02etRq\x03.",
613            ]
614
615        for pickle_ in pickles:
616            obj = pickle.loads(pickle_)
617            eq_(obj, [1, 2])
618            assert isinstance(obj, MutableList)
619
620
621class _MutableSetTestFixture(object):
622    @classmethod
623    def _type_fixture(cls):
624        return MutableSet
625
626    def teardown_test(self):
627        # clear out mapper events
628        Mapper.dispatch._clear()
629        ClassManager.dispatch._clear()
630
631
632class _MutableSetTestBase(_MutableSetTestFixture):
633    run_define_tables = "each"
634
635    def setup_mappers(cls):
636        foo = cls.tables.foo
637
638        cls.mapper_registry.map_imperatively(Foo, foo)
639
640    def test_coerce_none(self):
641        sess = fixture_session()
642        f1 = Foo(data=None)
643        sess.add(f1)
644        sess.commit()
645        eq_(f1.data, None)
646
647    def test_coerce_raise(self):
648        assert_raises_message(
649            ValueError,
650            "Attribute 'data' does not accept objects of type",
651            Foo,
652            data=[1, 2, 3],
653        )
654
655    def test_clear(self):
656        sess = fixture_session()
657
658        f1 = Foo(data=set([1, 2]))
659        sess.add(f1)
660        sess.commit()
661
662        f1.data.clear()
663        sess.commit()
664
665        eq_(f1.data, set())
666
667    def test_pop(self):
668        sess = fixture_session()
669
670        f1 = Foo(data=set([1]))
671        sess.add(f1)
672        sess.commit()
673
674        eq_(f1.data.pop(), 1)
675        sess.commit()
676
677        assert_raises(KeyError, f1.data.pop)
678
679        eq_(f1.data, set())
680
681    def test_add(self):
682        sess = fixture_session()
683
684        f1 = Foo(data=set([1, 2]))
685        sess.add(f1)
686        sess.commit()
687
688        f1.data.add(5)
689        sess.commit()
690
691        eq_(f1.data, set([1, 2, 5]))
692
693    def test_update(self):
694        sess = fixture_session()
695
696        f1 = Foo(data=set([1, 2]))
697        sess.add(f1)
698        sess.commit()
699
700        f1.data.update(set([2, 5]))
701        sess.commit()
702
703        eq_(f1.data, set([1, 2, 5]))
704
705    def test_binary_update(self):
706        sess = fixture_session()
707
708        f1 = Foo(data=set([1, 2]))
709        sess.add(f1)
710        sess.commit()
711
712        f1.data |= set([2, 5])
713        sess.commit()
714
715        eq_(f1.data, set([1, 2, 5]))
716
717    def test_intersection_update(self):
718        sess = fixture_session()
719
720        f1 = Foo(data=set([1, 2]))
721        sess.add(f1)
722        sess.commit()
723
724        f1.data.intersection_update(set([2, 5]))
725        sess.commit()
726
727        eq_(f1.data, set([2]))
728
729    def test_binary_intersection_update(self):
730        sess = fixture_session()
731
732        f1 = Foo(data=set([1, 2]))
733        sess.add(f1)
734        sess.commit()
735
736        f1.data &= set([2, 5])
737        sess.commit()
738
739        eq_(f1.data, set([2]))
740
741    def test_difference_update(self):
742        sess = fixture_session()
743
744        f1 = Foo(data=set([1, 2]))
745        sess.add(f1)
746        sess.commit()
747
748        f1.data.difference_update(set([2, 5]))
749        sess.commit()
750
751        eq_(f1.data, set([1]))
752
753    def test_operator_difference_update(self):
754        sess = fixture_session()
755
756        f1 = Foo(data=set([1, 2]))
757        sess.add(f1)
758        sess.commit()
759
760        f1.data -= set([2, 5])
761        sess.commit()
762
763        eq_(f1.data, set([1]))
764
765    def test_symmetric_difference_update(self):
766        sess = fixture_session()
767
768        f1 = Foo(data=set([1, 2]))
769        sess.add(f1)
770        sess.commit()
771
772        f1.data.symmetric_difference_update(set([2, 5]))
773        sess.commit()
774
775        eq_(f1.data, set([1, 5]))
776
777    def test_binary_symmetric_difference_update(self):
778        sess = fixture_session()
779
780        f1 = Foo(data=set([1, 2]))
781        sess.add(f1)
782        sess.commit()
783
784        f1.data ^= set([2, 5])
785        sess.commit()
786
787        eq_(f1.data, set([1, 5]))
788
789    def test_remove(self):
790        sess = fixture_session()
791
792        f1 = Foo(data=set([1, 2, 3]))
793        sess.add(f1)
794        sess.commit()
795
796        f1.data.remove(2)
797        sess.commit()
798
799        eq_(f1.data, set([1, 3]))
800
801    def test_discard(self):
802        sess = fixture_session()
803
804        f1 = Foo(data=set([1, 2, 3]))
805        sess.add(f1)
806        sess.commit()
807
808        f1.data.discard(2)
809        sess.commit()
810
811        eq_(f1.data, set([1, 3]))
812
813        f1.data.discard(2)
814        sess.commit()
815
816        eq_(f1.data, set([1, 3]))
817
818    def test_pickle_parent(self):
819        sess = fixture_session()
820
821        f1 = Foo(data=set([1, 2]))
822        sess.add(f1)
823        sess.commit()
824        f1.data
825        sess.close()
826
827        for loads, dumps in picklers():
828            sess = fixture_session()
829            f2 = loads(dumps(f1))
830            sess.add(f2)
831            f2.data.add(3)
832            assert f2 in sess.dirty
833
834    def test_unrelated_flush(self):
835        sess = fixture_session()
836        f1 = Foo(data=set([1, 2]), unrelated_data="unrelated")
837        sess.add(f1)
838        sess.flush()
839        f1.unrelated_data = "unrelated 2"
840        sess.flush()
841        f1.data.add(3)
842        sess.commit()
843        eq_(f1.data, set([1, 2, 3]))
844
845    def test_copy(self):
846        f1 = Foo(data=set([1, 2]))
847        f1.data = copy.copy(f1.data)
848        eq_(f1.data, set([1, 2]))
849
850    def test_deepcopy(self):
851        f1 = Foo(data=set([1, 2]))
852        f1.data = copy.deepcopy(f1.data)
853        eq_(f1.data, set([1, 2]))
854
855
856class _MutableNoHashFixture(object):
857    @testing.fixture(autouse=True, scope="class")
858    def set_class(self):
859        global Foo
860
861        _replace_foo = Foo
862        Foo = FooWNoHash
863
864        yield
865        Foo = _replace_foo
866
867    def test_ensure_not_hashable(self):
868        d = {}
869        obj = Foo()
870        with testing.expect_raises(TypeError):
871            d[obj] = True
872
873
874class MutableListNoHashTest(
875    _MutableNoHashFixture, _MutableListTestBase, fixtures.MappedTest
876):
877    @classmethod
878    def define_tables(cls, metadata):
879        MutableList = cls._type_fixture()
880
881        mutable_pickle = MutableList.as_mutable(PickleType)
882        Table(
883            "foo",
884            metadata,
885            Column(
886                "id", Integer, primary_key=True, test_needs_autoincrement=True
887            ),
888            Column("data", mutable_pickle),
889        )
890
891
892class MutableDictNoHashTest(
893    _MutableNoHashFixture, _MutableDictTestBase, fixtures.MappedTest
894):
895    @classmethod
896    def define_tables(cls, metadata):
897        MutableDict = cls._type_fixture()
898
899        mutable_pickle = MutableDict.as_mutable(PickleType)
900        Table(
901            "foo",
902            metadata,
903            Column(
904                "id", Integer, primary_key=True, test_needs_autoincrement=True
905            ),
906            Column("data", mutable_pickle),
907        )
908
909
910class MutableColumnDefaultTest(_MutableDictTestFixture, fixtures.MappedTest):
911    @classmethod
912    def define_tables(cls, metadata):
913        MutableDict = cls._type_fixture()
914
915        mutable_pickle = MutableDict.as_mutable(PickleType)
916        Table(
917            "foo",
918            metadata,
919            Column(
920                "id", Integer, primary_key=True, test_needs_autoincrement=True
921            ),
922            Column("data", mutable_pickle, default={}),
923        )
924
925    def setup_mappers(cls):
926        foo = cls.tables.foo
927
928        cls.mapper_registry.map_imperatively(Foo, foo)
929
930    def test_evt_on_flush_refresh(self):
931        # test for #3427
932
933        sess = fixture_session()
934
935        f1 = Foo()
936        sess.add(f1)
937        sess.flush()
938        assert isinstance(f1.data, self._type_fixture())
939        assert f1 not in sess.dirty
940        f1.data["foo"] = "bar"
941        assert f1 in sess.dirty
942
943
944class MutableWithScalarPickleTest(_MutableDictTestBase, fixtures.MappedTest):
945    @classmethod
946    def define_tables(cls, metadata):
947        MutableDict = cls._type_fixture()
948
949        mutable_pickle = MutableDict.as_mutable(PickleType)
950        Table(
951            "foo",
952            metadata,
953            Column(
954                "id", Integer, primary_key=True, test_needs_autoincrement=True
955            ),
956            Column("skip", mutable_pickle),
957            Column("data", mutable_pickle),
958            Column("non_mutable_data", PickleType),
959            Column("unrelated_data", String(50)),
960        )
961
962    def test_non_mutable(self):
963        self._test_non_mutable()
964
965
966class MutableWithScalarJSONTest(_MutableDictTestBase, fixtures.MappedTest):
967    @classmethod
968    def define_tables(cls, metadata):
969        import json
970
971        class JSONEncodedDict(TypeDecorator):
972            impl = VARCHAR(50)
973            cache_ok = True
974
975            def process_bind_param(self, value, dialect):
976                if value is not None:
977                    value = json.dumps(value)
978
979                return value
980
981            def process_result_value(self, value, dialect):
982                if value is not None:
983                    value = json.loads(value)
984                return value
985
986        MutableDict = cls._type_fixture()
987
988        Table(
989            "foo",
990            metadata,
991            Column(
992                "id", Integer, primary_key=True, test_needs_autoincrement=True
993            ),
994            Column("data", MutableDict.as_mutable(JSONEncodedDict)),
995            Column("non_mutable_data", JSONEncodedDict),
996            Column("unrelated_data", String(50)),
997        )
998
999    def test_non_mutable(self):
1000        self._test_non_mutable()
1001
1002
1003class MutableColumnCopyJSONTest(_MutableDictTestBase, fixtures.MappedTest):
1004    @classmethod
1005    def define_tables(cls, metadata):
1006        import json
1007
1008        class JSONEncodedDict(TypeDecorator):
1009            impl = VARCHAR(50)
1010            cache_ok = True
1011
1012            def process_bind_param(self, value, dialect):
1013                if value is not None:
1014                    value = json.dumps(value)
1015
1016                return value
1017
1018            def process_result_value(self, value, dialect):
1019                if value is not None:
1020                    value = json.loads(value)
1021                return value
1022
1023        MutableDict = cls._type_fixture()
1024
1025        Base = declarative_base(metadata=metadata)
1026
1027        class AbstractFoo(Base):
1028            __abstract__ = True
1029
1030            id = Column(
1031                Integer, primary_key=True, test_needs_autoincrement=True
1032            )
1033            data = Column(MutableDict.as_mutable(JSONEncodedDict))
1034            non_mutable_data = Column(JSONEncodedDict)
1035            unrelated_data = Column(String(50))
1036
1037        class Foo(AbstractFoo):
1038            __tablename__ = "foo"
1039            column_prop = column_property(
1040                func.lower(AbstractFoo.unrelated_data)
1041            )
1042
1043        assert Foo.data.property.columns[0].type is not AbstractFoo.data.type
1044
1045    def test_non_mutable(self):
1046        self._test_non_mutable()
1047
1048
1049class MutableColumnCopyArrayTest(_MutableListTestBase, fixtures.MappedTest):
1050    __requires__ = ("array_type",)
1051
1052    @classmethod
1053    def define_tables(cls, metadata):
1054        from sqlalchemy.sql.sqltypes import ARRAY
1055
1056        MutableList = cls._type_fixture()
1057
1058        Base = declarative_base(metadata=metadata)
1059
1060        class Mixin(object):
1061            data = Column(MutableList.as_mutable(ARRAY(Integer)))
1062
1063        class Foo(Mixin, Base):
1064            __tablename__ = "foo"
1065            id = Column(Integer, primary_key=True)
1066
1067
1068class MutableListWithScalarPickleTest(
1069    _MutableListTestBase, fixtures.MappedTest
1070):
1071    @classmethod
1072    def define_tables(cls, metadata):
1073        MutableList = cls._type_fixture()
1074
1075        mutable_pickle = MutableList.as_mutable(PickleType)
1076        Table(
1077            "foo",
1078            metadata,
1079            Column(
1080                "id", Integer, primary_key=True, test_needs_autoincrement=True
1081            ),
1082            Column("skip", mutable_pickle),
1083            Column("data", mutable_pickle),
1084            Column("non_mutable_data", PickleType),
1085            Column("unrelated_data", String(50)),
1086        )
1087
1088
1089class MutableSetWithScalarPickleTest(_MutableSetTestBase, fixtures.MappedTest):
1090    @classmethod
1091    def define_tables(cls, metadata):
1092        MutableSet = cls._type_fixture()
1093
1094        mutable_pickle = MutableSet.as_mutable(PickleType)
1095        Table(
1096            "foo",
1097            metadata,
1098            Column(
1099                "id", Integer, primary_key=True, test_needs_autoincrement=True
1100            ),
1101            Column("skip", mutable_pickle),
1102            Column("data", mutable_pickle),
1103            Column("non_mutable_data", PickleType),
1104            Column("unrelated_data", String(50)),
1105        )
1106
1107
1108class MutableAssocWithAttrInheritTest(
1109    _MutableDictTestBase, fixtures.MappedTest
1110):
1111    @classmethod
1112    def define_tables(cls, metadata):
1113
1114        Table(
1115            "foo",
1116            metadata,
1117            Column(
1118                "id", Integer, primary_key=True, test_needs_autoincrement=True
1119            ),
1120            Column("data", PickleType),
1121            Column("non_mutable_data", PickleType),
1122            Column("unrelated_data", String(50)),
1123        )
1124
1125        Table(
1126            "subfoo",
1127            metadata,
1128            Column("id", Integer, ForeignKey("foo.id"), primary_key=True),
1129        )
1130
1131    def setup_mappers(cls):
1132        foo = cls.tables.foo
1133        subfoo = cls.tables.subfoo
1134
1135        cls.mapper_registry.map_imperatively(Foo, foo)
1136        cls.mapper_registry.map_imperatively(SubFoo, subfoo, inherits=Foo)
1137        MutableDict.associate_with_attribute(Foo.data)
1138
1139    def test_in_place_mutation(self):
1140        sess = fixture_session()
1141
1142        f1 = SubFoo(data={"a": "b"})
1143        sess.add(f1)
1144        sess.commit()
1145
1146        f1.data["a"] = "c"
1147        sess.commit()
1148
1149        eq_(f1.data, {"a": "c"})
1150
1151    def test_replace(self):
1152        sess = fixture_session()
1153        f1 = SubFoo(data={"a": "b"})
1154        sess.add(f1)
1155        sess.flush()
1156
1157        f1.data = {"b": "c"}
1158        sess.commit()
1159        eq_(f1.data, {"b": "c"})
1160
1161
1162class MutableAssociationScalarPickleTest(
1163    _MutableDictTestBase, fixtures.MappedTest
1164):
1165    @classmethod
1166    def define_tables(cls, metadata):
1167        MutableDict = cls._type_fixture()
1168        MutableDict.associate_with(PickleType)
1169
1170        Table(
1171            "foo",
1172            metadata,
1173            Column(
1174                "id", Integer, primary_key=True, test_needs_autoincrement=True
1175            ),
1176            Column("skip", PickleType),
1177            Column("data", PickleType),
1178            Column("unrelated_data", String(50)),
1179        )
1180
1181
1182class MutableAssociationScalarJSONTest(
1183    _MutableDictTestBase, fixtures.MappedTest
1184):
1185    @classmethod
1186    def define_tables(cls, metadata):
1187        import json
1188
1189        class JSONEncodedDict(TypeDecorator):
1190            impl = VARCHAR(50)
1191            cache_ok = True
1192
1193            def process_bind_param(self, value, dialect):
1194                if value is not None:
1195                    value = json.dumps(value)
1196
1197                return value
1198
1199            def process_result_value(self, value, dialect):
1200                if value is not None:
1201                    value = json.loads(value)
1202                return value
1203
1204        MutableDict = cls._type_fixture()
1205        MutableDict.associate_with(JSONEncodedDict)
1206
1207        Table(
1208            "foo",
1209            metadata,
1210            Column(
1211                "id", Integer, primary_key=True, test_needs_autoincrement=True
1212            ),
1213            Column("data", JSONEncodedDict),
1214            Column("unrelated_data", String(50)),
1215        )
1216
1217
1218class CustomMutableAssociationScalarJSONTest(
1219    _MutableDictTestBase, fixtures.MappedTest
1220):
1221
1222    CustomMutableDict = None
1223
1224    @classmethod
1225    def _type_fixture(cls):
1226        if not (getattr(cls, "CustomMutableDict")):
1227            MutableDict = super(
1228                CustomMutableAssociationScalarJSONTest, cls
1229            )._type_fixture()
1230
1231            class CustomMutableDict(MutableDict):
1232                pass
1233
1234            cls.CustomMutableDict = CustomMutableDict
1235        return cls.CustomMutableDict
1236
1237    @classmethod
1238    def define_tables(cls, metadata):
1239        import json
1240
1241        class JSONEncodedDict(TypeDecorator):
1242            impl = VARCHAR(50)
1243            cache_ok = True
1244
1245            def process_bind_param(self, value, dialect):
1246                if value is not None:
1247                    value = json.dumps(value)
1248
1249                return value
1250
1251            def process_result_value(self, value, dialect):
1252                if value is not None:
1253                    value = json.loads(value)
1254                return value
1255
1256        CustomMutableDict = cls._type_fixture()
1257        CustomMutableDict.associate_with(JSONEncodedDict)
1258
1259        Table(
1260            "foo",
1261            metadata,
1262            Column(
1263                "id", Integer, primary_key=True, test_needs_autoincrement=True
1264            ),
1265            Column("data", JSONEncodedDict),
1266            Column("unrelated_data", String(50)),
1267        )
1268
1269    def test_pickle_parent(self):
1270        # Picklers don't know how to pickle CustomMutableDict,
1271        # but we aren't testing that here
1272        pass
1273
1274    def test_coerce(self):
1275        sess = fixture_session()
1276        f1 = Foo(data={"a": "b"})
1277        sess.add(f1)
1278        sess.flush()
1279        eq_(type(f1.data), self._type_fixture())
1280
1281
1282class _CompositeTestBase(object):
1283    @classmethod
1284    def define_tables(cls, metadata):
1285        Table(
1286            "foo",
1287            metadata,
1288            Column(
1289                "id", Integer, primary_key=True, test_needs_autoincrement=True
1290            ),
1291            Column("x", Integer),
1292            Column("y", Integer),
1293            Column("unrelated_data", String(50)),
1294        )
1295
1296    def setup_test(self):
1297        from sqlalchemy.ext import mutable
1298
1299        mutable._setup_composite_listener()
1300
1301    def teardown_test(self):
1302        # clear out mapper events
1303        Mapper.dispatch._clear()
1304        ClassManager.dispatch._clear()
1305
1306    @classmethod
1307    def _type_fixture(cls):
1308
1309        return Point
1310
1311
1312class MutableCompositeColumnDefaultTest(
1313    _CompositeTestBase, fixtures.MappedTest
1314):
1315    @classmethod
1316    def define_tables(cls, metadata):
1317        Table(
1318            "foo",
1319            metadata,
1320            Column(
1321                "id", Integer, primary_key=True, test_needs_autoincrement=True
1322            ),
1323            Column("x", Integer, default=5),
1324            Column("y", Integer, default=9),
1325            Column("unrelated_data", String(50)),
1326        )
1327
1328    @classmethod
1329    def setup_mappers(cls):
1330        foo = cls.tables.foo
1331
1332        cls.Point = cls._type_fixture()
1333
1334        cls.mapper_registry.map_imperatively(
1335            Foo,
1336            foo,
1337            properties={"data": composite(cls.Point, foo.c.x, foo.c.y)},
1338        )
1339
1340    def test_evt_on_flush_refresh(self):
1341        # this still worked prior to #3427 being fixed in any case
1342
1343        sess = fixture_session()
1344
1345        f1 = Foo(data=self.Point(None, None))
1346        sess.add(f1)
1347        sess.flush()
1348        eq_(f1.data, self.Point(5, 9))
1349        assert f1 not in sess.dirty
1350        f1.data.x = 10
1351        assert f1 in sess.dirty
1352
1353
1354class MutableCompositesUnpickleTest(_CompositeTestBase, fixtures.MappedTest):
1355    @classmethod
1356    def setup_mappers(cls):
1357        foo = cls.tables.foo
1358
1359        cls.Point = cls._type_fixture()
1360
1361        cls.mapper_registry.map_imperatively(
1362            FooWithEq,
1363            foo,
1364            properties={"data": composite(cls.Point, foo.c.x, foo.c.y)},
1365        )
1366
1367    def test_unpickle_modified_eq(self):
1368        u1 = FooWithEq(data=self.Point(3, 5))
1369        for loads, dumps in picklers():
1370            loads(dumps(u1))
1371
1372
1373class MutableCompositesTest(_CompositeTestBase, fixtures.MappedTest):
1374    @classmethod
1375    def setup_mappers(cls):
1376        foo = cls.tables.foo
1377
1378        Point = cls._type_fixture()
1379
1380        cls.mapper_registry.map_imperatively(
1381            Foo, foo, properties={"data": composite(Point, foo.c.x, foo.c.y)}
1382        )
1383
1384    def test_in_place_mutation(self):
1385        sess = fixture_session()
1386        d = Point(3, 4)
1387        f1 = Foo(data=d)
1388        sess.add(f1)
1389        sess.commit()
1390
1391        f1.data.y = 5
1392        sess.commit()
1393
1394        eq_(f1.data, Point(3, 5))
1395
1396    def test_pickle_of_parent(self):
1397        sess = fixture_session()
1398        d = Point(3, 4)
1399        f1 = Foo(data=d)
1400        sess.add(f1)
1401        sess.commit()
1402
1403        f1.data
1404        assert "data" in f1.__dict__
1405        sess.close()
1406
1407        for loads, dumps in picklers():
1408            sess = fixture_session()
1409            f2 = loads(dumps(f1))
1410            sess.add(f2)
1411            f2.data.y = 12
1412            assert f2 in sess.dirty
1413
1414    def test_set_none(self):
1415        sess = fixture_session()
1416        f1 = Foo(data=None)
1417        sess.add(f1)
1418        sess.commit()
1419        eq_(f1.data, Point(None, None))
1420
1421        f1.data.y = 5
1422        sess.commit()
1423        eq_(f1.data, Point(None, 5))
1424
1425    def test_set_illegal(self):
1426        f1 = Foo()
1427        assert_raises_message(
1428            ValueError,
1429            "Attribute 'data' does not accept objects",
1430            setattr,
1431            f1,
1432            "data",
1433            "foo",
1434        )
1435
1436    def test_unrelated_flush(self):
1437        sess = fixture_session()
1438        f1 = Foo(data=Point(3, 4), unrelated_data="unrelated")
1439        sess.add(f1)
1440        sess.flush()
1441        f1.unrelated_data = "unrelated 2"
1442        sess.flush()
1443        f1.data.x = 5
1444        sess.commit()
1445
1446        eq_(f1.data.x, 5)
1447
1448    def test_dont_reset_on_attr_refresh(self):
1449        sess = fixture_session()
1450        f1 = Foo(data=Point(3, 4), unrelated_data="unrelated")
1451        sess.add(f1)
1452        sess.flush()
1453
1454        f1.data.x = 5
1455
1456        # issue 6001, this would reload a new Point() that would be missed
1457        # by the mutable composite, and tracking would be lost
1458        sess.refresh(f1, ["unrelated_data"])
1459
1460        is_(list(f1.data._parents.keys())[0], f1._sa_instance_state)
1461
1462        f1.data.y = 9
1463
1464        sess.commit()
1465
1466        eq_(f1.data.x, 5)
1467        eq_(f1.data.y, 9)
1468
1469        f1.data.x = 12
1470
1471        sess.refresh(f1, ["unrelated_data", "y"])
1472
1473        is_(list(f1.data._parents.keys())[0], f1._sa_instance_state)
1474
1475        f1.data.y = 15
1476        sess.commit()
1477
1478        eq_(f1.data.x, 12)
1479        eq_(f1.data.y, 15)
1480
1481
1482class MutableCompositeCallableTest(_CompositeTestBase, fixtures.MappedTest):
1483    @classmethod
1484    def setup_mappers(cls):
1485        foo = cls.tables.foo
1486
1487        Point = cls._type_fixture()
1488
1489        # in this case, this is not actually a MutableComposite.
1490        # so we don't expect it to track changes
1491        cls.mapper_registry.map_imperatively(
1492            Foo,
1493            foo,
1494            properties={
1495                "data": composite(lambda x, y: Point(x, y), foo.c.x, foo.c.y)
1496            },
1497        )
1498
1499    def test_basic(self):
1500        sess = fixture_session()
1501        f1 = Foo(data=Point(3, 4))
1502        sess.add(f1)
1503        sess.flush()
1504        f1.data.x = 5
1505        sess.commit()
1506
1507        # we didn't get the change.
1508        eq_(f1.data.x, 3)
1509
1510
1511class MutableCompositeCustomCoerceTest(
1512    _CompositeTestBase, fixtures.MappedTest
1513):
1514    @classmethod
1515    def _type_fixture(cls):
1516
1517        return MyPoint
1518
1519    @classmethod
1520    def setup_mappers(cls):
1521        foo = cls.tables.foo
1522
1523        Point = cls._type_fixture()
1524
1525        cls.mapper_registry.map_imperatively(
1526            Foo, foo, properties={"data": composite(Point, foo.c.x, foo.c.y)}
1527        )
1528
1529    def test_custom_coerce(self):
1530        f = Foo()
1531        f.data = (3, 4)
1532        eq_(f.data, Point(3, 4))
1533
1534    def test_round_trip_ok(self):
1535        sess = fixture_session()
1536        f = Foo()
1537        f.data = (3, 4)
1538
1539        sess.add(f)
1540        sess.commit()
1541
1542        eq_(f.data, Point(3, 4))
1543
1544
1545class MutableInheritedCompositesTest(_CompositeTestBase, fixtures.MappedTest):
1546    @classmethod
1547    def define_tables(cls, metadata):
1548        Table(
1549            "foo",
1550            metadata,
1551            Column(
1552                "id", Integer, primary_key=True, test_needs_autoincrement=True
1553            ),
1554            Column("x", Integer),
1555            Column("y", Integer),
1556        )
1557        Table(
1558            "subfoo",
1559            metadata,
1560            Column("id", Integer, ForeignKey("foo.id"), primary_key=True),
1561        )
1562
1563    @classmethod
1564    def setup_mappers(cls):
1565        foo = cls.tables.foo
1566        subfoo = cls.tables.subfoo
1567
1568        Point = cls._type_fixture()
1569
1570        cls.mapper_registry.map_imperatively(
1571            Foo, foo, properties={"data": composite(Point, foo.c.x, foo.c.y)}
1572        )
1573        cls.mapper_registry.map_imperatively(SubFoo, subfoo, inherits=Foo)
1574
1575    def test_in_place_mutation_subclass(self):
1576        sess = fixture_session()
1577        d = Point(3, 4)
1578        f1 = SubFoo(data=d)
1579        sess.add(f1)
1580        sess.commit()
1581
1582        f1.data.y = 5
1583        sess.commit()
1584
1585        eq_(f1.data, Point(3, 5))
1586
1587    def test_pickle_of_parent_subclass(self):
1588        sess = fixture_session()
1589        d = Point(3, 4)
1590        f1 = SubFoo(data=d)
1591        sess.add(f1)
1592        sess.commit()
1593
1594        f1.data
1595        assert "data" in f1.__dict__
1596        sess.close()
1597
1598        for loads, dumps in picklers():
1599            sess = fixture_session()
1600            f2 = loads(dumps(f1))
1601            sess.add(f2)
1602            f2.data.y = 12
1603            assert f2 in sess.dirty
1604