base.py 26 KB

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