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

286 statements  

« prev     ^ index     » next       coverage.py v7.0.1, created at 2022-12-25 06:11 +0000

1# orm/descriptor_props.py 

2# Copyright (C) 2005-2022 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""" 

13 

14from . import attributes 

15from . import util as orm_util 

16from .interfaces import MapperProperty 

17from .interfaces import PropComparator 

18from .util import _none_set 

19from .. import event 

20from .. import exc as sa_exc 

21from .. import schema 

22from .. import sql 

23from .. import util 

24from ..sql import expression 

25from ..sql import operators 

26 

27 

28class DescriptorProperty(MapperProperty): 

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

30 user-defined descriptor.""" 

31 

32 doc = None 

33 

34 uses_objects = False 

35 _links_to_entity = False 

36 

37 def instrument_class(self, mapper): 

38 prop = self 

39 

40 class _ProxyImpl(object): 

41 accepts_scalar_loader = False 

42 load_on_unexpire = True 

43 collection = False 

44 

45 @property 

46 def uses_objects(self): 

47 return prop.uses_objects 

48 

49 def __init__(self, key): 

50 self.key = key 

51 

52 if hasattr(prop, "get_history"): 

53 

54 def get_history( 

55 self, state, dict_, passive=attributes.PASSIVE_OFF 

56 ): 

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

58 

59 if self.descriptor is None: 

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

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

62 self.descriptor = desc 

63 

64 if self.descriptor is None: 

65 

66 def fset(obj, value): 

67 setattr(obj, self.name, value) 

68 

69 def fdel(obj): 

70 delattr(obj, self.name) 

71 

72 def fget(obj): 

73 return getattr(obj, self.name) 

74 

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

76 

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

78 self.parent.class_, 

79 self.key, 

80 self.descriptor, 

81 lambda: self._comparator_factory(mapper), 

82 doc=self.doc, 

83 original_property=self, 

84 ) 

85 proxy_attr.impl = _ProxyImpl(self.key) 

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

87 

88 

89class CompositeProperty(DescriptorProperty): 

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

91 of columns as one attribute. 

92 

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

94 function. 

95 

96 .. seealso:: 

97 

98 :ref:`mapper_composite` 

99 

100 """ 

101 

102 def __init__(self, class_, *attrs, **kwargs): 

103 r"""Return a composite column-based property for use with a Mapper. 

104 

105 See the mapping documentation section :ref:`mapper_composite` for a 

106 full usage example. 

107 

108 The :class:`.MapperProperty` returned by :func:`.composite` 

109 is the :class:`.CompositeProperty`. 

110 

111 :param class\_: 

112 The "composite type" class, or any classmethod or callable which 

113 will produce a new instance of the composite object given the 

114 column values in order. 

115 

116 :param \*cols: 

117 List of Column objects to be mapped. 

118 

119 :param active_history=False: 

120 When ``True``, indicates that the "previous" value for a 

121 scalar attribute should be loaded when replaced, if not 

122 already loaded. See the same flag on :func:`.column_property`. 

123 

124 :param group: 

125 A group name for this property when marked as deferred. 

126 

127 :param deferred: 

128 When True, the column property is "deferred", meaning that it does 

129 not load immediately, and is instead loaded when the attribute is 

130 first accessed on an instance. See also 

131 :func:`~sqlalchemy.orm.deferred`. 

132 

133 :param comparator_factory: a class which extends 

134 :class:`.CompositeProperty.Comparator` which provides custom SQL 

135 clause generation for comparison operations. 

136 

137 :param doc: 

138 optional string that will be applied as the doc on the 

139 class-bound descriptor. 

140 

141 :param info: Optional data dictionary which will be populated into the 

142 :attr:`.MapperProperty.info` attribute of this object. 

143 

144 """ 

145 super(CompositeProperty, self).__init__() 

146 

147 self.attrs = attrs 

148 self.composite_class = class_ 

149 self.active_history = kwargs.get("active_history", False) 

150 self.deferred = kwargs.get("deferred", False) 

151 self.group = kwargs.get("group", None) 

152 self.comparator_factory = kwargs.pop( 

153 "comparator_factory", self.__class__.Comparator 

154 ) 

155 if "info" in kwargs: 

156 self.info = kwargs.pop("info") 

157 

158 util.set_creation_order(self) 

159 self._create_descriptor() 

160 

161 def instrument_class(self, mapper): 

162 super(CompositeProperty, self).instrument_class(mapper) 

163 self._setup_event_handlers() 

164 

165 def do_init(self): 

166 """Initialization which occurs after the :class:`.CompositeProperty` 

167 has been associated with its parent mapper. 

168 

169 """ 

170 self._setup_arguments_on_columns() 

171 

172 _COMPOSITE_FGET = object() 

173 

174 def _create_descriptor(self): 

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

176 the access point on instances of the mapped class. 

177 

178 """ 

179 

180 def fget(instance): 

181 dict_ = attributes.instance_dict(instance) 

182 state = attributes.instance_state(instance) 

183 

184 if self.key not in dict_: 

185 # key not present. Iterate through related 

186 # attributes, retrieve their values. This 

187 # ensures they all load. 

188 values = [ 

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

190 ] 

191 

192 # current expected behavior here is that the composite is 

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

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

195 # if the composite were created unconditionally, 

196 # but that would be a behavioral change. 

197 if self.key not in dict_ and ( 

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

199 ): 

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

201 state.manager.dispatch.refresh( 

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

203 ) 

204 

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

206 

207 def fset(instance, value): 

208 dict_ = attributes.instance_dict(instance) 

209 state = attributes.instance_state(instance) 

210 attr = state.manager[self.key] 

211 previous = dict_.get(self.key, attributes.NO_VALUE) 

212 for fn in attr.dispatch.set: 

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

214 dict_[self.key] = value 

215 if value is None: 

216 for key in self._attribute_keys: 

217 setattr(instance, key, None) 

218 else: 

219 for key, value in zip( 

220 self._attribute_keys, value.__composite_values__() 

221 ): 

222 setattr(instance, key, value) 

223 

224 def fdel(instance): 

225 state = attributes.instance_state(instance) 

226 dict_ = attributes.instance_dict(instance) 

227 previous = dict_.pop(self.key, attributes.NO_VALUE) 

228 attr = state.manager[self.key] 

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

230 for key in self._attribute_keys: 

231 setattr(instance, key, None) 

232 

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

234 

235 @util.memoized_property 

236 def _comparable_elements(self): 

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

238 

239 @util.memoized_property 

240 def props(self): 

241 props = [] 

242 for attr in self.attrs: 

243 if isinstance(attr, str): 

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

245 elif isinstance(attr, schema.Column): 

246 prop = self.parent._columntoproperty[attr] 

247 elif isinstance(attr, attributes.InstrumentedAttribute): 

248 prop = attr.property 

249 else: 

250 raise sa_exc.ArgumentError( 

251 "Composite expects Column objects or mapped " 

252 "attributes/attribute names as arguments, got: %r" 

253 % (attr,) 

254 ) 

255 props.append(prop) 

256 return props 

257 

258 @property 

259 def columns(self): 

260 return [a for a in self.attrs if isinstance(a, schema.Column)] 

261 

262 def _setup_arguments_on_columns(self): 

263 """Propagate configuration arguments made on this composite 

264 to the target columns, for those that apply. 

265 

266 """ 

267 for prop in self.props: 

268 prop.active_history = self.active_history 

269 if self.deferred: 

270 prop.deferred = self.deferred 

271 prop.strategy_key = (("deferred", True), ("instrument", True)) 

272 prop.group = self.group 

273 

274 def _setup_event_handlers(self): 

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

276 

277 def load_handler(state, context): 

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

279 

280 def refresh_handler(state, context, to_load): 

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

282 

283 if not to_load or ( 

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

285 ).intersection(to_load): 

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

287 

288 def _load_refresh_handler(state, context, to_load, is_refresh): 

289 dict_ = state.dict 

290 

291 # if context indicates we are coming from the 

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

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

294 # want to catch it) 

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

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

297 # be some other event that mutablecomposite can subscribe 

298 # towards for this. 

299 

300 if ( 

301 not is_refresh or context is self._COMPOSITE_FGET 

302 ) and self.key in dict_: 

303 return 

304 

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

306 # __get__() will initiate a load for those 

307 # columns 

308 for k in self._attribute_keys: 

309 if k not in dict_: 

310 return 

311 

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

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

314 ) 

315 

316 def expire_handler(state, keys): 

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

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

319 

320 def insert_update_handler(mapper, connection, state): 

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

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

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

324 recreates. 

325 

326 """ 

327 

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

329 

330 event.listen( 

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

332 ) 

333 event.listen( 

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

335 ) 

336 event.listen( 

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

338 ) 

339 event.listen( 

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

341 ) 

342 event.listen( 

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

344 ) 

345 

346 # TODO: need a deserialize hook here 

347 

348 @util.memoized_property 

349 def _attribute_keys(self): 

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

351 

352 def get_history(self, state, dict_, passive=attributes.PASSIVE_OFF): 

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

354 

355 added = [] 

356 deleted = [] 

357 

358 has_history = False 

359 for prop in self.props: 

360 key = prop.key 

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

362 if hist.has_changes(): 

363 has_history = True 

364 

365 non_deleted = hist.non_deleted() 

366 if non_deleted: 

367 added.extend(non_deleted) 

368 else: 

369 added.append(None) 

370 if hist.deleted: 

371 deleted.extend(hist.deleted) 

372 else: 

373 deleted.append(None) 

374 

375 if has_history: 

376 return attributes.History( 

377 [self.composite_class(*added)], 

378 (), 

379 [self.composite_class(*deleted)], 

380 ) 

381 else: 

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

383 

384 def _comparator_factory(self, mapper): 

385 return self.comparator_factory(self, mapper) 

386 

387 class CompositeBundle(orm_util.Bundle): 

388 def __init__(self, property_, expr): 

389 self.property = property_ 

390 super(CompositeProperty.CompositeBundle, self).__init__( 

391 property_.key, *expr 

392 ) 

393 

394 def create_row_processor(self, query, procs, labels): 

395 def proc(row): 

396 return self.property.composite_class( 

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

398 ) 

399 

400 return proc 

401 

402 class Comparator(PropComparator): 

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

404 :class:`.CompositeProperty` attributes. 

405 

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

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

408 

409 .. seealso:: 

410 

411 :class:`.PropComparator` 

412 

413 :class:`.ColumnOperators` 

414 

415 :ref:`types_operators` 

416 

417 :attr:`.TypeEngine.comparator_factory` 

418 

419 """ 

420 

421 __hash__ = None 

422 

423 @util.memoized_property 

424 def clauses(self): 

425 return expression.ClauseList( 

426 group=False, *self._comparable_elements 

427 ) 

428 

429 def __clause_element__(self): 

430 return self.expression 

431 

432 @util.memoized_property 

433 def expression(self): 

434 clauses = self.clauses._annotate( 

435 { 

436 "parententity": self._parententity, 

437 "parentmapper": self._parententity, 

438 "proxy_key": self.prop.key, 

439 } 

440 ) 

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

442 

443 def _bulk_update_tuples(self, value): 

444 if isinstance(value, sql.elements.BindParameter): 

445 value = value.value 

446 

447 if value is None: 

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

449 elif isinstance(value, self.prop.composite_class): 

450 values = value.__composite_values__() 

451 else: 

452 raise sa_exc.ArgumentError( 

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

454 % (self.prop, value) 

455 ) 

456 

457 return zip(self._comparable_elements, values) 

458 

459 @util.memoized_property 

460 def _comparable_elements(self): 

461 if self._adapt_to_entity: 

462 return [ 

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

464 for prop in self.prop._comparable_elements 

465 ] 

466 else: 

467 return self.prop._comparable_elements 

468 

469 def __eq__(self, other): 

470 if other is None: 

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

472 else: 

473 values = other.__composite_values__() 

474 comparisons = [ 

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

476 ] 

477 if self._adapt_to_entity: 

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

479 return sql.and_(*comparisons) 

480 

481 def __ne__(self, other): 

482 return sql.not_(self.__eq__(other)) 

483 

484 def __str__(self): 

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

486 

487 

488class ConcreteInheritedProperty(DescriptorProperty): 

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

490 an attribute on a concrete subclass that is only present 

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

492 

493 Cases where this occurs include: 

494 

495 * When the superclass mapper is mapped against a 

496 "polymorphic union", which includes all attributes from 

497 all subclasses. 

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

499 but not on the subclass mapper. Concrete mappers require 

500 that relationship() is configured explicitly on each 

501 subclass. 

502 

503 """ 

504 

505 def _comparator_factory(self, mapper): 

506 comparator_callable = None 

507 

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

509 p = m._props[self.key] 

510 if not isinstance(p, ConcreteInheritedProperty): 

511 comparator_callable = p.comparator_factory 

512 break 

513 return comparator_callable 

514 

515 def __init__(self): 

516 super(ConcreteInheritedProperty, self).__init__() 

517 

518 def warn(): 

519 raise AttributeError( 

520 "Concrete %s does not implement " 

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

522 "this property explicitly to %s." 

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

524 ) 

525 

526 class NoninheritedConcreteProp(object): 

527 def __set__(s, obj, value): 

528 warn() 

529 

530 def __delete__(s, obj): 

531 warn() 

532 

533 def __get__(s, obj, owner): 

534 if obj is None: 

535 return self.descriptor 

536 warn() 

537 

538 self.descriptor = NoninheritedConcreteProp() 

539 

540 

541class SynonymProperty(DescriptorProperty): 

542 def __init__( 

543 self, 

544 name, 

545 map_column=None, 

546 descriptor=None, 

547 comparator_factory=None, 

548 doc=None, 

549 info=None, 

550 ): 

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

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

553 of another attribute. 

554 

555 e.g.:: 

556 

557 class MyClass(Base): 

558 __tablename__ = 'my_table' 

559 

560 id = Column(Integer, primary_key=True) 

561 job_status = Column(String(50)) 

562 

563 status = synonym("job_status") 

564 

565 

566 :param name: the name of the existing mapped property. This 

567 can refer to the string name ORM-mapped attribute 

568 configured on the class, including column-bound attributes 

569 and relationships. 

570 

571 :param descriptor: a Python :term:`descriptor` that will be used 

572 as a getter (and potentially a setter) when this attribute is 

573 accessed at the instance level. 

574 

575 :param map_column: **For classical mappings and mappings against 

576 an existing Table object only**. if ``True``, the :func:`.synonym` 

577 construct will locate the :class:`_schema.Column` 

578 object upon the mapped 

579 table that would normally be associated with the attribute name of 

580 this synonym, and produce a new :class:`.ColumnProperty` that instead 

581 maps this :class:`_schema.Column` 

582 to the alternate name given as the "name" 

583 argument of the synonym; in this way, the usual step of redefining 

584 the mapping of the :class:`_schema.Column` 

585 to be under a different name is 

586 unnecessary. This is usually intended to be used when a 

587 :class:`_schema.Column` 

588 is to be replaced with an attribute that also uses a 

589 descriptor, that is, in conjunction with the 

590 :paramref:`.synonym.descriptor` parameter:: 

591 

592 my_table = Table( 

593 "my_table", metadata, 

594 Column('id', Integer, primary_key=True), 

595 Column('job_status', String(50)) 

596 ) 

597 

598 class MyClass(object): 

599 @property 

600 def _job_status_descriptor(self): 

601 return "Status: %s" % self._job_status 

602 

603 

604 mapper( 

605 MyClass, my_table, properties={ 

606 "job_status": synonym( 

607 "_job_status", map_column=True, 

608 descriptor=MyClass._job_status_descriptor) 

609 } 

610 ) 

611 

612 Above, the attribute named ``_job_status`` is automatically 

613 mapped to the ``job_status`` column:: 

614 

615 >>> j1 = MyClass() 

616 >>> j1._job_status = "employed" 

617 >>> j1.job_status 

618 Status: employed 

619 

620 When using Declarative, in order to provide a descriptor in 

621 conjunction with a synonym, use the 

622 :func:`sqlalchemy.ext.declarative.synonym_for` helper. However, 

623 note that the :ref:`hybrid properties <mapper_hybrids>` feature 

624 should usually be preferred, particularly when redefining attribute 

625 behavior. 

626 

627 :param info: Optional data dictionary which will be populated into the 

628 :attr:`.InspectionAttr.info` attribute of this object. 

629 

630 .. versionadded:: 1.0.0 

631 

632 :param comparator_factory: A subclass of :class:`.PropComparator` 

633 that will provide custom comparison behavior at the SQL expression 

634 level. 

635 

636 .. note:: 

637 

638 For the use case of providing an attribute which redefines both 

639 Python-level and SQL-expression level behavior of an attribute, 

640 please refer to the Hybrid attribute introduced at 

641 :ref:`mapper_hybrids` for a more effective technique. 

642 

643 .. seealso:: 

644 

645 :ref:`synonyms` - Overview of synonyms 

646 

647 :func:`.synonym_for` - a helper oriented towards Declarative 

648 

649 :ref:`mapper_hybrids` - The Hybrid Attribute extension provides an 

650 updated approach to augmenting attribute behavior more flexibly 

651 than can be achieved with synonyms. 

652 

653 """ 

654 super(SynonymProperty, self).__init__() 

655 

656 self.name = name 

657 self.map_column = map_column 

658 self.descriptor = descriptor 

659 self.comparator_factory = comparator_factory 

660 self.doc = doc or (descriptor and descriptor.__doc__) or None 

661 if info: 

662 self.info = info 

663 

664 util.set_creation_order(self) 

665 

666 @property 

667 def uses_objects(self): 

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

669 

670 # TODO: when initialized, check _proxied_object, 

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

672 

673 @util.memoized_property 

674 def _proxied_object(self): 

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

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

677 attr.property, MapperProperty 

678 ): 

679 # attribute is a non-MapperProprerty proxy such as 

680 # hybrid or association proxy 

681 if isinstance(attr, attributes.QueryableAttribute): 

682 return attr.comparator 

683 elif isinstance(attr, operators.ColumnOperators): 

684 return attr 

685 

686 raise sa_exc.InvalidRequestError( 

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

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

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

690 ) 

691 return attr.property 

692 

693 def _comparator_factory(self, mapper): 

694 prop = self._proxied_object 

695 

696 if isinstance(prop, MapperProperty): 

697 if self.comparator_factory: 

698 comp = self.comparator_factory(prop, mapper) 

699 else: 

700 comp = prop.comparator_factory(prop, mapper) 

701 return comp 

702 else: 

703 return prop 

704 

705 def get_history(self, *arg, **kw): 

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

707 return attr.impl.get_history(*arg, **kw) 

708 

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

710 def set_parent(self, parent, init): 

711 properties = util.preloaded.orm_properties 

712 

713 if self.map_column: 

714 # implement the 'map_column' option. 

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

716 raise sa_exc.ArgumentError( 

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

718 "'%s' named '%s'" 

719 % ( 

720 self.name, 

721 parent.persist_selectable.description, 

722 self.key, 

723 ) 

724 ) 

725 elif ( 

726 parent.persist_selectable.c[self.key] 

727 in parent._columntoproperty 

728 and parent._columntoproperty[ 

729 parent.persist_selectable.c[self.key] 

730 ].key 

731 == self.name 

732 ): 

733 raise sa_exc.ArgumentError( 

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

735 "a ColumnProperty already exists keyed to the name " 

736 "%r for column %r" 

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

738 ) 

739 p = properties.ColumnProperty( 

740 parent.persist_selectable.c[self.key] 

741 ) 

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

743 p._mapped_by_synonym = self.key 

744 

745 self.parent = parent