You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1156 lines
42KB

  1. # ext/declarative/base.py
  2. # Copyright (C) 2005-2021 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 __future__ import absolute_import
  9. import collections
  10. import weakref
  11. from sqlalchemy.orm import attributes
  12. from sqlalchemy.orm import instrumentation
  13. from . import clsregistry
  14. from . import exc as orm_exc
  15. from . import mapper as mapperlib
  16. from .attributes import InstrumentedAttribute
  17. from .attributes import QueryableAttribute
  18. from .base import _is_mapped_class
  19. from .base import InspectionAttr
  20. from .descriptor_props import CompositeProperty
  21. from .descriptor_props import SynonymProperty
  22. from .interfaces import MapperProperty
  23. from .mapper import Mapper as mapper
  24. from .properties import ColumnProperty
  25. from .util import class_mapper
  26. from .. import event
  27. from .. import exc
  28. from .. import util
  29. from ..sql import expression
  30. from ..sql.schema import Column
  31. from ..sql.schema import Table
  32. from ..util import topological
  33. def _declared_mapping_info(cls):
  34. # deferred mapping
  35. if _DeferredMapperConfig.has_cls(cls):
  36. return _DeferredMapperConfig.config_for_cls(cls)
  37. # regular mapping
  38. elif _is_mapped_class(cls):
  39. return class_mapper(cls, configure=False)
  40. else:
  41. return None
  42. def _resolve_for_abstract_or_classical(cls):
  43. if cls is object:
  44. return None
  45. if cls.__dict__.get("__abstract__", False):
  46. for sup in cls.__bases__:
  47. sup = _resolve_for_abstract_or_classical(sup)
  48. if sup is not None:
  49. return sup
  50. else:
  51. return None
  52. else:
  53. clsmanager = _dive_for_cls_manager(cls)
  54. if clsmanager:
  55. return clsmanager.class_
  56. else:
  57. return cls
  58. def _get_immediate_cls_attr(cls, attrname, strict=False):
  59. """return an attribute of the class that is either present directly
  60. on the class, e.g. not on a superclass, or is from a superclass but
  61. this superclass is a non-mapped mixin, that is, not a descendant of
  62. the declarative base and is also not classically mapped.
  63. This is used to detect attributes that indicate something about
  64. a mapped class independently from any mapped classes that it may
  65. inherit from.
  66. """
  67. # the rules are different for this name than others,
  68. # make sure we've moved it out. transitional
  69. assert attrname != "__abstract__"
  70. if not issubclass(cls, object):
  71. return None
  72. if attrname in cls.__dict__:
  73. return getattr(cls, attrname)
  74. for base in cls.__mro__[1:]:
  75. _is_classicial_inherits = _dive_for_cls_manager(base)
  76. if attrname in base.__dict__ and (
  77. base is cls
  78. or (
  79. (base in cls.__bases__ if strict else True)
  80. and not _is_classicial_inherits
  81. )
  82. ):
  83. return getattr(base, attrname)
  84. else:
  85. return None
  86. def _dive_for_cls_manager(cls):
  87. # because the class manager registration is pluggable,
  88. # we need to do the search for every class in the hierarchy,
  89. # rather than just a simple "cls._sa_class_manager"
  90. # python 2 old style class
  91. if not hasattr(cls, "__mro__"):
  92. return None
  93. for base in cls.__mro__:
  94. manager = attributes.manager_of_class(base)
  95. if manager:
  96. return manager
  97. return None
  98. def _as_declarative(registry, cls, dict_):
  99. # declarative scans the class for attributes. no table or mapper
  100. # args passed separately.
  101. return _MapperConfig.setup_mapping(registry, cls, dict_, None, {})
  102. def _mapper(registry, cls, table, mapper_kw):
  103. _ImperativeMapperConfig(registry, cls, table, mapper_kw)
  104. return cls.__mapper__
  105. @util.preload_module("sqlalchemy.orm.decl_api")
  106. def _is_declarative_props(obj):
  107. declared_attr = util.preloaded.orm_decl_api.declared_attr
  108. return isinstance(obj, (declared_attr, util.classproperty))
  109. def _check_declared_props_nocascade(obj, name, cls):
  110. if _is_declarative_props(obj):
  111. if getattr(obj, "_cascading", False):
  112. util.warn(
  113. "@declared_attr.cascading is not supported on the %s "
  114. "attribute on class %s. This attribute invokes for "
  115. "subclasses in any case." % (name, cls)
  116. )
  117. return True
  118. else:
  119. return False
  120. class _MapperConfig(object):
  121. __slots__ = ("cls", "classname", "properties", "declared_attr_reg")
  122. @classmethod
  123. def setup_mapping(cls, registry, cls_, dict_, table, mapper_kw):
  124. manager = attributes.manager_of_class(cls)
  125. if manager and manager.class_ is cls_:
  126. raise exc.InvalidRequestError(
  127. "Class %r already has been " "instrumented declaratively" % cls
  128. )
  129. if cls_.__dict__.get("__abstract__", False):
  130. return
  131. defer_map = _get_immediate_cls_attr(
  132. cls_, "_sa_decl_prepare_nocascade", strict=True
  133. ) or hasattr(cls_, "_sa_decl_prepare")
  134. if defer_map:
  135. cfg_cls = _DeferredMapperConfig
  136. else:
  137. cfg_cls = _ClassScanMapperConfig
  138. return cfg_cls(registry, cls_, dict_, table, mapper_kw)
  139. def __init__(self, registry, cls_):
  140. self.cls = cls_
  141. self.classname = cls_.__name__
  142. self.properties = util.OrderedDict()
  143. self.declared_attr_reg = {}
  144. instrumentation.register_class(
  145. self.cls,
  146. finalize=False,
  147. registry=registry,
  148. declarative_scan=self,
  149. init_method=registry.constructor,
  150. )
  151. def set_cls_attribute(self, attrname, value):
  152. manager = instrumentation.manager_of_class(self.cls)
  153. manager.install_member(attrname, value)
  154. return value
  155. def _early_mapping(self, mapper_kw):
  156. self.map(mapper_kw)
  157. class _ImperativeMapperConfig(_MapperConfig):
  158. __slots__ = ("dict_", "local_table", "inherits")
  159. def __init__(
  160. self,
  161. registry,
  162. cls_,
  163. table,
  164. mapper_kw,
  165. ):
  166. super(_ImperativeMapperConfig, self).__init__(registry, cls_)
  167. self.dict_ = {}
  168. self.local_table = self.set_cls_attribute("__table__", table)
  169. with mapperlib._CONFIGURE_MUTEX:
  170. clsregistry.add_class(
  171. self.classname, self.cls, registry._class_registry
  172. )
  173. self._setup_inheritance(mapper_kw)
  174. self._early_mapping(mapper_kw)
  175. def map(self, mapper_kw=util.EMPTY_DICT):
  176. mapper_cls = mapper
  177. return self.set_cls_attribute(
  178. "__mapper__",
  179. mapper_cls(self.cls, self.local_table, **mapper_kw),
  180. )
  181. def _setup_inheritance(self, mapper_kw):
  182. cls = self.cls
  183. inherits = mapper_kw.get("inherits", None)
  184. if inherits is None:
  185. # since we search for classical mappings now, search for
  186. # multiple mapped bases as well and raise an error.
  187. inherits_search = []
  188. for c in cls.__bases__:
  189. c = _resolve_for_abstract_or_classical(c)
  190. if c is None:
  191. continue
  192. if _declared_mapping_info(
  193. c
  194. ) is not None and not _get_immediate_cls_attr(
  195. c, "_sa_decl_prepare_nocascade", strict=True
  196. ):
  197. inherits_search.append(c)
  198. if inherits_search:
  199. if len(inherits_search) > 1:
  200. raise exc.InvalidRequestError(
  201. "Class %s has multiple mapped bases: %r"
  202. % (cls, inherits_search)
  203. )
  204. inherits = inherits_search[0]
  205. elif isinstance(inherits, mapper):
  206. inherits = inherits.class_
  207. self.inherits = inherits
  208. class _ClassScanMapperConfig(_MapperConfig):
  209. __slots__ = (
  210. "dict_",
  211. "local_table",
  212. "persist_selectable",
  213. "declared_columns",
  214. "column_copies",
  215. "table_args",
  216. "tablename",
  217. "mapper_args",
  218. "mapper_args_fn",
  219. "inherits",
  220. )
  221. def __init__(
  222. self,
  223. registry,
  224. cls_,
  225. dict_,
  226. table,
  227. mapper_kw,
  228. ):
  229. super(_ClassScanMapperConfig, self).__init__(registry, cls_)
  230. self.dict_ = dict(dict_) if dict_ else {}
  231. self.persist_selectable = None
  232. self.declared_columns = set()
  233. self.column_copies = {}
  234. self._setup_declared_events()
  235. self._scan_attributes()
  236. with mapperlib._CONFIGURE_MUTEX:
  237. clsregistry.add_class(
  238. self.classname, self.cls, registry._class_registry
  239. )
  240. self._extract_mappable_attributes()
  241. self._extract_declared_columns()
  242. self._setup_table(table)
  243. self._setup_inheritance(mapper_kw)
  244. self._early_mapping(mapper_kw)
  245. def _setup_declared_events(self):
  246. if _get_immediate_cls_attr(self.cls, "__declare_last__"):
  247. @event.listens_for(mapper, "after_configured")
  248. def after_configured():
  249. self.cls.__declare_last__()
  250. if _get_immediate_cls_attr(self.cls, "__declare_first__"):
  251. @event.listens_for(mapper, "before_configured")
  252. def before_configured():
  253. self.cls.__declare_first__()
  254. def _cls_attr_override_checker(self, cls):
  255. """Produce a function that checks if a class has overridden an
  256. attribute, taking SQLAlchemy-enabled dataclass fields into account.
  257. """
  258. sa_dataclass_metadata_key = _get_immediate_cls_attr(
  259. cls, "__sa_dataclass_metadata_key__", None
  260. )
  261. if sa_dataclass_metadata_key is None:
  262. def attribute_is_overridden(key, obj):
  263. return getattr(cls, key) is not obj
  264. else:
  265. all_datacls_fields = {
  266. f.name: f.metadata[sa_dataclass_metadata_key]
  267. for f in util.dataclass_fields(cls)
  268. if sa_dataclass_metadata_key in f.metadata
  269. }
  270. local_datacls_fields = {
  271. f.name: f.metadata[sa_dataclass_metadata_key]
  272. for f in util.local_dataclass_fields(cls)
  273. if sa_dataclass_metadata_key in f.metadata
  274. }
  275. absent = object()
  276. def attribute_is_overridden(key, obj):
  277. if _is_declarative_props(obj):
  278. obj = obj.fget
  279. # this function likely has some failure modes still if
  280. # someone is doing a deep mixing of the same attribute
  281. # name as plain Python attribute vs. dataclass field.
  282. ret = local_datacls_fields.get(key, absent)
  283. if _is_declarative_props(ret):
  284. ret = ret.fget
  285. if ret is obj:
  286. return False
  287. elif ret is not absent:
  288. return True
  289. all_field = all_datacls_fields.get(key, absent)
  290. ret = getattr(cls, key, obj)
  291. if ret is obj:
  292. return False
  293. # for dataclasses, this could be the
  294. # 'default' of the field. so filter more specifically
  295. # for an already-mapped InstrumentedAttribute
  296. if ret is not absent and isinstance(
  297. ret, InstrumentedAttribute
  298. ):
  299. return True
  300. if all_field is obj:
  301. return False
  302. elif all_field is not absent:
  303. return True
  304. # can't find another attribute
  305. return False
  306. return attribute_is_overridden
  307. def _cls_attr_resolver(self, cls):
  308. """produce a function to iterate the "attributes" of a class,
  309. adjusting for SQLAlchemy fields embedded in dataclass fields.
  310. """
  311. sa_dataclass_metadata_key = _get_immediate_cls_attr(
  312. cls, "__sa_dataclass_metadata_key__", None
  313. )
  314. if sa_dataclass_metadata_key is None:
  315. def local_attributes_for_class():
  316. for name, obj in vars(cls).items():
  317. yield name, obj, False
  318. else:
  319. field_names = set()
  320. def local_attributes_for_class():
  321. for field in util.local_dataclass_fields(cls):
  322. if sa_dataclass_metadata_key in field.metadata:
  323. field_names.add(field.name)
  324. yield field.name, _as_dc_declaredattr(
  325. field.metadata, sa_dataclass_metadata_key
  326. ), True
  327. for name, obj in vars(cls).items():
  328. if name not in field_names:
  329. yield name, obj, False
  330. return local_attributes_for_class
  331. def _scan_attributes(self):
  332. cls = self.cls
  333. dict_ = self.dict_
  334. column_copies = self.column_copies
  335. mapper_args_fn = None
  336. table_args = inherited_table_args = None
  337. tablename = None
  338. attribute_is_overridden = self._cls_attr_override_checker(self.cls)
  339. for base in cls.__mro__:
  340. class_mapped = (
  341. base is not cls
  342. and _declared_mapping_info(base) is not None
  343. and not _get_immediate_cls_attr(
  344. base, "_sa_decl_prepare_nocascade", strict=True
  345. )
  346. )
  347. local_attributes_for_class = self._cls_attr_resolver(base)
  348. if not class_mapped and base is not cls:
  349. self._produce_column_copies(
  350. local_attributes_for_class, attribute_is_overridden
  351. )
  352. for name, obj, is_dataclass in local_attributes_for_class():
  353. if name == "__mapper_args__":
  354. check_decl = _check_declared_props_nocascade(
  355. obj, name, cls
  356. )
  357. if not mapper_args_fn and (not class_mapped or check_decl):
  358. # don't even invoke __mapper_args__ until
  359. # after we've determined everything about the
  360. # mapped table.
  361. # make a copy of it so a class-level dictionary
  362. # is not overwritten when we update column-based
  363. # arguments.
  364. def mapper_args_fn():
  365. return dict(cls.__mapper_args__)
  366. elif name == "__tablename__":
  367. check_decl = _check_declared_props_nocascade(
  368. obj, name, cls
  369. )
  370. if not tablename and (not class_mapped or check_decl):
  371. tablename = cls.__tablename__
  372. elif name == "__table_args__":
  373. check_decl = _check_declared_props_nocascade(
  374. obj, name, cls
  375. )
  376. if not table_args and (not class_mapped or check_decl):
  377. table_args = cls.__table_args__
  378. if not isinstance(
  379. table_args, (tuple, dict, type(None))
  380. ):
  381. raise exc.ArgumentError(
  382. "__table_args__ value must be a tuple, "
  383. "dict, or None"
  384. )
  385. if base is not cls:
  386. inherited_table_args = True
  387. elif class_mapped:
  388. if _is_declarative_props(obj):
  389. util.warn(
  390. "Regular (i.e. not __special__) "
  391. "attribute '%s.%s' uses @declared_attr, "
  392. "but owning class %s is mapped - "
  393. "not applying to subclass %s."
  394. % (base.__name__, name, base, cls)
  395. )
  396. continue
  397. elif base is not cls:
  398. # we're a mixin, abstract base, or something that is
  399. # acting like that for now.
  400. if isinstance(obj, Column):
  401. # already copied columns to the mapped class.
  402. continue
  403. elif isinstance(obj, MapperProperty):
  404. raise exc.InvalidRequestError(
  405. "Mapper properties (i.e. deferred,"
  406. "column_property(), relationship(), etc.) must "
  407. "be declared as @declared_attr callables "
  408. "on declarative mixin classes. For dataclass "
  409. "field() objects, use a lambda:"
  410. )
  411. elif _is_declarative_props(obj):
  412. if obj._cascading:
  413. if name in dict_:
  414. # unfortunately, while we can use the user-
  415. # defined attribute here to allow a clean
  416. # override, if there's another
  417. # subclass below then it still tries to use
  418. # this. not sure if there is enough
  419. # information here to add this as a feature
  420. # later on.
  421. util.warn(
  422. "Attribute '%s' on class %s cannot be "
  423. "processed due to "
  424. "@declared_attr.cascading; "
  425. "skipping" % (name, cls)
  426. )
  427. dict_[name] = column_copies[
  428. obj
  429. ] = ret = obj.__get__(obj, cls)
  430. setattr(cls, name, ret)
  431. else:
  432. if is_dataclass:
  433. # access attribute using normal class access
  434. # first, to see if it's been mapped on a
  435. # superclass. note if the dataclasses.field()
  436. # has "default", this value can be anything.
  437. ret = getattr(cls, name, None)
  438. # so, if it's anything that's not ORM
  439. # mapped, assume we should invoke the
  440. # declared_attr
  441. if not isinstance(ret, InspectionAttr):
  442. ret = obj.fget()
  443. else:
  444. # access attribute using normal class access.
  445. # if the declared attr already took place
  446. # on a superclass that is mapped, then
  447. # this is no longer a declared_attr, it will
  448. # be the InstrumentedAttribute
  449. ret = getattr(cls, name)
  450. # correct for proxies created from hybrid_property
  451. # or similar. note there is no known case that
  452. # produces nested proxies, so we are only
  453. # looking one level deep right now.
  454. if (
  455. isinstance(ret, InspectionAttr)
  456. and ret._is_internal_proxy
  457. and not isinstance(
  458. ret.original_property, MapperProperty
  459. )
  460. ):
  461. ret = ret.descriptor
  462. dict_[name] = column_copies[obj] = ret
  463. if (
  464. isinstance(ret, (Column, MapperProperty))
  465. and ret.doc is None
  466. ):
  467. ret.doc = obj.__doc__
  468. # here, the attribute is some other kind of property that
  469. # we assume is not part of the declarative mapping.
  470. # however, check for some more common mistakes
  471. else:
  472. self._warn_for_decl_attributes(base, name, obj)
  473. elif is_dataclass and (
  474. name not in dict_ or dict_[name] is not obj
  475. ):
  476. # here, we are definitely looking at the target class
  477. # and not a superclass. this is currently a
  478. # dataclass-only path. if the name is only
  479. # a dataclass field and isn't in local cls.__dict__,
  480. # put the object there.
  481. # assert that the dataclass-enabled resolver agrees
  482. # with what we are seeing
  483. assert not attribute_is_overridden(name, obj)
  484. if _is_declarative_props(obj):
  485. obj = obj.fget()
  486. dict_[name] = obj
  487. if inherited_table_args and not tablename:
  488. table_args = None
  489. self.table_args = table_args
  490. self.tablename = tablename
  491. self.mapper_args_fn = mapper_args_fn
  492. def _warn_for_decl_attributes(self, cls, key, c):
  493. if isinstance(c, expression.ColumnClause):
  494. util.warn(
  495. "Attribute '%s' on class %s appears to be a non-schema "
  496. "'sqlalchemy.sql.column()' "
  497. "object; this won't be part of the declarative mapping"
  498. % (key, cls)
  499. )
  500. def _produce_column_copies(
  501. self, attributes_for_class, attribute_is_overridden
  502. ):
  503. cls = self.cls
  504. dict_ = self.dict_
  505. column_copies = self.column_copies
  506. # copy mixin columns to the mapped class
  507. for name, obj, is_dataclass in attributes_for_class():
  508. if isinstance(obj, Column):
  509. if attribute_is_overridden(name, obj):
  510. # if column has been overridden
  511. # (like by the InstrumentedAttribute of the
  512. # superclass), skip
  513. continue
  514. elif obj.foreign_keys:
  515. raise exc.InvalidRequestError(
  516. "Columns with foreign keys to other columns "
  517. "must be declared as @declared_attr callables "
  518. "on declarative mixin classes. For dataclass "
  519. "field() objects, use a lambda:."
  520. )
  521. elif name not in dict_ and not (
  522. "__table__" in dict_
  523. and (obj.name or name) in dict_["__table__"].c
  524. ):
  525. column_copies[obj] = copy_ = obj._copy()
  526. copy_._creation_order = obj._creation_order
  527. setattr(cls, name, copy_)
  528. dict_[name] = copy_
  529. def _extract_mappable_attributes(self):
  530. cls = self.cls
  531. dict_ = self.dict_
  532. our_stuff = self.properties
  533. late_mapped = _get_immediate_cls_attr(
  534. cls, "_sa_decl_prepare_nocascade", strict=True
  535. )
  536. for k in list(dict_):
  537. if k in ("__table__", "__tablename__", "__mapper_args__"):
  538. continue
  539. value = dict_[k]
  540. if _is_declarative_props(value):
  541. if value._cascading:
  542. util.warn(
  543. "Use of @declared_attr.cascading only applies to "
  544. "Declarative 'mixin' and 'abstract' classes. "
  545. "Currently, this flag is ignored on mapped class "
  546. "%s" % self.cls
  547. )
  548. value = getattr(cls, k)
  549. elif (
  550. isinstance(value, QueryableAttribute)
  551. and value.class_ is not cls
  552. and value.key != k
  553. ):
  554. # detect a QueryableAttribute that's already mapped being
  555. # assigned elsewhere in userland, turn into a synonym()
  556. value = SynonymProperty(value.key)
  557. setattr(cls, k, value)
  558. if (
  559. isinstance(value, tuple)
  560. and len(value) == 1
  561. and isinstance(value[0], (Column, MapperProperty))
  562. ):
  563. util.warn(
  564. "Ignoring declarative-like tuple value of attribute "
  565. "'%s': possibly a copy-and-paste error with a comma "
  566. "accidentally placed at the end of the line?" % k
  567. )
  568. continue
  569. elif not isinstance(value, (Column, MapperProperty)):
  570. # using @declared_attr for some object that
  571. # isn't Column/MapperProperty; remove from the dict_
  572. # and place the evaluated value onto the class.
  573. if not k.startswith("__"):
  574. dict_.pop(k)
  575. self._warn_for_decl_attributes(cls, k, value)
  576. if not late_mapped:
  577. setattr(cls, k, value)
  578. continue
  579. # we expect to see the name 'metadata' in some valid cases;
  580. # however at this point we see it's assigned to something trying
  581. # to be mapped, so raise for that.
  582. elif k == "metadata":
  583. raise exc.InvalidRequestError(
  584. "Attribute name 'metadata' is reserved "
  585. "for the MetaData instance when using a "
  586. "declarative base class."
  587. )
  588. our_stuff[k] = value
  589. def _extract_declared_columns(self):
  590. our_stuff = self.properties
  591. # set up attributes in the order they were created
  592. util.sort_dictionary(
  593. our_stuff, key=lambda key: our_stuff[key]._creation_order
  594. )
  595. # extract columns from the class dict
  596. declared_columns = self.declared_columns
  597. name_to_prop_key = collections.defaultdict(set)
  598. for key, c in list(our_stuff.items()):
  599. if isinstance(c, (ColumnProperty, CompositeProperty)):
  600. for col in c.columns:
  601. if isinstance(col, Column) and col.table is None:
  602. _undefer_column_name(key, col)
  603. if not isinstance(c, CompositeProperty):
  604. name_to_prop_key[col.name].add(key)
  605. declared_columns.add(col)
  606. elif isinstance(c, Column):
  607. _undefer_column_name(key, c)
  608. name_to_prop_key[c.name].add(key)
  609. declared_columns.add(c)
  610. # if the column is the same name as the key,
  611. # remove it from the explicit properties dict.
  612. # the normal rules for assigning column-based properties
  613. # will take over, including precedence of columns
  614. # in multi-column ColumnProperties.
  615. if key == c.key:
  616. del our_stuff[key]
  617. for name, keys in name_to_prop_key.items():
  618. if len(keys) > 1:
  619. util.warn(
  620. "On class %r, Column object %r named "
  621. "directly multiple times, "
  622. "only one will be used: %s. "
  623. "Consider using orm.synonym instead"
  624. % (self.classname, name, (", ".join(sorted(keys))))
  625. )
  626. def _setup_table(self, table=None):
  627. cls = self.cls
  628. tablename = self.tablename
  629. table_args = self.table_args
  630. dict_ = self.dict_
  631. declared_columns = self.declared_columns
  632. manager = attributes.manager_of_class(cls)
  633. declared_columns = self.declared_columns = sorted(
  634. declared_columns, key=lambda c: c._creation_order
  635. )
  636. if "__table__" not in dict_ and table is None:
  637. if hasattr(cls, "__table_cls__"):
  638. table_cls = util.unbound_method_to_callable(cls.__table_cls__)
  639. else:
  640. table_cls = Table
  641. if tablename is not None:
  642. args, table_kw = (), {}
  643. if table_args:
  644. if isinstance(table_args, dict):
  645. table_kw = table_args
  646. elif isinstance(table_args, tuple):
  647. if isinstance(table_args[-1], dict):
  648. args, table_kw = table_args[0:-1], table_args[-1]
  649. else:
  650. args = table_args
  651. autoload_with = dict_.get("__autoload_with__")
  652. if autoload_with:
  653. table_kw["autoload_with"] = autoload_with
  654. autoload = dict_.get("__autoload__")
  655. if autoload:
  656. table_kw["autoload"] = True
  657. table = self.set_cls_attribute(
  658. "__table__",
  659. table_cls(
  660. tablename,
  661. self._metadata_for_cls(manager),
  662. *(tuple(declared_columns) + tuple(args)),
  663. **table_kw
  664. ),
  665. )
  666. else:
  667. if table is None:
  668. table = cls.__table__
  669. if declared_columns:
  670. for c in declared_columns:
  671. if not table.c.contains_column(c):
  672. raise exc.ArgumentError(
  673. "Can't add additional column %r when "
  674. "specifying __table__" % c.key
  675. )
  676. self.local_table = table
  677. def _metadata_for_cls(self, manager):
  678. if hasattr(self.cls, "metadata"):
  679. return self.cls.metadata
  680. else:
  681. return manager.registry.metadata
  682. def _setup_inheritance(self, mapper_kw):
  683. table = self.local_table
  684. cls = self.cls
  685. table_args = self.table_args
  686. declared_columns = self.declared_columns
  687. inherits = mapper_kw.get("inherits", None)
  688. if inherits is None:
  689. # since we search for classical mappings now, search for
  690. # multiple mapped bases as well and raise an error.
  691. inherits_search = []
  692. for c in cls.__bases__:
  693. c = _resolve_for_abstract_or_classical(c)
  694. if c is None:
  695. continue
  696. if _declared_mapping_info(
  697. c
  698. ) is not None and not _get_immediate_cls_attr(
  699. c, "_sa_decl_prepare_nocascade", strict=True
  700. ):
  701. if c not in inherits_search:
  702. inherits_search.append(c)
  703. if inherits_search:
  704. if len(inherits_search) > 1:
  705. raise exc.InvalidRequestError(
  706. "Class %s has multiple mapped bases: %r"
  707. % (cls, inherits_search)
  708. )
  709. inherits = inherits_search[0]
  710. elif isinstance(inherits, mapper):
  711. inherits = inherits.class_
  712. self.inherits = inherits
  713. if (
  714. table is None
  715. and self.inherits is None
  716. and not _get_immediate_cls_attr(cls, "__no_table__")
  717. ):
  718. raise exc.InvalidRequestError(
  719. "Class %r does not have a __table__ or __tablename__ "
  720. "specified and does not inherit from an existing "
  721. "table-mapped class." % cls
  722. )
  723. elif self.inherits:
  724. inherited_mapper = _declared_mapping_info(self.inherits)
  725. inherited_table = inherited_mapper.local_table
  726. inherited_persist_selectable = inherited_mapper.persist_selectable
  727. if table is None:
  728. # single table inheritance.
  729. # ensure no table args
  730. if table_args:
  731. raise exc.ArgumentError(
  732. "Can't place __table_args__ on an inherited class "
  733. "with no table."
  734. )
  735. # add any columns declared here to the inherited table.
  736. for c in declared_columns:
  737. if c.name in inherited_table.c:
  738. if inherited_table.c[c.name] is c:
  739. continue
  740. raise exc.ArgumentError(
  741. "Column '%s' on class %s conflicts with "
  742. "existing column '%s'"
  743. % (c, cls, inherited_table.c[c.name])
  744. )
  745. if c.primary_key:
  746. raise exc.ArgumentError(
  747. "Can't place primary key columns on an inherited "
  748. "class with no table."
  749. )
  750. inherited_table.append_column(c)
  751. if (
  752. inherited_persist_selectable is not None
  753. and inherited_persist_selectable is not inherited_table
  754. ):
  755. inherited_persist_selectable._refresh_for_new_column(c)
  756. def _prepare_mapper_arguments(self, mapper_kw):
  757. properties = self.properties
  758. if self.mapper_args_fn:
  759. mapper_args = self.mapper_args_fn()
  760. else:
  761. mapper_args = {}
  762. if mapper_kw:
  763. mapper_args.update(mapper_kw)
  764. if "properties" in mapper_args:
  765. properties = dict(properties)
  766. properties.update(mapper_args["properties"])
  767. # make sure that column copies are used rather
  768. # than the original columns from any mixins
  769. for k in ("version_id_col", "polymorphic_on"):
  770. if k in mapper_args:
  771. v = mapper_args[k]
  772. mapper_args[k] = self.column_copies.get(v, v)
  773. if "inherits" in mapper_args:
  774. inherits_arg = mapper_args["inherits"]
  775. if isinstance(inherits_arg, mapper):
  776. inherits_arg = inherits_arg.class_
  777. if inherits_arg is not self.inherits:
  778. raise exc.InvalidRequestError(
  779. "mapper inherits argument given for non-inheriting "
  780. "class %s" % (mapper_args["inherits"])
  781. )
  782. if self.inherits:
  783. mapper_args["inherits"] = self.inherits
  784. if self.inherits and not mapper_args.get("concrete", False):
  785. # single or joined inheritance
  786. # exclude any cols on the inherited table which are
  787. # not mapped on the parent class, to avoid
  788. # mapping columns specific to sibling/nephew classes
  789. inherited_mapper = _declared_mapping_info(self.inherits)
  790. inherited_table = inherited_mapper.local_table
  791. if "exclude_properties" not in mapper_args:
  792. mapper_args["exclude_properties"] = exclude_properties = set(
  793. [
  794. c.key
  795. for c in inherited_table.c
  796. if c not in inherited_mapper._columntoproperty
  797. ]
  798. ).union(inherited_mapper.exclude_properties or ())
  799. exclude_properties.difference_update(
  800. [c.key for c in self.declared_columns]
  801. )
  802. # look through columns in the current mapper that
  803. # are keyed to a propname different than the colname
  804. # (if names were the same, we'd have popped it out above,
  805. # in which case the mapper makes this combination).
  806. # See if the superclass has a similar column property.
  807. # If so, join them together.
  808. for k, col in list(properties.items()):
  809. if not isinstance(col, expression.ColumnElement):
  810. continue
  811. if k in inherited_mapper._props:
  812. p = inherited_mapper._props[k]
  813. if isinstance(p, ColumnProperty):
  814. # note here we place the subclass column
  815. # first. See [ticket:1892] for background.
  816. properties[k] = [col] + p.columns
  817. result_mapper_args = mapper_args.copy()
  818. result_mapper_args["properties"] = properties
  819. self.mapper_args = result_mapper_args
  820. def map(self, mapper_kw=util.EMPTY_DICT):
  821. self._prepare_mapper_arguments(mapper_kw)
  822. if hasattr(self.cls, "__mapper_cls__"):
  823. mapper_cls = util.unbound_method_to_callable(
  824. self.cls.__mapper_cls__
  825. )
  826. else:
  827. mapper_cls = mapper
  828. return self.set_cls_attribute(
  829. "__mapper__",
  830. mapper_cls(self.cls, self.local_table, **self.mapper_args),
  831. )
  832. @util.preload_module("sqlalchemy.orm.decl_api")
  833. def _as_dc_declaredattr(field_metadata, sa_dataclass_metadata_key):
  834. # wrap lambdas inside dataclass fields inside an ad-hoc declared_attr.
  835. # we can't write it because field.metadata is immutable :( so we have
  836. # to go through extra trouble to compare these
  837. decl_api = util.preloaded.orm_decl_api
  838. obj = field_metadata[sa_dataclass_metadata_key]
  839. if callable(obj) and not isinstance(obj, decl_api.declared_attr):
  840. return decl_api.declared_attr(obj)
  841. else:
  842. return obj
  843. class _DeferredMapperConfig(_ClassScanMapperConfig):
  844. _configs = util.OrderedDict()
  845. def _early_mapping(self, mapper_kw):
  846. pass
  847. @property
  848. def cls(self):
  849. return self._cls()
  850. @cls.setter
  851. def cls(self, class_):
  852. self._cls = weakref.ref(class_, self._remove_config_cls)
  853. self._configs[self._cls] = self
  854. @classmethod
  855. def _remove_config_cls(cls, ref):
  856. cls._configs.pop(ref, None)
  857. @classmethod
  858. def has_cls(cls, class_):
  859. # 2.6 fails on weakref if class_ is an old style class
  860. return isinstance(class_, type) and weakref.ref(class_) in cls._configs
  861. @classmethod
  862. def raise_unmapped_for_cls(cls, class_):
  863. if hasattr(class_, "_sa_raise_deferred_config"):
  864. class_._sa_raise_deferred_config()
  865. raise orm_exc.UnmappedClassError(
  866. class_,
  867. msg="Class %s has a deferred mapping on it. It is not yet "
  868. "usable as a mapped class." % orm_exc._safe_cls_name(class_),
  869. )
  870. @classmethod
  871. def config_for_cls(cls, class_):
  872. return cls._configs[weakref.ref(class_)]
  873. @classmethod
  874. def classes_for_base(cls, base_cls, sort=True):
  875. classes_for_base = [
  876. m
  877. for m, cls_ in [(m, m.cls) for m in cls._configs.values()]
  878. if cls_ is not None and issubclass(cls_, base_cls)
  879. ]
  880. if not sort:
  881. return classes_for_base
  882. all_m_by_cls = dict((m.cls, m) for m in classes_for_base)
  883. tuples = []
  884. for m_cls in all_m_by_cls:
  885. tuples.extend(
  886. (all_m_by_cls[base_cls], all_m_by_cls[m_cls])
  887. for base_cls in m_cls.__bases__
  888. if base_cls in all_m_by_cls
  889. )
  890. return list(topological.sort(tuples, classes_for_base))
  891. def map(self, mapper_kw=util.EMPTY_DICT):
  892. self._configs.pop(self._cls, None)
  893. return super(_DeferredMapperConfig, self).map(mapper_kw)
  894. def _add_attribute(cls, key, value):
  895. """add an attribute to an existing declarative class.
  896. This runs through the logic to determine MapperProperty,
  897. adds it to the Mapper, adds a column to the mapped Table, etc.
  898. """
  899. if "__mapper__" in cls.__dict__:
  900. if isinstance(value, Column):
  901. _undefer_column_name(key, value)
  902. cls.__table__.append_column(value, replace_existing=True)
  903. cls.__mapper__.add_property(key, value)
  904. elif isinstance(value, ColumnProperty):
  905. for col in value.columns:
  906. if isinstance(col, Column) and col.table is None:
  907. _undefer_column_name(key, col)
  908. cls.__table__.append_column(col, replace_existing=True)
  909. cls.__mapper__.add_property(key, value)
  910. elif isinstance(value, MapperProperty):
  911. cls.__mapper__.add_property(key, value)
  912. elif isinstance(value, QueryableAttribute) and value.key != key:
  913. # detect a QueryableAttribute that's already mapped being
  914. # assigned elsewhere in userland, turn into a synonym()
  915. value = SynonymProperty(value.key)
  916. cls.__mapper__.add_property(key, value)
  917. else:
  918. type.__setattr__(cls, key, value)
  919. cls.__mapper__._expire_memoizations()
  920. else:
  921. type.__setattr__(cls, key, value)
  922. def _del_attribute(cls, key):
  923. if (
  924. "__mapper__" in cls.__dict__
  925. and key in cls.__dict__
  926. and not cls.__mapper__._dispose_called
  927. ):
  928. value = cls.__dict__[key]
  929. if isinstance(
  930. value, (Column, ColumnProperty, MapperProperty, QueryableAttribute)
  931. ):
  932. raise NotImplementedError(
  933. "Can't un-map individual mapped attributes on a mapped class."
  934. )
  935. else:
  936. type.__delattr__(cls, key)
  937. cls.__mapper__._expire_memoizations()
  938. else:
  939. type.__delattr__(cls, key)
  940. def _declarative_constructor(self, **kwargs):
  941. """A simple constructor that allows initialization from kwargs.
  942. Sets attributes on the constructed instance using the names and
  943. values in ``kwargs``.
  944. Only keys that are present as
  945. attributes of the instance's class are allowed. These could be,
  946. for example, any mapped columns or relationships.
  947. """
  948. cls_ = type(self)
  949. for k in kwargs:
  950. if not hasattr(cls_, k):
  951. raise TypeError(
  952. "%r is an invalid keyword argument for %s" % (k, cls_.__name__)
  953. )
  954. setattr(self, k, kwargs[k])
  955. _declarative_constructor.__name__ = "__init__"
  956. def _undefer_column_name(key, column):
  957. if column.key is None:
  958. column.key = key
  959. if column.name is None:
  960. column.name = key