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

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

479 statements  

1# orm/descriptor_props.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"""Descriptor properties are more "auxiliary" properties 

9that exist as configurational elements, but don't participate 

10as actively in the load/persist ORM loop. 

11 

12""" 

13from __future__ import annotations 

14 

15from dataclasses import is_dataclass 

16import inspect 

17import itertools 

18import operator 

19import typing 

20from typing import Any 

21from typing import Callable 

22from typing import Dict 

23from typing import List 

24from typing import NoReturn 

25from typing import Optional 

26from typing import Sequence 

27from typing import Tuple 

28from typing import Type 

29from typing import TYPE_CHECKING 

30from typing import TypeVar 

31from typing import Union 

32import weakref 

33 

34from . import attributes 

35from . import util as orm_util 

36from .base import _DeclarativeMapped 

37from .base import LoaderCallableStatus 

38from .base import Mapped 

39from .base import PassiveFlag 

40from .base import SQLORMOperations 

41from .interfaces import _AttributeOptions 

42from .interfaces import _IntrospectsAnnotations 

43from .interfaces import _MapsColumns 

44from .interfaces import MapperProperty 

45from .interfaces import PropComparator 

46from .util import _none_set 

47from .util import de_stringify_annotation 

48from .. import event 

49from .. import exc as sa_exc 

50from .. import schema 

51from .. import sql 

52from .. import util 

53from ..sql import expression 

54from ..sql import operators 

55from ..sql.elements import BindParameter 

56from ..util.typing import get_args 

57from ..util.typing import is_fwd_ref 

58from ..util.typing import is_pep593 

59 

60 

61if typing.TYPE_CHECKING: 

62 from ._typing import _InstanceDict 

63 from ._typing import _RegistryType 

64 from .attributes import History 

65 from .attributes import InstrumentedAttribute 

66 from .attributes import QueryableAttribute 

67 from .context import ORMCompileState 

68 from .decl_base import _ClassScanMapperConfig 

69 from .mapper import Mapper 

70 from .properties import ColumnProperty 

71 from .properties import MappedColumn 

72 from .state import InstanceState 

73 from ..engine.base import Connection 

74 from ..engine.row import Row 

75 from ..sql._typing import _DMLColumnArgument 

76 from ..sql._typing import _InfoType 

77 from ..sql.elements import ClauseList 

78 from ..sql.elements import ColumnElement 

79 from ..sql.operators import OperatorType 

80 from ..sql.schema import Column 

81 from ..sql.selectable import Select 

82 from ..util.typing import _AnnotationScanType 

83 from ..util.typing import CallableReference 

84 from ..util.typing import DescriptorReference 

85 from ..util.typing import RODescriptorReference 

86 

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

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

89 

90 

91class DescriptorProperty(MapperProperty[_T]): 

92 """:class:`.MapperProperty` which proxies access to a 

93 user-defined descriptor.""" 

94 

95 doc: Optional[str] = None 

96 

97 uses_objects = False 

98 _links_to_entity = False 

99 

100 descriptor: DescriptorReference[Any] 

101 

102 def get_history( 

103 self, 

104 state: InstanceState[Any], 

105 dict_: _InstanceDict, 

106 passive: PassiveFlag = PassiveFlag.PASSIVE_OFF, 

107 ) -> History: 

108 raise NotImplementedError() 

109 

110 def instrument_class(self, mapper: Mapper[Any]) -> None: 

111 prop = self 

112 

113 class _ProxyImpl(attributes.AttributeImpl): 

114 accepts_scalar_loader = False 

115 load_on_unexpire = True 

116 collection = False 

117 

118 @property 

119 def uses_objects(self) -> bool: # type: ignore 

120 return prop.uses_objects 

121 

122 def __init__(self, key: str): 

123 self.key = key 

124 

125 def get_history( 

126 self, 

127 state: InstanceState[Any], 

128 dict_: _InstanceDict, 

129 passive: PassiveFlag = PassiveFlag.PASSIVE_OFF, 

130 ) -> History: 

131 return prop.get_history(state, dict_, passive) 

132 

133 if self.descriptor is None: 

134 desc = getattr(mapper.class_, self.key, None) 

135 if mapper._is_userland_descriptor(self.key, desc): 

136 self.descriptor = desc 

137 

138 if self.descriptor is None: 

139 

140 def fset(obj: Any, value: Any) -> None: 

141 setattr(obj, self.name, value) 

142 

143 def fdel(obj: Any) -> None: 

144 delattr(obj, self.name) 

145 

146 def fget(obj: Any) -> Any: 

147 return getattr(obj, self.name) 

148 

149 self.descriptor = property(fget=fget, fset=fset, fdel=fdel) 

150 

151 proxy_attr = attributes.create_proxied_attribute(self.descriptor)( 

152 self.parent.class_, 

153 self.key, 

154 self.descriptor, 

155 lambda: self._comparator_factory(mapper), 

156 doc=self.doc, 

157 original_property=self, 

158 ) 

159 proxy_attr.impl = _ProxyImpl(self.key) 

160 mapper.class_manager.instrument_attribute(self.key, proxy_attr) 

161 

162 

163_CompositeAttrType = Union[ 

164 str, 

165 "Column[_T]", 

166 "MappedColumn[_T]", 

167 "InstrumentedAttribute[_T]", 

168 "Mapped[_T]", 

169] 

170 

171 

172_CC = TypeVar("_CC", bound=Any) 

173 

174 

175_composite_getters: weakref.WeakKeyDictionary[ 

176 Type[Any], Callable[[Any], Tuple[Any, ...]] 

177] = weakref.WeakKeyDictionary() 

178 

179 

180class CompositeProperty( 

181 _MapsColumns[_CC], _IntrospectsAnnotations, DescriptorProperty[_CC] 

182): 

183 """Defines a "composite" mapped attribute, representing a collection 

184 of columns as one attribute. 

185 

186 :class:`.CompositeProperty` is constructed using the :func:`.composite` 

187 function. 

188 

189 .. seealso:: 

190 

191 :ref:`mapper_composite` 

192 

193 """ 

194 

195 composite_class: Union[Type[_CC], Callable[..., _CC]] 

196 attrs: Tuple[_CompositeAttrType[Any], ...] 

197 

198 _generated_composite_accessor: CallableReference[ 

199 Optional[Callable[[_CC], Tuple[Any, ...]]] 

200 ] 

201 

202 comparator_factory: Type[Comparator[_CC]] 

203 

204 def __init__( 

205 self, 

206 _class_or_attr: Union[ 

207 None, Type[_CC], Callable[..., _CC], _CompositeAttrType[Any] 

208 ] = None, 

209 *attrs: _CompositeAttrType[Any], 

210 attribute_options: Optional[_AttributeOptions] = None, 

211 active_history: bool = False, 

212 deferred: bool = False, 

213 group: Optional[str] = None, 

214 comparator_factory: Optional[Type[Comparator[_CC]]] = None, 

215 info: Optional[_InfoType] = None, 

216 **kwargs: Any, 

217 ): 

218 super().__init__(attribute_options=attribute_options) 

219 

220 if isinstance(_class_or_attr, (Mapped, str, sql.ColumnElement)): 

221 self.attrs = (_class_or_attr,) + attrs 

222 # will initialize within declarative_scan 

223 self.composite_class = None # type: ignore 

224 else: 

225 self.composite_class = _class_or_attr # type: ignore 

226 self.attrs = attrs 

227 

228 self.active_history = active_history 

229 self.deferred = deferred 

230 self.group = group 

231 self.comparator_factory = ( 

232 comparator_factory 

233 if comparator_factory is not None 

234 else self.__class__.Comparator 

235 ) 

236 self._generated_composite_accessor = None 

237 if info is not None: 

238 self.info.update(info) 

239 

240 util.set_creation_order(self) 

241 self._create_descriptor() 

242 self._init_accessor() 

243 

244 def instrument_class(self, mapper: Mapper[Any]) -> None: 

245 super().instrument_class(mapper) 

246 self._setup_event_handlers() 

247 

248 def _composite_values_from_instance(self, value: _CC) -> Tuple[Any, ...]: 

249 if self._generated_composite_accessor: 

250 return self._generated_composite_accessor(value) 

251 else: 

252 try: 

253 accessor = value.__composite_values__ 

254 except AttributeError as ae: 

255 raise sa_exc.InvalidRequestError( 

256 f"Composite class {self.composite_class.__name__} is not " 

257 f"a dataclass and does not define a __composite_values__()" 

258 " method; can't get state" 

259 ) from ae 

260 else: 

261 return accessor() # type: ignore 

262 

263 def do_init(self) -> None: 

264 """Initialization which occurs after the :class:`.Composite` 

265 has been associated with its parent mapper. 

266 

267 """ 

268 self._setup_arguments_on_columns() 

269 

270 _COMPOSITE_FGET = object() 

271 

272 def _create_descriptor(self) -> None: 

273 """Create the Python descriptor that will serve as 

274 the access point on instances of the mapped class. 

275 

276 """ 

277 

278 def fget(instance: Any) -> Any: 

279 dict_ = attributes.instance_dict(instance) 

280 state = attributes.instance_state(instance) 

281 

282 if self.key not in dict_: 

283 # key not present. Iterate through related 

284 # attributes, retrieve their values. This 

285 # ensures they all load. 

286 values = [ 

287 getattr(instance, key) for key in self._attribute_keys 

288 ] 

289 

290 # current expected behavior here is that the composite is 

291 # created on access if the object is persistent or if 

292 # col attributes have non-None. This would be better 

293 # if the composite were created unconditionally, 

294 # but that would be a behavioral change. 

295 if self.key not in dict_ and ( 

296 state.key is not None or not _none_set.issuperset(values) 

297 ): 

298 dict_[self.key] = self.composite_class(*values) 

299 state.manager.dispatch.refresh( 

300 state, self._COMPOSITE_FGET, [self.key] 

301 ) 

302 

303 return dict_.get(self.key, None) 

304 

305 def fset(instance: Any, value: Any) -> None: 

306 dict_ = attributes.instance_dict(instance) 

307 state = attributes.instance_state(instance) 

308 attr = state.manager[self.key] 

309 

310 if attr.dispatch._active_history: 

311 previous = fget(instance) 

312 else: 

313 previous = dict_.get(self.key, LoaderCallableStatus.NO_VALUE) 

314 

315 for fn in attr.dispatch.set: 

316 value = fn(state, value, previous, attr.impl) 

317 dict_[self.key] = value 

318 if value is None: 

319 for key in self._attribute_keys: 

320 setattr(instance, key, None) 

321 else: 

322 for key, value in zip( 

323 self._attribute_keys, 

324 self._composite_values_from_instance(value), 

325 ): 

326 setattr(instance, key, value) 

327 

328 def fdel(instance: Any) -> None: 

329 state = attributes.instance_state(instance) 

330 dict_ = attributes.instance_dict(instance) 

331 attr = state.manager[self.key] 

332 

333 if attr.dispatch._active_history: 

334 previous = fget(instance) 

335 dict_.pop(self.key, None) 

336 else: 

337 previous = dict_.pop(self.key, LoaderCallableStatus.NO_VALUE) 

338 

339 attr = state.manager[self.key] 

340 attr.dispatch.remove(state, previous, attr.impl) 

341 for key in self._attribute_keys: 

342 setattr(instance, key, None) 

343 

344 self.descriptor = property(fget, fset, fdel) 

345 

346 @util.preload_module("sqlalchemy.orm.properties") 

347 def declarative_scan( 

348 self, 

349 decl_scan: _ClassScanMapperConfig, 

350 registry: _RegistryType, 

351 cls: Type[Any], 

352 originating_module: Optional[str], 

353 key: str, 

354 mapped_container: Optional[Type[Mapped[Any]]], 

355 annotation: Optional[_AnnotationScanType], 

356 extracted_mapped_annotation: Optional[_AnnotationScanType], 

357 is_dataclass_field: bool, 

358 ) -> None: 

359 MappedColumn = util.preloaded.orm_properties.MappedColumn 

360 if ( 

361 self.composite_class is None 

362 and extracted_mapped_annotation is None 

363 ): 

364 self._raise_for_required(key, cls) 

365 argument = extracted_mapped_annotation 

366 

367 if is_pep593(argument): 

368 argument = get_args(argument)[0] 

369 

370 if argument and self.composite_class is None: 

371 if isinstance(argument, str) or is_fwd_ref( 

372 argument, check_generic=True 

373 ): 

374 if originating_module is None: 

375 str_arg = ( 

376 argument.__forward_arg__ 

377 if hasattr(argument, "__forward_arg__") 

378 else str(argument) 

379 ) 

380 raise sa_exc.ArgumentError( 

381 f"Can't use forward ref {argument} for composite " 

382 f"class argument; set up the type as Mapped[{str_arg}]" 

383 ) 

384 argument = de_stringify_annotation( 

385 cls, argument, originating_module, include_generic=True 

386 ) 

387 

388 self.composite_class = argument 

389 

390 if is_dataclass(self.composite_class): 

391 self._setup_for_dataclass(registry, cls, originating_module, key) 

392 else: 

393 for attr in self.attrs: 

394 if ( 

395 isinstance(attr, (MappedColumn, schema.Column)) 

396 and attr.name is None 

397 ): 

398 raise sa_exc.ArgumentError( 

399 "Composite class column arguments must be named " 

400 "unless a dataclass is used" 

401 ) 

402 self._init_accessor() 

403 

404 def _init_accessor(self) -> None: 

405 if is_dataclass(self.composite_class) and not hasattr( 

406 self.composite_class, "__composite_values__" 

407 ): 

408 insp = inspect.signature(self.composite_class) 

409 getter = operator.attrgetter( 

410 *[p.name for p in insp.parameters.values()] 

411 ) 

412 if len(insp.parameters) == 1: 

413 self._generated_composite_accessor = lambda obj: (getter(obj),) 

414 else: 

415 self._generated_composite_accessor = getter 

416 

417 if ( 

418 self.composite_class is not None 

419 and isinstance(self.composite_class, type) 

420 and self.composite_class not in _composite_getters 

421 ): 

422 if self._generated_composite_accessor is not None: 

423 _composite_getters[self.composite_class] = ( 

424 self._generated_composite_accessor 

425 ) 

426 elif hasattr(self.composite_class, "__composite_values__"): 

427 _composite_getters[self.composite_class] = ( 

428 lambda obj: obj.__composite_values__() 

429 ) 

430 

431 @util.preload_module("sqlalchemy.orm.properties") 

432 @util.preload_module("sqlalchemy.orm.decl_base") 

433 def _setup_for_dataclass( 

434 self, 

435 registry: _RegistryType, 

436 cls: Type[Any], 

437 originating_module: Optional[str], 

438 key: str, 

439 ) -> None: 

440 MappedColumn = util.preloaded.orm_properties.MappedColumn 

441 

442 decl_base = util.preloaded.orm_decl_base 

443 

444 insp = inspect.signature(self.composite_class) 

445 for param, attr in itertools.zip_longest( 

446 insp.parameters.values(), self.attrs 

447 ): 

448 if param is None: 

449 raise sa_exc.ArgumentError( 

450 f"number of composite attributes " 

451 f"{len(self.attrs)} exceeds " 

452 f"that of the number of attributes in class " 

453 f"{self.composite_class.__name__} {len(insp.parameters)}" 

454 ) 

455 if attr is None: 

456 # fill in missing attr spots with empty MappedColumn 

457 attr = MappedColumn() 

458 self.attrs += (attr,) 

459 

460 if isinstance(attr, MappedColumn): 

461 attr.declarative_scan_for_composite( 

462 registry, 

463 cls, 

464 originating_module, 

465 key, 

466 param.name, 

467 param.annotation, 

468 ) 

469 elif isinstance(attr, schema.Column): 

470 decl_base._undefer_column_name(param.name, attr) 

471 

472 @util.memoized_property 

473 def _comparable_elements(self) -> Sequence[QueryableAttribute[Any]]: 

474 return [getattr(self.parent.class_, prop.key) for prop in self.props] 

475 

476 @util.memoized_property 

477 @util.preload_module("orm.properties") 

478 def props(self) -> Sequence[MapperProperty[Any]]: 

479 props = [] 

480 MappedColumn = util.preloaded.orm_properties.MappedColumn 

481 

482 for attr in self.attrs: 

483 if isinstance(attr, str): 

484 prop = self.parent.get_property(attr, _configure_mappers=False) 

485 elif isinstance(attr, schema.Column): 

486 prop = self.parent._columntoproperty[attr] 

487 elif isinstance(attr, MappedColumn): 

488 prop = self.parent._columntoproperty[attr.column] 

489 elif isinstance(attr, attributes.InstrumentedAttribute): 

490 prop = attr.property 

491 else: 

492 prop = None 

493 

494 if not isinstance(prop, MapperProperty): 

495 raise sa_exc.ArgumentError( 

496 "Composite expects Column objects or mapped " 

497 f"attributes/attribute names as arguments, got: {attr!r}" 

498 ) 

499 

500 props.append(prop) 

501 return props 

502 

503 @util.non_memoized_property 

504 @util.preload_module("orm.properties") 

505 def columns(self) -> Sequence[Column[Any]]: 

506 MappedColumn = util.preloaded.orm_properties.MappedColumn 

507 return [ 

508 a.column if isinstance(a, MappedColumn) else a 

509 for a in self.attrs 

510 if isinstance(a, (schema.Column, MappedColumn)) 

511 ] 

512 

513 @property 

514 def mapper_property_to_assign(self) -> Optional[MapperProperty[_CC]]: 

515 return self 

516 

517 @property 

518 def columns_to_assign(self) -> List[Tuple[schema.Column[Any], int]]: 

519 return [(c, 0) for c in self.columns if c.table is None] 

520 

521 @util.preload_module("orm.properties") 

522 def _setup_arguments_on_columns(self) -> None: 

523 """Propagate configuration arguments made on this composite 

524 to the target columns, for those that apply. 

525 

526 """ 

527 ColumnProperty = util.preloaded.orm_properties.ColumnProperty 

528 

529 for prop in self.props: 

530 if not isinstance(prop, ColumnProperty): 

531 continue 

532 else: 

533 cprop = prop 

534 

535 cprop.active_history = self.active_history 

536 if self.deferred: 

537 cprop.deferred = self.deferred 

538 cprop.strategy_key = (("deferred", True), ("instrument", True)) 

539 cprop.group = self.group 

540 

541 def _setup_event_handlers(self) -> None: 

542 """Establish events that populate/expire the composite attribute.""" 

543 

544 def load_handler( 

545 state: InstanceState[Any], context: ORMCompileState 

546 ) -> None: 

547 _load_refresh_handler(state, context, None, is_refresh=False) 

548 

549 def refresh_handler( 

550 state: InstanceState[Any], 

551 context: ORMCompileState, 

552 to_load: Optional[Sequence[str]], 

553 ) -> None: 

554 # note this corresponds to sqlalchemy.ext.mutable load_attrs() 

555 

556 if not to_load or ( 

557 {self.key}.union(self._attribute_keys) 

558 ).intersection(to_load): 

559 _load_refresh_handler(state, context, to_load, is_refresh=True) 

560 

561 def _load_refresh_handler( 

562 state: InstanceState[Any], 

563 context: ORMCompileState, 

564 to_load: Optional[Sequence[str]], 

565 is_refresh: bool, 

566 ) -> None: 

567 dict_ = state.dict 

568 

569 # if context indicates we are coming from the 

570 # fget() handler, this already set the value; skip the 

571 # handler here. (other handlers like mutablecomposite will still 

572 # want to catch it) 

573 # there's an insufficiency here in that the fget() handler 

574 # really should not be using the refresh event and there should 

575 # be some other event that mutablecomposite can subscribe 

576 # towards for this. 

577 

578 if ( 

579 not is_refresh or context is self._COMPOSITE_FGET 

580 ) and self.key in dict_: 

581 return 

582 

583 # if column elements aren't loaded, skip. 

584 # __get__() will initiate a load for those 

585 # columns 

586 for k in self._attribute_keys: 

587 if k not in dict_: 

588 return 

589 

590 dict_[self.key] = self.composite_class( 

591 *[state.dict[key] for key in self._attribute_keys] 

592 ) 

593 

594 def expire_handler( 

595 state: InstanceState[Any], keys: Optional[Sequence[str]] 

596 ) -> None: 

597 if keys is None or set(self._attribute_keys).intersection(keys): 

598 state.dict.pop(self.key, None) 

599 

600 def insert_update_handler( 

601 mapper: Mapper[Any], 

602 connection: Connection, 

603 state: InstanceState[Any], 

604 ) -> None: 

605 """After an insert or update, some columns may be expired due 

606 to server side defaults, or re-populated due to client side 

607 defaults. Pop out the composite value here so that it 

608 recreates. 

609 

610 """ 

611 

612 state.dict.pop(self.key, None) 

613 

614 event.listen( 

615 self.parent, "after_insert", insert_update_handler, raw=True 

616 ) 

617 event.listen( 

618 self.parent, "after_update", insert_update_handler, raw=True 

619 ) 

620 event.listen( 

621 self.parent, "load", load_handler, raw=True, propagate=True 

622 ) 

623 event.listen( 

624 self.parent, "refresh", refresh_handler, raw=True, propagate=True 

625 ) 

626 event.listen( 

627 self.parent, "expire", expire_handler, raw=True, propagate=True 

628 ) 

629 

630 proxy_attr = self.parent.class_manager[self.key] 

631 proxy_attr.impl.dispatch = proxy_attr.dispatch # type: ignore 

632 proxy_attr.impl.dispatch._active_history = self.active_history 

633 

634 # TODO: need a deserialize hook here 

635 

636 @util.memoized_property 

637 def _attribute_keys(self) -> Sequence[str]: 

638 return [prop.key for prop in self.props] 

639 

640 def _populate_composite_bulk_save_mappings_fn( 

641 self, 

642 ) -> Callable[[Dict[str, Any]], None]: 

643 if self._generated_composite_accessor: 

644 get_values = self._generated_composite_accessor 

645 else: 

646 

647 def get_values(val: Any) -> Tuple[Any]: 

648 return val.__composite_values__() # type: ignore 

649 

650 attrs = [prop.key for prop in self.props] 

651 

652 def populate(dest_dict: Dict[str, Any]) -> None: 

653 dest_dict.update( 

654 { 

655 key: val 

656 for key, val in zip( 

657 attrs, get_values(dest_dict.pop(self.key)) 

658 ) 

659 } 

660 ) 

661 

662 return populate 

663 

664 def get_history( 

665 self, 

666 state: InstanceState[Any], 

667 dict_: _InstanceDict, 

668 passive: PassiveFlag = PassiveFlag.PASSIVE_OFF, 

669 ) -> History: 

670 """Provided for userland code that uses attributes.get_history().""" 

671 

672 added: List[Any] = [] 

673 deleted: List[Any] = [] 

674 

675 has_history = False 

676 for prop in self.props: 

677 key = prop.key 

678 hist = state.manager[key].impl.get_history(state, dict_) 

679 if hist.has_changes(): 

680 has_history = True 

681 

682 non_deleted = hist.non_deleted() 

683 if non_deleted: 

684 added.extend(non_deleted) 

685 else: 

686 added.append(None) 

687 if hist.deleted: 

688 deleted.extend(hist.deleted) 

689 else: 

690 deleted.append(None) 

691 

692 if has_history: 

693 return attributes.History( 

694 [self.composite_class(*added)], 

695 (), 

696 [self.composite_class(*deleted)], 

697 ) 

698 else: 

699 return attributes.History((), [self.composite_class(*added)], ()) 

700 

701 def _comparator_factory( 

702 self, mapper: Mapper[Any] 

703 ) -> Composite.Comparator[_CC]: 

704 return self.comparator_factory(self, mapper) 

705 

706 class CompositeBundle(orm_util.Bundle[_T]): 

707 def __init__( 

708 self, 

709 property_: Composite[_T], 

710 expr: ClauseList, 

711 ): 

712 self.property = property_ 

713 super().__init__(property_.key, *expr) 

714 

715 def create_row_processor( 

716 self, 

717 query: Select[Any], 

718 procs: Sequence[Callable[[Row[Any]], Any]], 

719 labels: Sequence[str], 

720 ) -> Callable[[Row[Any]], Any]: 

721 def proc(row: Row[Any]) -> Any: 

722 return self.property.composite_class( 

723 *[proc(row) for proc in procs] 

724 ) 

725 

726 return proc 

727 

728 class Comparator(PropComparator[_PT]): 

729 """Produce boolean, comparison, and other operators for 

730 :class:`.Composite` attributes. 

731 

732 See the example in :ref:`composite_operations` for an overview 

733 of usage , as well as the documentation for :class:`.PropComparator`. 

734 

735 .. seealso:: 

736 

737 :class:`.PropComparator` 

738 

739 :class:`.ColumnOperators` 

740 

741 :ref:`types_operators` 

742 

743 :attr:`.TypeEngine.comparator_factory` 

744 

745 """ 

746 

747 # https://github.com/python/mypy/issues/4266 

748 __hash__ = None # type: ignore 

749 

750 prop: RODescriptorReference[Composite[_PT]] 

751 

752 @util.memoized_property 

753 def clauses(self) -> ClauseList: 

754 return expression.ClauseList( 

755 group=False, *self._comparable_elements 

756 ) 

757 

758 def __clause_element__(self) -> CompositeProperty.CompositeBundle[_PT]: 

759 return self.expression 

760 

761 @util.memoized_property 

762 def expression(self) -> CompositeProperty.CompositeBundle[_PT]: 

763 clauses = self.clauses._annotate( 

764 { 

765 "parententity": self._parententity, 

766 "parentmapper": self._parententity, 

767 "proxy_key": self.prop.key, 

768 } 

769 ) 

770 return CompositeProperty.CompositeBundle(self.prop, clauses) 

771 

772 def _bulk_update_tuples( 

773 self, value: Any 

774 ) -> Sequence[Tuple[_DMLColumnArgument, Any]]: 

775 if isinstance(value, BindParameter): 

776 value = value.value 

777 

778 values: Sequence[Any] 

779 

780 if value is None: 

781 values = [None for key in self.prop._attribute_keys] 

782 elif isinstance(self.prop.composite_class, type) and isinstance( 

783 value, self.prop.composite_class 

784 ): 

785 values = self.prop._composite_values_from_instance( 

786 value # type: ignore[arg-type] 

787 ) 

788 else: 

789 raise sa_exc.ArgumentError( 

790 "Can't UPDATE composite attribute %s to %r" 

791 % (self.prop, value) 

792 ) 

793 

794 return list(zip(self._comparable_elements, values)) 

795 

796 @util.memoized_property 

797 def _comparable_elements(self) -> Sequence[QueryableAttribute[Any]]: 

798 if self._adapt_to_entity: 

799 return [ 

800 getattr(self._adapt_to_entity.entity, prop.key) 

801 for prop in self.prop._comparable_elements 

802 ] 

803 else: 

804 return self.prop._comparable_elements 

805 

806 def __eq__(self, other: Any) -> ColumnElement[bool]: # type: ignore[override] # noqa: E501 

807 return self._compare(operators.eq, other) 

808 

809 def __ne__(self, other: Any) -> ColumnElement[bool]: # type: ignore[override] # noqa: E501 

810 return self._compare(operators.ne, other) 

811 

812 def __lt__(self, other: Any) -> ColumnElement[bool]: 

813 return self._compare(operators.lt, other) 

814 

815 def __gt__(self, other: Any) -> ColumnElement[bool]: 

816 return self._compare(operators.gt, other) 

817 

818 def __le__(self, other: Any) -> ColumnElement[bool]: 

819 return self._compare(operators.le, other) 

820 

821 def __ge__(self, other: Any) -> ColumnElement[bool]: 

822 return self._compare(operators.ge, other) 

823 

824 # what might be interesting would be if we create 

825 # an instance of the composite class itself with 

826 # the columns as data members, then use "hybrid style" comparison 

827 # to create these comparisons. then your Point.__eq__() method could 

828 # be where comparison behavior is defined for SQL also. Likely 

829 # not a good choice for default behavior though, not clear how it would 

830 # work w/ dataclasses, etc. also no demand for any of this anyway. 

831 def _compare( 

832 self, operator: OperatorType, other: Any 

833 ) -> ColumnElement[bool]: 

834 values: Sequence[Any] 

835 if other is None: 

836 values = [None] * len(self.prop._comparable_elements) 

837 else: 

838 values = self.prop._composite_values_from_instance(other) 

839 comparisons = [ 

840 operator(a, b) 

841 for a, b in zip(self.prop._comparable_elements, values) 

842 ] 

843 if self._adapt_to_entity: 

844 assert self.adapter is not None 

845 comparisons = [self.adapter(x) for x in comparisons] 

846 return sql.and_(*comparisons) 

847 

848 def __str__(self) -> str: 

849 return str(self.parent.class_.__name__) + "." + self.key 

850 

851 

852class Composite(CompositeProperty[_T], _DeclarativeMapped[_T]): 

853 """Declarative-compatible front-end for the :class:`.CompositeProperty` 

854 class. 

855 

856 Public constructor is the :func:`_orm.composite` function. 

857 

858 .. versionchanged:: 2.0 Added :class:`_orm.Composite` as a Declarative 

859 compatible subclass of :class:`_orm.CompositeProperty`. 

860 

861 .. seealso:: 

862 

863 :ref:`mapper_composite` 

864 

865 """ 

866 

867 inherit_cache = True 

868 """:meta private:""" 

869 

870 

871class ConcreteInheritedProperty(DescriptorProperty[_T]): 

872 """A 'do nothing' :class:`.MapperProperty` that disables 

873 an attribute on a concrete subclass that is only present 

874 on the inherited mapper, not the concrete classes' mapper. 

875 

876 Cases where this occurs include: 

877 

878 * When the superclass mapper is mapped against a 

879 "polymorphic union", which includes all attributes from 

880 all subclasses. 

881 * When a relationship() is configured on an inherited mapper, 

882 but not on the subclass mapper. Concrete mappers require 

883 that relationship() is configured explicitly on each 

884 subclass. 

885 

886 """ 

887 

888 def _comparator_factory( 

889 self, mapper: Mapper[Any] 

890 ) -> Type[PropComparator[_T]]: 

891 comparator_callable = None 

892 

893 for m in self.parent.iterate_to_root(): 

894 p = m._props[self.key] 

895 if getattr(p, "comparator_factory", None) is not None: 

896 comparator_callable = p.comparator_factory 

897 break 

898 assert comparator_callable is not None 

899 return comparator_callable(p, mapper) # type: ignore 

900 

901 def __init__(self) -> None: 

902 super().__init__() 

903 

904 def warn() -> NoReturn: 

905 raise AttributeError( 

906 "Concrete %s does not implement " 

907 "attribute %r at the instance level. Add " 

908 "this property explicitly to %s." 

909 % (self.parent, self.key, self.parent) 

910 ) 

911 

912 class NoninheritedConcreteProp: 

913 def __set__(s: Any, obj: Any, value: Any) -> NoReturn: 

914 warn() 

915 

916 def __delete__(s: Any, obj: Any) -> NoReturn: 

917 warn() 

918 

919 def __get__(s: Any, obj: Any, owner: Any) -> Any: 

920 if obj is None: 

921 return self.descriptor 

922 warn() 

923 

924 self.descriptor = NoninheritedConcreteProp() 

925 

926 

927class SynonymProperty(DescriptorProperty[_T]): 

928 """Denote an attribute name as a synonym to a mapped property, 

929 in that the attribute will mirror the value and expression behavior 

930 of another attribute. 

931 

932 :class:`.Synonym` is constructed using the :func:`_orm.synonym` 

933 function. 

934 

935 .. seealso:: 

936 

937 :ref:`synonyms` - Overview of synonyms 

938 

939 """ 

940 

941 comparator_factory: Optional[Type[PropComparator[_T]]] 

942 

943 def __init__( 

944 self, 

945 name: str, 

946 map_column: Optional[bool] = None, 

947 descriptor: Optional[Any] = None, 

948 comparator_factory: Optional[Type[PropComparator[_T]]] = None, 

949 attribute_options: Optional[_AttributeOptions] = None, 

950 info: Optional[_InfoType] = None, 

951 doc: Optional[str] = None, 

952 ): 

953 super().__init__(attribute_options=attribute_options) 

954 

955 self.name = name 

956 self.map_column = map_column 

957 self.descriptor = descriptor 

958 self.comparator_factory = comparator_factory 

959 if doc: 

960 self.doc = doc 

961 elif descriptor and descriptor.__doc__: 

962 self.doc = descriptor.__doc__ 

963 else: 

964 self.doc = None 

965 if info: 

966 self.info.update(info) 

967 

968 util.set_creation_order(self) 

969 

970 if not TYPE_CHECKING: 

971 

972 @property 

973 def uses_objects(self) -> bool: 

974 return getattr(self.parent.class_, self.name).impl.uses_objects 

975 

976 # TODO: when initialized, check _proxied_object, 

977 # emit a warning if its not a column-based property 

978 

979 @util.memoized_property 

980 def _proxied_object( 

981 self, 

982 ) -> Union[MapperProperty[_T], SQLORMOperations[_T]]: 

983 attr = getattr(self.parent.class_, self.name) 

984 if not hasattr(attr, "property") or not isinstance( 

985 attr.property, MapperProperty 

986 ): 

987 # attribute is a non-MapperProprerty proxy such as 

988 # hybrid or association proxy 

989 if isinstance(attr, attributes.QueryableAttribute): 

990 return attr.comparator 

991 elif isinstance(attr, SQLORMOperations): 

992 # assocaition proxy comes here 

993 return attr 

994 

995 raise sa_exc.InvalidRequestError( 

996 """synonym() attribute "%s.%s" only supports """ 

997 """ORM mapped attributes, got %r""" 

998 % (self.parent.class_.__name__, self.name, attr) 

999 ) 

1000 return attr.property 

1001 

1002 def _comparator_factory(self, mapper: Mapper[Any]) -> SQLORMOperations[_T]: 

1003 prop = self._proxied_object 

1004 

1005 if isinstance(prop, MapperProperty): 

1006 if self.comparator_factory: 

1007 comp = self.comparator_factory(prop, mapper) 

1008 else: 

1009 comp = prop.comparator_factory(prop, mapper) 

1010 return comp 

1011 else: 

1012 return prop 

1013 

1014 def get_history( 

1015 self, 

1016 state: InstanceState[Any], 

1017 dict_: _InstanceDict, 

1018 passive: PassiveFlag = PassiveFlag.PASSIVE_OFF, 

1019 ) -> History: 

1020 attr: QueryableAttribute[Any] = getattr(self.parent.class_, self.name) 

1021 return attr.impl.get_history(state, dict_, passive=passive) 

1022 

1023 @util.preload_module("sqlalchemy.orm.properties") 

1024 def set_parent(self, parent: Mapper[Any], init: bool) -> None: 

1025 properties = util.preloaded.orm_properties 

1026 

1027 if self.map_column: 

1028 # implement the 'map_column' option. 

1029 if self.key not in parent.persist_selectable.c: 

1030 raise sa_exc.ArgumentError( 

1031 "Can't compile synonym '%s': no column on table " 

1032 "'%s' named '%s'" 

1033 % ( 

1034 self.name, 

1035 parent.persist_selectable.description, 

1036 self.key, 

1037 ) 

1038 ) 

1039 elif ( 

1040 parent.persist_selectable.c[self.key] 

1041 in parent._columntoproperty 

1042 and parent._columntoproperty[ 

1043 parent.persist_selectable.c[self.key] 

1044 ].key 

1045 == self.name 

1046 ): 

1047 raise sa_exc.ArgumentError( 

1048 "Can't call map_column=True for synonym %r=%r, " 

1049 "a ColumnProperty already exists keyed to the name " 

1050 "%r for column %r" 

1051 % (self.key, self.name, self.name, self.key) 

1052 ) 

1053 p: ColumnProperty[Any] = properties.ColumnProperty( 

1054 parent.persist_selectable.c[self.key] 

1055 ) 

1056 parent._configure_property(self.name, p, init=init, setparent=True) 

1057 p._mapped_by_synonym = self.key 

1058 

1059 self.parent = parent 

1060 

1061 

1062class Synonym(SynonymProperty[_T], _DeclarativeMapped[_T]): 

1063 """Declarative front-end for the :class:`.SynonymProperty` class. 

1064 

1065 Public constructor is the :func:`_orm.synonym` function. 

1066 

1067 .. versionchanged:: 2.0 Added :class:`_orm.Synonym` as a Declarative 

1068 compatible subclass for :class:`_orm.SynonymProperty` 

1069 

1070 .. seealso:: 

1071 

1072 :ref:`synonyms` - Overview of synonyms 

1073 

1074 """ 

1075 

1076 inherit_cache = True 

1077 """:meta private:"""