Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/sqlalchemy/orm/decl_base.py: 22%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

934 statements  

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 _is_unmapped_container 

47from .base import InspectionAttr 

48from .descriptor_props import CompositeProperty 

49from .descriptor_props import SynonymProperty 

50from .interfaces import _AttributeOptions 

51from .interfaces import _DataclassArguments 

52from .interfaces import _DCAttributeOptions 

53from .interfaces import _IntrospectsAnnotations 

54from .interfaces import _MappedAttribute 

55from .interfaces import _MapsColumns 

56from .interfaces import MapperProperty 

57from .mapper import Mapper 

58from .properties import ColumnProperty 

59from .properties import MappedColumn 

60from .properties import UnmappedColumn 

61from .util import _extract_mapped_subtype 

62from .util import _is_mapped_annotation 

63from .util import _metadata_for_cls as _metadata_for_cls_fn 

64from .util import class_mapper 

65from .util import de_stringify_annotation 

66from .. import exc 

67from .. import util 

68from ..sql import expression 

69from ..sql._annotated_cols import TypedColumns 

70from ..sql.base import _NoArg 

71from ..sql.schema import Column 

72from ..sql.schema import MetaData 

73from ..sql.schema import Table 

74from ..util import topological 

75from ..util.typing import _AnnotationScanType 

76from ..util.typing import is_fwd_ref 

77from ..util.typing import is_literal 

78 

79if TYPE_CHECKING: 

80 from ._typing import _ClassDict 

81 from ._typing import _RegistryType 

82 from .base import Mapped 

83 from .decl_api import declared_attr 

84 from .instrumentation import ClassManager 

85 from ..sql.elements import NamedColumn 

86 from ..sql.selectable import FromClause 

87 

88_T = TypeVar("_T", bound=Any) 

89 

90_MapperKwArgs = Mapping[str, Any] 

91_TableArgsType = Union[Tuple[Any, ...], Dict[str, Any]] 

92 

93 

94class MappedClassProtocol(Protocol[_O]): 

95 """A protocol representing a SQLAlchemy mapped class. 

96 

97 The protocol is generic on the type of class, use 

98 ``MappedClassProtocol[Any]`` to allow any mapped class. 

99 """ 

100 

101 __name__: str 

102 __mapper__: Mapper[_O] 

103 __table__: FromClause 

104 

105 def __call__(self, **kw: Any) -> _O: ... 

106 

107 

108class _DeclMappedClassProtocol(MappedClassProtocol[_O], Protocol): 

109 "Internal more detailed version of ``MappedClassProtocol``." 

110 

111 metadata: MetaData 

112 __tablename__: str 

113 __mapper_args__: _MapperKwArgs 

114 __table_args__: Optional[_TableArgsType] 

115 

116 _sa_apply_dc_transforms: Optional[_DataclassArguments] 

117 

118 def __declare_first__(self) -> None: ... 

119 

120 def __declare_last__(self) -> None: ... 

121 

122 

123def _declared_mapping_info( 

124 cls: Type[Any], 

125) -> Optional[Union[_DeferredDeclarativeConfig, Mapper[Any]]]: 

126 # deferred mapping 

127 if _DeferredDeclarativeConfig.has_cls(cls): 

128 return _DeferredDeclarativeConfig.config_for_cls(cls) 

129 # regular mapping 

130 elif _is_mapped_class(cls): 

131 return class_mapper(cls, configure=False) 

132 else: 

133 return None 

134 

135 

136def _is_supercls_for_inherits(cls: Type[Any]) -> bool: 

137 """return True if this class will be used as a superclass to set in 

138 'inherits'. 

139 

140 This includes deferred mapper configs that aren't mapped yet, however does 

141 not include classes with _sa_decl_prepare_nocascade (e.g. 

142 ``AbstractConcreteBase``); these concrete-only classes are not set up as 

143 "inherits" until after mappers are configured using 

144 mapper._set_concrete_base() 

145 

146 """ 

147 if _DeferredDeclarativeConfig.has_cls(cls): 

148 return not _get_immediate_cls_attr( 

149 cls, "_sa_decl_prepare_nocascade", strict=True 

150 ) 

151 # regular mapping 

152 elif _is_mapped_class(cls): 

153 return True 

154 else: 

155 return False 

156 

157 

158def _resolve_for_abstract_or_classical(cls: Type[Any]) -> Optional[Type[Any]]: 

159 if cls is object: 

160 return None 

161 

162 sup: Optional[Type[Any]] 

163 

164 if cls.__dict__.get("__abstract__", False): 

165 for base_ in cls.__bases__: 

166 sup = _resolve_for_abstract_or_classical(base_) 

167 if sup is not None: 

168 return sup 

169 else: 

170 return None 

171 else: 

172 clsmanager = _dive_for_cls_manager(cls) 

173 

174 if clsmanager: 

175 return clsmanager.class_ 

176 else: 

177 return cls 

178 

179 

180def _get_immediate_cls_attr( 

181 cls: Type[Any], attrname: str, strict: bool = False 

182) -> Optional[Any]: 

183 """return an attribute of the class that is either present directly 

184 on the class, e.g. not on a superclass, or is from a superclass but 

185 this superclass is a non-mapped mixin, that is, not a descendant of 

186 the declarative base and is also not classically mapped. 

187 

188 This is used to detect attributes that indicate something about 

189 a mapped class independently from any mapped classes that it may 

190 inherit from. 

191 

192 """ 

193 

194 # the rules are different for this name than others, 

195 # make sure we've moved it out. transitional 

196 assert attrname != "__abstract__" 

197 

198 if not issubclass(cls, object): 

199 return None 

200 

201 if attrname in cls.__dict__: 

202 return getattr(cls, attrname) 

203 

204 for base in cls.__mro__[1:]: 

205 _is_classical_inherits = _dive_for_cls_manager(base) is not None 

206 

207 if attrname in base.__dict__ and ( 

208 base is cls 

209 or ( 

210 (base in cls.__bases__ if strict else True) 

211 and not _is_classical_inherits 

212 ) 

213 ): 

214 return getattr(base, attrname) 

215 else: 

216 return None 

217 

218 

219def _dive_for_cls_manager(cls: Type[_O]) -> Optional[ClassManager[_O]]: 

220 # because the class manager registration is pluggable, 

221 # we need to do the search for every class in the hierarchy, 

222 # rather than just a simple "cls._sa_class_manager" 

223 

224 for base in cls.__mro__: 

225 manager: Optional[ClassManager[_O]] = attributes.opt_manager_of_class( 

226 base 

227 ) 

228 if manager: 

229 return manager 

230 return None 

231 

232 

233@util.preload_module("sqlalchemy.orm.decl_api") 

234def _is_declarative_props(obj: Any) -> bool: 

235 _declared_attr_common = util.preloaded.orm_decl_api._declared_attr_common 

236 

237 return isinstance(obj, (_declared_attr_common, util.classproperty)) 

238 

239 

240def _check_declared_props_nocascade( 

241 obj: Any, name: str, cls: Type[_O] 

242) -> bool: 

243 if _is_declarative_props(obj): 

244 if getattr(obj, "_cascading", False): 

245 util.warn( 

246 "@declared_attr.cascading is not supported on the %s " 

247 "attribute on class %s. This attribute invokes for " 

248 "subclasses in any case." % (name, cls) 

249 ) 

250 return True 

251 else: 

252 return False 

253 

254 

255class _ORMClassConfigurator: 

256 """Object that configures a class that's potentially going to be 

257 mapped, and/or turned into an ORM dataclass. 

258 

259 This is the base class for all the configurator objects. 

260 

261 """ 

262 

263 __slots__ = ("cls", "classname", "__weakref__") 

264 

265 cls: Type[Any] 

266 classname: str 

267 

268 def __init__(self, cls_: Type[Any]): 

269 self.cls = util.assert_arg_type(cls_, type, "cls_") 

270 self.classname = cls_.__name__ 

271 

272 @classmethod 

273 def _as_declarative( 

274 cls, registry: _RegistryType, cls_: Type[Any], dict_: _ClassDict 

275 ) -> Optional[_MapperConfig]: 

276 manager = attributes.opt_manager_of_class(cls_) 

277 if manager and manager.class_ is cls_: 

278 raise exc.InvalidRequestError( 

279 f"Class {cls_!r} already has been instrumented declaratively" 

280 ) 

281 

282 # allow subclassing an orm class with typed columns without 

283 # generating an orm class 

284 if cls_.__dict__.get("__abstract__", False) or issubclass( 

285 cls_, TypedColumns 

286 ): 

287 return None 

288 

289 defer_map = _get_immediate_cls_attr( 

290 cls_, "_sa_decl_prepare_nocascade", strict=True 

291 ) or hasattr(cls_, "_sa_decl_prepare") 

292 

293 if defer_map: 

294 return _DeferredDeclarativeConfig(registry, cls_, dict_) 

295 else: 

296 return _DeclarativeMapperConfig(registry, cls_, dict_) 

297 

298 @classmethod 

299 def _as_unmapped_dataclass( 

300 cls, cls_: Type[Any], dict_: _ClassDict 

301 ) -> _UnmappedDataclassConfig: 

302 return _UnmappedDataclassConfig(cls_, dict_) 

303 

304 @classmethod 

305 def _mapper( 

306 cls, 

307 registry: _RegistryType, 

308 cls_: Type[_O], 

309 table: Optional[FromClause], 

310 mapper_kw: _MapperKwArgs, 

311 ) -> Mapper[_O]: 

312 _ImperativeMapperConfig(registry, cls_, table, mapper_kw) 

313 return cast("MappedClassProtocol[_O]", cls_).__mapper__ 

314 

315 

316class _MapperConfig(_ORMClassConfigurator): 

317 """Configurator that configures a class that's potentially going to be 

318 mapped, and optionally turned into a dataclass as well.""" 

319 

320 __slots__ = ( 

321 "properties", 

322 "declared_attr_reg", 

323 ) 

324 

325 is_declarative = False 

326 

327 properties: util.OrderedDict[ 

328 str, 

329 Union[ 

330 Sequence[NamedColumn[Any]], NamedColumn[Any], MapperProperty[Any] 

331 ], 

332 ] 

333 declared_attr_reg: Dict[declared_attr[Any], Any] 

334 

335 def __init__( 

336 self, 

337 registry: _RegistryType, 

338 cls_: Type[Any], 

339 ): 

340 super().__init__(cls_) 

341 self.properties = util.OrderedDict() 

342 self.declared_attr_reg = {} 

343 

344 instrumentation.register_class( 

345 self.cls, 

346 finalize=False, 

347 registry=registry, 

348 declarative_scan=self, 

349 init_method=registry.constructor, 

350 ) 

351 

352 def set_cls_attribute(self, attrname: str, value: _T) -> _T: 

353 manager = instrumentation.manager_of_class(self.cls) 

354 manager.install_member(attrname, value) 

355 return value 

356 

357 def map(self, mapper_kw: _MapperKwArgs) -> Mapper[Any]: 

358 raise NotImplementedError() 

359 

360 def _early_mapping(self, mapper_kw: _MapperKwArgs) -> None: 

361 self.map(mapper_kw) 

362 

363 

364class _ImperativeMapperConfig(_MapperConfig): 

365 """Configurator that configures a class for an imperative mapping.""" 

366 

367 __slots__ = ("local_table", "inherits") 

368 

369 def __init__( 

370 self, 

371 registry: _RegistryType, 

372 cls_: Type[_O], 

373 table: Optional[FromClause], 

374 mapper_kw: _MapperKwArgs, 

375 ): 

376 super().__init__(registry, cls_) 

377 

378 self.local_table = self.set_cls_attribute("__table__", table) 

379 

380 with mapperlib._CONFIGURE_MUTEX: 

381 clsregistry._add_class( 

382 self.classname, self.cls, registry._class_registry 

383 ) 

384 

385 self._setup_inheritance(mapper_kw) 

386 

387 self._early_mapping(mapper_kw) 

388 

389 def map(self, mapper_kw: _MapperKwArgs = util.EMPTY_DICT) -> Mapper[Any]: 

390 mapper_cls = Mapper 

391 

392 return self.set_cls_attribute( 

393 "__mapper__", 

394 mapper_cls(self.cls, self.local_table, **mapper_kw), 

395 ) 

396 

397 def _setup_inheritance(self, mapper_kw: _MapperKwArgs) -> None: 

398 cls = self.cls 

399 

400 inherits = None 

401 inherits_search = [] 

402 

403 # since we search for classical mappings now, search for 

404 # multiple mapped bases as well and raise an error. 

405 for base_ in cls.__bases__: 

406 c = _resolve_for_abstract_or_classical(base_) 

407 if c is None: 

408 continue 

409 

410 if _is_supercls_for_inherits(c) and c not in inherits_search: 

411 inherits_search.append(c) 

412 

413 if inherits_search: 

414 if len(inherits_search) > 1: 

415 raise exc.InvalidRequestError( 

416 "Class %s has multiple mapped bases: %r" 

417 % (cls, inherits_search) 

418 ) 

419 inherits = inherits_search[0] 

420 

421 self.inherits = inherits 

422 

423 

424class _CollectedAnnotation(NamedTuple): 

425 raw_annotation: _AnnotationScanType 

426 mapped_container: Optional[Type[Mapped[Any]]] 

427 extracted_mapped_annotation: Union[_AnnotationScanType, str] 

428 is_dataclass: bool 

429 attr_value: Any 

430 originating_module: str 

431 originating_class: Type[Any] 

432 

433 

434class _ClassScanAbstractConfig(_ORMClassConfigurator): 

435 """Abstract base for a configurator that configures a class for a 

436 declarative mapping, or an unmapped ORM dataclass. 

437 

438 Defines scanning of pep-484 annotations as well as ORM dataclass 

439 applicators 

440 

441 """ 

442 

443 __slots__ = () 

444 

445 clsdict_view: _ClassDict 

446 collected_annotations: Dict[str, _CollectedAnnotation] 

447 collected_attributes: Dict[str, Any] 

448 

449 is_dataclass_prior_to_mapping: bool 

450 allow_unmapped_annotations: bool 

451 

452 dataclass_setup_arguments: Optional[_DataclassArguments] 

453 """if the class has SQLAlchemy native dataclass parameters, where 

454 we will turn the class into a dataclass within the declarative mapping 

455 process. 

456 

457 """ 

458 

459 allow_dataclass_fields: bool 

460 """if true, look for dataclass-processed Field objects on the target 

461 class as well as superclasses and extract ORM mapping directives from 

462 the "metadata" attribute of each Field. 

463 

464 if False, dataclass fields can still be used, however they won't be 

465 mapped. 

466 

467 """ 

468 

469 _include_dunders = { 

470 "__table__", 

471 "__mapper_args__", 

472 "__tablename__", 

473 "__table_args__", 

474 } 

475 

476 _match_exclude_dunders = re.compile(r"^(?:_sa_|__)") 

477 

478 def _scan_attributes(self) -> None: 

479 raise NotImplementedError() 

480 

481 def _setup_dataclasses_transforms( 

482 self, *, enable_descriptor_defaults: bool, revert: bool = False 

483 ) -> None: 

484 dataclass_setup_arguments = self.dataclass_setup_arguments 

485 if not dataclass_setup_arguments: 

486 return 

487 

488 # can't use is_dataclass since it uses hasattr 

489 if "__dataclass_fields__" in self.cls.__dict__: 

490 raise exc.InvalidRequestError( 

491 f"Class {self.cls} is already a dataclass; ensure that " 

492 "base classes / decorator styles of establishing dataclasses " 

493 "are not being mixed. " 

494 "This can happen if a class that inherits from " 

495 "'MappedAsDataclass', even indirectly, is been mapped with " 

496 "'@registry.mapped_as_dataclass'" 

497 ) 

498 

499 # can't create a dataclass if __table__ is already there. This would 

500 # fail an assertion when calling _get_arguments_for_make_dataclass: 

501 # assert False, "Mapped[] received without a mapping declaration" 

502 if "__table__" in self.cls.__dict__: 

503 raise exc.InvalidRequestError( 

504 f"Class {self.cls} already defines a '__table__'. " 

505 "ORM Annotated Dataclasses do not support a pre-existing " 

506 "'__table__' element" 

507 ) 

508 

509 raise_for_non_dc_attrs = collections.defaultdict(list) 

510 

511 def _allow_dataclass_field( 

512 key: str, originating_class: Type[Any] 

513 ) -> bool: 

514 if ( 

515 originating_class is not self.cls 

516 and "__dataclass_fields__" not in originating_class.__dict__ 

517 ): 

518 raise_for_non_dc_attrs[originating_class].append(key) 

519 

520 return True 

521 

522 field_list = [ 

523 _AttributeOptions._get_arguments_for_make_dataclass( 

524 self, 

525 key, 

526 anno, 

527 mapped_container, 

528 self.collected_attributes.get(key, _NoArg.NO_ARG), 

529 dataclass_setup_arguments, 

530 enable_descriptor_defaults, 

531 ) 

532 for key, anno, mapped_container in ( 

533 ( 

534 key, 

535 raw_anno, 

536 mapped_container, 

537 ) 

538 for key, ( 

539 raw_anno, 

540 mapped_container, 

541 mapped_anno, 

542 is_dc, 

543 attr_value, 

544 originating_module, 

545 originating_class, 

546 ) in self.collected_annotations.items() 

547 if not _is_unmapped_container(mapped_container) 

548 and _allow_dataclass_field(key, originating_class) 

549 and ( 

550 key not in self.collected_attributes 

551 # issue #9226; check for attributes that we've collected 

552 # which are already instrumented, which we would assume 

553 # mean we are in an ORM inheritance mapping and this 

554 # attribute is already mapped on the superclass. Under 

555 # no circumstance should any QueryableAttribute be sent to 

556 # the dataclass() function; anything that's mapped should 

557 # be Field and that's it 

558 or not isinstance( 

559 self.collected_attributes[key], QueryableAttribute 

560 ) 

561 ) 

562 ) 

563 ] 

564 if raise_for_non_dc_attrs: 

565 for ( 

566 originating_class, 

567 non_dc_attrs, 

568 ) in raise_for_non_dc_attrs.items(): 

569 raise exc.InvalidRequestError( 

570 f"When transforming {self.cls} to a dataclass, " 

571 f"attribute(s) " 

572 f"{', '.join(repr(key) for key in non_dc_attrs)} " 

573 f"originates from superclass " 

574 f"{originating_class}, which is not a dataclass. When " 

575 f"declaring SQLAlchemy Declarative " 

576 f"Dataclasses, ensure that all mixin classes and other " 

577 f"superclasses which include attributes are also a " 

578 f"subclass of MappedAsDataclass or make use of the " 

579 f"@unmapped_dataclass decorator.", 

580 code="dcmx", 

581 ) 

582 

583 if revert: 

584 # the "revert" case is used only by an unmapped mixin class 

585 # that is nonetheless using Mapped construct and needs to 

586 # itself be a dataclass. revert_dict includes all non-dunder 

587 # attributes, not just fields, in class __dict__ order, so that 

588 # the finally: block below can restore that order 

589 revert_dict = { 

590 name: value 

591 for name, value in self.cls.__dict__.items() 

592 if not util.dunders_re.match(name) 

593 } 

594 else: 

595 revert_dict = None 

596 

597 # get original annotations using ForwardRef for symbols that 

598 # are unresolvable. Unmapped[] attributes are not part of the 

599 # dataclass, so omit their annotations, #13650 

600 orig_annotations = { 

601 k: v 

602 for k, v in util.get_annotations(self.cls).items() 

603 if k not in self.collected_annotations 

604 or not _is_unmapped_container( 

605 self.collected_annotations[k].mapped_container 

606 ) 

607 } 

608 

609 # build a new __annotations__ dict from the fields we have. 

610 # this has to be done carefully since we have to maintain 

611 # the correct order! wow 

612 swap_annotations = {} 

613 defaults = {} 

614 

615 for item in field_list: 

616 if len(item) == 2: 

617 name, tp = item 

618 elif len(item) == 3: 

619 name, tp, spec = item 

620 defaults[name] = spec 

621 else: 

622 assert False 

623 

624 # add the annotation to the new dict we are creating. 

625 # note that if name is in orig_annotations, we expect 

626 # tp and orig_annotations[name] to be identical. 

627 swap_annotations[name] = orig_annotations.get(name, tp) 

628 

629 for k, v in defaults.items(): 

630 setattr(self.cls, k, v) 

631 

632 self._assert_dc_arguments(dataclass_setup_arguments) 

633 

634 dataclass_callable = dataclass_setup_arguments["dataclass_callable"] 

635 if dataclass_callable is _NoArg.NO_ARG: 

636 dataclass_callable = dataclasses.dataclass 

637 

638 # create a merged __annotations__ dictionary, maintaining order 

639 # as best we can: 

640 

641 # 1. merge all keys in orig_annotations that occur before 

642 # we see any of our mapped fields (this can be attributes like 

643 # __table_args__ etc.) 

644 new_annotations = { 

645 k: orig_annotations[k] 

646 for k in itertools.takewhile( 

647 lambda k: k not in swap_annotations, orig_annotations 

648 ) 

649 } 

650 

651 # 2. then put in all the dataclass annotations we have 

652 new_annotations |= swap_annotations 

653 

654 # 3. them merge all of orig_annotations which will add remaining 

655 # keys 

656 new_annotations |= orig_annotations 

657 

658 # 4. this becomes the new class annotations. 

659 restore_anno = util.restore_annotations(self.cls, new_annotations) 

660 

661 try: 

662 dataclass_callable( # type: ignore[call-overload] 

663 self.cls, 

664 **{ # type: ignore[call-overload,unused-ignore] 

665 k: v 

666 for k, v in dataclass_setup_arguments.items() 

667 if v is not _NoArg.NO_ARG 

668 and k not in ("dataclass_callable",) 

669 }, 

670 ) 

671 except (TypeError, ValueError) as ex: 

672 raise exc.InvalidRequestError( 

673 f"Python dataclasses error encountered when creating " 

674 f"dataclass for {self.cls.__name__!r}: " 

675 f"{ex!r}. Please refer to Python dataclasses " 

676 "documentation for additional information.", 

677 code="dcte", 

678 ) from ex 

679 else: 

680 # as of Python 3.15, dataclasses no longer renders the 

681 # auto-generated class docstring immediately; it instead installs 

682 # a descriptor that renders the ``__init__`` signature the first 

683 # time ``__doc__`` is accessed (see 

684 # ``dataclasses._AutoDocstring``). As the ``finally:`` block 

685 # below puts the class' original annotations back, that deferred 

686 # render would no longer see the dataclass-oriented annotations 

687 # we swapped in above, and would omit them from the docstring 

688 # entirely. Read the attribute now, while those annotations are 

689 # still in place, so the docstring we generate is the same on 

690 # every Python version. 

691 self.cls.__doc__ 

692 

693 finally: 

694 if revert and revert_dict: 

695 # used for mixin dataclasses; we have to restore the 

696 # mapped_column(), relationship() etc. to the class so these 

697 # take place for a mapped class scan. dataclasses has 

698 # deleted the attribute for fields with no plain default and 

699 # replaced it in place for the others, so delete and re-set 

700 # every attribute, restoring declaration order for the scan, 

701 # #13634 

702 for k, v in revert_dict.items(): 

703 if k in self.cls.__dict__: 

704 delattr(self.cls, k) 

705 setattr(self.cls, k, v) 

706 

707 restore_anno() 

708 

709 def _collect_annotation( 

710 self, 

711 name: str, 

712 raw_annotation: _AnnotationScanType, 

713 originating_class: Type[Any], 

714 expect_mapped: Optional[bool], 

715 attr_value: Any, 

716 ) -> Optional[_CollectedAnnotation]: 

717 if name in self.collected_annotations: 

718 return self.collected_annotations[name] 

719 

720 if raw_annotation is None: 

721 return None 

722 

723 is_dataclass = self.is_dataclass_prior_to_mapping 

724 allow_unmapped = self.allow_unmapped_annotations 

725 

726 if expect_mapped is None: 

727 is_dataclass_field = isinstance(attr_value, dataclasses.Field) 

728 expect_mapped = ( 

729 not is_dataclass_field 

730 and not allow_unmapped 

731 and ( 

732 attr_value is None 

733 or isinstance(attr_value, _MappedAttribute) 

734 ) 

735 ) 

736 

737 is_dataclass_field = False 

738 extracted = _extract_mapped_subtype( 

739 raw_annotation, 

740 self.cls, 

741 originating_class.__module__, 

742 name, 

743 type(attr_value), 

744 required=False, 

745 is_dataclass_field=is_dataclass_field, 

746 expect_mapped=expect_mapped and not is_dataclass, 

747 ) 

748 if extracted is None: 

749 # ClassVar can come out here 

750 return None 

751 

752 extracted_mapped_annotation, mapped_container = extracted 

753 

754 if attr_value is None and not is_literal(extracted_mapped_annotation): 

755 for elem in get_args(extracted_mapped_annotation): 

756 if is_fwd_ref( 

757 elem, check_generic=True, check_for_plain_string=True 

758 ): 

759 elem = de_stringify_annotation( 

760 self.cls, 

761 elem, 

762 originating_class.__module__, 

763 include_generic=True, 

764 ) 

765 # look in Annotated[...] for an ORM construct, 

766 # such as Annotated[int, mapped_column(primary_key=True)] 

767 if isinstance(elem, _IntrospectsAnnotations): 

768 attr_value = elem.found_in_pep593_annotated() 

769 

770 # check for use of the above Annotated[int, 

771 # mapped_column(...)] inside of Unmapped[]. if so, migrate 

772 # the mapped_column() to be an UnmappedColumn instead. 

773 if _is_unmapped_container(mapped_container) and isinstance( 

774 attr_value, MappedColumn 

775 ): 

776 attr_value = attr_value._as_unmapped_column() 

777 

778 self.collected_annotations[name] = ca = _CollectedAnnotation( 

779 raw_annotation, 

780 mapped_container, 

781 extracted_mapped_annotation, 

782 is_dataclass, 

783 attr_value, 

784 originating_class.__module__, 

785 originating_class, 

786 ) 

787 return ca 

788 

789 @classmethod 

790 def _assert_dc_arguments(cls, arguments: _DataclassArguments) -> None: 

791 allowed = { 

792 "init", 

793 "repr", 

794 "order", 

795 "eq", 

796 "unsafe_hash", 

797 "kw_only", 

798 "match_args", 

799 "dataclass_callable", 

800 } 

801 disallowed_args = set(arguments).difference(allowed) 

802 if disallowed_args: 

803 msg = ", ".join(f"{arg!r}" for arg in sorted(disallowed_args)) 

804 raise exc.ArgumentError( 

805 f"Dataclass argument(s) {msg} are not accepted" 

806 ) 

807 

808 def _cls_attr_override_checker( 

809 self, cls: Type[_O] 

810 ) -> Callable[[str, Any], bool]: 

811 """Produce a function that checks if a class has overridden an 

812 attribute, taking SQLAlchemy-enabled dataclass fields into account. 

813 

814 """ 

815 

816 if self.allow_dataclass_fields: 

817 sa_dataclass_metadata_key = _get_immediate_cls_attr( 

818 cls, "__sa_dataclass_metadata_key__" 

819 ) 

820 else: 

821 sa_dataclass_metadata_key = None 

822 

823 if not sa_dataclass_metadata_key: 

824 

825 def attribute_is_overridden(key: str, obj: Any) -> bool: 

826 return getattr(cls, key, obj) is not obj 

827 

828 else: 

829 all_datacls_fields = { 

830 f.name: f.metadata[sa_dataclass_metadata_key] 

831 for f in util.dataclass_fields(cls) 

832 if sa_dataclass_metadata_key in f.metadata 

833 } 

834 local_datacls_fields = { 

835 f.name: f.metadata[sa_dataclass_metadata_key] 

836 for f in util.local_dataclass_fields(cls) 

837 if sa_dataclass_metadata_key in f.metadata 

838 } 

839 

840 absent = object() 

841 

842 def attribute_is_overridden(key: str, obj: Any) -> bool: 

843 if _is_declarative_props(obj): 

844 obj = obj.fget 

845 

846 # this function likely has some failure modes still if 

847 # someone is doing a deep mixing of the same attribute 

848 # name as plain Python attribute vs. dataclass field. 

849 

850 ret = local_datacls_fields.get(key, absent) 

851 if _is_declarative_props(ret): 

852 ret = ret.fget 

853 

854 if ret is obj: 

855 return False 

856 elif ret is not absent: 

857 return True 

858 

859 all_field = all_datacls_fields.get(key, absent) 

860 

861 ret = getattr(cls, key, obj) 

862 

863 if ret is obj: 

864 return False 

865 

866 # for dataclasses, this could be the 

867 # 'default' of the field. so filter more specifically 

868 # for an already-mapped InstrumentedAttribute 

869 if ret is not absent and isinstance( 

870 ret, InstrumentedAttribute 

871 ): 

872 return True 

873 

874 if all_field is obj: 

875 return False 

876 elif all_field is not absent: 

877 return True 

878 

879 # can't find another attribute 

880 return False 

881 

882 return attribute_is_overridden 

883 

884 def _cls_attr_resolver( 

885 self, cls: Type[Any] 

886 ) -> Callable[[], Iterable[Tuple[str, Any, Any, bool]]]: 

887 """produce a function to iterate the "attributes" of a class 

888 which we want to consider for mapping, adjusting for SQLAlchemy fields 

889 embedded in dataclass fields. 

890 

891 """ 

892 cls_annotations = util.get_annotations(cls) 

893 

894 cls_vars = vars(cls) 

895 

896 _include_dunders = self._include_dunders 

897 _match_exclude_dunders = self._match_exclude_dunders 

898 

899 names = [ 

900 n 

901 for n in util.merge_lists_w_ordering( 

902 list(cls_vars), list(cls_annotations) 

903 ) 

904 if not _match_exclude_dunders.match(n) or n in _include_dunders 

905 ] 

906 

907 if self.allow_dataclass_fields: 

908 sa_dataclass_metadata_key: Optional[str] = _get_immediate_cls_attr( 

909 cls, "__sa_dataclass_metadata_key__" 

910 ) 

911 else: 

912 sa_dataclass_metadata_key = None 

913 

914 if not sa_dataclass_metadata_key: 

915 

916 def local_attributes_for_class() -> ( 

917 Iterable[Tuple[str, Any, Any, bool]] 

918 ): 

919 return ( 

920 ( 

921 name, 

922 cls_vars.get(name), 

923 cls_annotations.get(name), 

924 False, 

925 ) 

926 for name in names 

927 ) 

928 

929 else: 

930 dataclass_fields = { 

931 field.name: field for field in util.local_dataclass_fields(cls) 

932 } 

933 

934 fixed_sa_dataclass_metadata_key = sa_dataclass_metadata_key 

935 

936 def local_attributes_for_class() -> ( 

937 Iterable[Tuple[str, Any, Any, bool]] 

938 ): 

939 for name in names: 

940 field = dataclass_fields.get(name, None) 

941 if field and sa_dataclass_metadata_key in field.metadata: 

942 yield field.name, _as_dc_declaredattr( 

943 field.metadata, fixed_sa_dataclass_metadata_key 

944 ), cls_annotations.get(field.name), True 

945 else: 

946 yield name, cls_vars.get(name), cls_annotations.get( 

947 name 

948 ), False 

949 

950 return local_attributes_for_class 

951 

952 

953class _DeclarativeMapperConfig(_MapperConfig, _ClassScanAbstractConfig): 

954 """Configurator that will produce a declarative mapped class""" 

955 

956 __slots__ = ( 

957 "registry", 

958 "local_table", 

959 "persist_selectable", 

960 "declared_columns", 

961 "unmapped_columns", 

962 "column_ordering", 

963 "column_copies", 

964 "table_args", 

965 "tablename", 

966 "mapper_args", 

967 "mapper_args_fn", 

968 "table_fn", 

969 "inherits", 

970 "single", 

971 "clsdict_view", 

972 "collected_attributes", 

973 "collected_annotations", 

974 "allow_dataclass_fields", 

975 "dataclass_setup_arguments", 

976 "is_dataclass_prior_to_mapping", 

977 "allow_unmapped_annotations", 

978 ) 

979 

980 is_declarative = True 

981 is_deferred = False 

982 registry: _RegistryType 

983 local_table: Optional[FromClause] 

984 persist_selectable: Optional[FromClause] 

985 declared_columns: util.OrderedSet[Column[Any]] 

986 unmapped_columns: util.OrderedSet[Column[Any]] 

987 column_ordering: Dict[Column[Any], int] 

988 column_copies: Dict[ 

989 Union[MappedColumn[Any], Column[Any]], 

990 Union[MappedColumn[Any], Column[Any]], 

991 ] 

992 tablename: Optional[str] 

993 mapper_args: Mapping[str, Any] 

994 table_args: Optional[_TableArgsType] 

995 mapper_args_fn: Optional[Callable[[], Dict[str, Any]]] 

996 inherits: Optional[Type[Any]] 

997 single: bool 

998 

999 def __init__( 

1000 self, 

1001 registry: _RegistryType, 

1002 cls_: Type[_O], 

1003 dict_: _ClassDict, 

1004 ): 

1005 # grab class dict before the instrumentation manager has been added. 

1006 # reduces cycles 

1007 self.clsdict_view = ( 

1008 util.immutabledict(dict_) if dict_ else util.EMPTY_DICT 

1009 ) 

1010 super().__init__(registry, cls_) 

1011 self.registry = registry 

1012 self.persist_selectable = None 

1013 

1014 self.collected_attributes = {} 

1015 self.collected_annotations = {} 

1016 self.declared_columns = util.OrderedSet() 

1017 self.unmapped_columns = util.OrderedSet() 

1018 self.column_ordering = {} 

1019 self.column_copies = {} 

1020 self.single = False 

1021 self.dataclass_setup_arguments = dca = getattr( 

1022 self.cls, "_sa_apply_dc_transforms", None 

1023 ) 

1024 

1025 self.allow_unmapped_annotations = getattr( 

1026 self.cls, "__allow_unmapped__", False 

1027 ) or bool(self.dataclass_setup_arguments) 

1028 

1029 self.is_dataclass_prior_to_mapping = cld = dataclasses.is_dataclass( 

1030 cls_ 

1031 ) 

1032 

1033 sdk = _get_immediate_cls_attr(cls_, "__sa_dataclass_metadata_key__") 

1034 

1035 # we don't want to consume Field objects from a not-already-dataclass. 

1036 # the Field objects won't have their "name" or "type" populated, 

1037 # and while it seems like we could just set these on Field as we 

1038 # read them, Field is documented as "user read only" and we need to 

1039 # stay far away from any off-label use of dataclasses APIs. 

1040 if (not cld or dca) and sdk: 

1041 raise exc.InvalidRequestError( 

1042 "SQLAlchemy mapped dataclasses can't consume mapping " 

1043 "information from dataclass.Field() objects if the immediate " 

1044 "class is not already a dataclass." 

1045 ) 

1046 

1047 # if already a dataclass, and __sa_dataclass_metadata_key__ present, 

1048 # then also look inside of dataclass.Field() objects yielded by 

1049 # dataclasses.get_fields(cls) when scanning for attributes 

1050 self.allow_dataclass_fields = bool(sdk and cld) 

1051 

1052 self._scan_attributes() 

1053 

1054 self._setup_dataclasses_transforms(enable_descriptor_defaults=True) 

1055 

1056 with mapperlib._CONFIGURE_MUTEX: 

1057 clsregistry._add_class( 

1058 self.classname, self.cls, registry._class_registry 

1059 ) 

1060 

1061 self._setup_inheriting_mapper() 

1062 

1063 self._extract_mappable_attributes() 

1064 

1065 self._extract_declared_columns() 

1066 

1067 self._setup_table() 

1068 

1069 self._setup_inheriting_columns() 

1070 

1071 self._early_mapping(util.EMPTY_DICT) 

1072 

1073 def _scan_attributes(self) -> None: 

1074 cls = self.cls 

1075 

1076 cls_as_Decl = cast("_DeclMappedClassProtocol[Any]", cls) 

1077 

1078 clsdict_view = self.clsdict_view 

1079 collected_attributes = self.collected_attributes 

1080 column_copies = self.column_copies 

1081 _include_dunders = self._include_dunders 

1082 mapper_args_fn = None 

1083 table_args = inherited_table_args = None 

1084 table_fn = None 

1085 tablename = None 

1086 fixed_table = "__table__" in clsdict_view 

1087 

1088 attribute_is_overridden = self._cls_attr_override_checker(self.cls) 

1089 

1090 bases = [] 

1091 

1092 for base in cls.__mro__: 

1093 # collect bases and make sure standalone columns are copied 

1094 # to be the column they will ultimately be on the class, 

1095 # so that declared_attr functions use the right columns. 

1096 # need to do this all the way up the hierarchy first 

1097 # (see #8190) 

1098 

1099 class_mapped = base is not cls and _is_supercls_for_inherits(base) 

1100 

1101 local_attributes_for_class = self._cls_attr_resolver(base) 

1102 

1103 if not class_mapped and base is not cls: 

1104 locally_collected_columns = self._produce_column_copies( 

1105 local_attributes_for_class, 

1106 attribute_is_overridden, 

1107 fixed_table, 

1108 base, 

1109 ) 

1110 else: 

1111 locally_collected_columns = {} 

1112 

1113 bases.append( 

1114 ( 

1115 base, 

1116 class_mapped, 

1117 local_attributes_for_class, 

1118 locally_collected_columns, 

1119 ) 

1120 ) 

1121 

1122 for ( 

1123 base, 

1124 class_mapped, 

1125 local_attributes_for_class, 

1126 locally_collected_columns, 

1127 ) in bases: 

1128 # this transfer can also take place as we scan each name 

1129 # for finer-grained control of how collected_attributes is 

1130 # populated, as this is what impacts column ordering. 

1131 # however it's simpler to get it out of the way here. 

1132 collected_attributes.update(locally_collected_columns) 

1133 

1134 for ( 

1135 name, 

1136 obj, 

1137 annotation, 

1138 is_dataclass_field, 

1139 ) in local_attributes_for_class(): 

1140 if name in _include_dunders: 

1141 if name == "__mapper_args__": 

1142 check_decl = _check_declared_props_nocascade( 

1143 obj, name, cls 

1144 ) 

1145 if not mapper_args_fn and ( 

1146 not class_mapped or check_decl 

1147 ): 

1148 # don't even invoke __mapper_args__ until 

1149 # after we've determined everything about the 

1150 # mapped table. 

1151 # make a copy of it so a class-level dictionary 

1152 # is not overwritten when we update column-based 

1153 # arguments. 

1154 def _mapper_args_fn() -> Dict[str, Any]: 

1155 return dict(cls_as_Decl.__mapper_args__) 

1156 

1157 mapper_args_fn = _mapper_args_fn 

1158 

1159 elif name == "__tablename__": 

1160 check_decl = _check_declared_props_nocascade( 

1161 obj, name, cls 

1162 ) 

1163 if not tablename and (not class_mapped or check_decl): 

1164 tablename = cls_as_Decl.__tablename__ 

1165 elif name == "__table__": 

1166 check_decl = _check_declared_props_nocascade( 

1167 obj, name, cls 

1168 ) 

1169 # if a @declared_attr using "__table__" is detected, 

1170 # wrap up a callable to look for "__table__" from 

1171 # the final concrete class when we set up a table. 

1172 # this was fixed by 

1173 # #11509, regression in 2.0 from version 1.4. 

1174 if check_decl and not table_fn: 

1175 # don't even invoke __table__ until we're ready 

1176 def _table_fn() -> FromClause: 

1177 return cls_as_Decl.__table__ 

1178 

1179 table_fn = _table_fn 

1180 

1181 elif name == "__table_args__": 

1182 check_decl = _check_declared_props_nocascade( 

1183 obj, name, cls 

1184 ) 

1185 if not table_args and (not class_mapped or check_decl): 

1186 table_args = cls_as_Decl.__table_args__ 

1187 if not isinstance( 

1188 table_args, (tuple, dict, type(None)) 

1189 ): 

1190 raise exc.ArgumentError( 

1191 "__table_args__ value must be a tuple, " 

1192 "dict, or None" 

1193 ) 

1194 if base is not cls: 

1195 inherited_table_args = True 

1196 else: 

1197 # any other dunder names; should not be here 

1198 # as we have tested for all four names in 

1199 # _include_dunders 

1200 assert False 

1201 elif class_mapped: 

1202 if _is_declarative_props(obj) and not obj._quiet: 

1203 util.warn( 

1204 "Regular (i.e. not __special__) " 

1205 "attribute '%s.%s' uses @declared_attr, " 

1206 "but owning class %s is mapped - " 

1207 "not applying to subclass %s." 

1208 % (base.__name__, name, base, cls) 

1209 ) 

1210 

1211 continue 

1212 elif base is not cls: 

1213 # we're a mixin, abstract base, or something that is 

1214 # acting like that for now. 

1215 

1216 if isinstance(obj, (Column, MappedColumn)): 

1217 # already copied columns to the mapped class. 

1218 continue 

1219 elif isinstance(obj, MapperProperty): 

1220 raise exc.InvalidRequestError( 

1221 "Mapper properties (i.e. deferred," 

1222 "column_property(), relationship(), etc.) must " 

1223 "be declared as @declared_attr callables " 

1224 "on declarative mixin classes. For dataclass " 

1225 "field() objects, use a lambda:" 

1226 ) 

1227 elif _is_declarative_props(obj): 

1228 # tried to get overloads to tell this to 

1229 # pylance, no luck 

1230 assert obj is not None 

1231 

1232 if obj._cascading: 

1233 if name in clsdict_view: 

1234 # unfortunately, while we can use the user- 

1235 # defined attribute here to allow a clean 

1236 # override, if there's another 

1237 # subclass below then it still tries to use 

1238 # this. not sure if there is enough 

1239 # information here to add this as a feature 

1240 # later on. 

1241 util.warn( 

1242 "Attribute '%s' on class %s cannot be " 

1243 "processed due to " 

1244 "@declared_attr.cascading; " 

1245 "skipping" % (name, cls) 

1246 ) 

1247 collected_attributes[name] = column_copies[obj] = ( 

1248 ret 

1249 ) = obj.__get__(obj, cls) 

1250 setattr(cls, name, ret) 

1251 else: 

1252 if is_dataclass_field: 

1253 # access attribute using normal class access 

1254 # first, to see if it's been mapped on a 

1255 # superclass. note if the dataclasses.field() 

1256 # has "default", this value can be anything. 

1257 ret = getattr(cls, name, None) 

1258 

1259 # so, if it's anything that's not ORM 

1260 # mapped, assume we should invoke the 

1261 # declared_attr 

1262 if not isinstance(ret, InspectionAttr): 

1263 ret = obj.fget() 

1264 else: 

1265 # access attribute using normal class access. 

1266 # if the declared attr already took place 

1267 # on a superclass that is mapped, then 

1268 # this is no longer a declared_attr, it will 

1269 # be the InstrumentedAttribute 

1270 ret = getattr(cls, name) 

1271 

1272 # correct for proxies created from hybrid_property 

1273 # or similar. note there is no known case that 

1274 # produces nested proxies, so we are only 

1275 # looking one level deep right now. 

1276 

1277 if ( 

1278 isinstance(ret, InspectionAttr) 

1279 and attr_is_internal_proxy(ret) 

1280 and not isinstance( 

1281 ret.original_property, MapperProperty 

1282 ) 

1283 ): 

1284 ret = ret.descriptor 

1285 

1286 collected_attributes[name] = column_copies[obj] = ( 

1287 ret 

1288 ) 

1289 

1290 if ( 

1291 isinstance(ret, (Column, MapperProperty)) 

1292 and ret.doc is None 

1293 ): 

1294 ret.doc = obj.__doc__ 

1295 

1296 self._collect_annotation( 

1297 name, 

1298 obj._collect_return_annotation(), 

1299 base, 

1300 True, 

1301 obj, 

1302 ) 

1303 elif _is_mapped_annotation(annotation, cls, base): 

1304 # Mapped annotation without any object. 

1305 # product_column_copies should have handled this. 

1306 # if future support for other MapperProperty, 

1307 # then test if this name is already handled and 

1308 # otherwise proceed to generate. 

1309 if not fixed_table: 

1310 assert ( 

1311 name in collected_attributes 

1312 or attribute_is_overridden(name, None) 

1313 ) 

1314 continue 

1315 else: 

1316 # here, the attribute is some other kind of 

1317 # property that we assume is not part of the 

1318 # declarative mapping. however, check for some 

1319 # more common mistakes 

1320 self._warn_for_decl_attributes(base, name, obj) 

1321 elif is_dataclass_field and ( 

1322 name not in clsdict_view or clsdict_view[name] is not obj 

1323 ): 

1324 # here, we are definitely looking at the target class 

1325 # and not a superclass. this is currently a 

1326 # dataclass-only path. if the name is only 

1327 # a dataclass field and isn't in local cls.__dict__, 

1328 # put the object there. 

1329 # assert that the dataclass-enabled resolver agrees 

1330 # with what we are seeing 

1331 

1332 assert not attribute_is_overridden(name, obj) 

1333 

1334 if _is_declarative_props(obj): 

1335 obj = obj.fget() 

1336 

1337 collected_attributes[name] = obj 

1338 self._collect_annotation( 

1339 name, annotation, base, False, obj 

1340 ) 

1341 else: 

1342 collected_annotation = self._collect_annotation( 

1343 name, annotation, base, None, obj 

1344 ) 

1345 is_mapped = ( 

1346 collected_annotation is not None 

1347 and collected_annotation.mapped_container is not None 

1348 ) 

1349 if obj is None and not fixed_table and is_mapped: 

1350 assert collected_annotation is not None 

1351 collected_attributes[name] = ( 

1352 _mapped_column_for_annotation(collected_annotation) 

1353 ) 

1354 elif name in clsdict_view: 

1355 collected_attributes[name] = obj 

1356 # else if the name is not in the cls.__dict__, 

1357 # don't collect it as an attribute. 

1358 # we will see the annotation only, which is meaningful 

1359 # both for mapping and dataclasses setup 

1360 

1361 if inherited_table_args and not tablename: 

1362 table_args = None 

1363 

1364 self.table_args = table_args 

1365 self.tablename = tablename 

1366 self.mapper_args_fn = mapper_args_fn 

1367 self.table_fn = table_fn 

1368 

1369 @classmethod 

1370 def _update_annotations_for_non_mapped_class( 

1371 cls, klass: Type[_O] 

1372 ) -> Mapping[str, _AnnotationScanType]: 

1373 cls_annotations = util.get_annotations(klass) 

1374 

1375 new_anno = {} 

1376 for name, annotation in cls_annotations.items(): 

1377 if _is_mapped_annotation(annotation, klass, klass): 

1378 extracted = _extract_mapped_subtype( 

1379 annotation, 

1380 klass, 

1381 klass.__module__, 

1382 name, 

1383 type(None), 

1384 required=False, 

1385 is_dataclass_field=False, 

1386 expect_mapped=False, 

1387 ) 

1388 if extracted: 

1389 inner, _ = extracted 

1390 new_anno[name] = inner 

1391 else: 

1392 new_anno[name] = annotation 

1393 return new_anno 

1394 

1395 def _warn_for_decl_attributes( 

1396 self, cls: Type[Any], key: str, c: Any 

1397 ) -> None: 

1398 if isinstance(c, expression.ColumnElement): 

1399 util.warn( 

1400 f"Attribute '{key}' on class {cls} appears to " 

1401 "be a non-schema SQLAlchemy expression " 

1402 "object; this won't be part of the declarative mapping. " 

1403 "To map arbitrary expressions, use ``column_property()`` " 

1404 "or a similar function such as ``deferred()``, " 

1405 "``query_expression()`` etc. " 

1406 ) 

1407 

1408 def _produce_column_copies( 

1409 self, 

1410 attributes_for_class: Callable[ 

1411 [], Iterable[Tuple[str, Any, Any, bool]] 

1412 ], 

1413 attribute_is_overridden: Callable[[str, Any], bool], 

1414 fixed_table: bool, 

1415 originating_class: Type[Any], 

1416 ) -> Dict[str, Union[Column[Any], MappedColumn[Any]]]: 

1417 cls = self.cls 

1418 dict_ = self.clsdict_view 

1419 locally_collected_attributes: Dict[ 

1420 str, Union[Column[Any], MappedColumn[Any]] 

1421 ] = {} 

1422 column_copies = self.column_copies 

1423 # copy mixin columns to the mapped class 

1424 

1425 for name, obj, annotation, is_dataclass in attributes_for_class(): 

1426 if ( 

1427 not fixed_table 

1428 and obj is None 

1429 and _is_mapped_annotation(annotation, cls, originating_class) 

1430 ): 

1431 # obj is None means this is the annotation only path 

1432 

1433 if attribute_is_overridden(name, obj): 

1434 # perform same "overridden" check as we do for 

1435 # Column/MappedColumn, this is how a mixin col is not 

1436 # applied to an inherited subclass that does not have 

1437 # the mixin. the anno-only path added here for 

1438 # #9564 

1439 continue 

1440 

1441 collected_annotation = self._collect_annotation( 

1442 name, annotation, originating_class, True, obj 

1443 ) 

1444 assert collected_annotation is not None 

1445 mapped_col = _mapped_column_for_annotation( 

1446 collected_annotation 

1447 ) 

1448 

1449 locally_collected_attributes[name] = mapped_col 

1450 setattr(cls, name, mapped_col) 

1451 

1452 elif isinstance(obj, (Column, MappedColumn)): 

1453 if attribute_is_overridden(name, obj): 

1454 # if column has been overridden 

1455 # (like by the InstrumentedAttribute of the 

1456 # superclass), skip. don't collect the annotation 

1457 # either (issue #8718) 

1458 continue 

1459 

1460 collected_annotation = self._collect_annotation( 

1461 name, annotation, originating_class, True, obj 

1462 ) 

1463 obj = ( 

1464 collected_annotation.attr_value 

1465 if collected_annotation is not None 

1466 else obj 

1467 ) 

1468 

1469 if name not in dict_ and not ( 

1470 "__table__" in dict_ 

1471 and (getattr(obj, "name", None) or name) 

1472 in dict_["__table__"].c 

1473 ): 

1474 if obj.foreign_keys: 

1475 for fk in obj.foreign_keys: 

1476 if ( 

1477 fk._table_column is not None 

1478 and fk._table_column.table is None 

1479 ): 

1480 raise exc.InvalidRequestError( 

1481 "Columns with foreign keys to " 

1482 "non-table-bound " 

1483 "columns must be declared as " 

1484 "@declared_attr callables " 

1485 "on declarative mixin classes. " 

1486 "For dataclass " 

1487 "field() objects, use a lambda:." 

1488 ) 

1489 

1490 column_copies[obj] = copy_ = obj._copy() 

1491 

1492 locally_collected_attributes[name] = copy_ 

1493 setattr(cls, name, copy_) 

1494 

1495 return locally_collected_attributes 

1496 

1497 def _extract_mappable_attributes(self) -> None: 

1498 cls = self.cls 

1499 collected_attributes = self.collected_attributes 

1500 

1501 our_stuff = self.properties 

1502 

1503 _include_dunders = self._include_dunders 

1504 

1505 late_mapped = _get_immediate_cls_attr( 

1506 cls, "_sa_decl_prepare_nocascade", strict=True 

1507 ) 

1508 

1509 allow_unmapped_annotations = self.allow_unmapped_annotations 

1510 expect_annotations_wo_mapped = ( 

1511 allow_unmapped_annotations or self.is_dataclass_prior_to_mapping 

1512 ) 

1513 

1514 look_for_dataclass_things = bool(self.dataclass_setup_arguments) 

1515 

1516 for k in list(collected_attributes): 

1517 if k in _include_dunders: 

1518 continue 

1519 

1520 value = collected_attributes[k] 

1521 

1522 if _is_declarative_props(value): 

1523 # @declared_attr in collected_attributes only occurs here for a 

1524 # @declared_attr that's directly on the mapped class; 

1525 # for a mixin, these have already been evaluated 

1526 if value._cascading: 

1527 util.warn( 

1528 "Use of @declared_attr.cascading only applies to " 

1529 "Declarative 'mixin' and 'abstract' classes. " 

1530 "Currently, this flag is ignored on mapped class " 

1531 "%s" % self.cls 

1532 ) 

1533 

1534 value = getattr(cls, k) 

1535 

1536 elif ( 

1537 isinstance(value, QueryableAttribute) 

1538 and value.class_ is not cls 

1539 and value.key != k 

1540 ): 

1541 # detect a QueryableAttribute that's already mapped being 

1542 # assigned elsewhere in userland, turn into a synonym() 

1543 value = SynonymProperty(value.key) 

1544 setattr(cls, k, value) 

1545 

1546 if k == "registry" or ( 

1547 k == "metadata" and not isinstance(value, MetaData) 

1548 ): 

1549 noun = { 

1550 "metadata": "MetaData instance", 

1551 "registry": "Declarative class registry", 

1552 } 

1553 util.warn( 

1554 f"Attribute name '{k}' should be left reserved for the " 

1555 f"{noun[k]} when using a Declarative class." 

1556 ) 

1557 

1558 # Unmapped[] and unmapped_column() must be used together 

1559 collected_annotation = self.collected_annotations.get(k) 

1560 anno_container = ( 

1561 collected_annotation.mapped_container 

1562 if collected_annotation is not None 

1563 else None 

1564 ) 

1565 if anno_container is None: 

1566 pass 

1567 elif _is_unmapped_container(anno_container): 

1568 if not isinstance(value, UnmappedColumn): 

1569 raise exc.ArgumentError( 

1570 f'Attribute "{cls.__name__}.{k}" uses the Unmapped[] ' 

1571 "annotation, which must be used with " 

1572 "unmapped_column() or with no value on the right side" 

1573 ) 

1574 elif isinstance(value, UnmappedColumn): 

1575 raise exc.ArgumentError( 

1576 f'Attribute "{cls.__name__}.{k}" uses unmapped_column() ' 

1577 f"with the {anno_container.__name__}[] annotation; " 

1578 "unmapped_column() must be used with the Unmapped[] " 

1579 "annotation" 

1580 ) 

1581 

1582 if ( 

1583 isinstance(value, tuple) 

1584 and len(value) == 1 

1585 and isinstance(value[0], (Column, _MappedAttribute)) 

1586 ): 

1587 util.warn( 

1588 "Ignoring declarative-like tuple value of attribute " 

1589 "'%s': possibly a copy-and-paste error with a comma " 

1590 "accidentally placed at the end of the line?" % k 

1591 ) 

1592 continue 

1593 elif look_for_dataclass_things and isinstance( 

1594 value, dataclasses.Field 

1595 ): 

1596 # we collected a dataclass Field; dataclasses would have 

1597 # set up the correct state on the class 

1598 continue 

1599 elif not isinstance(value, (Column, _DCAttributeOptions)): 

1600 # using @declared_attr for some object that 

1601 # isn't Column/MapperProperty/_DCAttributeOptions; remove 

1602 # from the clsdict_view 

1603 # and place the evaluated value onto the class. 

1604 collected_attributes.pop(k) 

1605 self._warn_for_decl_attributes(cls, k, value) 

1606 if not late_mapped: 

1607 setattr(cls, k, value) 

1608 continue 

1609 elif isinstance(value, Column): 

1610 _undefer_column_name( 

1611 k, self.column_copies.get(value, value) # type: ignore[arg-type] # noqa: E501 

1612 ) 

1613 else: 

1614 if isinstance(value, _IntrospectsAnnotations): 

1615 ( 

1616 annotation, 

1617 mapped_container, 

1618 extracted_mapped_annotation, 

1619 is_dataclass, 

1620 attr_value, 

1621 originating_module, 

1622 originating_class, 

1623 ) = self.collected_annotations.get( 

1624 k, (None, None, None, False, None, None, None) 

1625 ) 

1626 

1627 # issue #8692 - don't do any annotation interpretation if 

1628 # an annotation were present and a container such as 

1629 # Mapped[] etc. were not used. If annotation is None, 

1630 # do declarative_scan so that the property can raise 

1631 # for required 

1632 if ( 

1633 mapped_container is not None 

1634 or annotation is None 

1635 # issue #10516: need to do declarative_scan even with 

1636 # a non-Mapped annotation if we are doing 

1637 # __allow_unmapped__, for things like col.name 

1638 # assignment 

1639 or allow_unmapped_annotations 

1640 ): 

1641 try: 

1642 value.declarative_scan( 

1643 self, 

1644 self.registry, 

1645 cls, 

1646 originating_module, 

1647 k, 

1648 mapped_container, 

1649 annotation, 

1650 extracted_mapped_annotation, 

1651 is_dataclass, 

1652 ) 

1653 except NameError as ne: 

1654 raise orm_exc.MappedAnnotationError( 

1655 f"Could not resolve all types within mapped " 

1656 f'annotation: "{annotation}". Ensure all ' 

1657 f"types are written correctly and are " 

1658 f"imported within the module in use." 

1659 ) from ne 

1660 else: 

1661 # assert that we were expecting annotations 

1662 # without Mapped[] were going to be passed. 

1663 # otherwise an error should have been raised 

1664 # by util._extract_mapped_subtype before we got here. 

1665 assert expect_annotations_wo_mapped 

1666 

1667 if isinstance(value, _DCAttributeOptions): 

1668 if ( 

1669 value._has_dataclass_arguments 

1670 and not look_for_dataclass_things 

1671 ): 

1672 if isinstance(value, MapperProperty): 

1673 argnames = [ 

1674 "init", 

1675 "default_factory", 

1676 "repr", 

1677 "default", 

1678 "dataclass_metadata", 

1679 ] 

1680 else: 

1681 argnames = [ 

1682 "init", 

1683 "default_factory", 

1684 "repr", 

1685 "dataclass_metadata", 

1686 ] 

1687 

1688 args = { 

1689 a 

1690 for a in argnames 

1691 if getattr( 

1692 value._attribute_options, f"dataclasses_{a}" 

1693 ) 

1694 is not _NoArg.NO_ARG 

1695 } 

1696 

1697 raise exc.ArgumentError( 

1698 f"Attribute '{k}' on class {cls} includes " 

1699 f"dataclasses argument(s): " 

1700 f"{', '.join(sorted(repr(a) for a in args))} but " 

1701 f"class does not specify " 

1702 "SQLAlchemy native dataclass configuration." 

1703 ) 

1704 

1705 if not isinstance(value, (MapperProperty, _MapsColumns)): 

1706 # filter for _DCAttributeOptions objects that aren't 

1707 # MapperProperty / mapped_column(). Currently this 

1708 # includes AssociationProxy. pop it from the things 

1709 # we're going to map and set it up as a descriptor 

1710 # on the class. 

1711 collected_attributes.pop(k) 

1712 

1713 # Assoc Prox (or other descriptor object that may 

1714 # use _DCAttributeOptions) is usually here, except if 

1715 # 1. we're a 

1716 # dataclass, dataclasses would have removed the 

1717 # attr here or 2. assoc proxy is coming from a 

1718 # superclass, we want it to be direct here so it 

1719 # tracks state or 3. assoc prox comes from 

1720 # declared_attr, uncommon case 

1721 setattr(cls, k, value) 

1722 continue 

1723 

1724 our_stuff[k] = value 

1725 

1726 def _extract_declared_columns(self) -> None: 

1727 our_stuff = self.properties 

1728 

1729 # extract columns from the class dict 

1730 declared_columns = self.declared_columns 

1731 column_ordering = self.column_ordering 

1732 name_to_prop_key = collections.defaultdict(set) 

1733 

1734 for key, c in list(our_stuff.items()): 

1735 if isinstance(c, _MapsColumns): 

1736 mp_to_assign = c.mapper_property_to_assign 

1737 if mp_to_assign: 

1738 our_stuff[key] = mp_to_assign 

1739 else: 

1740 # if no mapper property to assign, this currently means 

1741 # this is a MappedColumn that will produce a Column for us 

1742 del our_stuff[key] 

1743 

1744 for col, sort_order in c.columns_to_assign: 

1745 if not isinstance(c, CompositeProperty): 

1746 name_to_prop_key[col.name].add(key) 

1747 declared_columns.add(col) 

1748 

1749 if isinstance(c, UnmappedColumn): 

1750 # the column goes into the Table, and is then 

1751 # excluded from the mapping in 

1752 # _prepare_mapper_arguments(), #13650 

1753 self.unmapped_columns.add(col) 

1754 

1755 # we would assert this, however we want the below 

1756 # warning to take effect instead. See #9630 

1757 # assert col not in column_ordering 

1758 

1759 column_ordering[col] = sort_order 

1760 

1761 # if this is a MappedColumn and the attribute key we 

1762 # have is not what the column has for its key, map the 

1763 # Column explicitly under the attribute key name. 

1764 # otherwise, Mapper will map it under the column key. 

1765 if ( 

1766 mp_to_assign is None 

1767 and key != col.key 

1768 and not isinstance(c, UnmappedColumn) 

1769 ): 

1770 our_stuff[key] = col 

1771 

1772 if isinstance(c, UnmappedColumn) and key in self.cls.__dict__: 

1773 # the attribute is not part of the class at all 

1774 delattr(self.cls, key) 

1775 elif isinstance(c, Column): 

1776 # undefer previously occurred here, and now occurs earlier. 

1777 # ensure every column we get here has been named 

1778 assert c.name is not None 

1779 name_to_prop_key[c.name].add(key) 

1780 declared_columns.add(c) 

1781 # if the column is the same name as the key, 

1782 # remove it from the explicit properties dict. 

1783 # the normal rules for assigning column-based properties 

1784 # will take over, including precedence of columns 

1785 # in multi-column ColumnProperties. 

1786 if key == c.key: 

1787 del our_stuff[key] 

1788 

1789 for name, keys in name_to_prop_key.items(): 

1790 if len(keys) > 1: 

1791 util.warn( 

1792 "On class %r, Column object %r named " 

1793 "directly multiple times, " 

1794 "only one will be used: %s. " 

1795 "Consider using orm.synonym instead" 

1796 % (self.classname, name, (", ".join(sorted(keys)))) 

1797 ) 

1798 

1799 def _setup_table(self, table: Optional[FromClause] = None) -> None: 

1800 cls = self.cls 

1801 cls_as_Decl = cast("MappedClassProtocol[Any]", cls) 

1802 

1803 tablename = self.tablename 

1804 table_args = self.table_args 

1805 clsdict_view = self.clsdict_view 

1806 declared_columns = self.declared_columns 

1807 column_ordering = self.column_ordering 

1808 

1809 manager = attributes.manager_of_class(cls) 

1810 

1811 if ( 

1812 self.table_fn is None 

1813 and "__table__" not in clsdict_view 

1814 and table is None 

1815 ): 

1816 if hasattr(cls, "__table_cls__"): 

1817 table_cls = cast( 

1818 Type[Table], 

1819 util.unbound_method_to_callable(cls.__table_cls__), # type: ignore[no-untyped-call] # noqa: E501 

1820 ) 

1821 else: 

1822 table_cls = Table 

1823 

1824 if tablename is not None: 

1825 args: Tuple[Any, ...] = () 

1826 table_kw: Dict[str, Any] = {} 

1827 

1828 if table_args: 

1829 if isinstance(table_args, dict): 

1830 table_kw = table_args 

1831 elif isinstance(table_args, tuple): 

1832 if isinstance(table_args[-1], dict): 

1833 args, table_kw = table_args[0:-1], table_args[-1] 

1834 else: 

1835 args = table_args 

1836 

1837 autoload_with = clsdict_view.get("__autoload_with__") 

1838 if autoload_with: 

1839 table_kw["autoload_with"] = autoload_with 

1840 

1841 autoload = clsdict_view.get("__autoload__") 

1842 if autoload: 

1843 table_kw["autoload"] = True 

1844 

1845 sorted_columns = sorted( 

1846 declared_columns, 

1847 key=lambda c: column_ordering.get(c, 0), 

1848 ) 

1849 table = self.set_cls_attribute( 

1850 "__table__", 

1851 table_cls( 

1852 tablename, 

1853 self._metadata_for_cls(manager), 

1854 *sorted_columns, 

1855 *args, 

1856 **table_kw, 

1857 ), 

1858 ) 

1859 else: 

1860 if table is None: 

1861 if self.table_fn: 

1862 table = self.set_cls_attribute( 

1863 "__table__", self.table_fn() 

1864 ) 

1865 else: 

1866 table = cls_as_Decl.__table__ 

1867 if declared_columns: 

1868 for c in declared_columns: 

1869 if not table.c.contains_column(c): 

1870 raise exc.ArgumentError( 

1871 "Can't add additional column %r when " 

1872 "specifying __table__" % c.key 

1873 ) 

1874 

1875 self.local_table = table 

1876 

1877 def _metadata_for_cls(self, manager: ClassManager[Any]) -> MetaData: 

1878 return _metadata_for_cls_fn(self.cls, manager.registry) 

1879 

1880 def _setup_inheriting_mapper(self) -> None: 

1881 cls = self.cls 

1882 

1883 inherits = None 

1884 

1885 if inherits is None: 

1886 # since we search for classical mappings now, search for 

1887 # multiple mapped bases as well and raise an error. 

1888 inherits_search = [] 

1889 for base_ in cls.__bases__: 

1890 c = _resolve_for_abstract_or_classical(base_) 

1891 if c is None: 

1892 continue 

1893 

1894 if _is_supercls_for_inherits(c) and c not in inherits_search: 

1895 inherits_search.append(c) 

1896 

1897 if inherits_search: 

1898 if len(inherits_search) > 1: 

1899 raise exc.InvalidRequestError( 

1900 "Class %s has multiple mapped bases: %r" 

1901 % (cls, inherits_search) 

1902 ) 

1903 inherits = inherits_search[0] 

1904 elif isinstance(inherits, Mapper): 

1905 inherits = inherits.class_ 

1906 

1907 self.inherits = inherits 

1908 

1909 clsdict_view = self.clsdict_view 

1910 if "__table__" not in clsdict_view and self.tablename is None: 

1911 self.single = True 

1912 

1913 def _setup_inheriting_columns(self) -> None: 

1914 table = self.local_table 

1915 cls = self.cls 

1916 table_args = self.table_args 

1917 declared_columns = self.declared_columns 

1918 

1919 if ( 

1920 table is None 

1921 and self.inherits is None 

1922 and not _get_immediate_cls_attr(cls, "__no_table__") 

1923 ): 

1924 raise exc.InvalidRequestError( 

1925 "Class %r does not have a __table__ or __tablename__ " 

1926 "specified and does not inherit from an existing " 

1927 "table-mapped class." % cls 

1928 ) 

1929 elif self.inherits: 

1930 inherited_mapper_or_config = _declared_mapping_info(self.inherits) 

1931 assert inherited_mapper_or_config is not None 

1932 inherited_table = inherited_mapper_or_config.local_table 

1933 inherited_persist_selectable = ( 

1934 inherited_mapper_or_config.persist_selectable 

1935 ) 

1936 

1937 if table is None: 

1938 # single table inheritance. 

1939 # ensure no table args 

1940 if table_args: 

1941 raise exc.ArgumentError( 

1942 "Can't place __table_args__ on an inherited class " 

1943 "with no table." 

1944 ) 

1945 

1946 # add any columns declared here to the inherited table. 

1947 if declared_columns and not isinstance(inherited_table, Table): 

1948 raise exc.ArgumentError( 

1949 f"Can't declare columns on single-table-inherited " 

1950 f"subclass {self.cls}; superclass {self.inherits} " 

1951 "is not mapped to a Table" 

1952 ) 

1953 

1954 for col in declared_columns: 

1955 assert inherited_table is not None 

1956 if col.name in inherited_table.c: 

1957 if inherited_table.c[col.name] is col: 

1958 continue 

1959 raise exc.ArgumentError( 

1960 f"Column '{col}' on class {cls.__name__} " 

1961 f"conflicts with existing column " 

1962 f"'{inherited_table.c[col.name]}'. If using " 

1963 f"Declarative, consider using the " 

1964 "use_existing_column parameter of mapped_column() " 

1965 "to resolve conflicts." 

1966 ) 

1967 if col.primary_key: 

1968 raise exc.ArgumentError( 

1969 "Can't place primary key columns on an inherited " 

1970 "class with no table." 

1971 ) 

1972 

1973 if TYPE_CHECKING: 

1974 assert isinstance(inherited_table, Table) 

1975 

1976 inherited_table.append_column(col) 

1977 if ( 

1978 inherited_persist_selectable is not None 

1979 and inherited_persist_selectable is not inherited_table 

1980 ): 

1981 inherited_persist_selectable._refresh_for_new_column( 

1982 col 

1983 ) 

1984 

1985 def _prepare_mapper_arguments(self, mapper_kw: _MapperKwArgs) -> None: 

1986 properties = self.properties 

1987 

1988 if self.mapper_args_fn: 

1989 mapper_args = self.mapper_args_fn() 

1990 else: 

1991 mapper_args = {} 

1992 

1993 if mapper_kw: 

1994 mapper_args.update(mapper_kw) 

1995 

1996 if "properties" in mapper_args: 

1997 properties = dict(properties) 

1998 properties.update(mapper_args["properties"]) 

1999 

2000 # make sure that column copies are used rather 

2001 # than the original columns from any mixins 

2002 for k in ("version_id_col", "polymorphic_on"): 

2003 if k in mapper_args: 

2004 v = mapper_args[k] 

2005 mapper_args[k] = self.column_copies.get(v, v) 

2006 

2007 if "primary_key" in mapper_args: 

2008 mapper_args["primary_key"] = [ 

2009 self.column_copies.get(v, v) 

2010 for v in util.to_list(mapper_args["primary_key"]) 

2011 ] 

2012 

2013 if "inherits" in mapper_args: 

2014 inherits_arg = mapper_args["inherits"] 

2015 if isinstance(inherits_arg, Mapper): 

2016 inherits_arg = inherits_arg.class_ 

2017 

2018 if inherits_arg is not self.inherits: 

2019 raise exc.InvalidRequestError( 

2020 "mapper inherits argument given for non-inheriting " 

2021 "class %s" % (mapper_args["inherits"]) 

2022 ) 

2023 

2024 if self.inherits: 

2025 mapper_args["inherits"] = self.inherits 

2026 

2027 if self.inherits and not mapper_args.get("concrete", False): 

2028 # note the superclass is expected to have a Mapper assigned and 

2029 # not be a deferred config, as this is called within map() 

2030 inherited_mapper = class_mapper(self.inherits, False) 

2031 inherited_table = inherited_mapper.local_table 

2032 

2033 # single or joined inheritance 

2034 # exclude any cols on the inherited table which are 

2035 # not mapped on the parent class, to avoid 

2036 # mapping columns specific to sibling/nephew classes 

2037 if "exclude_properties" not in mapper_args: 

2038 mapper_args["exclude_properties"] = exclude_properties = { 

2039 c.key 

2040 for c in inherited_table.c 

2041 if c not in inherited_mapper._columntoproperty 

2042 }.union(inherited_mapper.exclude_properties or ()) 

2043 exclude_properties.difference_update( 

2044 [c.key for c in self.declared_columns] 

2045 ) 

2046 

2047 # look through columns in the current mapper that 

2048 # are keyed to a propname different than the colname 

2049 # (if names were the same, we'd have popped it out above, 

2050 # in which case the mapper makes this combination). 

2051 # See if the superclass has a similar column property. 

2052 # If so, join them together. 

2053 for k, col in list(properties.items()): 

2054 if not isinstance(col, expression.ColumnElement): 

2055 continue 

2056 if k in inherited_mapper._props: 

2057 p = inherited_mapper._props[k] 

2058 if isinstance(p, ColumnProperty): 

2059 # note here we place the subclass column 

2060 # first. See [ticket:1892] for background. 

2061 properties[k] = [col] + p.columns 

2062 

2063 if self.unmapped_columns: 

2064 # exclude unmapped_column() columns by Column object, so that 

2065 # the exclusion applies regardless of key, #13650 

2066 mapper_args["exclude_properties"] = set( 

2067 mapper_args.get("exclude_properties") or () 

2068 ).union(self.unmapped_columns) 

2069 

2070 result_mapper_args = mapper_args.copy() 

2071 result_mapper_args["properties"] = properties 

2072 self.mapper_args = result_mapper_args 

2073 

2074 def map(self, mapper_kw: _MapperKwArgs = util.EMPTY_DICT) -> Mapper[Any]: 

2075 self._prepare_mapper_arguments(mapper_kw) 

2076 if hasattr(self.cls, "__mapper_cls__"): 

2077 mapper_cls = cast( 

2078 "Type[Mapper[Any]]", 

2079 util.unbound_method_to_callable( 

2080 self.cls.__mapper_cls__ # type: ignore[no-untyped-call] 

2081 ), 

2082 ) 

2083 else: 

2084 mapper_cls = Mapper 

2085 

2086 return self.set_cls_attribute( 

2087 "__mapper__", 

2088 mapper_cls(self.cls, self.local_table, **self.mapper_args), 

2089 ) 

2090 

2091 

2092class _UnmappedDataclassConfig(_ClassScanAbstractConfig): 

2093 """Configurator that will produce an unmapped dataclass.""" 

2094 

2095 __slots__ = ( 

2096 "clsdict_view", 

2097 "collected_attributes", 

2098 "collected_annotations", 

2099 "allow_dataclass_fields", 

2100 "dataclass_setup_arguments", 

2101 "is_dataclass_prior_to_mapping", 

2102 "allow_unmapped_annotations", 

2103 ) 

2104 

2105 def __init__( 

2106 self, 

2107 cls_: Type[_O], 

2108 dict_: _ClassDict, 

2109 ): 

2110 super().__init__(cls_) 

2111 self.clsdict_view = ( 

2112 util.immutabledict(dict_) if dict_ else util.EMPTY_DICT 

2113 ) 

2114 self.dataclass_setup_arguments = getattr( 

2115 self.cls, "_sa_apply_dc_transforms", None 

2116 ) 

2117 

2118 self.is_dataclass_prior_to_mapping = dataclasses.is_dataclass(cls_) 

2119 self.allow_dataclass_fields = False 

2120 self.allow_unmapped_annotations = True 

2121 self.collected_attributes = {} 

2122 self.collected_annotations = {} 

2123 

2124 self._scan_attributes() 

2125 

2126 self._setup_dataclasses_transforms( 

2127 enable_descriptor_defaults=False, revert=True 

2128 ) 

2129 

2130 def _scan_attributes(self) -> None: 

2131 cls = self.cls 

2132 

2133 clsdict_view = self.clsdict_view 

2134 collected_attributes = self.collected_attributes 

2135 _include_dunders = self._include_dunders 

2136 

2137 attribute_is_overridden = self._cls_attr_override_checker(self.cls) 

2138 

2139 local_attributes_for_class = self._cls_attr_resolver(cls) 

2140 for ( 

2141 name, 

2142 obj, 

2143 annotation, 

2144 is_dataclass_field, 

2145 ) in local_attributes_for_class(): 

2146 if name in _include_dunders: 

2147 continue 

2148 elif is_dataclass_field and ( 

2149 name not in clsdict_view or clsdict_view[name] is not obj 

2150 ): 

2151 # here, we are definitely looking at the target class 

2152 # and not a superclass. this is currently a 

2153 # dataclass-only path. if the name is only 

2154 # a dataclass field and isn't in local cls.__dict__, 

2155 # put the object there. 

2156 # assert that the dataclass-enabled resolver agrees 

2157 # with what we are seeing 

2158 

2159 assert not attribute_is_overridden(name, obj) 

2160 

2161 if _is_declarative_props(obj): 

2162 obj = obj.fget() 

2163 

2164 collected_attributes[name] = obj 

2165 self._collect_annotation(name, annotation, cls, False, obj) 

2166 else: 

2167 self._collect_annotation(name, annotation, cls, None, obj) 

2168 if name in clsdict_view: 

2169 collected_attributes[name] = obj 

2170 

2171 

2172@util.preload_module("sqlalchemy.orm.decl_api") 

2173def _as_dc_declaredattr( 

2174 field_metadata: Mapping[str, Any], sa_dataclass_metadata_key: str 

2175) -> Any: 

2176 # wrap lambdas inside dataclass fields inside an ad-hoc declared_attr. 

2177 # we can't write it because field.metadata is immutable :( so we have 

2178 # to go through extra trouble to compare these 

2179 decl_api = util.preloaded.orm_decl_api 

2180 obj = field_metadata[sa_dataclass_metadata_key] 

2181 if callable(obj) and not isinstance(obj, decl_api.declared_attr): 

2182 return decl_api.declared_attr(obj) 

2183 else: 

2184 return obj 

2185 

2186 

2187class _DeferredDeclarativeConfig(_DeclarativeMapperConfig): 

2188 """Configurator that extends _DeclarativeMapperConfig to add a 

2189 "deferred" step, to allow extensions like AbstractConcreteBase, 

2190 DeferredMapping to partially set up a mapping that is "prepared" 

2191 when table metadata is ready. 

2192 

2193 """ 

2194 

2195 _cls: weakref.ref[Type[Any]] 

2196 

2197 is_deferred = True 

2198 

2199 _configs: util.OrderedDict[ 

2200 weakref.ref[Type[Any]], _DeferredDeclarativeConfig 

2201 ] = util.OrderedDict() 

2202 

2203 def _early_mapping(self, mapper_kw: _MapperKwArgs) -> None: 

2204 pass 

2205 

2206 @property 

2207 def cls(self) -> Type[Any]: 

2208 return self._cls() # type: ignore[return-value] 

2209 

2210 @cls.setter 

2211 def cls(self, class_: Type[Any]) -> None: 

2212 self._cls = weakref.ref(class_, self._remove_config_cls) 

2213 self._configs[self._cls] = self 

2214 

2215 @classmethod 

2216 def _remove_config_cls(cls, ref: weakref.ref[Type[Any]]) -> None: 

2217 cls._configs.pop(ref, None) 

2218 

2219 @classmethod 

2220 def has_cls(cls, class_: Type[Any]) -> bool: 

2221 # 2.6 fails on weakref if class_ is an old style class 

2222 return isinstance(class_, type) and weakref.ref(class_) in cls._configs 

2223 

2224 @classmethod 

2225 def raise_unmapped_for_cls(cls, class_: Type[Any]) -> NoReturn: 

2226 if hasattr(class_, "_sa_raise_deferred_config"): 

2227 class_._sa_raise_deferred_config() 

2228 

2229 raise orm_exc.UnmappedClassError( 

2230 class_, 

2231 msg=( 

2232 f"Class {orm_exc._safe_cls_name(class_)} has a deferred " 

2233 "mapping on it. It is not yet usable as a mapped class." 

2234 ), 

2235 ) 

2236 

2237 @classmethod 

2238 def config_for_cls(cls, class_: Type[Any]) -> _DeferredDeclarativeConfig: 

2239 return cls._configs[weakref.ref(class_)] 

2240 

2241 @classmethod 

2242 def classes_for_base( 

2243 cls, base_cls: Type[Any], sort: bool = True 

2244 ) -> List[_DeferredDeclarativeConfig]: 

2245 classes_for_base = [ 

2246 m 

2247 for m, cls_ in [(m, m.cls) for m in cls._configs.values()] 

2248 if cls_ is not None and issubclass(cls_, base_cls) 

2249 ] 

2250 

2251 if not sort: 

2252 return classes_for_base 

2253 

2254 all_m_by_cls = {m.cls: m for m in classes_for_base} 

2255 

2256 tuples: List[ 

2257 Tuple[_DeferredDeclarativeConfig, _DeferredDeclarativeConfig] 

2258 ] = [] 

2259 for m_cls in all_m_by_cls: 

2260 tuples.extend( 

2261 (all_m_by_cls[base_cls], all_m_by_cls[m_cls]) 

2262 for base_cls in m_cls.__bases__ 

2263 if base_cls in all_m_by_cls 

2264 ) 

2265 return list(topological.sort(tuples, classes_for_base)) 

2266 

2267 def map(self, mapper_kw: _MapperKwArgs = util.EMPTY_DICT) -> Mapper[Any]: 

2268 self._configs.pop(self._cls, None) 

2269 return super().map(mapper_kw) 

2270 

2271 

2272def _add_attribute( 

2273 cls: Type[Any], key: str, value: MapperProperty[Any] 

2274) -> None: 

2275 """add an attribute to an existing declarative class. 

2276 

2277 This runs through the logic to determine MapperProperty, 

2278 adds it to the Mapper, adds a column to the mapped Table, etc. 

2279 

2280 """ 

2281 

2282 if "__mapper__" in cls.__dict__: 

2283 mapped_cls = cast("MappedClassProtocol[Any]", cls) 

2284 

2285 def _table_or_raise(mc: MappedClassProtocol[Any]) -> Table: 

2286 if isinstance(mc.__table__, Table): 

2287 return mc.__table__ 

2288 raise exc.InvalidRequestError( 

2289 f"Cannot add a new attribute to mapped class {mc.__name__!r} " 

2290 "because it's not mapped against a table." 

2291 ) 

2292 

2293 if isinstance(value, Column): 

2294 _undefer_column_name(key, value) 

2295 _table_or_raise(mapped_cls).append_column( 

2296 value, replace_existing=True 

2297 ) 

2298 mapped_cls.__mapper__.add_property(key, value) 

2299 elif isinstance(value, _MapsColumns): 

2300 mp = value.mapper_property_to_assign 

2301 for col, _ in value.columns_to_assign: 

2302 _undefer_column_name(key, col) 

2303 _table_or_raise(mapped_cls).append_column( 

2304 col, replace_existing=True 

2305 ) 

2306 if not mp and not isinstance(value, UnmappedColumn): 

2307 mapped_cls.__mapper__.add_property(key, col) 

2308 if mp: 

2309 mapped_cls.__mapper__.add_property(key, mp) 

2310 elif isinstance(value, MapperProperty): 

2311 mapped_cls.__mapper__.add_property(key, value) 

2312 elif isinstance(value, QueryableAttribute) and value.key != key: 

2313 # detect a QueryableAttribute that's already mapped being 

2314 # assigned elsewhere in userland, turn into a synonym() 

2315 value = SynonymProperty(value.key) 

2316 mapped_cls.__mapper__.add_property(key, value) 

2317 else: 

2318 type.__setattr__(cls, key, value) 

2319 mapped_cls.__mapper__._expire_memoizations() 

2320 else: 

2321 type.__setattr__(cls, key, value) 

2322 

2323 

2324def _del_attribute(cls: Type[Any], key: str) -> None: 

2325 if ( 

2326 "__mapper__" in cls.__dict__ 

2327 and key in cls.__dict__ 

2328 and not cast( 

2329 "MappedClassProtocol[Any]", cls 

2330 ).__mapper__._dispose_called 

2331 ): 

2332 value = cls.__dict__[key] 

2333 if isinstance( 

2334 value, (Column, _MapsColumns, MapperProperty, QueryableAttribute) 

2335 ): 

2336 raise NotImplementedError( 

2337 "Can't un-map individual mapped attributes on a mapped class." 

2338 ) 

2339 else: 

2340 type.__delattr__(cls, key) 

2341 cast( 

2342 "MappedClassProtocol[Any]", cls 

2343 ).__mapper__._expire_memoizations() 

2344 else: 

2345 type.__delattr__(cls, key) 

2346 

2347 

2348def _declarative_constructor(self: Any, **kwargs: Any) -> None: 

2349 """A simple constructor that allows initialization from kwargs. 

2350 

2351 Sets attributes on the constructed instance using the names and 

2352 values in ``kwargs``. 

2353 

2354 Only keys that are present as 

2355 attributes of the instance's class are allowed. These could be, 

2356 for example, any mapped columns or relationships. 

2357 """ 

2358 cls_ = type(self) 

2359 for k in kwargs: 

2360 if not hasattr(cls_, k): 

2361 raise TypeError( 

2362 "%r is an invalid keyword argument for %s" % (k, cls_.__name__) 

2363 ) 

2364 setattr(self, k, kwargs[k]) 

2365 

2366 

2367_declarative_constructor.__name__ = "__init__" 

2368 

2369 

2370def _mapped_column_for_annotation( 

2371 collected_annotation: _CollectedAnnotation, 

2372) -> MappedColumn[Any]: 

2373 """produce the column construct for an annotation with no right side, 

2374 e.g. ``x: Mapped[int]`` or ``x: Unmapped[int]``. 

2375 

2376 This is the construct that was located inside of an ``Annotated`` type 

2377 if present, else a blank :class:`.MappedColumn` or 

2378 :class:`.UnmappedColumn` based on the annotation's container type. 

2379 

2380 """ 

2381 

2382 if collected_annotation.attr_value is not None: 

2383 return collected_annotation.attr_value # type: ignore[no-any-return] 

2384 elif _is_unmapped_container(collected_annotation.mapped_container): 

2385 return UnmappedColumn() 

2386 else: 

2387 return MappedColumn() 

2388 

2389 

2390def _undefer_column_name(key: str, column: Column[Any]) -> None: 

2391 if column.key is None: 

2392 column.key = key 

2393 if column.name is None: 

2394 column.name = key