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