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