1# orm/decl_base.py
2# Copyright (C) 2005-2026 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: https://www.opensource.org/licenses/mit-license.php
7
8"""Internal implementation for declarative."""
9
10from __future__ import annotations
11
12import collections
13import dataclasses
14import itertools
15import re
16from typing import Any
17from typing import Callable
18from typing import cast
19from typing import Dict
20from typing import get_args
21from typing import Iterable
22from typing import List
23from typing import Mapping
24from typing import NamedTuple
25from typing import NoReturn
26from typing import Optional
27from typing import Protocol
28from typing import Sequence
29from typing import Tuple
30from typing import Type
31from typing import TYPE_CHECKING
32from typing import TypeVar
33from typing import Union
34import weakref
35
36from . import attributes
37from . import clsregistry
38from . import exc as orm_exc
39from . import instrumentation
40from . import mapperlib
41from ._typing import _O
42from ._typing import attr_is_internal_proxy
43from .attributes import InstrumentedAttribute
44from .attributes import QueryableAttribute
45from .base import _is_mapped_class
46from .base import InspectionAttr
47from .descriptor_props import CompositeProperty
48from .descriptor_props import SynonymProperty
49from .interfaces import _AttributeOptions
50from .interfaces import _DataclassArguments
51from .interfaces import _DCAttributeOptions
52from .interfaces import _IntrospectsAnnotations
53from .interfaces import _MappedAttribute
54from .interfaces import _MapsColumns
55from .interfaces import MapperProperty
56from .mapper import Mapper
57from .properties import ColumnProperty
58from .properties import MappedColumn
59from .util import _extract_mapped_subtype
60from .util import _is_mapped_annotation
61from .util import _metadata_for_cls as _metadata_for_cls_fn
62from .util import class_mapper
63from .util import de_stringify_annotation
64from .. import exc
65from .. import util
66from ..sql import expression
67from ..sql._annotated_cols import TypedColumns
68from ..sql.base import _NoArg
69from ..sql.schema import Column
70from ..sql.schema import MetaData
71from ..sql.schema import Table
72from ..util import topological
73from ..util.typing import _AnnotationScanType
74from ..util.typing import is_fwd_ref
75from ..util.typing import is_literal
76
77if TYPE_CHECKING:
78 from ._typing import _ClassDict
79 from ._typing import _RegistryType
80 from .base import Mapped
81 from .decl_api import declared_attr
82 from .instrumentation import ClassManager
83 from ..sql.elements import NamedColumn
84 from ..sql.selectable import FromClause
85
86_T = TypeVar("_T", bound=Any)
87
88_MapperKwArgs = Mapping[str, Any]
89_TableArgsType = Union[Tuple[Any, ...], Dict[str, Any]]
90
91
92class MappedClassProtocol(Protocol[_O]):
93 """A protocol representing a SQLAlchemy mapped class.
94
95 The protocol is generic on the type of class, use
96 ``MappedClassProtocol[Any]`` to allow any mapped class.
97 """
98
99 __name__: str
100 __mapper__: Mapper[_O]
101 __table__: FromClause
102
103 def __call__(self, **kw: Any) -> _O: ...
104
105
106class _DeclMappedClassProtocol(MappedClassProtocol[_O], Protocol):
107 "Internal more detailed version of ``MappedClassProtocol``."
108
109 metadata: MetaData
110 __tablename__: str
111 __mapper_args__: _MapperKwArgs
112 __table_args__: Optional[_TableArgsType]
113
114 _sa_apply_dc_transforms: Optional[_DataclassArguments]
115
116 def __declare_first__(self) -> None: ...
117
118 def __declare_last__(self) -> None: ...
119
120
121def _declared_mapping_info(
122 cls: Type[Any],
123) -> Optional[Union[_DeferredDeclarativeConfig, Mapper[Any]]]:
124 # deferred mapping
125 if _DeferredDeclarativeConfig.has_cls(cls):
126 return _DeferredDeclarativeConfig.config_for_cls(cls)
127 # regular mapping
128 elif _is_mapped_class(cls):
129 return class_mapper(cls, configure=False)
130 else:
131 return None
132
133
134def _is_supercls_for_inherits(cls: Type[Any]) -> bool:
135 """return True if this class will be used as a superclass to set in
136 'inherits'.
137
138 This includes deferred mapper configs that aren't mapped yet, however does
139 not include classes with _sa_decl_prepare_nocascade (e.g.
140 ``AbstractConcreteBase``); these concrete-only classes are not set up as
141 "inherits" until after mappers are configured using
142 mapper._set_concrete_base()
143
144 """
145 if _DeferredDeclarativeConfig.has_cls(cls):
146 return not _get_immediate_cls_attr(
147 cls, "_sa_decl_prepare_nocascade", strict=True
148 )
149 # regular mapping
150 elif _is_mapped_class(cls):
151 return True
152 else:
153 return False
154
155
156def _resolve_for_abstract_or_classical(cls: Type[Any]) -> Optional[Type[Any]]:
157 if cls is object:
158 return None
159
160 sup: Optional[Type[Any]]
161
162 if cls.__dict__.get("__abstract__", False):
163 for base_ in cls.__bases__:
164 sup = _resolve_for_abstract_or_classical(base_)
165 if sup is not None:
166 return sup
167 else:
168 return None
169 else:
170 clsmanager = _dive_for_cls_manager(cls)
171
172 if clsmanager:
173 return clsmanager.class_
174 else:
175 return cls
176
177
178def _get_immediate_cls_attr(
179 cls: Type[Any], attrname: str, strict: bool = False
180) -> Optional[Any]:
181 """return an attribute of the class that is either present directly
182 on the class, e.g. not on a superclass, or is from a superclass but
183 this superclass is a non-mapped mixin, that is, not a descendant of
184 the declarative base and is also not classically mapped.
185
186 This is used to detect attributes that indicate something about
187 a mapped class independently from any mapped classes that it may
188 inherit from.
189
190 """
191
192 # the rules are different for this name than others,
193 # make sure we've moved it out. transitional
194 assert attrname != "__abstract__"
195
196 if not issubclass(cls, object):
197 return None
198
199 if attrname in cls.__dict__:
200 return getattr(cls, attrname)
201
202 for base in cls.__mro__[1:]:
203 _is_classical_inherits = _dive_for_cls_manager(base) is not None
204
205 if attrname in base.__dict__ and (
206 base is cls
207 or (
208 (base in cls.__bases__ if strict else True)
209 and not _is_classical_inherits
210 )
211 ):
212 return getattr(base, attrname)
213 else:
214 return None
215
216
217def _dive_for_cls_manager(cls: Type[_O]) -> Optional[ClassManager[_O]]:
218 # because the class manager registration is pluggable,
219 # we need to do the search for every class in the hierarchy,
220 # rather than just a simple "cls._sa_class_manager"
221
222 for base in cls.__mro__:
223 manager: Optional[ClassManager[_O]] = attributes.opt_manager_of_class(
224 base
225 )
226 if manager:
227 return manager
228 return None
229
230
231@util.preload_module("sqlalchemy.orm.decl_api")
232def _is_declarative_props(obj: Any) -> bool:
233 _declared_attr_common = util.preloaded.orm_decl_api._declared_attr_common
234
235 return isinstance(obj, (_declared_attr_common, util.classproperty))
236
237
238def _check_declared_props_nocascade(
239 obj: Any, name: str, cls: Type[_O]
240) -> bool:
241 if _is_declarative_props(obj):
242 if getattr(obj, "_cascading", False):
243 util.warn(
244 "@declared_attr.cascading is not supported on the %s "
245 "attribute on class %s. This attribute invokes for "
246 "subclasses in any case." % (name, cls)
247 )
248 return True
249 else:
250 return False
251
252
253class _ORMClassConfigurator:
254 """Object that configures a class that's potentially going to be
255 mapped, and/or turned into an ORM dataclass.
256
257 This is the base class for all the configurator objects.
258
259 """
260
261 __slots__ = ("cls", "classname", "__weakref__")
262
263 cls: Type[Any]
264 classname: str
265
266 def __init__(self, cls_: Type[Any]):
267 self.cls = util.assert_arg_type(cls_, type, "cls_")
268 self.classname = cls_.__name__
269
270 @classmethod
271 def _as_declarative(
272 cls, registry: _RegistryType, cls_: Type[Any], dict_: _ClassDict
273 ) -> Optional[_MapperConfig]:
274 manager = attributes.opt_manager_of_class(cls_)
275 if manager and manager.class_ is cls_:
276 raise exc.InvalidRequestError(
277 f"Class {cls_!r} already has been instrumented declaratively"
278 )
279
280 # allow subclassing an orm class with typed columns without
281 # generating an orm class
282 if cls_.__dict__.get("__abstract__", False) or issubclass(
283 cls_, TypedColumns
284 ):
285 return None
286
287 defer_map = _get_immediate_cls_attr(
288 cls_, "_sa_decl_prepare_nocascade", strict=True
289 ) or hasattr(cls_, "_sa_decl_prepare")
290
291 if defer_map:
292 return _DeferredDeclarativeConfig(registry, cls_, dict_)
293 else:
294 return _DeclarativeMapperConfig(registry, cls_, dict_)
295
296 @classmethod
297 def _as_unmapped_dataclass(
298 cls, cls_: Type[Any], dict_: _ClassDict
299 ) -> _UnmappedDataclassConfig:
300 return _UnmappedDataclassConfig(cls_, dict_)
301
302 @classmethod
303 def _mapper(
304 cls,
305 registry: _RegistryType,
306 cls_: Type[_O],
307 table: Optional[FromClause],
308 mapper_kw: _MapperKwArgs,
309 ) -> Mapper[_O]:
310 _ImperativeMapperConfig(registry, cls_, table, mapper_kw)
311 return cast("MappedClassProtocol[_O]", cls_).__mapper__
312
313
314class _MapperConfig(_ORMClassConfigurator):
315 """Configurator that configures a class that's potentially going to be
316 mapped, and optionally turned into a dataclass as well."""
317
318 __slots__ = (
319 "properties",
320 "declared_attr_reg",
321 )
322
323 properties: util.OrderedDict[
324 str,
325 Union[
326 Sequence[NamedColumn[Any]], NamedColumn[Any], MapperProperty[Any]
327 ],
328 ]
329 declared_attr_reg: Dict[declared_attr[Any], Any]
330
331 def __init__(
332 self,
333 registry: _RegistryType,
334 cls_: Type[Any],
335 ):
336 super().__init__(cls_)
337 self.properties = util.OrderedDict()
338 self.declared_attr_reg = {}
339
340 instrumentation.register_class(
341 self.cls,
342 finalize=False,
343 registry=registry,
344 declarative_scan=self,
345 init_method=registry.constructor,
346 )
347
348 def set_cls_attribute(self, attrname: str, value: _T) -> _T:
349 manager = instrumentation.manager_of_class(self.cls)
350 manager.install_member(attrname, value)
351 return value
352
353 def map(self, mapper_kw: _MapperKwArgs) -> Mapper[Any]:
354 raise NotImplementedError()
355
356 def _early_mapping(self, mapper_kw: _MapperKwArgs) -> None:
357 self.map(mapper_kw)
358
359
360class _ImperativeMapperConfig(_MapperConfig):
361 """Configurator that configures a class for an imperative mapping."""
362
363 __slots__ = ("local_table", "inherits")
364
365 def __init__(
366 self,
367 registry: _RegistryType,
368 cls_: Type[_O],
369 table: Optional[FromClause],
370 mapper_kw: _MapperKwArgs,
371 ):
372 super().__init__(registry, cls_)
373
374 self.local_table = self.set_cls_attribute("__table__", table)
375
376 with mapperlib._CONFIGURE_MUTEX:
377 clsregistry._add_class(
378 self.classname, self.cls, registry._class_registry
379 )
380
381 self._setup_inheritance(mapper_kw)
382
383 self._early_mapping(mapper_kw)
384
385 def map(self, mapper_kw: _MapperKwArgs = util.EMPTY_DICT) -> Mapper[Any]:
386 mapper_cls = Mapper
387
388 return self.set_cls_attribute(
389 "__mapper__",
390 mapper_cls(self.cls, self.local_table, **mapper_kw),
391 )
392
393 def _setup_inheritance(self, mapper_kw: _MapperKwArgs) -> None:
394 cls = self.cls
395
396 inherits = None
397 inherits_search = []
398
399 # since we search for classical mappings now, search for
400 # multiple mapped bases as well and raise an error.
401 for base_ in cls.__bases__:
402 c = _resolve_for_abstract_or_classical(base_)
403 if c is None:
404 continue
405
406 if _is_supercls_for_inherits(c) and c not in inherits_search:
407 inherits_search.append(c)
408
409 if inherits_search:
410 if len(inherits_search) > 1:
411 raise exc.InvalidRequestError(
412 "Class %s has multiple mapped bases: %r"
413 % (cls, inherits_search)
414 )
415 inherits = inherits_search[0]
416
417 self.inherits = inherits
418
419
420class _CollectedAnnotation(NamedTuple):
421 raw_annotation: _AnnotationScanType
422 mapped_container: Optional[Type[Mapped[Any]]]
423 extracted_mapped_annotation: Union[_AnnotationScanType, str]
424 is_dataclass: bool
425 attr_value: Any
426 originating_module: str
427 originating_class: Type[Any]
428
429
430class _ClassScanAbstractConfig(_ORMClassConfigurator):
431 """Abstract base for a configurator that configures a class for a
432 declarative mapping, or an unmapped ORM dataclass.
433
434 Defines scanning of pep-484 annotations as well as ORM dataclass
435 applicators
436
437 """
438
439 __slots__ = ()
440
441 clsdict_view: _ClassDict
442 collected_annotations: Dict[str, _CollectedAnnotation]
443 collected_attributes: Dict[str, Any]
444
445 is_dataclass_prior_to_mapping: bool
446 allow_unmapped_annotations: bool
447
448 dataclass_setup_arguments: Optional[_DataclassArguments]
449 """if the class has SQLAlchemy native dataclass parameters, where
450 we will turn the class into a dataclass within the declarative mapping
451 process.
452
453 """
454
455 allow_dataclass_fields: bool
456 """if true, look for dataclass-processed Field objects on the target
457 class as well as superclasses and extract ORM mapping directives from
458 the "metadata" attribute of each Field.
459
460 if False, dataclass fields can still be used, however they won't be
461 mapped.
462
463 """
464
465 _include_dunders = {
466 "__table__",
467 "__mapper_args__",
468 "__tablename__",
469 "__table_args__",
470 }
471
472 _match_exclude_dunders = re.compile(r"^(?:_sa_|__)")
473
474 def _scan_attributes(self) -> None:
475 raise NotImplementedError()
476
477 def _setup_dataclasses_transforms(
478 self, *, enable_descriptor_defaults: bool, revert: bool = False
479 ) -> None:
480 dataclass_setup_arguments = self.dataclass_setup_arguments
481 if not dataclass_setup_arguments:
482 return
483
484 # can't use is_dataclass since it uses hasattr
485 if "__dataclass_fields__" in self.cls.__dict__:
486 raise exc.InvalidRequestError(
487 f"Class {self.cls} is already a dataclass; ensure that "
488 "base classes / decorator styles of establishing dataclasses "
489 "are not being mixed. "
490 "This can happen if a class that inherits from "
491 "'MappedAsDataclass', even indirectly, is been mapped with "
492 "'@registry.mapped_as_dataclass'"
493 )
494
495 # can't create a dataclass if __table__ is already there. This would
496 # fail an assertion when calling _get_arguments_for_make_dataclass:
497 # assert False, "Mapped[] received without a mapping declaration"
498 if "__table__" in self.cls.__dict__:
499 raise exc.InvalidRequestError(
500 f"Class {self.cls} already defines a '__table__'. "
501 "ORM Annotated Dataclasses do not support a pre-existing "
502 "'__table__' element"
503 )
504
505 raise_for_non_dc_attrs = collections.defaultdict(list)
506
507 def _allow_dataclass_field(
508 key: str, originating_class: Type[Any]
509 ) -> bool:
510 if (
511 originating_class is not self.cls
512 and "__dataclass_fields__" not in originating_class.__dict__
513 ):
514 raise_for_non_dc_attrs[originating_class].append(key)
515
516 return True
517
518 field_list = [
519 _AttributeOptions._get_arguments_for_make_dataclass(
520 self,
521 key,
522 anno,
523 mapped_container,
524 self.collected_attributes.get(key, _NoArg.NO_ARG),
525 dataclass_setup_arguments,
526 enable_descriptor_defaults,
527 )
528 for key, anno, mapped_container in (
529 (
530 key,
531 raw_anno,
532 mapped_container,
533 )
534 for key, (
535 raw_anno,
536 mapped_container,
537 mapped_anno,
538 is_dc,
539 attr_value,
540 originating_module,
541 originating_class,
542 ) in self.collected_annotations.items()
543 if _allow_dataclass_field(key, originating_class)
544 and (
545 key not in self.collected_attributes
546 # issue #9226; check for attributes that we've collected
547 # which are already instrumented, which we would assume
548 # mean we are in an ORM inheritance mapping and this
549 # attribute is already mapped on the superclass. Under
550 # no circumstance should any QueryableAttribute be sent to
551 # the dataclass() function; anything that's mapped should
552 # be Field and that's it
553 or not isinstance(
554 self.collected_attributes[key], QueryableAttribute
555 )
556 )
557 )
558 ]
559 if raise_for_non_dc_attrs:
560 for (
561 originating_class,
562 non_dc_attrs,
563 ) in raise_for_non_dc_attrs.items():
564 raise exc.InvalidRequestError(
565 f"When transforming {self.cls} to a dataclass, "
566 f"attribute(s) "
567 f"{', '.join(repr(key) for key in non_dc_attrs)} "
568 f"originates from superclass "
569 f"{originating_class}, which is not a dataclass. When "
570 f"declaring SQLAlchemy Declarative "
571 f"Dataclasses, ensure that all mixin classes and other "
572 f"superclasses which include attributes are also a "
573 f"subclass of MappedAsDataclass or make use of the "
574 f"@unmapped_dataclass decorator.",
575 code="dcmx",
576 )
577
578 if revert:
579 # the "revert" case is used only by an unmapped mixin class
580 # that is nonetheless using Mapped construct and needs to
581 # itself be a dataclass. revert_dict includes all non-dunder
582 # attributes, not just fields, in class __dict__ order, so that
583 # the finally: block below can restore that order
584 revert_dict = {
585 name: value
586 for name, value in self.cls.__dict__.items()
587 if not util.dunders_re.match(name)
588 }
589 else:
590 revert_dict = None
591
592 # get original annotations using ForwardRef for symbols that
593 # are unresolvable
594 orig_annotations = util.get_annotations(self.cls)
595
596 # build a new __annotations__ dict from the fields we have.
597 # this has to be done carefully since we have to maintain
598 # the correct order! wow
599 swap_annotations = {}
600 defaults = {}
601
602 for item in field_list:
603 if len(item) == 2:
604 name, tp = item
605 elif len(item) == 3:
606 name, tp, spec = item
607 defaults[name] = spec
608 else:
609 assert False
610
611 # add the annotation to the new dict we are creating.
612 # note that if name is in orig_annotations, we expect
613 # tp and orig_annotations[name] to be identical.
614 swap_annotations[name] = orig_annotations.get(name, tp)
615
616 for k, v in defaults.items():
617 setattr(self.cls, k, v)
618
619 self._assert_dc_arguments(dataclass_setup_arguments)
620
621 dataclass_callable = dataclass_setup_arguments["dataclass_callable"]
622 if dataclass_callable is _NoArg.NO_ARG:
623 dataclass_callable = dataclasses.dataclass
624
625 # create a merged __annotations__ dictionary, maintaining order
626 # as best we can:
627
628 # 1. merge all keys in orig_annotations that occur before
629 # we see any of our mapped fields (this can be attributes like
630 # __table_args__ etc.)
631 new_annotations = {
632 k: orig_annotations[k]
633 for k in itertools.takewhile(
634 lambda k: k not in swap_annotations, orig_annotations
635 )
636 }
637
638 # 2. then put in all the dataclass annotations we have
639 new_annotations |= swap_annotations
640
641 # 3. them merge all of orig_annotations which will add remaining
642 # keys
643 new_annotations |= orig_annotations
644
645 # 4. this becomes the new class annotations.
646 restore_anno = util.restore_annotations(self.cls, new_annotations)
647
648 try:
649 dataclass_callable( # type: ignore[call-overload]
650 self.cls,
651 **{ # type: ignore[call-overload,unused-ignore]
652 k: v
653 for k, v in dataclass_setup_arguments.items()
654 if v is not _NoArg.NO_ARG
655 and k not in ("dataclass_callable",)
656 },
657 )
658 except (TypeError, ValueError) as ex:
659 raise exc.InvalidRequestError(
660 f"Python dataclasses error encountered when creating "
661 f"dataclass for {self.cls.__name__!r}: "
662 f"{ex!r}. Please refer to Python dataclasses "
663 "documentation for additional information.",
664 code="dcte",
665 ) from ex
666 else:
667 # as of Python 3.15, dataclasses no longer renders the
668 # auto-generated class docstring immediately; it instead installs
669 # a descriptor that renders the ``__init__`` signature the first
670 # time ``__doc__`` is accessed (see
671 # ``dataclasses._AutoDocstring``). As the ``finally:`` block
672 # below puts the class' original annotations back, that deferred
673 # render would no longer see the dataclass-oriented annotations
674 # we swapped in above, and would omit them from the docstring
675 # entirely. Read the attribute now, while those annotations are
676 # still in place, so the docstring we generate is the same on
677 # every Python version.
678 self.cls.__doc__
679
680 finally:
681 if revert and revert_dict:
682 # used for mixin dataclasses; we have to restore the
683 # mapped_column(), relationship() etc. to the class so these
684 # take place for a mapped class scan. dataclasses has
685 # deleted the attribute for fields with no plain default and
686 # replaced it in place for the others, so delete and re-set
687 # every attribute, restoring declaration order for the scan,
688 # #13634
689 for k, v in revert_dict.items():
690 if k in self.cls.__dict__:
691 delattr(self.cls, k)
692 setattr(self.cls, k, v)
693
694 restore_anno()
695
696 def _collect_annotation(
697 self,
698 name: str,
699 raw_annotation: _AnnotationScanType,
700 originating_class: Type[Any],
701 expect_mapped: Optional[bool],
702 attr_value: Any,
703 ) -> Optional[_CollectedAnnotation]:
704 if name in self.collected_annotations:
705 return self.collected_annotations[name]
706
707 if raw_annotation is None:
708 return None
709
710 is_dataclass = self.is_dataclass_prior_to_mapping
711 allow_unmapped = self.allow_unmapped_annotations
712
713 if expect_mapped is None:
714 is_dataclass_field = isinstance(attr_value, dataclasses.Field)
715 expect_mapped = (
716 not is_dataclass_field
717 and not allow_unmapped
718 and (
719 attr_value is None
720 or isinstance(attr_value, _MappedAttribute)
721 )
722 )
723
724 is_dataclass_field = False
725 extracted = _extract_mapped_subtype(
726 raw_annotation,
727 self.cls,
728 originating_class.__module__,
729 name,
730 type(attr_value),
731 required=False,
732 is_dataclass_field=is_dataclass_field,
733 expect_mapped=expect_mapped and not is_dataclass,
734 )
735 if extracted is None:
736 # ClassVar can come out here
737 return None
738
739 extracted_mapped_annotation, mapped_container = extracted
740
741 if attr_value is None and not is_literal(extracted_mapped_annotation):
742 for elem in get_args(extracted_mapped_annotation):
743 if is_fwd_ref(
744 elem, check_generic=True, check_for_plain_string=True
745 ):
746 elem = de_stringify_annotation(
747 self.cls,
748 elem,
749 originating_class.__module__,
750 include_generic=True,
751 )
752 # look in Annotated[...] for an ORM construct,
753 # such as Annotated[int, mapped_column(primary_key=True)]
754 if isinstance(elem, _IntrospectsAnnotations):
755 attr_value = elem.found_in_pep593_annotated()
756
757 self.collected_annotations[name] = ca = _CollectedAnnotation(
758 raw_annotation,
759 mapped_container,
760 extracted_mapped_annotation,
761 is_dataclass,
762 attr_value,
763 originating_class.__module__,
764 originating_class,
765 )
766 return ca
767
768 @classmethod
769 def _assert_dc_arguments(cls, arguments: _DataclassArguments) -> None:
770 allowed = {
771 "init",
772 "repr",
773 "order",
774 "eq",
775 "unsafe_hash",
776 "kw_only",
777 "match_args",
778 "dataclass_callable",
779 }
780 disallowed_args = set(arguments).difference(allowed)
781 if disallowed_args:
782 msg = ", ".join(f"{arg!r}" for arg in sorted(disallowed_args))
783 raise exc.ArgumentError(
784 f"Dataclass argument(s) {msg} are not accepted"
785 )
786
787 def _cls_attr_override_checker(
788 self, cls: Type[_O]
789 ) -> Callable[[str, Any], bool]:
790 """Produce a function that checks if a class has overridden an
791 attribute, taking SQLAlchemy-enabled dataclass fields into account.
792
793 """
794
795 if self.allow_dataclass_fields:
796 sa_dataclass_metadata_key = _get_immediate_cls_attr(
797 cls, "__sa_dataclass_metadata_key__"
798 )
799 else:
800 sa_dataclass_metadata_key = None
801
802 if not sa_dataclass_metadata_key:
803
804 def attribute_is_overridden(key: str, obj: Any) -> bool:
805 return getattr(cls, key, obj) is not obj
806
807 else:
808 all_datacls_fields = {
809 f.name: f.metadata[sa_dataclass_metadata_key]
810 for f in util.dataclass_fields(cls)
811 if sa_dataclass_metadata_key in f.metadata
812 }
813 local_datacls_fields = {
814 f.name: f.metadata[sa_dataclass_metadata_key]
815 for f in util.local_dataclass_fields(cls)
816 if sa_dataclass_metadata_key in f.metadata
817 }
818
819 absent = object()
820
821 def attribute_is_overridden(key: str, obj: Any) -> bool:
822 if _is_declarative_props(obj):
823 obj = obj.fget
824
825 # this function likely has some failure modes still if
826 # someone is doing a deep mixing of the same attribute
827 # name as plain Python attribute vs. dataclass field.
828
829 ret = local_datacls_fields.get(key, absent)
830 if _is_declarative_props(ret):
831 ret = ret.fget
832
833 if ret is obj:
834 return False
835 elif ret is not absent:
836 return True
837
838 all_field = all_datacls_fields.get(key, absent)
839
840 ret = getattr(cls, key, obj)
841
842 if ret is obj:
843 return False
844
845 # for dataclasses, this could be the
846 # 'default' of the field. so filter more specifically
847 # for an already-mapped InstrumentedAttribute
848 if ret is not absent and isinstance(
849 ret, InstrumentedAttribute
850 ):
851 return True
852
853 if all_field is obj:
854 return False
855 elif all_field is not absent:
856 return True
857
858 # can't find another attribute
859 return False
860
861 return attribute_is_overridden
862
863 def _cls_attr_resolver(
864 self, cls: Type[Any]
865 ) -> Callable[[], Iterable[Tuple[str, Any, Any, bool]]]:
866 """produce a function to iterate the "attributes" of a class
867 which we want to consider for mapping, adjusting for SQLAlchemy fields
868 embedded in dataclass fields.
869
870 """
871 cls_annotations = util.get_annotations(cls)
872
873 cls_vars = vars(cls)
874
875 _include_dunders = self._include_dunders
876 _match_exclude_dunders = self._match_exclude_dunders
877
878 names = [
879 n
880 for n in util.merge_lists_w_ordering(
881 list(cls_vars), list(cls_annotations)
882 )
883 if not _match_exclude_dunders.match(n) or n in _include_dunders
884 ]
885
886 if self.allow_dataclass_fields:
887 sa_dataclass_metadata_key: Optional[str] = _get_immediate_cls_attr(
888 cls, "__sa_dataclass_metadata_key__"
889 )
890 else:
891 sa_dataclass_metadata_key = None
892
893 if not sa_dataclass_metadata_key:
894
895 def local_attributes_for_class() -> (
896 Iterable[Tuple[str, Any, Any, bool]]
897 ):
898 return (
899 (
900 name,
901 cls_vars.get(name),
902 cls_annotations.get(name),
903 False,
904 )
905 for name in names
906 )
907
908 else:
909 dataclass_fields = {
910 field.name: field for field in util.local_dataclass_fields(cls)
911 }
912
913 fixed_sa_dataclass_metadata_key = sa_dataclass_metadata_key
914
915 def local_attributes_for_class() -> (
916 Iterable[Tuple[str, Any, Any, bool]]
917 ):
918 for name in names:
919 field = dataclass_fields.get(name, None)
920 if field and sa_dataclass_metadata_key in field.metadata:
921 yield field.name, _as_dc_declaredattr(
922 field.metadata, fixed_sa_dataclass_metadata_key
923 ), cls_annotations.get(field.name), True
924 else:
925 yield name, cls_vars.get(name), cls_annotations.get(
926 name
927 ), False
928
929 return local_attributes_for_class
930
931
932class _DeclarativeMapperConfig(_MapperConfig, _ClassScanAbstractConfig):
933 """Configurator that will produce a declarative mapped class"""
934
935 __slots__ = (
936 "registry",
937 "local_table",
938 "persist_selectable",
939 "declared_columns",
940 "column_ordering",
941 "column_copies",
942 "table_args",
943 "tablename",
944 "mapper_args",
945 "mapper_args_fn",
946 "table_fn",
947 "inherits",
948 "single",
949 "clsdict_view",
950 "collected_attributes",
951 "collected_annotations",
952 "allow_dataclass_fields",
953 "dataclass_setup_arguments",
954 "is_dataclass_prior_to_mapping",
955 "allow_unmapped_annotations",
956 )
957
958 is_deferred = False
959 registry: _RegistryType
960 local_table: Optional[FromClause]
961 persist_selectable: Optional[FromClause]
962 declared_columns: util.OrderedSet[Column[Any]]
963 column_ordering: Dict[Column[Any], int]
964 column_copies: Dict[
965 Union[MappedColumn[Any], Column[Any]],
966 Union[MappedColumn[Any], Column[Any]],
967 ]
968 tablename: Optional[str]
969 mapper_args: Mapping[str, Any]
970 table_args: Optional[_TableArgsType]
971 mapper_args_fn: Optional[Callable[[], Dict[str, Any]]]
972 inherits: Optional[Type[Any]]
973 single: bool
974
975 def __init__(
976 self,
977 registry: _RegistryType,
978 cls_: Type[_O],
979 dict_: _ClassDict,
980 ):
981 # grab class dict before the instrumentation manager has been added.
982 # reduces cycles
983 self.clsdict_view = (
984 util.immutabledict(dict_) if dict_ else util.EMPTY_DICT
985 )
986 super().__init__(registry, cls_)
987 self.registry = registry
988 self.persist_selectable = None
989
990 self.collected_attributes = {}
991 self.collected_annotations = {}
992 self.declared_columns = util.OrderedSet()
993 self.column_ordering = {}
994 self.column_copies = {}
995 self.single = False
996 self.dataclass_setup_arguments = dca = getattr(
997 self.cls, "_sa_apply_dc_transforms", None
998 )
999
1000 self.allow_unmapped_annotations = getattr(
1001 self.cls, "__allow_unmapped__", False
1002 ) or bool(self.dataclass_setup_arguments)
1003
1004 self.is_dataclass_prior_to_mapping = cld = dataclasses.is_dataclass(
1005 cls_
1006 )
1007
1008 sdk = _get_immediate_cls_attr(cls_, "__sa_dataclass_metadata_key__")
1009
1010 # we don't want to consume Field objects from a not-already-dataclass.
1011 # the Field objects won't have their "name" or "type" populated,
1012 # and while it seems like we could just set these on Field as we
1013 # read them, Field is documented as "user read only" and we need to
1014 # stay far away from any off-label use of dataclasses APIs.
1015 if (not cld or dca) and sdk:
1016 raise exc.InvalidRequestError(
1017 "SQLAlchemy mapped dataclasses can't consume mapping "
1018 "information from dataclass.Field() objects if the immediate "
1019 "class is not already a dataclass."
1020 )
1021
1022 # if already a dataclass, and __sa_dataclass_metadata_key__ present,
1023 # then also look inside of dataclass.Field() objects yielded by
1024 # dataclasses.get_fields(cls) when scanning for attributes
1025 self.allow_dataclass_fields = bool(sdk and cld)
1026
1027 self._scan_attributes()
1028
1029 self._setup_dataclasses_transforms(enable_descriptor_defaults=True)
1030
1031 with mapperlib._CONFIGURE_MUTEX:
1032 clsregistry._add_class(
1033 self.classname, self.cls, registry._class_registry
1034 )
1035
1036 self._setup_inheriting_mapper()
1037
1038 self._extract_mappable_attributes()
1039
1040 self._extract_declared_columns()
1041
1042 self._setup_table()
1043
1044 self._setup_inheriting_columns()
1045
1046 self._early_mapping(util.EMPTY_DICT)
1047
1048 def _scan_attributes(self) -> None:
1049 cls = self.cls
1050
1051 cls_as_Decl = cast("_DeclMappedClassProtocol[Any]", cls)
1052
1053 clsdict_view = self.clsdict_view
1054 collected_attributes = self.collected_attributes
1055 column_copies = self.column_copies
1056 _include_dunders = self._include_dunders
1057 mapper_args_fn = None
1058 table_args = inherited_table_args = None
1059 table_fn = None
1060 tablename = None
1061 fixed_table = "__table__" in clsdict_view
1062
1063 attribute_is_overridden = self._cls_attr_override_checker(self.cls)
1064
1065 bases = []
1066
1067 for base in cls.__mro__:
1068 # collect bases and make sure standalone columns are copied
1069 # to be the column they will ultimately be on the class,
1070 # so that declared_attr functions use the right columns.
1071 # need to do this all the way up the hierarchy first
1072 # (see #8190)
1073
1074 class_mapped = base is not cls and _is_supercls_for_inherits(base)
1075
1076 local_attributes_for_class = self._cls_attr_resolver(base)
1077
1078 if not class_mapped and base is not cls:
1079 locally_collected_columns = self._produce_column_copies(
1080 local_attributes_for_class,
1081 attribute_is_overridden,
1082 fixed_table,
1083 base,
1084 )
1085 else:
1086 locally_collected_columns = {}
1087
1088 bases.append(
1089 (
1090 base,
1091 class_mapped,
1092 local_attributes_for_class,
1093 locally_collected_columns,
1094 )
1095 )
1096
1097 for (
1098 base,
1099 class_mapped,
1100 local_attributes_for_class,
1101 locally_collected_columns,
1102 ) in bases:
1103 # this transfer can also take place as we scan each name
1104 # for finer-grained control of how collected_attributes is
1105 # populated, as this is what impacts column ordering.
1106 # however it's simpler to get it out of the way here.
1107 collected_attributes.update(locally_collected_columns)
1108
1109 for (
1110 name,
1111 obj,
1112 annotation,
1113 is_dataclass_field,
1114 ) in local_attributes_for_class():
1115 if name in _include_dunders:
1116 if name == "__mapper_args__":
1117 check_decl = _check_declared_props_nocascade(
1118 obj, name, cls
1119 )
1120 if not mapper_args_fn and (
1121 not class_mapped or check_decl
1122 ):
1123 # don't even invoke __mapper_args__ until
1124 # after we've determined everything about the
1125 # mapped table.
1126 # make a copy of it so a class-level dictionary
1127 # is not overwritten when we update column-based
1128 # arguments.
1129 def _mapper_args_fn() -> Dict[str, Any]:
1130 return dict(cls_as_Decl.__mapper_args__)
1131
1132 mapper_args_fn = _mapper_args_fn
1133
1134 elif name == "__tablename__":
1135 check_decl = _check_declared_props_nocascade(
1136 obj, name, cls
1137 )
1138 if not tablename and (not class_mapped or check_decl):
1139 tablename = cls_as_Decl.__tablename__
1140 elif name == "__table__":
1141 check_decl = _check_declared_props_nocascade(
1142 obj, name, cls
1143 )
1144 # if a @declared_attr using "__table__" is detected,
1145 # wrap up a callable to look for "__table__" from
1146 # the final concrete class when we set up a table.
1147 # this was fixed by
1148 # #11509, regression in 2.0 from version 1.4.
1149 if check_decl and not table_fn:
1150 # don't even invoke __table__ until we're ready
1151 def _table_fn() -> FromClause:
1152 return cls_as_Decl.__table__
1153
1154 table_fn = _table_fn
1155
1156 elif name == "__table_args__":
1157 check_decl = _check_declared_props_nocascade(
1158 obj, name, cls
1159 )
1160 if not table_args and (not class_mapped or check_decl):
1161 table_args = cls_as_Decl.__table_args__
1162 if not isinstance(
1163 table_args, (tuple, dict, type(None))
1164 ):
1165 raise exc.ArgumentError(
1166 "__table_args__ value must be a tuple, "
1167 "dict, or None"
1168 )
1169 if base is not cls:
1170 inherited_table_args = True
1171 else:
1172 # any other dunder names; should not be here
1173 # as we have tested for all four names in
1174 # _include_dunders
1175 assert False
1176 elif class_mapped:
1177 if _is_declarative_props(obj) and not obj._quiet:
1178 util.warn(
1179 "Regular (i.e. not __special__) "
1180 "attribute '%s.%s' uses @declared_attr, "
1181 "but owning class %s is mapped - "
1182 "not applying to subclass %s."
1183 % (base.__name__, name, base, cls)
1184 )
1185
1186 continue
1187 elif base is not cls:
1188 # we're a mixin, abstract base, or something that is
1189 # acting like that for now.
1190
1191 if isinstance(obj, (Column, MappedColumn)):
1192 # already copied columns to the mapped class.
1193 continue
1194 elif isinstance(obj, MapperProperty):
1195 raise exc.InvalidRequestError(
1196 "Mapper properties (i.e. deferred,"
1197 "column_property(), relationship(), etc.) must "
1198 "be declared as @declared_attr callables "
1199 "on declarative mixin classes. For dataclass "
1200 "field() objects, use a lambda:"
1201 )
1202 elif _is_declarative_props(obj):
1203 # tried to get overloads to tell this to
1204 # pylance, no luck
1205 assert obj is not None
1206
1207 if obj._cascading:
1208 if name in clsdict_view:
1209 # unfortunately, while we can use the user-
1210 # defined attribute here to allow a clean
1211 # override, if there's another
1212 # subclass below then it still tries to use
1213 # this. not sure if there is enough
1214 # information here to add this as a feature
1215 # later on.
1216 util.warn(
1217 "Attribute '%s' on class %s cannot be "
1218 "processed due to "
1219 "@declared_attr.cascading; "
1220 "skipping" % (name, cls)
1221 )
1222 collected_attributes[name] = column_copies[obj] = (
1223 ret
1224 ) = obj.__get__(obj, cls)
1225 setattr(cls, name, ret)
1226 else:
1227 if is_dataclass_field:
1228 # access attribute using normal class access
1229 # first, to see if it's been mapped on a
1230 # superclass. note if the dataclasses.field()
1231 # has "default", this value can be anything.
1232 ret = getattr(cls, name, None)
1233
1234 # so, if it's anything that's not ORM
1235 # mapped, assume we should invoke the
1236 # declared_attr
1237 if not isinstance(ret, InspectionAttr):
1238 ret = obj.fget()
1239 else:
1240 # access attribute using normal class access.
1241 # if the declared attr already took place
1242 # on a superclass that is mapped, then
1243 # this is no longer a declared_attr, it will
1244 # be the InstrumentedAttribute
1245 ret = getattr(cls, name)
1246
1247 # correct for proxies created from hybrid_property
1248 # or similar. note there is no known case that
1249 # produces nested proxies, so we are only
1250 # looking one level deep right now.
1251
1252 if (
1253 isinstance(ret, InspectionAttr)
1254 and attr_is_internal_proxy(ret)
1255 and not isinstance(
1256 ret.original_property, MapperProperty
1257 )
1258 ):
1259 ret = ret.descriptor
1260
1261 collected_attributes[name] = column_copies[obj] = (
1262 ret
1263 )
1264
1265 if (
1266 isinstance(ret, (Column, MapperProperty))
1267 and ret.doc is None
1268 ):
1269 ret.doc = obj.__doc__
1270
1271 self._collect_annotation(
1272 name,
1273 obj._collect_return_annotation(),
1274 base,
1275 True,
1276 obj,
1277 )
1278 elif _is_mapped_annotation(annotation, cls, base):
1279 # Mapped annotation without any object.
1280 # product_column_copies should have handled this.
1281 # if future support for other MapperProperty,
1282 # then test if this name is already handled and
1283 # otherwise proceed to generate.
1284 if not fixed_table:
1285 assert (
1286 name in collected_attributes
1287 or attribute_is_overridden(name, None)
1288 )
1289 continue
1290 else:
1291 # here, the attribute is some other kind of
1292 # property that we assume is not part of the
1293 # declarative mapping. however, check for some
1294 # more common mistakes
1295 self._warn_for_decl_attributes(base, name, obj)
1296 elif is_dataclass_field and (
1297 name not in clsdict_view or clsdict_view[name] is not obj
1298 ):
1299 # here, we are definitely looking at the target class
1300 # and not a superclass. this is currently a
1301 # dataclass-only path. if the name is only
1302 # a dataclass field and isn't in local cls.__dict__,
1303 # put the object there.
1304 # assert that the dataclass-enabled resolver agrees
1305 # with what we are seeing
1306
1307 assert not attribute_is_overridden(name, obj)
1308
1309 if _is_declarative_props(obj):
1310 obj = obj.fget()
1311
1312 collected_attributes[name] = obj
1313 self._collect_annotation(
1314 name, annotation, base, False, obj
1315 )
1316 else:
1317 collected_annotation = self._collect_annotation(
1318 name, annotation, base, None, obj
1319 )
1320 is_mapped = (
1321 collected_annotation is not None
1322 and collected_annotation.mapped_container is not None
1323 )
1324 generated_obj = (
1325 collected_annotation.attr_value
1326 if collected_annotation is not None
1327 else obj
1328 )
1329 if obj is None and not fixed_table and is_mapped:
1330 collected_attributes[name] = (
1331 generated_obj
1332 if generated_obj is not None
1333 else MappedColumn()
1334 )
1335 elif name in clsdict_view:
1336 collected_attributes[name] = obj
1337 # else if the name is not in the cls.__dict__,
1338 # don't collect it as an attribute.
1339 # we will see the annotation only, which is meaningful
1340 # both for mapping and dataclasses setup
1341
1342 if inherited_table_args and not tablename:
1343 table_args = None
1344
1345 self.table_args = table_args
1346 self.tablename = tablename
1347 self.mapper_args_fn = mapper_args_fn
1348 self.table_fn = table_fn
1349
1350 @classmethod
1351 def _update_annotations_for_non_mapped_class(
1352 cls, klass: Type[_O]
1353 ) -> Mapping[str, _AnnotationScanType]:
1354 cls_annotations = util.get_annotations(klass)
1355
1356 new_anno = {}
1357 for name, annotation in cls_annotations.items():
1358 if _is_mapped_annotation(annotation, klass, klass):
1359 extracted = _extract_mapped_subtype(
1360 annotation,
1361 klass,
1362 klass.__module__,
1363 name,
1364 type(None),
1365 required=False,
1366 is_dataclass_field=False,
1367 expect_mapped=False,
1368 )
1369 if extracted:
1370 inner, _ = extracted
1371 new_anno[name] = inner
1372 else:
1373 new_anno[name] = annotation
1374 return new_anno
1375
1376 def _warn_for_decl_attributes(
1377 self, cls: Type[Any], key: str, c: Any
1378 ) -> None:
1379 if isinstance(c, expression.ColumnElement):
1380 util.warn(
1381 f"Attribute '{key}' on class {cls} appears to "
1382 "be a non-schema SQLAlchemy expression "
1383 "object; this won't be part of the declarative mapping. "
1384 "To map arbitrary expressions, use ``column_property()`` "
1385 "or a similar function such as ``deferred()``, "
1386 "``query_expression()`` etc. "
1387 )
1388
1389 def _produce_column_copies(
1390 self,
1391 attributes_for_class: Callable[
1392 [], Iterable[Tuple[str, Any, Any, bool]]
1393 ],
1394 attribute_is_overridden: Callable[[str, Any], bool],
1395 fixed_table: bool,
1396 originating_class: Type[Any],
1397 ) -> Dict[str, Union[Column[Any], MappedColumn[Any]]]:
1398 cls = self.cls
1399 dict_ = self.clsdict_view
1400 locally_collected_attributes = {}
1401 column_copies = self.column_copies
1402 # copy mixin columns to the mapped class
1403
1404 for name, obj, annotation, is_dataclass in attributes_for_class():
1405 if (
1406 not fixed_table
1407 and obj is None
1408 and _is_mapped_annotation(annotation, cls, originating_class)
1409 ):
1410 # obj is None means this is the annotation only path
1411
1412 if attribute_is_overridden(name, obj):
1413 # perform same "overridden" check as we do for
1414 # Column/MappedColumn, this is how a mixin col is not
1415 # applied to an inherited subclass that does not have
1416 # the mixin. the anno-only path added here for
1417 # #9564
1418 continue
1419
1420 collected_annotation = self._collect_annotation(
1421 name, annotation, originating_class, True, obj
1422 )
1423 obj = (
1424 collected_annotation.attr_value
1425 if collected_annotation is not None
1426 else obj
1427 )
1428 if obj is None:
1429 obj = MappedColumn()
1430
1431 locally_collected_attributes[name] = obj
1432 setattr(cls, name, obj)
1433
1434 elif isinstance(obj, (Column, MappedColumn)):
1435 if attribute_is_overridden(name, obj):
1436 # if column has been overridden
1437 # (like by the InstrumentedAttribute of the
1438 # superclass), skip. don't collect the annotation
1439 # either (issue #8718)
1440 continue
1441
1442 collected_annotation = self._collect_annotation(
1443 name, annotation, originating_class, True, obj
1444 )
1445 obj = (
1446 collected_annotation.attr_value
1447 if collected_annotation is not None
1448 else obj
1449 )
1450
1451 if name not in dict_ and not (
1452 "__table__" in dict_
1453 and (getattr(obj, "name", None) or name)
1454 in dict_["__table__"].c
1455 ):
1456 if obj.foreign_keys:
1457 for fk in obj.foreign_keys:
1458 if (
1459 fk._table_column is not None
1460 and fk._table_column.table is None
1461 ):
1462 raise exc.InvalidRequestError(
1463 "Columns with foreign keys to "
1464 "non-table-bound "
1465 "columns must be declared as "
1466 "@declared_attr callables "
1467 "on declarative mixin classes. "
1468 "For dataclass "
1469 "field() objects, use a lambda:."
1470 )
1471
1472 column_copies[obj] = copy_ = obj._copy()
1473
1474 locally_collected_attributes[name] = copy_
1475 setattr(cls, name, copy_)
1476
1477 return locally_collected_attributes
1478
1479 def _extract_mappable_attributes(self) -> None:
1480 cls = self.cls
1481 collected_attributes = self.collected_attributes
1482
1483 our_stuff = self.properties
1484
1485 _include_dunders = self._include_dunders
1486
1487 late_mapped = _get_immediate_cls_attr(
1488 cls, "_sa_decl_prepare_nocascade", strict=True
1489 )
1490
1491 allow_unmapped_annotations = self.allow_unmapped_annotations
1492 expect_annotations_wo_mapped = (
1493 allow_unmapped_annotations or self.is_dataclass_prior_to_mapping
1494 )
1495
1496 look_for_dataclass_things = bool(self.dataclass_setup_arguments)
1497
1498 for k in list(collected_attributes):
1499 if k in _include_dunders:
1500 continue
1501
1502 value = collected_attributes[k]
1503
1504 if _is_declarative_props(value):
1505 # @declared_attr in collected_attributes only occurs here for a
1506 # @declared_attr that's directly on the mapped class;
1507 # for a mixin, these have already been evaluated
1508 if value._cascading:
1509 util.warn(
1510 "Use of @declared_attr.cascading only applies to "
1511 "Declarative 'mixin' and 'abstract' classes. "
1512 "Currently, this flag is ignored on mapped class "
1513 "%s" % self.cls
1514 )
1515
1516 value = getattr(cls, k)
1517
1518 elif (
1519 isinstance(value, QueryableAttribute)
1520 and value.class_ is not cls
1521 and value.key != k
1522 ):
1523 # detect a QueryableAttribute that's already mapped being
1524 # assigned elsewhere in userland, turn into a synonym()
1525 value = SynonymProperty(value.key)
1526 setattr(cls, k, value)
1527
1528 if k == "registry" or (
1529 k == "metadata" and not isinstance(value, MetaData)
1530 ):
1531 noun = {
1532 "metadata": "MetaData instance",
1533 "registry": "Declarative class registry",
1534 }
1535 util.warn(
1536 f"Attribute name '{k}' should be left reserved for the "
1537 f"{noun[k]} when using a Declarative class."
1538 )
1539
1540 if (
1541 isinstance(value, tuple)
1542 and len(value) == 1
1543 and isinstance(value[0], (Column, _MappedAttribute))
1544 ):
1545 util.warn(
1546 "Ignoring declarative-like tuple value of attribute "
1547 "'%s': possibly a copy-and-paste error with a comma "
1548 "accidentally placed at the end of the line?" % k
1549 )
1550 continue
1551 elif look_for_dataclass_things and isinstance(
1552 value, dataclasses.Field
1553 ):
1554 # we collected a dataclass Field; dataclasses would have
1555 # set up the correct state on the class
1556 continue
1557 elif not isinstance(value, (Column, _DCAttributeOptions)):
1558 # using @declared_attr for some object that
1559 # isn't Column/MapperProperty/_DCAttributeOptions; remove
1560 # from the clsdict_view
1561 # and place the evaluated value onto the class.
1562 collected_attributes.pop(k)
1563 self._warn_for_decl_attributes(cls, k, value)
1564 if not late_mapped:
1565 setattr(cls, k, value)
1566 continue
1567 elif isinstance(value, Column):
1568 _undefer_column_name(
1569 k, self.column_copies.get(value, value) # type: ignore[arg-type] # noqa: E501
1570 )
1571 else:
1572 if isinstance(value, _IntrospectsAnnotations):
1573 (
1574 annotation,
1575 mapped_container,
1576 extracted_mapped_annotation,
1577 is_dataclass,
1578 attr_value,
1579 originating_module,
1580 originating_class,
1581 ) = self.collected_annotations.get(
1582 k, (None, None, None, False, None, None, None)
1583 )
1584
1585 # issue #8692 - don't do any annotation interpretation if
1586 # an annotation were present and a container such as
1587 # Mapped[] etc. were not used. If annotation is None,
1588 # do declarative_scan so that the property can raise
1589 # for required
1590 if (
1591 mapped_container is not None
1592 or annotation is None
1593 # issue #10516: need to do declarative_scan even with
1594 # a non-Mapped annotation if we are doing
1595 # __allow_unmapped__, for things like col.name
1596 # assignment
1597 or allow_unmapped_annotations
1598 ):
1599 try:
1600 value.declarative_scan(
1601 self,
1602 self.registry,
1603 cls,
1604 originating_module,
1605 k,
1606 mapped_container,
1607 annotation,
1608 extracted_mapped_annotation,
1609 is_dataclass,
1610 )
1611 except NameError as ne:
1612 raise orm_exc.MappedAnnotationError(
1613 f"Could not resolve all types within mapped "
1614 f'annotation: "{annotation}". Ensure all '
1615 f"types are written correctly and are "
1616 f"imported within the module in use."
1617 ) from ne
1618 else:
1619 # assert that we were expecting annotations
1620 # without Mapped[] were going to be passed.
1621 # otherwise an error should have been raised
1622 # by util._extract_mapped_subtype before we got here.
1623 assert expect_annotations_wo_mapped
1624
1625 if isinstance(value, _DCAttributeOptions):
1626 if (
1627 value._has_dataclass_arguments
1628 and not look_for_dataclass_things
1629 ):
1630 if isinstance(value, MapperProperty):
1631 argnames = [
1632 "init",
1633 "default_factory",
1634 "repr",
1635 "default",
1636 "dataclass_metadata",
1637 ]
1638 else:
1639 argnames = [
1640 "init",
1641 "default_factory",
1642 "repr",
1643 "dataclass_metadata",
1644 ]
1645
1646 args = {
1647 a
1648 for a in argnames
1649 if getattr(
1650 value._attribute_options, f"dataclasses_{a}"
1651 )
1652 is not _NoArg.NO_ARG
1653 }
1654
1655 raise exc.ArgumentError(
1656 f"Attribute '{k}' on class {cls} includes "
1657 f"dataclasses argument(s): "
1658 f"{', '.join(sorted(repr(a) for a in args))} but "
1659 f"class does not specify "
1660 "SQLAlchemy native dataclass configuration."
1661 )
1662
1663 if not isinstance(value, (MapperProperty, _MapsColumns)):
1664 # filter for _DCAttributeOptions objects that aren't
1665 # MapperProperty / mapped_column(). Currently this
1666 # includes AssociationProxy. pop it from the things
1667 # we're going to map and set it up as a descriptor
1668 # on the class.
1669 collected_attributes.pop(k)
1670
1671 # Assoc Prox (or other descriptor object that may
1672 # use _DCAttributeOptions) is usually here, except if
1673 # 1. we're a
1674 # dataclass, dataclasses would have removed the
1675 # attr here or 2. assoc proxy is coming from a
1676 # superclass, we want it to be direct here so it
1677 # tracks state or 3. assoc prox comes from
1678 # declared_attr, uncommon case
1679 setattr(cls, k, value)
1680 continue
1681
1682 our_stuff[k] = value
1683
1684 def _extract_declared_columns(self) -> None:
1685 our_stuff = self.properties
1686
1687 # extract columns from the class dict
1688 declared_columns = self.declared_columns
1689 column_ordering = self.column_ordering
1690 name_to_prop_key = collections.defaultdict(set)
1691
1692 for key, c in list(our_stuff.items()):
1693 if isinstance(c, _MapsColumns):
1694 mp_to_assign = c.mapper_property_to_assign
1695 if mp_to_assign:
1696 our_stuff[key] = mp_to_assign
1697 else:
1698 # if no mapper property to assign, this currently means
1699 # this is a MappedColumn that will produce a Column for us
1700 del our_stuff[key]
1701
1702 for col, sort_order in c.columns_to_assign:
1703 if not isinstance(c, CompositeProperty):
1704 name_to_prop_key[col.name].add(key)
1705 declared_columns.add(col)
1706
1707 # we would assert this, however we want the below
1708 # warning to take effect instead. See #9630
1709 # assert col not in column_ordering
1710
1711 column_ordering[col] = sort_order
1712
1713 # if this is a MappedColumn and the attribute key we
1714 # have is not what the column has for its key, map the
1715 # Column explicitly under the attribute key name.
1716 # otherwise, Mapper will map it under the column key.
1717 if mp_to_assign is None and key != col.key:
1718 our_stuff[key] = col
1719 elif isinstance(c, Column):
1720 # undefer previously occurred here, and now occurs earlier.
1721 # ensure every column we get here has been named
1722 assert c.name is not None
1723 name_to_prop_key[c.name].add(key)
1724 declared_columns.add(c)
1725 # if the column is the same name as the key,
1726 # remove it from the explicit properties dict.
1727 # the normal rules for assigning column-based properties
1728 # will take over, including precedence of columns
1729 # in multi-column ColumnProperties.
1730 if key == c.key:
1731 del our_stuff[key]
1732
1733 for name, keys in name_to_prop_key.items():
1734 if len(keys) > 1:
1735 util.warn(
1736 "On class %r, Column object %r named "
1737 "directly multiple times, "
1738 "only one will be used: %s. "
1739 "Consider using orm.synonym instead"
1740 % (self.classname, name, (", ".join(sorted(keys))))
1741 )
1742
1743 def _setup_table(self, table: Optional[FromClause] = None) -> None:
1744 cls = self.cls
1745 cls_as_Decl = cast("MappedClassProtocol[Any]", cls)
1746
1747 tablename = self.tablename
1748 table_args = self.table_args
1749 clsdict_view = self.clsdict_view
1750 declared_columns = self.declared_columns
1751 column_ordering = self.column_ordering
1752
1753 manager = attributes.manager_of_class(cls)
1754
1755 if (
1756 self.table_fn is None
1757 and "__table__" not in clsdict_view
1758 and table is None
1759 ):
1760 if hasattr(cls, "__table_cls__"):
1761 table_cls = cast(
1762 Type[Table],
1763 util.unbound_method_to_callable(cls.__table_cls__), # type: ignore[no-untyped-call] # noqa: E501
1764 )
1765 else:
1766 table_cls = Table
1767
1768 if tablename is not None:
1769 args: Tuple[Any, ...] = ()
1770 table_kw: Dict[str, Any] = {}
1771
1772 if table_args:
1773 if isinstance(table_args, dict):
1774 table_kw = table_args
1775 elif isinstance(table_args, tuple):
1776 if isinstance(table_args[-1], dict):
1777 args, table_kw = table_args[0:-1], table_args[-1]
1778 else:
1779 args = table_args
1780
1781 autoload_with = clsdict_view.get("__autoload_with__")
1782 if autoload_with:
1783 table_kw["autoload_with"] = autoload_with
1784
1785 autoload = clsdict_view.get("__autoload__")
1786 if autoload:
1787 table_kw["autoload"] = True
1788
1789 sorted_columns = sorted(
1790 declared_columns,
1791 key=lambda c: column_ordering.get(c, 0),
1792 )
1793 table = self.set_cls_attribute(
1794 "__table__",
1795 table_cls(
1796 tablename,
1797 self._metadata_for_cls(manager),
1798 *sorted_columns,
1799 *args,
1800 **table_kw,
1801 ),
1802 )
1803 else:
1804 if table is None:
1805 if self.table_fn:
1806 table = self.set_cls_attribute(
1807 "__table__", self.table_fn()
1808 )
1809 else:
1810 table = cls_as_Decl.__table__
1811 if declared_columns:
1812 for c in declared_columns:
1813 if not table.c.contains_column(c):
1814 raise exc.ArgumentError(
1815 "Can't add additional column %r when "
1816 "specifying __table__" % c.key
1817 )
1818
1819 self.local_table = table
1820
1821 def _metadata_for_cls(self, manager: ClassManager[Any]) -> MetaData:
1822 return _metadata_for_cls_fn(self.cls, manager.registry)
1823
1824 def _setup_inheriting_mapper(self) -> None:
1825 cls = self.cls
1826
1827 inherits = None
1828
1829 if inherits is None:
1830 # since we search for classical mappings now, search for
1831 # multiple mapped bases as well and raise an error.
1832 inherits_search = []
1833 for base_ in cls.__bases__:
1834 c = _resolve_for_abstract_or_classical(base_)
1835 if c is None:
1836 continue
1837
1838 if _is_supercls_for_inherits(c) and c not in inherits_search:
1839 inherits_search.append(c)
1840
1841 if inherits_search:
1842 if len(inherits_search) > 1:
1843 raise exc.InvalidRequestError(
1844 "Class %s has multiple mapped bases: %r"
1845 % (cls, inherits_search)
1846 )
1847 inherits = inherits_search[0]
1848 elif isinstance(inherits, Mapper):
1849 inherits = inherits.class_
1850
1851 self.inherits = inherits
1852
1853 clsdict_view = self.clsdict_view
1854 if "__table__" not in clsdict_view and self.tablename is None:
1855 self.single = True
1856
1857 def _setup_inheriting_columns(self) -> None:
1858 table = self.local_table
1859 cls = self.cls
1860 table_args = self.table_args
1861 declared_columns = self.declared_columns
1862
1863 if (
1864 table is None
1865 and self.inherits is None
1866 and not _get_immediate_cls_attr(cls, "__no_table__")
1867 ):
1868 raise exc.InvalidRequestError(
1869 "Class %r does not have a __table__ or __tablename__ "
1870 "specified and does not inherit from an existing "
1871 "table-mapped class." % cls
1872 )
1873 elif self.inherits:
1874 inherited_mapper_or_config = _declared_mapping_info(self.inherits)
1875 assert inherited_mapper_or_config is not None
1876 inherited_table = inherited_mapper_or_config.local_table
1877 inherited_persist_selectable = (
1878 inherited_mapper_or_config.persist_selectable
1879 )
1880
1881 if table is None:
1882 # single table inheritance.
1883 # ensure no table args
1884 if table_args:
1885 raise exc.ArgumentError(
1886 "Can't place __table_args__ on an inherited class "
1887 "with no table."
1888 )
1889
1890 # add any columns declared here to the inherited table.
1891 if declared_columns and not isinstance(inherited_table, Table):
1892 raise exc.ArgumentError(
1893 f"Can't declare columns on single-table-inherited "
1894 f"subclass {self.cls}; superclass {self.inherits} "
1895 "is not mapped to a Table"
1896 )
1897
1898 for col in declared_columns:
1899 assert inherited_table is not None
1900 if col.name in inherited_table.c:
1901 if inherited_table.c[col.name] is col:
1902 continue
1903 raise exc.ArgumentError(
1904 f"Column '{col}' on class {cls.__name__} "
1905 f"conflicts with existing column "
1906 f"'{inherited_table.c[col.name]}'. If using "
1907 f"Declarative, consider using the "
1908 "use_existing_column parameter of mapped_column() "
1909 "to resolve conflicts."
1910 )
1911 if col.primary_key:
1912 raise exc.ArgumentError(
1913 "Can't place primary key columns on an inherited "
1914 "class with no table."
1915 )
1916
1917 if TYPE_CHECKING:
1918 assert isinstance(inherited_table, Table)
1919
1920 inherited_table.append_column(col)
1921 if (
1922 inherited_persist_selectable is not None
1923 and inherited_persist_selectable is not inherited_table
1924 ):
1925 inherited_persist_selectable._refresh_for_new_column(
1926 col
1927 )
1928
1929 def _prepare_mapper_arguments(self, mapper_kw: _MapperKwArgs) -> None:
1930 properties = self.properties
1931
1932 if self.mapper_args_fn:
1933 mapper_args = self.mapper_args_fn()
1934 else:
1935 mapper_args = {}
1936
1937 if mapper_kw:
1938 mapper_args.update(mapper_kw)
1939
1940 if "properties" in mapper_args:
1941 properties = dict(properties)
1942 properties.update(mapper_args["properties"])
1943
1944 # make sure that column copies are used rather
1945 # than the original columns from any mixins
1946 for k in ("version_id_col", "polymorphic_on"):
1947 if k in mapper_args:
1948 v = mapper_args[k]
1949 mapper_args[k] = self.column_copies.get(v, v)
1950
1951 if "primary_key" in mapper_args:
1952 mapper_args["primary_key"] = [
1953 self.column_copies.get(v, v)
1954 for v in util.to_list(mapper_args["primary_key"])
1955 ]
1956
1957 if "inherits" in mapper_args:
1958 inherits_arg = mapper_args["inherits"]
1959 if isinstance(inherits_arg, Mapper):
1960 inherits_arg = inherits_arg.class_
1961
1962 if inherits_arg is not self.inherits:
1963 raise exc.InvalidRequestError(
1964 "mapper inherits argument given for non-inheriting "
1965 "class %s" % (mapper_args["inherits"])
1966 )
1967
1968 if self.inherits:
1969 mapper_args["inherits"] = self.inherits
1970
1971 if self.inherits and not mapper_args.get("concrete", False):
1972 # note the superclass is expected to have a Mapper assigned and
1973 # not be a deferred config, as this is called within map()
1974 inherited_mapper = class_mapper(self.inherits, False)
1975 inherited_table = inherited_mapper.local_table
1976
1977 # single or joined inheritance
1978 # exclude any cols on the inherited table which are
1979 # not mapped on the parent class, to avoid
1980 # mapping columns specific to sibling/nephew classes
1981 if "exclude_properties" not in mapper_args:
1982 mapper_args["exclude_properties"] = exclude_properties = {
1983 c.key
1984 for c in inherited_table.c
1985 if c not in inherited_mapper._columntoproperty
1986 }.union(inherited_mapper.exclude_properties or ())
1987 exclude_properties.difference_update(
1988 [c.key for c in self.declared_columns]
1989 )
1990
1991 # look through columns in the current mapper that
1992 # are keyed to a propname different than the colname
1993 # (if names were the same, we'd have popped it out above,
1994 # in which case the mapper makes this combination).
1995 # See if the superclass has a similar column property.
1996 # If so, join them together.
1997 for k, col in list(properties.items()):
1998 if not isinstance(col, expression.ColumnElement):
1999 continue
2000 if k in inherited_mapper._props:
2001 p = inherited_mapper._props[k]
2002 if isinstance(p, ColumnProperty):
2003 # note here we place the subclass column
2004 # first. See [ticket:1892] for background.
2005 properties[k] = [col] + p.columns
2006 result_mapper_args = mapper_args.copy()
2007 result_mapper_args["properties"] = properties
2008 self.mapper_args = result_mapper_args
2009
2010 def map(self, mapper_kw: _MapperKwArgs = util.EMPTY_DICT) -> Mapper[Any]:
2011 self._prepare_mapper_arguments(mapper_kw)
2012 if hasattr(self.cls, "__mapper_cls__"):
2013 mapper_cls = cast(
2014 "Type[Mapper[Any]]",
2015 util.unbound_method_to_callable(
2016 self.cls.__mapper_cls__ # type: ignore[no-untyped-call]
2017 ),
2018 )
2019 else:
2020 mapper_cls = Mapper
2021
2022 return self.set_cls_attribute(
2023 "__mapper__",
2024 mapper_cls(self.cls, self.local_table, **self.mapper_args),
2025 )
2026
2027
2028class _UnmappedDataclassConfig(_ClassScanAbstractConfig):
2029 """Configurator that will produce an unmapped dataclass."""
2030
2031 __slots__ = (
2032 "clsdict_view",
2033 "collected_attributes",
2034 "collected_annotations",
2035 "allow_dataclass_fields",
2036 "dataclass_setup_arguments",
2037 "is_dataclass_prior_to_mapping",
2038 "allow_unmapped_annotations",
2039 )
2040
2041 def __init__(
2042 self,
2043 cls_: Type[_O],
2044 dict_: _ClassDict,
2045 ):
2046 super().__init__(cls_)
2047 self.clsdict_view = (
2048 util.immutabledict(dict_) if dict_ else util.EMPTY_DICT
2049 )
2050 self.dataclass_setup_arguments = getattr(
2051 self.cls, "_sa_apply_dc_transforms", None
2052 )
2053
2054 self.is_dataclass_prior_to_mapping = dataclasses.is_dataclass(cls_)
2055 self.allow_dataclass_fields = False
2056 self.allow_unmapped_annotations = True
2057 self.collected_attributes = {}
2058 self.collected_annotations = {}
2059
2060 self._scan_attributes()
2061
2062 self._setup_dataclasses_transforms(
2063 enable_descriptor_defaults=False, revert=True
2064 )
2065
2066 def _scan_attributes(self) -> None:
2067 cls = self.cls
2068
2069 clsdict_view = self.clsdict_view
2070 collected_attributes = self.collected_attributes
2071 _include_dunders = self._include_dunders
2072
2073 attribute_is_overridden = self._cls_attr_override_checker(self.cls)
2074
2075 local_attributes_for_class = self._cls_attr_resolver(cls)
2076 for (
2077 name,
2078 obj,
2079 annotation,
2080 is_dataclass_field,
2081 ) in local_attributes_for_class():
2082 if name in _include_dunders:
2083 continue
2084 elif is_dataclass_field and (
2085 name not in clsdict_view or clsdict_view[name] is not obj
2086 ):
2087 # here, we are definitely looking at the target class
2088 # and not a superclass. this is currently a
2089 # dataclass-only path. if the name is only
2090 # a dataclass field and isn't in local cls.__dict__,
2091 # put the object there.
2092 # assert that the dataclass-enabled resolver agrees
2093 # with what we are seeing
2094
2095 assert not attribute_is_overridden(name, obj)
2096
2097 if _is_declarative_props(obj):
2098 obj = obj.fget()
2099
2100 collected_attributes[name] = obj
2101 self._collect_annotation(name, annotation, cls, False, obj)
2102 else:
2103 self._collect_annotation(name, annotation, cls, None, obj)
2104 if name in clsdict_view:
2105 collected_attributes[name] = obj
2106
2107
2108@util.preload_module("sqlalchemy.orm.decl_api")
2109def _as_dc_declaredattr(
2110 field_metadata: Mapping[str, Any], sa_dataclass_metadata_key: str
2111) -> Any:
2112 # wrap lambdas inside dataclass fields inside an ad-hoc declared_attr.
2113 # we can't write it because field.metadata is immutable :( so we have
2114 # to go through extra trouble to compare these
2115 decl_api = util.preloaded.orm_decl_api
2116 obj = field_metadata[sa_dataclass_metadata_key]
2117 if callable(obj) and not isinstance(obj, decl_api.declared_attr):
2118 return decl_api.declared_attr(obj)
2119 else:
2120 return obj
2121
2122
2123class _DeferredDeclarativeConfig(_DeclarativeMapperConfig):
2124 """Configurator that extends _DeclarativeMapperConfig to add a
2125 "deferred" step, to allow extensions like AbstractConcreteBase,
2126 DeferredMapping to partially set up a mapping that is "prepared"
2127 when table metadata is ready.
2128
2129 """
2130
2131 _cls: weakref.ref[Type[Any]]
2132
2133 is_deferred = True
2134
2135 _configs: util.OrderedDict[
2136 weakref.ref[Type[Any]], _DeferredDeclarativeConfig
2137 ] = util.OrderedDict()
2138
2139 def _early_mapping(self, mapper_kw: _MapperKwArgs) -> None:
2140 pass
2141
2142 @property
2143 def cls(self) -> Type[Any]:
2144 return self._cls() # type: ignore[return-value]
2145
2146 @cls.setter
2147 def cls(self, class_: Type[Any]) -> None:
2148 self._cls = weakref.ref(class_, self._remove_config_cls)
2149 self._configs[self._cls] = self
2150
2151 @classmethod
2152 def _remove_config_cls(cls, ref: weakref.ref[Type[Any]]) -> None:
2153 cls._configs.pop(ref, None)
2154
2155 @classmethod
2156 def has_cls(cls, class_: Type[Any]) -> bool:
2157 # 2.6 fails on weakref if class_ is an old style class
2158 return isinstance(class_, type) and weakref.ref(class_) in cls._configs
2159
2160 @classmethod
2161 def raise_unmapped_for_cls(cls, class_: Type[Any]) -> NoReturn:
2162 if hasattr(class_, "_sa_raise_deferred_config"):
2163 class_._sa_raise_deferred_config()
2164
2165 raise orm_exc.UnmappedClassError(
2166 class_,
2167 msg=(
2168 f"Class {orm_exc._safe_cls_name(class_)} has a deferred "
2169 "mapping on it. It is not yet usable as a mapped class."
2170 ),
2171 )
2172
2173 @classmethod
2174 def config_for_cls(cls, class_: Type[Any]) -> _DeferredDeclarativeConfig:
2175 return cls._configs[weakref.ref(class_)]
2176
2177 @classmethod
2178 def classes_for_base(
2179 cls, base_cls: Type[Any], sort: bool = True
2180 ) -> List[_DeferredDeclarativeConfig]:
2181 classes_for_base = [
2182 m
2183 for m, cls_ in [(m, m.cls) for m in cls._configs.values()]
2184 if cls_ is not None and issubclass(cls_, base_cls)
2185 ]
2186
2187 if not sort:
2188 return classes_for_base
2189
2190 all_m_by_cls = {m.cls: m for m in classes_for_base}
2191
2192 tuples: List[
2193 Tuple[_DeferredDeclarativeConfig, _DeferredDeclarativeConfig]
2194 ] = []
2195 for m_cls in all_m_by_cls:
2196 tuples.extend(
2197 (all_m_by_cls[base_cls], all_m_by_cls[m_cls])
2198 for base_cls in m_cls.__bases__
2199 if base_cls in all_m_by_cls
2200 )
2201 return list(topological.sort(tuples, classes_for_base))
2202
2203 def map(self, mapper_kw: _MapperKwArgs = util.EMPTY_DICT) -> Mapper[Any]:
2204 self._configs.pop(self._cls, None)
2205 return super().map(mapper_kw)
2206
2207
2208def _add_attribute(
2209 cls: Type[Any], key: str, value: MapperProperty[Any]
2210) -> None:
2211 """add an attribute to an existing declarative class.
2212
2213 This runs through the logic to determine MapperProperty,
2214 adds it to the Mapper, adds a column to the mapped Table, etc.
2215
2216 """
2217
2218 if "__mapper__" in cls.__dict__:
2219 mapped_cls = cast("MappedClassProtocol[Any]", cls)
2220
2221 def _table_or_raise(mc: MappedClassProtocol[Any]) -> Table:
2222 if isinstance(mc.__table__, Table):
2223 return mc.__table__
2224 raise exc.InvalidRequestError(
2225 f"Cannot add a new attribute to mapped class {mc.__name__!r} "
2226 "because it's not mapped against a table."
2227 )
2228
2229 if isinstance(value, Column):
2230 _undefer_column_name(key, value)
2231 _table_or_raise(mapped_cls).append_column(
2232 value, replace_existing=True
2233 )
2234 mapped_cls.__mapper__.add_property(key, value)
2235 elif isinstance(value, _MapsColumns):
2236 mp = value.mapper_property_to_assign
2237 for col, _ in value.columns_to_assign:
2238 _undefer_column_name(key, col)
2239 _table_or_raise(mapped_cls).append_column(
2240 col, replace_existing=True
2241 )
2242 if not mp:
2243 mapped_cls.__mapper__.add_property(key, col)
2244 if mp:
2245 mapped_cls.__mapper__.add_property(key, mp)
2246 elif isinstance(value, MapperProperty):
2247 mapped_cls.__mapper__.add_property(key, value)
2248 elif isinstance(value, QueryableAttribute) and value.key != key:
2249 # detect a QueryableAttribute that's already mapped being
2250 # assigned elsewhere in userland, turn into a synonym()
2251 value = SynonymProperty(value.key)
2252 mapped_cls.__mapper__.add_property(key, value)
2253 else:
2254 type.__setattr__(cls, key, value)
2255 mapped_cls.__mapper__._expire_memoizations()
2256 else:
2257 type.__setattr__(cls, key, value)
2258
2259
2260def _del_attribute(cls: Type[Any], key: str) -> None:
2261 if (
2262 "__mapper__" in cls.__dict__
2263 and key in cls.__dict__
2264 and not cast(
2265 "MappedClassProtocol[Any]", cls
2266 ).__mapper__._dispose_called
2267 ):
2268 value = cls.__dict__[key]
2269 if isinstance(
2270 value, (Column, _MapsColumns, MapperProperty, QueryableAttribute)
2271 ):
2272 raise NotImplementedError(
2273 "Can't un-map individual mapped attributes on a mapped class."
2274 )
2275 else:
2276 type.__delattr__(cls, key)
2277 cast(
2278 "MappedClassProtocol[Any]", cls
2279 ).__mapper__._expire_memoizations()
2280 else:
2281 type.__delattr__(cls, key)
2282
2283
2284def _declarative_constructor(self: Any, **kwargs: Any) -> None:
2285 """A simple constructor that allows initialization from kwargs.
2286
2287 Sets attributes on the constructed instance using the names and
2288 values in ``kwargs``.
2289
2290 Only keys that are present as
2291 attributes of the instance's class are allowed. These could be,
2292 for example, any mapped columns or relationships.
2293 """
2294 cls_ = type(self)
2295 for k in kwargs:
2296 if not hasattr(cls_, k):
2297 raise TypeError(
2298 "%r is an invalid keyword argument for %s" % (k, cls_.__name__)
2299 )
2300 setattr(self, k, kwargs[k])
2301
2302
2303_declarative_constructor.__name__ = "__init__"
2304
2305
2306def _undefer_column_name(key: str, column: Column[Any]) -> None:
2307 if column.key is None:
2308 column.key = key
2309 if column.name is None:
2310 column.name = key