1# ext/declarative/base.py 2# Copyright (C) 2005-2018 the SQLAlchemy authors and contributors 3# <see AUTHORS file> 4# 5# This module is part of SQLAlchemy and is released under 6# the MIT License: http://www.opensource.org/licenses/mit-license.php 7"""Internal implementation for declarative.""" 8 9from ...schema import Table, Column 10from ...orm import mapper, class_mapper, synonym 11from ...orm.interfaces import MapperProperty 12from ...orm.properties import ColumnProperty, CompositeProperty 13from ...orm.attributes import QueryableAttribute 14from ...orm.base import _is_mapped_class 15from ... import util, exc 16from ...util import topological 17from ...sql import expression 18from ... import event 19from . import clsregistry 20import collections 21import weakref 22from sqlalchemy.orm import instrumentation 23 24declared_attr = declarative_props = None 25 26 27def _declared_mapping_info(cls): 28 # deferred mapping 29 if _DeferredMapperConfig.has_cls(cls): 30 return _DeferredMapperConfig.config_for_cls(cls) 31 # regular mapping 32 elif _is_mapped_class(cls): 33 return class_mapper(cls, configure=False) 34 else: 35 return None 36 37 38def _resolve_for_abstract(cls): 39 if cls is object: 40 return None 41 42 if _get_immediate_cls_attr(cls, '__abstract__', strict=True): 43 for sup in cls.__bases__: 44 sup = _resolve_for_abstract(sup) 45 if sup is not None: 46 return sup 47 else: 48 return None 49 else: 50 return cls 51 52 53def _get_immediate_cls_attr(cls, attrname, strict=False): 54 """return an attribute of the class that is either present directly 55 on the class, e.g. not on a superclass, or is from a superclass but 56 this superclass is a mixin, that is, not a descendant of 57 the declarative base. 58 59 This is used to detect attributes that indicate something about 60 a mapped class independently from any mapped classes that it may 61 inherit from. 62 63 """ 64 if not issubclass(cls, object): 65 return None 66 67 for base in cls.__mro__: 68 _is_declarative_inherits = hasattr(base, '_decl_class_registry') 69 if attrname in base.__dict__ and ( 70 base is cls or 71 ((base in cls.__bases__ if strict else True) 72 and not _is_declarative_inherits) 73 ): 74 return getattr(base, attrname) 75 else: 76 return None 77 78 79def _as_declarative(cls, classname, dict_): 80 global declared_attr, declarative_props 81 if declared_attr is None: 82 from .api import declared_attr 83 declarative_props = (declared_attr, util.classproperty) 84 85 if _get_immediate_cls_attr(cls, '__abstract__', strict=True): 86 return 87 88 _MapperConfig.setup_mapping(cls, classname, dict_) 89 90 91class _MapperConfig(object): 92 93 @classmethod 94 def setup_mapping(cls, cls_, classname, dict_): 95 defer_map = _get_immediate_cls_attr( 96 cls_, '_sa_decl_prepare_nocascade', strict=True) or \ 97 hasattr(cls_, '_sa_decl_prepare') 98 99 if defer_map: 100 cfg_cls = _DeferredMapperConfig 101 else: 102 cfg_cls = _MapperConfig 103 cfg_cls(cls_, classname, dict_) 104 105 def __init__(self, cls_, classname, dict_): 106 107 self.cls = cls_ 108 109 # dict_ will be a dictproxy, which we can't write to, and we need to! 110 self.dict_ = dict(dict_) 111 self.classname = classname 112 self.mapped_table = None 113 self.properties = util.OrderedDict() 114 self.declared_columns = set() 115 self.column_copies = {} 116 self._setup_declared_events() 117 118 # temporary registry. While early 1.0 versions 119 # set up the ClassManager here, by API contract 120 # we can't do that until there's a mapper. 121 self.cls._sa_declared_attr_reg = {} 122 123 self._scan_attributes() 124 125 clsregistry.add_class(self.classname, self.cls) 126 127 self._extract_mappable_attributes() 128 129 self._extract_declared_columns() 130 131 self._setup_table() 132 133 self._setup_inheritance() 134 135 self._early_mapping() 136 137 def _early_mapping(self): 138 self.map() 139 140 def _setup_declared_events(self): 141 if _get_immediate_cls_attr(self.cls, '__declare_last__'): 142 @event.listens_for(mapper, "after_configured") 143 def after_configured(): 144 self.cls.__declare_last__() 145 146 if _get_immediate_cls_attr(self.cls, '__declare_first__'): 147 @event.listens_for(mapper, "before_configured") 148 def before_configured(): 149 self.cls.__declare_first__() 150 151 def _scan_attributes(self): 152 cls = self.cls 153 dict_ = self.dict_ 154 column_copies = self.column_copies 155 mapper_args_fn = None 156 table_args = inherited_table_args = None 157 tablename = None 158 159 for base in cls.__mro__: 160 class_mapped = base is not cls and \ 161 _declared_mapping_info(base) is not None and \ 162 not _get_immediate_cls_attr( 163 base, '_sa_decl_prepare_nocascade', strict=True) 164 165 if not class_mapped and base is not cls: 166 self._produce_column_copies(base) 167 168 for name, obj in vars(base).items(): 169 if name == '__mapper_args__': 170 if not mapper_args_fn and ( 171 not class_mapped or 172 isinstance(obj, declarative_props) 173 ): 174 # don't even invoke __mapper_args__ until 175 # after we've determined everything about the 176 # mapped table. 177 # make a copy of it so a class-level dictionary 178 # is not overwritten when we update column-based 179 # arguments. 180 mapper_args_fn = lambda: dict(cls.__mapper_args__) 181 elif name == '__tablename__': 182 if not tablename and ( 183 not class_mapped or 184 isinstance(obj, declarative_props) 185 ): 186 tablename = cls.__tablename__ 187 elif name == '__table_args__': 188 if not table_args and ( 189 not class_mapped or 190 isinstance(obj, declarative_props) 191 ): 192 table_args = cls.__table_args__ 193 if not isinstance( 194 table_args, (tuple, dict, type(None))): 195 raise exc.ArgumentError( 196 "__table_args__ value must be a tuple, " 197 "dict, or None") 198 if base is not cls: 199 inherited_table_args = True 200 elif class_mapped: 201 if isinstance(obj, declarative_props): 202 util.warn("Regular (i.e. not __special__) " 203 "attribute '%s.%s' uses @declared_attr, " 204 "but owning class %s is mapped - " 205 "not applying to subclass %s." 206 % (base.__name__, name, base, cls)) 207 continue 208 elif base is not cls: 209 # we're a mixin, abstract base, or something that is 210 # acting like that for now. 211 if isinstance(obj, Column): 212 # already copied columns to the mapped class. 213 continue 214 elif isinstance(obj, MapperProperty): 215 raise exc.InvalidRequestError( 216 "Mapper properties (i.e. deferred," 217 "column_property(), relationship(), etc.) must " 218 "be declared as @declared_attr callables " 219 "on declarative mixin classes.") 220 elif isinstance(obj, declarative_props): 221 oldclassprop = isinstance(obj, util.classproperty) 222 if not oldclassprop and obj._cascading: 223 dict_[name] = column_copies[obj] = \ 224 ret = obj.__get__(obj, cls) 225 setattr(cls, name, ret) 226 else: 227 if oldclassprop: 228 util.warn_deprecated( 229 "Use of sqlalchemy.util.classproperty on " 230 "declarative classes is deprecated.") 231 dict_[name] = column_copies[obj] = \ 232 ret = getattr(cls, name) 233 if isinstance(ret, (Column, MapperProperty)) and \ 234 ret.doc is None: 235 ret.doc = obj.__doc__ 236 237 if inherited_table_args and not tablename: 238 table_args = None 239 240 self.table_args = table_args 241 self.tablename = tablename 242 self.mapper_args_fn = mapper_args_fn 243 244 def _produce_column_copies(self, base): 245 cls = self.cls 246 dict_ = self.dict_ 247 column_copies = self.column_copies 248 # copy mixin columns to the mapped class 249 for name, obj in vars(base).items(): 250 if isinstance(obj, Column): 251 if getattr(cls, name) is not obj: 252 # if column has been overridden 253 # (like by the InstrumentedAttribute of the 254 # superclass), skip 255 continue 256 elif obj.foreign_keys: 257 raise exc.InvalidRequestError( 258 "Columns with foreign keys to other columns " 259 "must be declared as @declared_attr callables " 260 "on declarative mixin classes. ") 261 elif name not in dict_ and not ( 262 '__table__' in dict_ and 263 (obj.name or name) in dict_['__table__'].c 264 ): 265 column_copies[obj] = copy_ = obj.copy() 266 copy_._creation_order = obj._creation_order 267 setattr(cls, name, copy_) 268 dict_[name] = copy_ 269 270 def _extract_mappable_attributes(self): 271 cls = self.cls 272 dict_ = self.dict_ 273 274 our_stuff = self.properties 275 276 for k in list(dict_): 277 278 if k in ('__table__', '__tablename__', '__mapper_args__'): 279 continue 280 281 value = dict_[k] 282 if isinstance(value, declarative_props): 283 value = getattr(cls, k) 284 285 elif isinstance(value, QueryableAttribute) and \ 286 value.class_ is not cls and \ 287 value.key != k: 288 # detect a QueryableAttribute that's already mapped being 289 # assigned elsewhere in userland, turn into a synonym() 290 value = synonym(value.key) 291 setattr(cls, k, value) 292 293 if (isinstance(value, tuple) and len(value) == 1 and 294 isinstance(value[0], (Column, MapperProperty))): 295 util.warn("Ignoring declarative-like tuple value of attribute " 296 "%s: possibly a copy-and-paste error with a comma " 297 "left at the end of the line?" % k) 298 continue 299 elif not isinstance(value, (Column, MapperProperty)): 300 # using @declared_attr for some object that 301 # isn't Column/MapperProperty; remove from the dict_ 302 # and place the evaluated value onto the class. 303 if not k.startswith('__'): 304 dict_.pop(k) 305 setattr(cls, k, value) 306 continue 307 # we expect to see the name 'metadata' in some valid cases; 308 # however at this point we see it's assigned to something trying 309 # to be mapped, so raise for that. 310 elif k == 'metadata': 311 raise exc.InvalidRequestError( 312 "Attribute name 'metadata' is reserved " 313 "for the MetaData instance when using a " 314 "declarative base class." 315 ) 316 prop = clsregistry._deferred_relationship(cls, value) 317 our_stuff[k] = prop 318 319 def _extract_declared_columns(self): 320 our_stuff = self.properties 321 322 # set up attributes in the order they were created 323 our_stuff.sort(key=lambda key: our_stuff[key]._creation_order) 324 325 # extract columns from the class dict 326 declared_columns = self.declared_columns 327 name_to_prop_key = collections.defaultdict(set) 328 for key, c in list(our_stuff.items()): 329 if isinstance(c, (ColumnProperty, CompositeProperty)): 330 for col in c.columns: 331 if isinstance(col, Column) and \ 332 col.table is None: 333 _undefer_column_name(key, col) 334 if not isinstance(c, CompositeProperty): 335 name_to_prop_key[col.name].add(key) 336 declared_columns.add(col) 337 elif isinstance(c, Column): 338 _undefer_column_name(key, c) 339 name_to_prop_key[c.name].add(key) 340 declared_columns.add(c) 341 # if the column is the same name as the key, 342 # remove it from the explicit properties dict. 343 # the normal rules for assigning column-based properties 344 # will take over, including precedence of columns 345 # in multi-column ColumnProperties. 346 if key == c.key: 347 del our_stuff[key] 348 349 for name, keys in name_to_prop_key.items(): 350 if len(keys) > 1: 351 util.warn( 352 "On class %r, Column object %r named " 353 "directly multiple times, " 354 "only one will be used: %s. " 355 "Consider using orm.synonym instead" % 356 (self.classname, name, (", ".join(sorted(keys)))) 357 ) 358 359 def _setup_table(self): 360 cls = self.cls 361 tablename = self.tablename 362 table_args = self.table_args 363 dict_ = self.dict_ 364 declared_columns = self.declared_columns 365 366 declared_columns = self.declared_columns = sorted( 367 declared_columns, key=lambda c: c._creation_order) 368 table = None 369 370 if hasattr(cls, '__table_cls__'): 371 table_cls = util.unbound_method_to_callable(cls.__table_cls__) 372 else: 373 table_cls = Table 374 375 if '__table__' not in dict_: 376 if tablename is not None: 377 378 args, table_kw = (), {} 379 if table_args: 380 if isinstance(table_args, dict): 381 table_kw = table_args 382 elif isinstance(table_args, tuple): 383 if isinstance(table_args[-1], dict): 384 args, table_kw = table_args[0:-1], table_args[-1] 385 else: 386 args = table_args 387 388 autoload = dict_.get('__autoload__') 389 if autoload: 390 table_kw['autoload'] = True 391 392 cls.__table__ = table = table_cls( 393 tablename, cls.metadata, 394 *(tuple(declared_columns) + tuple(args)), 395 **table_kw) 396 else: 397 table = cls.__table__ 398 if declared_columns: 399 for c in declared_columns: 400 if not table.c.contains_column(c): 401 raise exc.ArgumentError( 402 "Can't add additional column %r when " 403 "specifying __table__" % c.key 404 ) 405 self.local_table = table 406 407 def _setup_inheritance(self): 408 table = self.local_table 409 cls = self.cls 410 table_args = self.table_args 411 declared_columns = self.declared_columns 412 for c in cls.__bases__: 413 c = _resolve_for_abstract(c) 414 if c is None: 415 continue 416 if _declared_mapping_info(c) is not None and \ 417 not _get_immediate_cls_attr( 418 c, '_sa_decl_prepare_nocascade', strict=True): 419 self.inherits = c 420 break 421 else: 422 self.inherits = None 423 424 if table is None and self.inherits is None and \ 425 not _get_immediate_cls_attr(cls, '__no_table__'): 426 427 raise exc.InvalidRequestError( 428 "Class %r does not have a __table__ or __tablename__ " 429 "specified and does not inherit from an existing " 430 "table-mapped class." % cls 431 ) 432 elif self.inherits: 433 inherited_mapper = _declared_mapping_info(self.inherits) 434 inherited_table = inherited_mapper.local_table 435 inherited_mapped_table = inherited_mapper.mapped_table 436 437 if table is None: 438 # single table inheritance. 439 # ensure no table args 440 if table_args: 441 raise exc.ArgumentError( 442 "Can't place __table_args__ on an inherited class " 443 "with no table." 444 ) 445 # add any columns declared here to the inherited table. 446 for c in declared_columns: 447 if c.primary_key: 448 raise exc.ArgumentError( 449 "Can't place primary key columns on an inherited " 450 "class with no table." 451 ) 452 if c.name in inherited_table.c: 453 if inherited_table.c[c.name] is c: 454 continue 455 raise exc.ArgumentError( 456 "Column '%s' on class %s conflicts with " 457 "existing column '%s'" % 458 (c, cls, inherited_table.c[c.name]) 459 ) 460 inherited_table.append_column(c) 461 if inherited_mapped_table is not None and \ 462 inherited_mapped_table is not inherited_table: 463 inherited_mapped_table._refresh_for_new_column(c) 464 465 def _prepare_mapper_arguments(self): 466 properties = self.properties 467 if self.mapper_args_fn: 468 mapper_args = self.mapper_args_fn() 469 else: 470 mapper_args = {} 471 472 # make sure that column copies are used rather 473 # than the original columns from any mixins 474 for k in ('version_id_col', 'polymorphic_on',): 475 if k in mapper_args: 476 v = mapper_args[k] 477 mapper_args[k] = self.column_copies.get(v, v) 478 479 assert 'inherits' not in mapper_args, \ 480 "Can't specify 'inherits' explicitly with declarative mappings" 481 482 if self.inherits: 483 mapper_args['inherits'] = self.inherits 484 485 if self.inherits and not mapper_args.get('concrete', False): 486 # single or joined inheritance 487 # exclude any cols on the inherited table which are 488 # not mapped on the parent class, to avoid 489 # mapping columns specific to sibling/nephew classes 490 inherited_mapper = _declared_mapping_info(self.inherits) 491 inherited_table = inherited_mapper.local_table 492 493 if 'exclude_properties' not in mapper_args: 494 mapper_args['exclude_properties'] = exclude_properties = \ 495 set( 496 [c.key for c in inherited_table.c 497 if c not in inherited_mapper._columntoproperty] 498 ).union( 499 inherited_mapper.exclude_properties or () 500 ) 501 exclude_properties.difference_update( 502 [c.key for c in self.declared_columns]) 503 504 # look through columns in the current mapper that 505 # are keyed to a propname different than the colname 506 # (if names were the same, we'd have popped it out above, 507 # in which case the mapper makes this combination). 508 # See if the superclass has a similar column property. 509 # If so, join them together. 510 for k, col in list(properties.items()): 511 if not isinstance(col, expression.ColumnElement): 512 continue 513 if k in inherited_mapper._props: 514 p = inherited_mapper._props[k] 515 if isinstance(p, ColumnProperty): 516 # note here we place the subclass column 517 # first. See [ticket:1892] for background. 518 properties[k] = [col] + p.columns 519 result_mapper_args = mapper_args.copy() 520 result_mapper_args['properties'] = properties 521 self.mapper_args = result_mapper_args 522 523 def map(self): 524 self._prepare_mapper_arguments() 525 if hasattr(self.cls, '__mapper_cls__'): 526 mapper_cls = util.unbound_method_to_callable( 527 self.cls.__mapper_cls__) 528 else: 529 mapper_cls = mapper 530 531 self.cls.__mapper__ = mp_ = mapper_cls( 532 self.cls, 533 self.local_table, 534 **self.mapper_args 535 ) 536 del self.cls._sa_declared_attr_reg 537 return mp_ 538 539 540class _DeferredMapperConfig(_MapperConfig): 541 _configs = util.OrderedDict() 542 543 def _early_mapping(self): 544 pass 545 546 @property 547 def cls(self): 548 return self._cls() 549 550 @cls.setter 551 def cls(self, class_): 552 self._cls = weakref.ref(class_, self._remove_config_cls) 553 self._configs[self._cls] = self 554 555 @classmethod 556 def _remove_config_cls(cls, ref): 557 cls._configs.pop(ref, None) 558 559 @classmethod 560 def has_cls(cls, class_): 561 # 2.6 fails on weakref if class_ is an old style class 562 return isinstance(class_, type) and \ 563 weakref.ref(class_) in cls._configs 564 565 @classmethod 566 def config_for_cls(cls, class_): 567 return cls._configs[weakref.ref(class_)] 568 569 @classmethod 570 def classes_for_base(cls, base_cls, sort=True): 571 classes_for_base = [ 572 m for m, cls_ in 573 [(m, m.cls) for m in cls._configs.values()] 574 if cls_ is not None and issubclass(cls_, base_cls) 575 ] 576 577 if not sort: 578 return classes_for_base 579 580 all_m_by_cls = dict( 581 (m.cls, m) 582 for m in classes_for_base 583 ) 584 585 tuples = [] 586 for m_cls in all_m_by_cls: 587 tuples.extend( 588 (all_m_by_cls[base_cls], all_m_by_cls[m_cls]) 589 for base_cls in m_cls.__bases__ 590 if base_cls in all_m_by_cls 591 ) 592 return list( 593 topological.sort( 594 tuples, 595 classes_for_base 596 ) 597 ) 598 599 def map(self): 600 self._configs.pop(self._cls, None) 601 return super(_DeferredMapperConfig, self).map() 602 603 604def _add_attribute(cls, key, value): 605 """add an attribute to an existing declarative class. 606 607 This runs through the logic to determine MapperProperty, 608 adds it to the Mapper, adds a column to the mapped Table, etc. 609 610 """ 611 612 if '__mapper__' in cls.__dict__: 613 if isinstance(value, Column): 614 _undefer_column_name(key, value) 615 cls.__table__.append_column(value) 616 cls.__mapper__.add_property(key, value) 617 elif isinstance(value, ColumnProperty): 618 for col in value.columns: 619 if isinstance(col, Column) and col.table is None: 620 _undefer_column_name(key, col) 621 cls.__table__.append_column(col) 622 cls.__mapper__.add_property(key, value) 623 elif isinstance(value, MapperProperty): 624 cls.__mapper__.add_property( 625 key, 626 clsregistry._deferred_relationship(cls, value) 627 ) 628 elif isinstance(value, QueryableAttribute) and value.key != key: 629 # detect a QueryableAttribute that's already mapped being 630 # assigned elsewhere in userland, turn into a synonym() 631 value = synonym(value.key) 632 cls.__mapper__.add_property( 633 key, 634 clsregistry._deferred_relationship(cls, value) 635 ) 636 else: 637 type.__setattr__(cls, key, value) 638 else: 639 type.__setattr__(cls, key, value) 640 641 642def _declarative_constructor(self, **kwargs): 643 """A simple constructor that allows initialization from kwargs. 644 645 Sets attributes on the constructed instance using the names and 646 values in ``kwargs``. 647 648 Only keys that are present as 649 attributes of the instance's class are allowed. These could be, 650 for example, any mapped columns or relationships. 651 """ 652 cls_ = type(self) 653 for k in kwargs: 654 if not hasattr(cls_, k): 655 raise TypeError( 656 "%r is an invalid keyword argument for %s" % 657 (k, cls_.__name__)) 658 setattr(self, k, kwargs[k]) 659_declarative_constructor.__name__ = '__init__' 660 661 662def _undefer_column_name(key, column): 663 if column.key is None: 664 column.key = key 665 if column.name is None: 666 column.name = key 667