30
30
from sqlalchemy .orm .exc import UnmappedClassError
31
31
from sqlalchemy .orm .session import Session as SessionBase
32
32
33
- from ._compat import iteritems , itervalues , string_types , xrange
33
+ from ._compat import itervalues , string_types , xrange
34
34
35
35
__version__ = '2.2.1'
36
36
@@ -551,31 +551,39 @@ def get_engine(self):
551
551
552
552
553
553
def _should_set_tablename (cls ):
554
- """Traverse the model's MRO. If a primary key column is found before a
555
- table or tablename, then a new tablename should be generated.
556
-
557
- This supports:
558
-
559
- * Joined table inheritance without explicitly naming sub-models.
560
- * Single table inheritance.
561
- * Inheriting from mixins or abstract models.
562
-
563
- :param cls: model to check
564
- :return: True if tablename should be set
554
+ """Determine whether ``__tablename__`` should be automatically generated
555
+ for a model.
556
+
557
+ * If no class in the MRO sets a name, one should be generated.
558
+ * If a declared attr is found, it should be used instead.
559
+ * If a name is found, it should be used if the class is a mixin, otherwise
560
+ one should be generated.
561
+ * Abstract models should not have one generated.
562
+
563
+ Later, :meth:`._BoundDeclarativeMeta.__table_cls__` will determine if the
564
+ model looks like single or joined-table inheritance. If no primary key is
565
+ found, the name will be unset.
565
566
"""
567
+ if (
568
+ cls .__dict__ .get ('__abstract__' , False )
569
+ or not any (isinstance (b , DeclarativeMeta ) for b in cls .__mro__ [1 :])
570
+ ):
571
+ return False
566
572
567
573
for base in cls .__mro__ :
568
- d = base .__dict__
574
+ if '__tablename__' not in base .__dict__ :
575
+ continue
569
576
570
- if '__tablename__' in d or '__table__' in d :
577
+ if isinstance ( base . __dict__ [ '__tablename__' ], declared_attr ) :
571
578
return False
572
579
573
- for name , obj in iteritems (d ):
574
- if isinstance (obj , declared_attr ):
575
- obj = getattr (cls , name )
580
+ return not (
581
+ base is cls
582
+ or base .__dict__ .get ('__abstract__' , False )
583
+ or not isinstance (base , DeclarativeMeta )
584
+ )
576
585
577
- if isinstance (obj , sqlalchemy .Column ) and obj .primary_key :
578
- return True
586
+ return True
579
587
580
588
581
589
def camel_to_snake_case (name ):
@@ -591,20 +599,36 @@ def _join(match):
591
599
592
600
593
601
class _BoundDeclarativeMeta (DeclarativeMeta ):
594
- def __new__ (cls , name , bases , d ):
595
- # if tablename is set explicitly, move it to the cache attribute so
596
- # that future subclasses still have auto behavior
597
- if '__tablename__' in d :
598
- d ['_cached_tablename' ] = d .pop ('__tablename__' )
602
+ def __init__ (cls , name , bases , d ):
603
+ if _should_set_tablename (cls ):
604
+ cls .__tablename__ = camel_to_snake_case (cls .__name__ )
605
+
606
+ bind_key = (
607
+ d .pop ('__bind_key__' , None )
608
+ or getattr (cls , '__bind_key__' , None )
609
+ )
599
610
600
- return DeclarativeMeta . __new__ ( cls , name , bases , d )
611
+ super ( _BoundDeclarativeMeta , cls ). __init__ ( name , bases , d )
601
612
602
- def __init__ (self , name , bases , d ):
603
- bind_key = d .pop ('__bind_key__' , None ) or getattr (self , '__bind_key__' , None )
604
- DeclarativeMeta .__init__ (self , name , bases , d )
613
+ if bind_key is not None and hasattr (cls , '__table__' ):
614
+ cls .__table__ .info ['bind_key' ] = bind_key
605
615
606
- if bind_key is not None and hasattr (self , '__table__' ):
607
- self .__table__ .info ['bind_key' ] = bind_key
616
+ def __table_cls__ (cls , * args , ** kwargs ):
617
+ """This is called by SQLAlchemy during mapper setup. It determines the
618
+ final table object that the model will use.
619
+
620
+ If no primary key is found, that indicates single-table inheritance,
621
+ so no table will be created and ``__tablename__`` will be unset.
622
+ """
623
+ for arg in args :
624
+ if (
625
+ (isinstance (arg , sqlalchemy .Column ) and arg .primary_key )
626
+ or isinstance (arg , sqlalchemy .PrimaryKeyConstraint )
627
+ ):
628
+ return sqlalchemy .Table (* args , ** kwargs )
629
+
630
+ if '__tablename__' in cls .__dict__ :
631
+ del cls .__tablename__
608
632
609
633
610
634
def get_state (app ):
@@ -638,18 +662,6 @@ class Model(object):
638
662
#: Equivalent to ``db.session.query(Model)`` unless :attr:`query_class` has been changed.
639
663
query = None
640
664
641
- _cached_tablename = None
642
-
643
- @declared_attr
644
- def __tablename__ (cls ):
645
- if (
646
- '_cached_tablename' not in cls .__dict__ and
647
- _should_set_tablename (cls )
648
- ):
649
- cls ._cached_tablename = camel_to_snake_case (cls .__name__ )
650
-
651
- return cls ._cached_tablename
652
-
653
665
654
666
class SQLAlchemy (object ):
655
667
"""This class is used to control the SQLAlchemy integration to one
0 commit comments