1from sqlalchemy.testing import eq_, assert_raises, \ 2 assert_raises_message, is_ 3from sqlalchemy.ext import declarative as decl 4import sqlalchemy as sa 5from sqlalchemy import testing 6from sqlalchemy import Integer, String, ForeignKey, select, func 7from sqlalchemy.testing.schema import Table, Column 8from sqlalchemy.orm import relationship, create_session, class_mapper, \ 9 configure_mappers, clear_mappers, \ 10 deferred, column_property, Session, base as orm_base 11from sqlalchemy.util import classproperty 12from sqlalchemy.ext.declarative import declared_attr, declarative_base 13from sqlalchemy.orm import events as orm_events 14from sqlalchemy.testing import fixtures, mock 15from sqlalchemy.testing.util import gc_collect 16 17Base = None 18 19 20class DeclarativeTestBase(fixtures.TestBase, testing.AssertsExecutionResults): 21 22 def setup(self): 23 global Base 24 Base = decl.declarative_base(testing.db) 25 26 def teardown(self): 27 Session.close_all() 28 clear_mappers() 29 Base.metadata.drop_all() 30 31 32class DeclarativeMixinTest(DeclarativeTestBase): 33 34 def test_simple(self): 35 36 class MyMixin(object): 37 38 id = Column(Integer, primary_key=True, 39 test_needs_autoincrement=True) 40 41 def foo(self): 42 return 'bar' + str(self.id) 43 44 class MyModel(Base, MyMixin): 45 46 __tablename__ = 'test' 47 name = Column(String(100), nullable=False, index=True) 48 49 Base.metadata.create_all() 50 session = create_session() 51 session.add(MyModel(name='testing')) 52 session.flush() 53 session.expunge_all() 54 obj = session.query(MyModel).one() 55 eq_(obj.id, 1) 56 eq_(obj.name, 'testing') 57 eq_(obj.foo(), 'bar1') 58 59 def test_unique_column(self): 60 61 class MyMixin(object): 62 63 id = Column(Integer, primary_key=True) 64 value = Column(String, unique=True) 65 66 class MyModel(Base, MyMixin): 67 68 __tablename__ = 'test' 69 70 assert MyModel.__table__.c.value.unique 71 72 def test_hierarchical_bases(self): 73 74 class MyMixinParent: 75 76 id = Column(Integer, primary_key=True, 77 test_needs_autoincrement=True) 78 79 def foo(self): 80 return 'bar' + str(self.id) 81 82 class MyMixin(MyMixinParent): 83 84 baz = Column(String(100), nullable=False, index=True) 85 86 class MyModel(Base, MyMixin): 87 88 __tablename__ = 'test' 89 name = Column(String(100), nullable=False, index=True) 90 91 Base.metadata.create_all() 92 session = create_session() 93 session.add(MyModel(name='testing', baz='fu')) 94 session.flush() 95 session.expunge_all() 96 obj = session.query(MyModel).one() 97 eq_(obj.id, 1) 98 eq_(obj.name, 'testing') 99 eq_(obj.foo(), 'bar1') 100 eq_(obj.baz, 'fu') 101 102 def test_mixin_overrides(self): 103 """test a mixin that overrides a column on a superclass.""" 104 105 class MixinA(object): 106 foo = Column(String(50)) 107 108 class MixinB(MixinA): 109 foo = Column(Integer) 110 111 class MyModelA(Base, MixinA): 112 __tablename__ = 'testa' 113 id = Column(Integer, primary_key=True) 114 115 class MyModelB(Base, MixinB): 116 __tablename__ = 'testb' 117 id = Column(Integer, primary_key=True) 118 119 eq_(MyModelA.__table__.c.foo.type.__class__, String) 120 eq_(MyModelB.__table__.c.foo.type.__class__, Integer) 121 122 def test_not_allowed(self): 123 124 class MyMixin: 125 foo = Column(Integer, ForeignKey('bar.id')) 126 127 def go(): 128 class MyModel(Base, MyMixin): 129 __tablename__ = 'foo' 130 131 assert_raises(sa.exc.InvalidRequestError, go) 132 133 class MyRelMixin: 134 foo = relationship('Bar') 135 136 def go(): 137 class MyModel(Base, MyRelMixin): 138 139 __tablename__ = 'foo' 140 141 assert_raises(sa.exc.InvalidRequestError, go) 142 143 class MyDefMixin: 144 foo = deferred(Column('foo', String)) 145 146 def go(): 147 class MyModel(Base, MyDefMixin): 148 __tablename__ = 'foo' 149 150 assert_raises(sa.exc.InvalidRequestError, go) 151 152 class MyCPropMixin: 153 foo = column_property(Column('foo', String)) 154 155 def go(): 156 class MyModel(Base, MyCPropMixin): 157 __tablename__ = 'foo' 158 159 assert_raises(sa.exc.InvalidRequestError, go) 160 161 def test_table_name_inherited(self): 162 163 class MyMixin: 164 165 @declared_attr 166 def __tablename__(cls): 167 return cls.__name__.lower() 168 id = Column(Integer, primary_key=True) 169 170 class MyModel(Base, MyMixin): 171 pass 172 173 eq_(MyModel.__table__.name, 'mymodel') 174 175 def test_classproperty_still_works(self): 176 class MyMixin(object): 177 178 @classproperty 179 def __tablename__(cls): 180 return cls.__name__.lower() 181 id = Column(Integer, primary_key=True) 182 183 class MyModel(Base, MyMixin): 184 __tablename__ = 'overridden' 185 186 eq_(MyModel.__table__.name, 'overridden') 187 188 def test_table_name_not_inherited(self): 189 190 class MyMixin: 191 192 @declared_attr 193 def __tablename__(cls): 194 return cls.__name__.lower() 195 id = Column(Integer, primary_key=True) 196 197 class MyModel(Base, MyMixin): 198 __tablename__ = 'overridden' 199 200 eq_(MyModel.__table__.name, 'overridden') 201 202 def test_table_name_inheritance_order(self): 203 204 class MyMixin1: 205 206 @declared_attr 207 def __tablename__(cls): 208 return cls.__name__.lower() + '1' 209 210 class MyMixin2: 211 212 @declared_attr 213 def __tablename__(cls): 214 return cls.__name__.lower() + '2' 215 216 class MyModel(Base, MyMixin1, MyMixin2): 217 id = Column(Integer, primary_key=True) 218 219 eq_(MyModel.__table__.name, 'mymodel1') 220 221 def test_table_name_dependent_on_subclass(self): 222 223 class MyHistoryMixin: 224 225 @declared_attr 226 def __tablename__(cls): 227 return cls.parent_name + '_changelog' 228 229 class MyModel(Base, MyHistoryMixin): 230 parent_name = 'foo' 231 id = Column(Integer, primary_key=True) 232 233 eq_(MyModel.__table__.name, 'foo_changelog') 234 235 def test_table_args_inherited(self): 236 237 class MyMixin: 238 __table_args__ = {'mysql_engine': 'InnoDB'} 239 240 class MyModel(Base, MyMixin): 241 __tablename__ = 'test' 242 id = Column(Integer, primary_key=True) 243 244 eq_(MyModel.__table__.kwargs, {'mysql_engine': 'InnoDB'}) 245 246 def test_table_args_inherited_descriptor(self): 247 248 class MyMixin: 249 250 @declared_attr 251 def __table_args__(cls): 252 return {'info': cls.__name__} 253 254 class MyModel(Base, MyMixin): 255 __tablename__ = 'test' 256 id = Column(Integer, primary_key=True) 257 258 eq_(MyModel.__table__.info, 'MyModel') 259 260 def test_table_args_inherited_single_table_inheritance(self): 261 262 class MyMixin: 263 __table_args__ = {'mysql_engine': 'InnoDB'} 264 265 class General(Base, MyMixin): 266 __tablename__ = 'test' 267 id = Column(Integer, primary_key=True) 268 type_ = Column(String(50)) 269 __mapper__args = {'polymorphic_on': type_} 270 271 class Specific(General): 272 __mapper_args__ = {'polymorphic_identity': 'specific'} 273 274 assert Specific.__table__ is General.__table__ 275 eq_(General.__table__.kwargs, {'mysql_engine': 'InnoDB'}) 276 277 def test_columns_single_table_inheritance(self): 278 """Test a column on a mixin with an alternate attribute name, 279 mapped to a superclass and single-table inheritance subclass. 280 The superclass table gets the column, the subclass shares 281 the MapperProperty. 282 283 """ 284 285 class MyMixin(object): 286 foo = Column('foo', Integer) 287 bar = Column('bar_newname', Integer) 288 289 class General(Base, MyMixin): 290 __tablename__ = 'test' 291 id = Column(Integer, primary_key=True) 292 type_ = Column(String(50)) 293 __mapper__args = {'polymorphic_on': type_} 294 295 class Specific(General): 296 __mapper_args__ = {'polymorphic_identity': 'specific'} 297 298 assert General.bar.prop.columns[0] is General.__table__.c.bar_newname 299 assert len(General.bar.prop.columns) == 1 300 assert Specific.bar.prop is General.bar.prop 301 302 @testing.skip_if(lambda: testing.against('oracle'), 303 "Test has an empty insert in it at the moment") 304 def test_columns_single_inheritance_conflict_resolution(self): 305 """Test that a declared_attr can return the existing column and it will 306 be ignored. this allows conditional columns to be added. 307 308 See [ticket:2472]. 309 310 """ 311 class Person(Base): 312 __tablename__ = 'person' 313 id = Column(Integer, primary_key=True) 314 315 class Mixin(object): 316 317 @declared_attr 318 def target_id(cls): 319 return cls.__table__.c.get( 320 'target_id', 321 Column(Integer, ForeignKey('other.id')) 322 ) 323 324 @declared_attr 325 def target(cls): 326 return relationship("Other") 327 328 class Engineer(Mixin, Person): 329 330 """single table inheritance""" 331 332 class Manager(Mixin, Person): 333 334 """single table inheritance""" 335 336 class Other(Base): 337 __tablename__ = 'other' 338 id = Column(Integer, primary_key=True) 339 340 is_( 341 Engineer.target_id.property.columns[0], 342 Person.__table__.c.target_id 343 ) 344 is_( 345 Manager.target_id.property.columns[0], 346 Person.__table__.c.target_id 347 ) 348 # do a brief round trip on this 349 Base.metadata.create_all() 350 session = Session() 351 o1, o2 = Other(), Other() 352 session.add_all([ 353 Engineer(target=o1), 354 Manager(target=o2), 355 Manager(target=o1) 356 ]) 357 session.commit() 358 eq_(session.query(Engineer).first().target, o1) 359 360 def test_columns_joined_table_inheritance(self): 361 """Test a column on a mixin with an alternate attribute name, 362 mapped to a superclass and joined-table inheritance subclass. 363 Both tables get the column, in the case of the subclass the two 364 columns are joined under one MapperProperty. 365 366 """ 367 368 class MyMixin(object): 369 foo = Column('foo', Integer) 370 bar = Column('bar_newname', Integer) 371 372 class General(Base, MyMixin): 373 __tablename__ = 'test' 374 id = Column(Integer, primary_key=True) 375 type_ = Column(String(50)) 376 __mapper__args = {'polymorphic_on': type_} 377 378 class Specific(General): 379 __tablename__ = 'sub' 380 id = Column(Integer, ForeignKey('test.id'), primary_key=True) 381 __mapper_args__ = {'polymorphic_identity': 'specific'} 382 383 assert General.bar.prop.columns[0] is General.__table__.c.bar_newname 384 assert len(General.bar.prop.columns) == 1 385 assert Specific.bar.prop is General.bar.prop 386 eq_(len(Specific.bar.prop.columns), 1) 387 assert Specific.bar.prop.columns[0] is General.__table__.c.bar_newname 388 389 def test_column_join_checks_superclass_type(self): 390 """Test that the logic which joins subclass props to those 391 of the superclass checks that the superclass property is a column. 392 393 """ 394 395 class General(Base): 396 __tablename__ = 'test' 397 id = Column(Integer, primary_key=True) 398 general_id = Column(Integer, ForeignKey('test.id')) 399 type_ = relationship("General") 400 401 class Specific(General): 402 __tablename__ = 'sub' 403 id = Column(Integer, ForeignKey('test.id'), primary_key=True) 404 type_ = Column('foob', String(50)) 405 406 assert isinstance(General.type_.property, sa.orm.RelationshipProperty) 407 assert Specific.type_.property.columns[0] is Specific.__table__.c.foob 408 409 def test_column_join_checks_subclass_type(self): 410 """Test that the logic which joins subclass props to those 411 of the superclass checks that the subclass property is a column. 412 413 """ 414 415 def go(): 416 class General(Base): 417 __tablename__ = 'test' 418 id = Column(Integer, primary_key=True) 419 type_ = Column('foob', Integer) 420 421 class Specific(General): 422 __tablename__ = 'sub' 423 id = Column(Integer, ForeignKey('test.id'), primary_key=True) 424 specific_id = Column(Integer, ForeignKey('sub.id')) 425 type_ = relationship("Specific") 426 assert_raises_message( 427 sa.exc.ArgumentError, "column 'foob' conflicts with property", go 428 ) 429 430 def test_table_args_overridden(self): 431 432 class MyMixin: 433 __table_args__ = {'mysql_engine': 'Foo'} 434 435 class MyModel(Base, MyMixin): 436 __tablename__ = 'test' 437 __table_args__ = {'mysql_engine': 'InnoDB'} 438 id = Column(Integer, primary_key=True) 439 440 eq_(MyModel.__table__.kwargs, {'mysql_engine': 'InnoDB'}) 441 442 @testing.teardown_events(orm_events.MapperEvents) 443 def test_declare_first_mixin(self): 444 canary = mock.Mock() 445 446 class MyMixin(object): 447 @classmethod 448 def __declare_first__(cls): 449 canary.declare_first__(cls) 450 451 @classmethod 452 def __declare_last__(cls): 453 canary.declare_last__(cls) 454 455 class MyModel(Base, MyMixin): 456 __tablename__ = 'test' 457 id = Column(Integer, primary_key=True) 458 459 configure_mappers() 460 461 eq_( 462 canary.mock_calls, 463 [ 464 mock.call.declare_first__(MyModel), 465 mock.call.declare_last__(MyModel), 466 ] 467 ) 468 469 @testing.teardown_events(orm_events.MapperEvents) 470 def test_declare_first_base(self): 471 canary = mock.Mock() 472 473 class MyMixin(object): 474 @classmethod 475 def __declare_first__(cls): 476 canary.declare_first__(cls) 477 478 @classmethod 479 def __declare_last__(cls): 480 canary.declare_last__(cls) 481 482 class Base(MyMixin): 483 pass 484 Base = declarative_base(cls=Base) 485 486 class MyModel(Base): 487 __tablename__ = 'test' 488 id = Column(Integer, primary_key=True) 489 490 configure_mappers() 491 492 eq_( 493 canary.mock_calls, 494 [ 495 mock.call.declare_first__(MyModel), 496 mock.call.declare_last__(MyModel), 497 ] 498 ) 499 500 @testing.teardown_events(orm_events.MapperEvents) 501 def test_declare_first_direct(self): 502 canary = mock.Mock() 503 504 class MyOtherModel(Base): 505 __tablename__ = 'test2' 506 id = Column(Integer, primary_key=True) 507 508 @classmethod 509 def __declare_first__(cls): 510 canary.declare_first__(cls) 511 512 @classmethod 513 def __declare_last__(cls): 514 canary.declare_last__(cls) 515 516 configure_mappers() 517 518 eq_( 519 canary.mock_calls, 520 [ 521 mock.call.declare_first__(MyOtherModel), 522 mock.call.declare_last__(MyOtherModel) 523 ] 524 ) 525 526 def test_mapper_args_declared_attr(self): 527 528 class ComputedMapperArgs: 529 530 @declared_attr 531 def __mapper_args__(cls): 532 if cls.__name__ == 'Person': 533 return {'polymorphic_on': cls.discriminator} 534 else: 535 return {'polymorphic_identity': cls.__name__} 536 537 class Person(Base, ComputedMapperArgs): 538 __tablename__ = 'people' 539 id = Column(Integer, primary_key=True) 540 discriminator = Column('type', String(50)) 541 542 class Engineer(Person): 543 pass 544 545 configure_mappers() 546 assert class_mapper(Person).polymorphic_on \ 547 is Person.__table__.c.type 548 eq_(class_mapper(Engineer).polymorphic_identity, 'Engineer') 549 550 def test_mapper_args_declared_attr_two(self): 551 552 # same as test_mapper_args_declared_attr, but we repeat 553 # ComputedMapperArgs on both classes for no apparent reason. 554 555 class ComputedMapperArgs: 556 557 @declared_attr 558 def __mapper_args__(cls): 559 if cls.__name__ == 'Person': 560 return {'polymorphic_on': cls.discriminator} 561 else: 562 return {'polymorphic_identity': cls.__name__} 563 564 class Person(Base, ComputedMapperArgs): 565 566 __tablename__ = 'people' 567 id = Column(Integer, primary_key=True) 568 discriminator = Column('type', String(50)) 569 570 class Engineer(Person, ComputedMapperArgs): 571 pass 572 573 configure_mappers() 574 assert class_mapper(Person).polymorphic_on \ 575 is Person.__table__.c.type 576 eq_(class_mapper(Engineer).polymorphic_identity, 'Engineer') 577 578 def test_table_args_composite(self): 579 580 class MyMixin1: 581 582 __table_args__ = {'info': {'baz': 'bob'}} 583 584 class MyMixin2: 585 586 __table_args__ = {'info': {'foo': 'bar'}} 587 588 class MyModel(Base, MyMixin1, MyMixin2): 589 590 __tablename__ = 'test' 591 592 @declared_attr 593 def __table_args__(self): 594 info = {} 595 args = dict(info=info) 596 info.update(MyMixin1.__table_args__['info']) 597 info.update(MyMixin2.__table_args__['info']) 598 return args 599 id = Column(Integer, primary_key=True) 600 601 eq_(MyModel.__table__.info, {'foo': 'bar', 'baz': 'bob'}) 602 603 def test_mapper_args_inherited(self): 604 605 class MyMixin: 606 607 __mapper_args__ = {'always_refresh': True} 608 609 class MyModel(Base, MyMixin): 610 611 __tablename__ = 'test' 612 id = Column(Integer, primary_key=True) 613 614 eq_(MyModel.__mapper__.always_refresh, True) 615 616 def test_mapper_args_inherited_descriptor(self): 617 618 class MyMixin: 619 620 @declared_attr 621 def __mapper_args__(cls): 622 623 # tenuous, but illustrates the problem! 624 625 if cls.__name__ == 'MyModel': 626 return dict(always_refresh=True) 627 else: 628 return dict(always_refresh=False) 629 630 class MyModel(Base, MyMixin): 631 632 __tablename__ = 'test' 633 id = Column(Integer, primary_key=True) 634 635 eq_(MyModel.__mapper__.always_refresh, True) 636 637 def test_mapper_args_polymorphic_on_inherited(self): 638 639 class MyMixin: 640 641 type_ = Column(String(50)) 642 __mapper_args__ = {'polymorphic_on': type_} 643 644 class MyModel(Base, MyMixin): 645 646 __tablename__ = 'test' 647 id = Column(Integer, primary_key=True) 648 649 col = MyModel.__mapper__.polymorphic_on 650 eq_(col.name, 'type_') 651 assert col.table is not None 652 653 def test_mapper_args_overridden(self): 654 655 class MyMixin: 656 657 __mapper_args__ = dict(always_refresh=True) 658 659 class MyModel(Base, MyMixin): 660 661 __tablename__ = 'test' 662 __mapper_args__ = dict(always_refresh=False) 663 id = Column(Integer, primary_key=True) 664 665 eq_(MyModel.__mapper__.always_refresh, False) 666 667 def test_mapper_args_composite(self): 668 669 class MyMixin1: 670 671 type_ = Column(String(50)) 672 __mapper_args__ = {'polymorphic_on': type_} 673 674 class MyMixin2: 675 676 __mapper_args__ = {'always_refresh': True} 677 678 class MyModel(Base, MyMixin1, MyMixin2): 679 680 __tablename__ = 'test' 681 682 @declared_attr 683 def __mapper_args__(cls): 684 args = {} 685 args.update(MyMixin1.__mapper_args__) 686 args.update(MyMixin2.__mapper_args__) 687 if cls.__name__ != 'MyModel': 688 args.pop('polymorphic_on') 689 args['polymorphic_identity'] = cls.__name__ 690 691 return args 692 id = Column(Integer, primary_key=True) 693 694 class MySubModel(MyModel): 695 pass 696 697 eq_( 698 MyModel.__mapper__.polymorphic_on.name, 699 'type_' 700 ) 701 assert MyModel.__mapper__.polymorphic_on.table is not None 702 eq_(MyModel.__mapper__.always_refresh, True) 703 eq_(MySubModel.__mapper__.always_refresh, True) 704 eq_(MySubModel.__mapper__.polymorphic_identity, 'MySubModel') 705 706 def test_mapper_args_property(self): 707 class MyModel(Base): 708 709 @declared_attr 710 def __tablename__(cls): 711 return cls.__name__.lower() 712 713 @declared_attr 714 def __table_args__(cls): 715 return {'mysql_engine': 'InnoDB'} 716 717 @declared_attr 718 def __mapper_args__(cls): 719 args = {} 720 args['polymorphic_identity'] = cls.__name__ 721 return args 722 id = Column(Integer, primary_key=True) 723 724 class MySubModel(MyModel): 725 id = Column(Integer, ForeignKey('mymodel.id'), primary_key=True) 726 727 class MySubModel2(MyModel): 728 __tablename__ = 'sometable' 729 id = Column(Integer, ForeignKey('mymodel.id'), primary_key=True) 730 731 eq_(MyModel.__mapper__.polymorphic_identity, 'MyModel') 732 eq_(MySubModel.__mapper__.polymorphic_identity, 'MySubModel') 733 eq_(MyModel.__table__.kwargs['mysql_engine'], 'InnoDB') 734 eq_(MySubModel.__table__.kwargs['mysql_engine'], 'InnoDB') 735 eq_(MySubModel2.__table__.kwargs['mysql_engine'], 'InnoDB') 736 eq_(MyModel.__table__.name, 'mymodel') 737 eq_(MySubModel.__table__.name, 'mysubmodel') 738 739 def test_mapper_args_custom_base(self): 740 """test the @declared_attr approach from a custom base.""" 741 742 class Base(object): 743 744 @declared_attr 745 def __tablename__(cls): 746 return cls.__name__.lower() 747 748 @declared_attr 749 def __table_args__(cls): 750 return {'mysql_engine': 'InnoDB'} 751 752 @declared_attr 753 def id(self): 754 return Column(Integer, primary_key=True) 755 756 Base = decl.declarative_base(cls=Base) 757 758 class MyClass(Base): 759 pass 760 761 class MyOtherClass(Base): 762 pass 763 764 eq_(MyClass.__table__.kwargs['mysql_engine'], 'InnoDB') 765 eq_(MyClass.__table__.name, 'myclass') 766 eq_(MyOtherClass.__table__.name, 'myotherclass') 767 assert MyClass.__table__.c.id.table is MyClass.__table__ 768 assert MyOtherClass.__table__.c.id.table is MyOtherClass.__table__ 769 770 def test_single_table_no_propagation(self): 771 772 class IdColumn: 773 774 id = Column(Integer, primary_key=True) 775 776 class Generic(Base, IdColumn): 777 778 __tablename__ = 'base' 779 discriminator = Column('type', String(50)) 780 __mapper_args__ = dict(polymorphic_on=discriminator) 781 value = Column(Integer()) 782 783 class Specific(Generic): 784 785 __mapper_args__ = dict(polymorphic_identity='specific') 786 787 assert Specific.__table__ is Generic.__table__ 788 eq_(list(Generic.__table__.c.keys()), ['id', 'type', 'value']) 789 assert class_mapper(Specific).polymorphic_on \ 790 is Generic.__table__.c.type 791 eq_(class_mapper(Specific).polymorphic_identity, 'specific') 792 793 def test_joined_table_propagation(self): 794 795 class CommonMixin: 796 797 @declared_attr 798 def __tablename__(cls): 799 return cls.__name__.lower() 800 __table_args__ = {'mysql_engine': 'InnoDB'} 801 timestamp = Column(Integer) 802 id = Column(Integer, primary_key=True) 803 804 class Generic(Base, CommonMixin): 805 806 discriminator = Column('python_type', String(50)) 807 __mapper_args__ = dict(polymorphic_on=discriminator) 808 809 class Specific(Generic): 810 811 __mapper_args__ = dict(polymorphic_identity='specific') 812 id = Column(Integer, ForeignKey('generic.id'), 813 primary_key=True) 814 815 eq_(Generic.__table__.name, 'generic') 816 eq_(Specific.__table__.name, 'specific') 817 eq_(list(Generic.__table__.c.keys()), ['timestamp', 'id', 818 'python_type']) 819 eq_(list(Specific.__table__.c.keys()), ['id']) 820 eq_(Generic.__table__.kwargs, {'mysql_engine': 'InnoDB'}) 821 eq_(Specific.__table__.kwargs, {'mysql_engine': 'InnoDB'}) 822 823 def test_some_propagation(self): 824 825 class CommonMixin: 826 827 @declared_attr 828 def __tablename__(cls): 829 return cls.__name__.lower() 830 __table_args__ = {'mysql_engine': 'InnoDB'} 831 timestamp = Column(Integer) 832 833 class BaseType(Base, CommonMixin): 834 835 discriminator = Column('type', String(50)) 836 __mapper_args__ = dict(polymorphic_on=discriminator) 837 id = Column(Integer, primary_key=True) 838 value = Column(Integer()) 839 840 class Single(BaseType): 841 842 __tablename__ = None 843 __mapper_args__ = dict(polymorphic_identity='type1') 844 845 class Joined(BaseType): 846 847 __mapper_args__ = dict(polymorphic_identity='type2') 848 id = Column(Integer, ForeignKey('basetype.id'), 849 primary_key=True) 850 851 eq_(BaseType.__table__.name, 'basetype') 852 eq_(list(BaseType.__table__.c.keys()), ['timestamp', 'type', 'id', 853 'value']) 854 eq_(BaseType.__table__.kwargs, {'mysql_engine': 'InnoDB'}) 855 assert Single.__table__ is BaseType.__table__ 856 eq_(Joined.__table__.name, 'joined') 857 eq_(list(Joined.__table__.c.keys()), ['id']) 858 eq_(Joined.__table__.kwargs, {'mysql_engine': 'InnoDB'}) 859 860 def test_col_copy_vs_declared_attr_joined_propagation(self): 861 class Mixin(object): 862 a = Column(Integer) 863 864 @declared_attr 865 def b(cls): 866 return Column(Integer) 867 868 class A(Mixin, Base): 869 __tablename__ = 'a' 870 id = Column(Integer, primary_key=True) 871 872 class B(A): 873 __tablename__ = 'b' 874 id = Column(Integer, ForeignKey('a.id'), primary_key=True) 875 876 assert 'a' in A.__table__.c 877 assert 'b' in A.__table__.c 878 assert 'a' not in B.__table__.c 879 assert 'b' not in B.__table__.c 880 881 def test_col_copy_vs_declared_attr_joined_propagation_newname(self): 882 class Mixin(object): 883 a = Column('a1', Integer) 884 885 @declared_attr 886 def b(cls): 887 return Column('b1', Integer) 888 889 class A(Mixin, Base): 890 __tablename__ = 'a' 891 id = Column(Integer, primary_key=True) 892 893 class B(A): 894 __tablename__ = 'b' 895 id = Column(Integer, ForeignKey('a.id'), primary_key=True) 896 897 assert 'a1' in A.__table__.c 898 assert 'b1' in A.__table__.c 899 assert 'a1' not in B.__table__.c 900 assert 'b1' not in B.__table__.c 901 902 def test_col_copy_vs_declared_attr_single_propagation(self): 903 class Mixin(object): 904 a = Column(Integer) 905 906 @declared_attr 907 def b(cls): 908 return Column(Integer) 909 910 class A(Mixin, Base): 911 __tablename__ = 'a' 912 id = Column(Integer, primary_key=True) 913 914 class B(A): 915 pass 916 917 assert 'a' in A.__table__.c 918 assert 'b' in A.__table__.c 919 920 def test_non_propagating_mixin(self): 921 922 class NoJoinedTableNameMixin: 923 924 @declared_attr 925 def __tablename__(cls): 926 if decl.has_inherited_table(cls): 927 return None 928 return cls.__name__.lower() 929 930 class BaseType(Base, NoJoinedTableNameMixin): 931 932 discriminator = Column('type', String(50)) 933 __mapper_args__ = dict(polymorphic_on=discriminator) 934 id = Column(Integer, primary_key=True) 935 value = Column(Integer()) 936 937 class Specific(BaseType): 938 939 __mapper_args__ = dict(polymorphic_identity='specific') 940 941 eq_(BaseType.__table__.name, 'basetype') 942 eq_(list(BaseType.__table__.c.keys()), ['type', 'id', 'value']) 943 assert Specific.__table__ is BaseType.__table__ 944 assert class_mapper(Specific).polymorphic_on \ 945 is BaseType.__table__.c.type 946 eq_(class_mapper(Specific).polymorphic_identity, 'specific') 947 948 def test_non_propagating_mixin_used_for_joined(self): 949 950 class TableNameMixin: 951 952 @declared_attr 953 def __tablename__(cls): 954 if decl.has_inherited_table(cls) and TableNameMixin \ 955 not in cls.__bases__: 956 return None 957 return cls.__name__.lower() 958 959 class BaseType(Base, TableNameMixin): 960 961 discriminator = Column('type', String(50)) 962 __mapper_args__ = dict(polymorphic_on=discriminator) 963 id = Column(Integer, primary_key=True) 964 value = Column(Integer()) 965 966 class Specific(BaseType, TableNameMixin): 967 968 __mapper_args__ = dict(polymorphic_identity='specific') 969 id = Column(Integer, ForeignKey('basetype.id'), 970 primary_key=True) 971 972 eq_(BaseType.__table__.name, 'basetype') 973 eq_(list(BaseType.__table__.c.keys()), ['type', 'id', 'value']) 974 eq_(Specific.__table__.name, 'specific') 975 eq_(list(Specific.__table__.c.keys()), ['id']) 976 977 def test_single_back_propagate(self): 978 979 class ColumnMixin: 980 981 timestamp = Column(Integer) 982 983 class BaseType(Base): 984 985 __tablename__ = 'foo' 986 discriminator = Column('type', String(50)) 987 __mapper_args__ = dict(polymorphic_on=discriminator) 988 id = Column(Integer, primary_key=True) 989 990 class Specific(BaseType, ColumnMixin): 991 992 __mapper_args__ = dict(polymorphic_identity='specific') 993 994 eq_(list(BaseType.__table__.c.keys()), ['type', 'id', 'timestamp']) 995 996 def test_table_in_model_and_same_column_in_mixin(self): 997 998 class ColumnMixin: 999 1000 data = Column(Integer) 1001 1002 class Model(Base, ColumnMixin): 1003 1004 __table__ = Table('foo', Base.metadata, 1005 Column('data', Integer), 1006 Column('id', Integer, primary_key=True)) 1007 1008 model_col = Model.__table__.c.data 1009 mixin_col = ColumnMixin.data 1010 assert model_col is not mixin_col 1011 eq_(model_col.name, 'data') 1012 assert model_col.type.__class__ is mixin_col.type.__class__ 1013 1014 def test_table_in_model_and_different_named_column_in_mixin(self): 1015 1016 class ColumnMixin: 1017 tada = Column(Integer) 1018 1019 def go(): 1020 1021 class Model(Base, ColumnMixin): 1022 1023 __table__ = Table('foo', Base.metadata, 1024 Column('data', Integer), 1025 Column('id', Integer, primary_key=True)) 1026 foo = relationship("Dest") 1027 1028 assert_raises_message(sa.exc.ArgumentError, 1029 "Can't add additional column 'tada' when " 1030 "specifying __table__", go) 1031 1032 def test_table_in_model_and_different_named_alt_key_column_in_mixin(self): 1033 1034 # here, the __table__ has a column 'tada'. We disallow 1035 # the add of the 'foobar' column, even though it's 1036 # keyed to 'tada'. 1037 1038 class ColumnMixin: 1039 tada = Column('foobar', Integer) 1040 1041 def go(): 1042 1043 class Model(Base, ColumnMixin): 1044 1045 __table__ = Table('foo', Base.metadata, 1046 Column('data', Integer), 1047 Column('tada', Integer), 1048 Column('id', Integer, primary_key=True)) 1049 foo = relationship("Dest") 1050 1051 assert_raises_message(sa.exc.ArgumentError, 1052 "Can't add additional column 'foobar' when " 1053 "specifying __table__", go) 1054 1055 def test_table_in_model_overrides_different_typed_column_in_mixin(self): 1056 1057 class ColumnMixin: 1058 1059 data = Column(String) 1060 1061 class Model(Base, ColumnMixin): 1062 1063 __table__ = Table('foo', Base.metadata, 1064 Column('data', Integer), 1065 Column('id', Integer, primary_key=True)) 1066 1067 model_col = Model.__table__.c.data 1068 mixin_col = ColumnMixin.data 1069 assert model_col is not mixin_col 1070 eq_(model_col.name, 'data') 1071 assert model_col.type.__class__ is Integer 1072 1073 def test_mixin_column_ordering(self): 1074 1075 class Foo(object): 1076 1077 col1 = Column(Integer) 1078 col3 = Column(Integer) 1079 1080 class Bar(object): 1081 1082 col2 = Column(Integer) 1083 col4 = Column(Integer) 1084 1085 class Model(Base, Foo, Bar): 1086 1087 id = Column(Integer, primary_key=True) 1088 __tablename__ = 'model' 1089 1090 eq_(list(Model.__table__.c.keys()), ['col1', 'col3', 'col2', 'col4', 1091 'id']) 1092 1093 def test_honor_class_mro_one(self): 1094 class HasXMixin(object): 1095 1096 @declared_attr 1097 def x(self): 1098 return Column(Integer) 1099 1100 class Parent(HasXMixin, Base): 1101 __tablename__ = 'parent' 1102 id = Column(Integer, primary_key=True) 1103 1104 class Child(Parent): 1105 __tablename__ = 'child' 1106 id = Column(Integer, ForeignKey('parent.id'), primary_key=True) 1107 1108 assert "x" not in Child.__table__.c 1109 1110 def test_honor_class_mro_two(self): 1111 class HasXMixin(object): 1112 1113 @declared_attr 1114 def x(self): 1115 return Column(Integer) 1116 1117 class Parent(HasXMixin, Base): 1118 __tablename__ = 'parent' 1119 id = Column(Integer, primary_key=True) 1120 1121 def x(self): 1122 return "hi" 1123 1124 class C(Parent): 1125 __tablename__ = 'c' 1126 id = Column(Integer, ForeignKey('parent.id'), primary_key=True) 1127 1128 assert C().x() == 'hi' 1129 1130 def test_arbitrary_attrs_one(self): 1131 class HasMixin(object): 1132 1133 @declared_attr 1134 def some_attr(cls): 1135 return cls.__name__ + "SOME ATTR" 1136 1137 class Mapped(HasMixin, Base): 1138 __tablename__ = 't' 1139 id = Column(Integer, primary_key=True) 1140 1141 eq_(Mapped.some_attr, "MappedSOME ATTR") 1142 eq_(Mapped.__dict__['some_attr'], "MappedSOME ATTR") 1143 1144 def test_arbitrary_attrs_two(self): 1145 from sqlalchemy.ext.associationproxy import association_proxy 1146 1147 class FilterA(Base): 1148 __tablename__ = 'filter_a' 1149 id = Column(Integer(), primary_key=True) 1150 parent_id = Column(Integer(), 1151 ForeignKey('type_a.id')) 1152 filter = Column(String()) 1153 1154 def __init__(self, filter_, **kw): 1155 self.filter = filter_ 1156 1157 class FilterB(Base): 1158 __tablename__ = 'filter_b' 1159 id = Column(Integer(), primary_key=True) 1160 parent_id = Column(Integer(), 1161 ForeignKey('type_b.id')) 1162 filter = Column(String()) 1163 1164 def __init__(self, filter_, **kw): 1165 self.filter = filter_ 1166 1167 class FilterMixin(object): 1168 1169 @declared_attr 1170 def _filters(cls): 1171 return relationship(cls.filter_class, 1172 cascade='all,delete,delete-orphan') 1173 1174 @declared_attr 1175 def filters(cls): 1176 return association_proxy('_filters', 'filter') 1177 1178 class TypeA(Base, FilterMixin): 1179 __tablename__ = 'type_a' 1180 filter_class = FilterA 1181 id = Column(Integer(), primary_key=True) 1182 1183 class TypeB(Base, FilterMixin): 1184 __tablename__ = 'type_b' 1185 filter_class = FilterB 1186 id = Column(Integer(), primary_key=True) 1187 1188 TypeA(filters=['foo']) 1189 TypeB(filters=['foo']) 1190 1191 1192class DeclarativeMixinPropertyTest(DeclarativeTestBase): 1193 1194 def test_column_property(self): 1195 1196 class MyMixin(object): 1197 1198 @declared_attr 1199 def prop_hoho(cls): 1200 return column_property(Column('prop', String(50))) 1201 1202 class MyModel(Base, MyMixin): 1203 1204 __tablename__ = 'test' 1205 id = Column(Integer, primary_key=True, 1206 test_needs_autoincrement=True) 1207 1208 class MyOtherModel(Base, MyMixin): 1209 1210 __tablename__ = 'othertest' 1211 id = Column(Integer, primary_key=True, 1212 test_needs_autoincrement=True) 1213 1214 assert MyModel.__table__.c.prop is not None 1215 assert MyOtherModel.__table__.c.prop is not None 1216 assert MyModel.__table__.c.prop \ 1217 is not MyOtherModel.__table__.c.prop 1218 assert MyModel.prop_hoho.property.columns \ 1219 == [MyModel.__table__.c.prop] 1220 assert MyOtherModel.prop_hoho.property.columns \ 1221 == [MyOtherModel.__table__.c.prop] 1222 assert MyModel.prop_hoho.property \ 1223 is not MyOtherModel.prop_hoho.property 1224 Base.metadata.create_all() 1225 sess = create_session() 1226 m1, m2 = MyModel(prop_hoho='foo'), MyOtherModel(prop_hoho='bar') 1227 sess.add_all([m1, m2]) 1228 sess.flush() 1229 eq_(sess.query(MyModel).filter(MyModel.prop_hoho == 'foo' 1230 ).one(), m1) 1231 eq_(sess.query(MyOtherModel).filter(MyOtherModel.prop_hoho 1232 == 'bar').one(), m2) 1233 1234 def test_doc(self): 1235 """test documentation transfer. 1236 1237 the documentation situation with @declared_attr is problematic. 1238 at least see if mapped subclasses get the doc. 1239 1240 """ 1241 1242 class MyMixin(object): 1243 1244 @declared_attr 1245 def type_(cls): 1246 """this is a document.""" 1247 1248 return Column(String(50)) 1249 1250 @declared_attr 1251 def t2(cls): 1252 """this is another document.""" 1253 1254 return column_property(Column(String(50))) 1255 1256 class MyModel(Base, MyMixin): 1257 1258 __tablename__ = 'test' 1259 id = Column(Integer, primary_key=True) 1260 1261 configure_mappers() 1262 eq_(MyModel.type_.__doc__, """this is a document.""") 1263 eq_(MyModel.t2.__doc__, """this is another document.""") 1264 1265 def test_column_in_mapper_args(self): 1266 1267 class MyMixin(object): 1268 1269 @declared_attr 1270 def type_(cls): 1271 return Column(String(50)) 1272 __mapper_args__ = {'polymorphic_on': type_} 1273 1274 class MyModel(Base, MyMixin): 1275 1276 __tablename__ = 'test' 1277 id = Column(Integer, primary_key=True) 1278 1279 configure_mappers() 1280 col = MyModel.__mapper__.polymorphic_on 1281 eq_(col.name, 'type_') 1282 assert col.table is not None 1283 1284 def test_column_in_mapper_args_used_multiple_times(self): 1285 1286 class MyMixin(object): 1287 1288 version_id = Column(Integer) 1289 __mapper_args__ = {'version_id_col': version_id} 1290 1291 class ModelOne(Base, MyMixin): 1292 1293 __tablename__ = 'm1' 1294 id = Column(Integer, primary_key=True) 1295 1296 class ModelTwo(Base, MyMixin): 1297 1298 __tablename__ = 'm2' 1299 id = Column(Integer, primary_key=True) 1300 1301 is_( 1302 ModelOne.__mapper__.version_id_col, 1303 ModelOne.__table__.c.version_id 1304 ) 1305 is_( 1306 ModelTwo.__mapper__.version_id_col, 1307 ModelTwo.__table__.c.version_id 1308 ) 1309 1310 def test_deferred(self): 1311 1312 class MyMixin(object): 1313 1314 @declared_attr 1315 def data(cls): 1316 return deferred(Column('data', String(50))) 1317 1318 class MyModel(Base, MyMixin): 1319 1320 __tablename__ = 'test' 1321 id = Column(Integer, primary_key=True, 1322 test_needs_autoincrement=True) 1323 1324 Base.metadata.create_all() 1325 sess = create_session() 1326 sess.add_all([MyModel(data='d1'), MyModel(data='d2')]) 1327 sess.flush() 1328 sess.expunge_all() 1329 d1, d2 = sess.query(MyModel).order_by(MyModel.data) 1330 assert 'data' not in d1.__dict__ 1331 assert d1.data == 'd1' 1332 assert 'data' in d1.__dict__ 1333 1334 def _test_relationship(self, usestring): 1335 1336 class RefTargetMixin(object): 1337 1338 @declared_attr 1339 def target_id(cls): 1340 return Column('target_id', ForeignKey('target.id')) 1341 if usestring: 1342 1343 @declared_attr 1344 def target(cls): 1345 return relationship('Target', 1346 primaryjoin='Target.id==%s.target_id' 1347 % cls.__name__) 1348 else: 1349 1350 @declared_attr 1351 def target(cls): 1352 return relationship('Target') 1353 1354 class Foo(Base, RefTargetMixin): 1355 1356 __tablename__ = 'foo' 1357 id = Column(Integer, primary_key=True, 1358 test_needs_autoincrement=True) 1359 1360 class Bar(Base, RefTargetMixin): 1361 1362 __tablename__ = 'bar' 1363 id = Column(Integer, primary_key=True, 1364 test_needs_autoincrement=True) 1365 1366 class Target(Base): 1367 1368 __tablename__ = 'target' 1369 id = Column(Integer, primary_key=True, 1370 test_needs_autoincrement=True) 1371 1372 Base.metadata.create_all() 1373 sess = create_session() 1374 t1, t2 = Target(), Target() 1375 f1, f2, b1 = Foo(target=t1), Foo(target=t2), Bar(target=t1) 1376 sess.add_all([f1, f2, b1]) 1377 sess.flush() 1378 eq_(sess.query(Foo).filter(Foo.target == t2).one(), f2) 1379 eq_(sess.query(Bar).filter(Bar.target == t2).first(), None) 1380 sess.expire_all() 1381 eq_(f1.target, t1) 1382 1383 def test_relationship(self): 1384 self._test_relationship(False) 1385 1386 def test_relationship_primryjoin(self): 1387 self._test_relationship(True) 1388 1389 1390class DeclaredAttrTest(DeclarativeTestBase, testing.AssertsCompiledSQL): 1391 __dialect__ = 'default' 1392 1393 def test_singleton_behavior_within_decl(self): 1394 counter = mock.Mock() 1395 1396 class Mixin(object): 1397 @declared_attr 1398 def my_prop(cls): 1399 counter(cls) 1400 return Column('x', Integer) 1401 1402 class A(Base, Mixin): 1403 __tablename__ = 'a' 1404 id = Column(Integer, primary_key=True) 1405 1406 @declared_attr 1407 def my_other_prop(cls): 1408 return column_property(cls.my_prop + 5) 1409 1410 eq_(counter.mock_calls, [mock.call(A)]) 1411 1412 class B(Base, Mixin): 1413 __tablename__ = 'b' 1414 id = Column(Integer, primary_key=True) 1415 1416 @declared_attr 1417 def my_other_prop(cls): 1418 return column_property(cls.my_prop + 5) 1419 1420 eq_( 1421 counter.mock_calls, 1422 [mock.call(A), mock.call(B)]) 1423 1424 # this is why we need singleton-per-class behavior. We get 1425 # an un-bound "x" column otherwise here, because my_prop() generates 1426 # multiple columns. 1427 a_col = A.my_other_prop.__clause_element__().element.left 1428 b_col = B.my_other_prop.__clause_element__().element.left 1429 is_(a_col.table, A.__table__) 1430 is_(b_col.table, B.__table__) 1431 is_(a_col, A.__table__.c.x) 1432 is_(b_col, B.__table__.c.x) 1433 1434 s = Session() 1435 self.assert_compile( 1436 s.query(A), 1437 "SELECT a.x AS a_x, a.x + :x_1 AS anon_1, a.id AS a_id FROM a" 1438 ) 1439 self.assert_compile( 1440 s.query(B), 1441 "SELECT b.x AS b_x, b.x + :x_1 AS anon_1, b.id AS b_id FROM b" 1442 ) 1443 1444 @testing.requires.predictable_gc 1445 def test_singleton_gc(self): 1446 counter = mock.Mock() 1447 1448 class Mixin(object): 1449 @declared_attr 1450 def my_prop(cls): 1451 counter(cls.__name__) 1452 return Column('x', Integer) 1453 1454 class A(Base, Mixin): 1455 __tablename__ = 'b' 1456 id = Column(Integer, primary_key=True) 1457 1458 @declared_attr 1459 def my_other_prop(cls): 1460 return column_property(cls.my_prop + 5) 1461 1462 eq_(counter.mock_calls, [mock.call("A")]) 1463 del A 1464 gc_collect() 1465 assert "A" not in Base._decl_class_registry 1466 1467 def test_can_we_access_the_mixin_straight(self): 1468 class Mixin(object): 1469 @declared_attr 1470 def my_prop(cls): 1471 return Column('x', Integer) 1472 1473 assert_raises_message( 1474 sa.exc.SAWarning, 1475 "Unmanaged access of declarative attribute my_prop " 1476 "from non-mapped class Mixin", 1477 getattr, Mixin, "my_prop" 1478 ) 1479 1480 def test_non_decl_access(self): 1481 counter = mock.Mock() 1482 1483 class Mixin(object): 1484 @declared_attr 1485 def __tablename__(cls): 1486 counter(cls) 1487 return "foo" 1488 1489 class Foo(Mixin, Base): 1490 id = Column(Integer, primary_key=True) 1491 1492 @declared_attr 1493 def x(cls): 1494 cls.__tablename__ 1495 1496 @declared_attr 1497 def y(cls): 1498 cls.__tablename__ 1499 1500 eq_( 1501 counter.mock_calls, 1502 [mock.call(Foo)] 1503 ) 1504 1505 eq_(Foo.__tablename__, 'foo') 1506 eq_(Foo.__tablename__, 'foo') 1507 1508 eq_( 1509 counter.mock_calls, 1510 [mock.call(Foo), mock.call(Foo), mock.call(Foo)] 1511 ) 1512 1513 def test_property_noncascade(self): 1514 counter = mock.Mock() 1515 1516 class Mixin(object): 1517 @declared_attr 1518 def my_prop(cls): 1519 counter(cls) 1520 return column_property(cls.x + 2) 1521 1522 class A(Base, Mixin): 1523 __tablename__ = 'a' 1524 1525 id = Column(Integer, primary_key=True) 1526 x = Column(Integer) 1527 1528 class B(A): 1529 pass 1530 1531 eq_(counter.mock_calls, [mock.call(A)]) 1532 1533 def test_property_cascade(self): 1534 counter = mock.Mock() 1535 1536 class Mixin(object): 1537 @declared_attr.cascading 1538 def my_prop(cls): 1539 counter(cls) 1540 return column_property(cls.x + 2) 1541 1542 class A(Base, Mixin): 1543 __tablename__ = 'a' 1544 1545 id = Column(Integer, primary_key=True) 1546 x = Column(Integer) 1547 1548 class B(A): 1549 pass 1550 1551 eq_(counter.mock_calls, [mock.call(A), mock.call(B)]) 1552 1553 def test_col_prop_attrs_associated_w_class_for_mapper_args(self): 1554 from sqlalchemy import Column 1555 import collections 1556 1557 asserted = collections.defaultdict(set) 1558 1559 class Mixin(object): 1560 @declared_attr.cascading 1561 def my_attr(cls): 1562 if decl.has_inherited_table(cls): 1563 id = Column(ForeignKey('a.my_attr'), primary_key=True) 1564 asserted['b'].add(id) 1565 else: 1566 id = Column(Integer, primary_key=True) 1567 asserted['a'].add(id) 1568 return id 1569 1570 class A(Base, Mixin): 1571 __tablename__ = 'a' 1572 1573 @declared_attr 1574 def __mapper_args__(cls): 1575 asserted['a'].add(cls.my_attr) 1576 return {} 1577 1578 # here: 1579 # 1. A is mapped. so A.my_attr is now the InstrumentedAttribute. 1580 # 2. B wants to call my_attr also. Due to .cascading, it has been 1581 # invoked specific to B, and is present in the dict_ that will 1582 # be used when we map the class. But except for the 1583 # special setattr() we do in _scan_attributes() in this case, would 1584 # otherwise not been set on the class as anything from this call; 1585 # the usual mechanics of calling it from the descriptor also do not 1586 # work because A is fully mapped and because A set it up, is currently 1587 # that non-expected InstrumentedAttribute and replaces the 1588 # descriptor from being invoked. 1589 1590 class B(A): 1591 __tablename__ = 'b' 1592 1593 @declared_attr 1594 def __mapper_args__(cls): 1595 asserted['b'].add(cls.my_attr) 1596 return {} 1597 1598 eq_( 1599 asserted, 1600 { 1601 'a': set([A.my_attr.property.columns[0]]), 1602 'b': set([B.my_attr.property.columns[0]]) 1603 } 1604 ) 1605 1606 def test_column_pre_map(self): 1607 counter = mock.Mock() 1608 1609 class Mixin(object): 1610 @declared_attr 1611 def my_col(cls): 1612 counter(cls) 1613 assert not orm_base._mapper_or_none(cls) 1614 return Column('x', Integer) 1615 1616 class A(Base, Mixin): 1617 __tablename__ = 'a' 1618 1619 id = Column(Integer, primary_key=True) 1620 1621 eq_(counter.mock_calls, [mock.call(A)]) 1622 1623 def test_mixin_attr_refers_to_column_copies(self): 1624 # this @declared_attr can refer to User.id 1625 # freely because we now do the "copy column" operation 1626 # before the declared_attr is invoked. 1627 1628 counter = mock.Mock() 1629 1630 class HasAddressCount(object): 1631 id = Column(Integer, primary_key=True) 1632 1633 @declared_attr 1634 def address_count(cls): 1635 counter(cls.id) 1636 return column_property( 1637 select([func.count(Address.id)]). 1638 where(Address.user_id == cls.id). 1639 as_scalar() 1640 ) 1641 1642 class Address(Base): 1643 __tablename__ = 'address' 1644 id = Column(Integer, primary_key=True) 1645 user_id = Column(ForeignKey('user.id')) 1646 1647 class User(Base, HasAddressCount): 1648 __tablename__ = 'user' 1649 1650 eq_( 1651 counter.mock_calls, 1652 [mock.call(User.id)] 1653 ) 1654 1655 sess = Session() 1656 self.assert_compile( 1657 sess.query(User).having(User.address_count > 5), 1658 'SELECT (SELECT count(address.id) AS ' 1659 'count_1 FROM address WHERE address.user_id = "user".id) ' 1660 'AS anon_1, "user".id AS user_id FROM "user" ' 1661 'HAVING (SELECT count(address.id) AS ' 1662 'count_1 FROM address WHERE address.user_id = "user".id) ' 1663 '> :param_1' 1664 ) 1665 1666 1667class AbstractTest(DeclarativeTestBase): 1668 1669 def test_abstract_boolean(self): 1670 1671 class A(Base): 1672 __abstract__ = True 1673 __tablename__ = 'x' 1674 id = Column(Integer, primary_key=True) 1675 1676 class B(Base): 1677 __abstract__ = False 1678 __tablename__ = 'y' 1679 id = Column(Integer, primary_key=True) 1680 1681 class C(Base): 1682 __abstract__ = False 1683 __tablename__ = 'z' 1684 id = Column(Integer, primary_key=True) 1685 1686 class D(Base): 1687 __tablename__ = 'q' 1688 id = Column(Integer, primary_key=True) 1689 1690 eq_(set(Base.metadata.tables), set(['y', 'z', 'q'])) 1691 1692 def test_middle_abstract_attributes(self): 1693 # test for [ticket:3219] 1694 class A(Base): 1695 __tablename__ = 'a' 1696 1697 id = Column(Integer, primary_key=True) 1698 name = Column(String) 1699 1700 class B(A): 1701 __abstract__ = True 1702 data = Column(String) 1703 1704 class C(B): 1705 c_value = Column(String) 1706 1707 eq_( 1708 sa.inspect(C).attrs.keys(), ['id', 'name', 'data', 'c_value'] 1709 ) 1710 1711 def test_middle_abstract_inherits(self): 1712 # test for [ticket:3240] 1713 1714 class A(Base): 1715 __tablename__ = 'a' 1716 id = Column(Integer, primary_key=True) 1717 1718 class AAbs(A): 1719 __abstract__ = True 1720 1721 class B1(A): 1722 __tablename__ = 'b1' 1723 id = Column(ForeignKey('a.id'), primary_key=True) 1724 1725 class B2(AAbs): 1726 __tablename__ = 'b2' 1727 id = Column(ForeignKey('a.id'), primary_key=True) 1728 1729 assert B1.__mapper__.inherits is A.__mapper__ 1730 1731 assert B2.__mapper__.inherits is A.__mapper__ 1732