Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# ext/associationproxy.py 

2# Copyright (C) 2005-2020 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: http://www.opensource.org/licenses/mit-license.php 

7 

8"""Contain the ``AssociationProxy`` class. 

9 

10The ``AssociationProxy`` is a Python property object which provides 

11transparent proxied access to the endpoint of an association object. 

12 

13See the example ``examples/association/proxied_association.py``. 

14 

15""" 

16import operator 

17 

18from .. import exc 

19from .. import inspect 

20from .. import orm 

21from .. import util 

22from ..orm import collections 

23from ..orm import interfaces 

24from ..sql import or_ 

25from ..sql.operators import ColumnOperators 

26 

27 

28def association_proxy(target_collection, attr, **kw): 

29 r"""Return a Python property implementing a view of a target 

30 attribute which references an attribute on members of the 

31 target. 

32 

33 The returned value is an instance of :class:`.AssociationProxy`. 

34 

35 Implements a Python property representing a relationship as a collection 

36 of simpler values, or a scalar value. The proxied property will mimic 

37 the collection type of the target (list, dict or set), or, in the case of 

38 a one to one relationship, a simple scalar value. 

39 

40 :param target_collection: Name of the attribute we'll proxy to. 

41 This attribute is typically mapped by 

42 :func:`~sqlalchemy.orm.relationship` to link to a target collection, but 

43 can also be a many-to-one or non-scalar relationship. 

44 

45 :param attr: Attribute on the associated instance or instances we'll 

46 proxy for. 

47 

48 For example, given a target collection of [obj1, obj2], a list created 

49 by this proxy property would look like [getattr(obj1, *attr*), 

50 getattr(obj2, *attr*)] 

51 

52 If the relationship is one-to-one or otherwise uselist=False, then 

53 simply: getattr(obj, *attr*) 

54 

55 :param creator: optional. 

56 

57 When new items are added to this proxied collection, new instances of 

58 the class collected by the target collection will be created. For list 

59 and set collections, the target class constructor will be called with 

60 the 'value' for the new instance. For dict types, two arguments are 

61 passed: key and value. 

62 

63 If you want to construct instances differently, supply a *creator* 

64 function that takes arguments as above and returns instances. 

65 

66 For scalar relationships, creator() will be called if the target is None. 

67 If the target is present, set operations are proxied to setattr() on the 

68 associated object. 

69 

70 If you have an associated object with multiple attributes, you may set 

71 up multiple association proxies mapping to different attributes. See 

72 the unit tests for examples, and for examples of how creator() functions 

73 can be used to construct the scalar relationship on-demand in this 

74 situation. 

75 

76 :param \*\*kw: Passes along any other keyword arguments to 

77 :class:`.AssociationProxy`. 

78 

79 """ 

80 return AssociationProxy(target_collection, attr, **kw) 

81 

82 

83ASSOCIATION_PROXY = util.symbol("ASSOCIATION_PROXY") 

84"""Symbol indicating an :class:`.InspectionAttr` that's 

85 of type :class:`.AssociationProxy`. 

86 

87 Is assigned to the :attr:`.InspectionAttr.extension_type` 

88 attribute. 

89 

90""" 

91 

92 

93class AssociationProxy(interfaces.InspectionAttrInfo): 

94 """A descriptor that presents a read/write view of an object attribute.""" 

95 

96 is_attribute = True 

97 extension_type = ASSOCIATION_PROXY 

98 

99 def __init__( 

100 self, 

101 target_collection, 

102 attr, 

103 creator=None, 

104 getset_factory=None, 

105 proxy_factory=None, 

106 proxy_bulk_set=None, 

107 info=None, 

108 cascade_scalar_deletes=False, 

109 ): 

110 """Construct a new :class:`.AssociationProxy`. 

111 

112 The :func:`.association_proxy` function is provided as the usual 

113 entrypoint here, though :class:`.AssociationProxy` can be instantiated 

114 and/or subclassed directly. 

115 

116 :param target_collection: Name of the collection we'll proxy to, 

117 usually created with :func:`_orm.relationship`. 

118 

119 :param attr: Attribute on the collected instances we'll proxy 

120 for. For example, given a target collection of [obj1, obj2], a 

121 list created by this proxy property would look like 

122 [getattr(obj1, attr), getattr(obj2, attr)] 

123 

124 :param creator: Optional. When new items are added to this proxied 

125 collection, new instances of the class collected by the target 

126 collection will be created. For list and set collections, the 

127 target class constructor will be called with the 'value' for the 

128 new instance. For dict types, two arguments are passed: 

129 key and value. 

130 

131 If you want to construct instances differently, supply a 'creator' 

132 function that takes arguments as above and returns instances. 

133 

134 :param cascade_scalar_deletes: when True, indicates that setting 

135 the proxied value to ``None``, or deleting it via ``del``, should 

136 also remove the source object. Only applies to scalar attributes. 

137 Normally, removing the proxied target will not remove the proxy 

138 source, as this object may have other state that is still to be 

139 kept. 

140 

141 .. versionadded:: 1.3 

142 

143 .. seealso:: 

144 

145 :ref:`cascade_scalar_deletes` - complete usage example 

146 

147 :param getset_factory: Optional. Proxied attribute access is 

148 automatically handled by routines that get and set values based on 

149 the `attr` argument for this proxy. 

150 

151 If you would like to customize this behavior, you may supply a 

152 `getset_factory` callable that produces a tuple of `getter` and 

153 `setter` functions. The factory is called with two arguments, the 

154 abstract type of the underlying collection and this proxy instance. 

155 

156 :param proxy_factory: Optional. The type of collection to emulate is 

157 determined by sniffing the target collection. If your collection 

158 type can't be determined by duck typing or you'd like to use a 

159 different collection implementation, you may supply a factory 

160 function to produce those collections. Only applicable to 

161 non-scalar relationships. 

162 

163 :param proxy_bulk_set: Optional, use with proxy_factory. See 

164 the _set() method for details. 

165 

166 :param info: optional, will be assigned to 

167 :attr:`.AssociationProxy.info` if present. 

168 

169 .. versionadded:: 1.0.9 

170 

171 """ 

172 self.target_collection = target_collection 

173 self.value_attr = attr 

174 self.creator = creator 

175 self.getset_factory = getset_factory 

176 self.proxy_factory = proxy_factory 

177 self.proxy_bulk_set = proxy_bulk_set 

178 self.cascade_scalar_deletes = cascade_scalar_deletes 

179 

180 self.key = "_%s_%s_%s" % ( 

181 type(self).__name__, 

182 target_collection, 

183 id(self), 

184 ) 

185 if info: 

186 self.info = info 

187 

188 def __get__(self, obj, class_): 

189 if class_ is None: 

190 return self 

191 inst = self._as_instance(class_, obj) 

192 if inst: 

193 return inst.get(obj) 

194 

195 # obj has to be None here 

196 # assert obj is None 

197 

198 return self 

199 

200 def __set__(self, obj, values): 

201 class_ = type(obj) 

202 return self._as_instance(class_, obj).set(obj, values) 

203 

204 def __delete__(self, obj): 

205 class_ = type(obj) 

206 return self._as_instance(class_, obj).delete(obj) 

207 

208 def for_class(self, class_, obj=None): 

209 r"""Return the internal state local to a specific mapped class. 

210 

211 E.g., given a class ``User``:: 

212 

213 class User(Base): 

214 # ... 

215 

216 keywords = association_proxy('kws', 'keyword') 

217 

218 If we access this :class:`.AssociationProxy` from 

219 :attr:`_orm.Mapper.all_orm_descriptors`, and we want to view the 

220 target class for this proxy as mapped by ``User``:: 

221 

222 inspect(User).all_orm_descriptors["keywords"].for_class(User).target_class 

223 

224 This returns an instance of :class:`.AssociationProxyInstance` that 

225 is specific to the ``User`` class. The :class:`.AssociationProxy` 

226 object remains agnostic of its parent class. 

227 

228 :param class\_: the class that we are returning state for. 

229 

230 :param obj: optional, an instance of the class that is required 

231 if the attribute refers to a polymorphic target, e.g. where we have 

232 to look at the type of the actual destination object to get the 

233 complete path. 

234 

235 .. versionadded:: 1.3 - :class:`.AssociationProxy` no longer stores 

236 any state specific to a particular parent class; the state is now 

237 stored in per-class :class:`.AssociationProxyInstance` objects. 

238 

239 

240 """ 

241 return self._as_instance(class_, obj) 

242 

243 def _as_instance(self, class_, obj): 

244 try: 

245 inst = class_.__dict__[self.key + "_inst"] 

246 except KeyError: 

247 inst = None 

248 

249 # avoid exception context 

250 if inst is None: 

251 owner = self._calc_owner(class_) 

252 if owner is not None: 

253 inst = AssociationProxyInstance.for_proxy(self, owner, obj) 

254 setattr(class_, self.key + "_inst", inst) 

255 else: 

256 inst = None 

257 

258 if inst is not None and not inst._is_canonical: 

259 # the AssociationProxyInstance can't be generalized 

260 # since the proxied attribute is not on the targeted 

261 # class, only on subclasses of it, which might be 

262 # different. only return for the specific 

263 # object's current value 

264 return inst._non_canonical_get_for_object(obj) 

265 else: 

266 return inst 

267 

268 def _calc_owner(self, target_cls): 

269 # we might be getting invoked for a subclass 

270 # that is not mapped yet, in some declarative situations. 

271 # save until we are mapped 

272 try: 

273 insp = inspect(target_cls) 

274 except exc.NoInspectionAvailable: 

275 # can't find a mapper, don't set owner. if we are a not-yet-mapped 

276 # subclass, we can also scan through __mro__ to find a mapped 

277 # class, but instead just wait for us to be called again against a 

278 # mapped class normally. 

279 return None 

280 else: 

281 return insp.mapper.class_manager.class_ 

282 

283 def _default_getset(self, collection_class): 

284 attr = self.value_attr 

285 _getter = operator.attrgetter(attr) 

286 

287 def getter(target): 

288 return _getter(target) if target is not None else None 

289 

290 if collection_class is dict: 

291 

292 def setter(o, k, v): 

293 setattr(o, attr, v) 

294 

295 else: 

296 

297 def setter(o, v): 

298 setattr(o, attr, v) 

299 

300 return getter, setter 

301 

302 def __repr__(self): 

303 return "AssociationProxy(%r, %r)" % ( 

304 self.target_collection, 

305 self.value_attr, 

306 ) 

307 

308 

309class AssociationProxyInstance(object): 

310 """A per-class object that serves class- and object-specific results. 

311 

312 This is used by :class:`.AssociationProxy` when it is invoked 

313 in terms of a specific class or instance of a class, i.e. when it is 

314 used as a regular Python descriptor. 

315 

316 When referring to the :class:`.AssociationProxy` as a normal Python 

317 descriptor, the :class:`.AssociationProxyInstance` is the object that 

318 actually serves the information. Under normal circumstances, its presence 

319 is transparent:: 

320 

321 >>> User.keywords.scalar 

322 False 

323 

324 In the special case that the :class:`.AssociationProxy` object is being 

325 accessed directly, in order to get an explicit handle to the 

326 :class:`.AssociationProxyInstance`, use the 

327 :meth:`.AssociationProxy.for_class` method:: 

328 

329 proxy_state = inspect(User).all_orm_descriptors["keywords"].for_class(User) 

330 

331 # view if proxy object is scalar or not 

332 >>> proxy_state.scalar 

333 False 

334 

335 .. versionadded:: 1.3 

336 

337 """ # noqa 

338 

339 def __init__(self, parent, owning_class, target_class, value_attr): 

340 self.parent = parent 

341 self.key = parent.key 

342 self.owning_class = owning_class 

343 self.target_collection = parent.target_collection 

344 self.collection_class = None 

345 self.target_class = target_class 

346 self.value_attr = value_attr 

347 

348 target_class = None 

349 """The intermediary class handled by this 

350 :class:`.AssociationProxyInstance`. 

351 

352 Intercepted append/set/assignment events will result 

353 in the generation of new instances of this class. 

354 

355 """ 

356 

357 @classmethod 

358 def for_proxy(cls, parent, owning_class, parent_instance): 

359 target_collection = parent.target_collection 

360 value_attr = parent.value_attr 

361 prop = orm.class_mapper(owning_class).get_property(target_collection) 

362 

363 # this was never asserted before but this should be made clear. 

364 if not isinstance(prop, orm.RelationshipProperty): 

365 util.raise_( 

366 NotImplementedError( 

367 "association proxy to a non-relationship " 

368 "intermediary is not supported" 

369 ), 

370 replace_context=None, 

371 ) 

372 

373 target_class = prop.mapper.class_ 

374 

375 try: 

376 target_assoc = cls._cls_unwrap_target_assoc_proxy( 

377 target_class, value_attr 

378 ) 

379 except AttributeError: 

380 # the proxied attribute doesn't exist on the target class; 

381 # return an "ambiguous" instance that will work on a per-object 

382 # basis 

383 return AmbiguousAssociationProxyInstance( 

384 parent, owning_class, target_class, value_attr 

385 ) 

386 else: 

387 return cls._construct_for_assoc( 

388 target_assoc, parent, owning_class, target_class, value_attr 

389 ) 

390 

391 @classmethod 

392 def _construct_for_assoc( 

393 cls, target_assoc, parent, owning_class, target_class, value_attr 

394 ): 

395 if target_assoc is not None: 

396 return ObjectAssociationProxyInstance( 

397 parent, owning_class, target_class, value_attr 

398 ) 

399 

400 attr = getattr(target_class, value_attr) 

401 if not hasattr(attr, "_is_internal_proxy"): 

402 return AmbiguousAssociationProxyInstance( 

403 parent, owning_class, target_class, value_attr 

404 ) 

405 is_object = attr._impl_uses_objects 

406 if is_object: 

407 return ObjectAssociationProxyInstance( 

408 parent, owning_class, target_class, value_attr 

409 ) 

410 else: 

411 return ColumnAssociationProxyInstance( 

412 parent, owning_class, target_class, value_attr 

413 ) 

414 

415 def _get_property(self): 

416 return orm.class_mapper(self.owning_class).get_property( 

417 self.target_collection 

418 ) 

419 

420 @property 

421 def _comparator(self): 

422 return self._get_property().comparator 

423 

424 @classmethod 

425 def _cls_unwrap_target_assoc_proxy(cls, target_class, value_attr): 

426 attr = getattr(target_class, value_attr) 

427 if isinstance(attr, (AssociationProxy, AssociationProxyInstance)): 

428 return attr 

429 return None 

430 

431 @util.memoized_property 

432 def _unwrap_target_assoc_proxy(self): 

433 return self._cls_unwrap_target_assoc_proxy( 

434 self.target_class, self.value_attr 

435 ) 

436 

437 @property 

438 def remote_attr(self): 

439 """The 'remote' class attribute referenced by this 

440 :class:`.AssociationProxyInstance`. 

441 

442 .. seealso:: 

443 

444 :attr:`.AssociationProxyInstance.attr` 

445 

446 :attr:`.AssociationProxyInstance.local_attr` 

447 

448 """ 

449 return getattr(self.target_class, self.value_attr) 

450 

451 @property 

452 def local_attr(self): 

453 """The 'local' class attribute referenced by this 

454 :class:`.AssociationProxyInstance`. 

455 

456 .. seealso:: 

457 

458 :attr:`.AssociationProxyInstance.attr` 

459 

460 :attr:`.AssociationProxyInstance.remote_attr` 

461 

462 """ 

463 return getattr(self.owning_class, self.target_collection) 

464 

465 @property 

466 def attr(self): 

467 """Return a tuple of ``(local_attr, remote_attr)``. 

468 

469 This attribute is convenient when specifying a join 

470 using :meth:`_query.Query.join` across two relationships:: 

471 

472 sess.query(Parent).join(*Parent.proxied.attr) 

473 

474 .. seealso:: 

475 

476 :attr:`.AssociationProxyInstance.local_attr` 

477 

478 :attr:`.AssociationProxyInstance.remote_attr` 

479 

480 """ 

481 return (self.local_attr, self.remote_attr) 

482 

483 @util.memoized_property 

484 def scalar(self): 

485 """Return ``True`` if this :class:`.AssociationProxyInstance` 

486 proxies a scalar relationship on the local side.""" 

487 

488 scalar = not self._get_property().uselist 

489 if scalar: 

490 self._initialize_scalar_accessors() 

491 return scalar 

492 

493 @util.memoized_property 

494 def _value_is_scalar(self): 

495 return ( 

496 not self._get_property() 

497 .mapper.get_property(self.value_attr) 

498 .uselist 

499 ) 

500 

501 @property 

502 def _target_is_object(self): 

503 raise NotImplementedError() 

504 

505 def _initialize_scalar_accessors(self): 

506 if self.parent.getset_factory: 

507 get, set_ = self.parent.getset_factory(None, self) 

508 else: 

509 get, set_ = self.parent._default_getset(None) 

510 self._scalar_get, self._scalar_set = get, set_ 

511 

512 def _default_getset(self, collection_class): 

513 attr = self.value_attr 

514 _getter = operator.attrgetter(attr) 

515 

516 def getter(target): 

517 return _getter(target) if target is not None else None 

518 

519 if collection_class is dict: 

520 

521 def setter(o, k, v): 

522 return setattr(o, attr, v) 

523 

524 else: 

525 

526 def setter(o, v): 

527 return setattr(o, attr, v) 

528 

529 return getter, setter 

530 

531 @property 

532 def info(self): 

533 return self.parent.info 

534 

535 def get(self, obj): 

536 if obj is None: 

537 return self 

538 

539 if self.scalar: 

540 target = getattr(obj, self.target_collection) 

541 return self._scalar_get(target) 

542 else: 

543 try: 

544 # If the owning instance is reborn (orm session resurrect, 

545 # etc.), refresh the proxy cache. 

546 creator_id, self_id, proxy = getattr(obj, self.key) 

547 except AttributeError: 

548 pass 

549 else: 

550 if id(obj) == creator_id and id(self) == self_id: 

551 assert self.collection_class is not None 

552 return proxy 

553 

554 self.collection_class, proxy = self._new( 

555 _lazy_collection(obj, self.target_collection) 

556 ) 

557 setattr(obj, self.key, (id(obj), id(self), proxy)) 

558 return proxy 

559 

560 def set(self, obj, values): 

561 if self.scalar: 

562 creator = ( 

563 self.parent.creator 

564 if self.parent.creator 

565 else self.target_class 

566 ) 

567 target = getattr(obj, self.target_collection) 

568 if target is None: 

569 if values is None: 

570 return 

571 setattr(obj, self.target_collection, creator(values)) 

572 else: 

573 self._scalar_set(target, values) 

574 if values is None and self.parent.cascade_scalar_deletes: 

575 setattr(obj, self.target_collection, None) 

576 else: 

577 proxy = self.get(obj) 

578 assert self.collection_class is not None 

579 if proxy is not values: 

580 proxy._bulk_replace(self, values) 

581 

582 def delete(self, obj): 

583 if self.owning_class is None: 

584 self._calc_owner(obj, None) 

585 

586 if self.scalar: 

587 target = getattr(obj, self.target_collection) 

588 if target is not None: 

589 delattr(target, self.value_attr) 

590 delattr(obj, self.target_collection) 

591 

592 def _new(self, lazy_collection): 

593 creator = ( 

594 self.parent.creator if self.parent.creator else self.target_class 

595 ) 

596 collection_class = util.duck_type_collection(lazy_collection()) 

597 

598 if self.parent.proxy_factory: 

599 return ( 

600 collection_class, 

601 self.parent.proxy_factory( 

602 lazy_collection, creator, self.value_attr, self 

603 ), 

604 ) 

605 

606 if self.parent.getset_factory: 

607 getter, setter = self.parent.getset_factory(collection_class, self) 

608 else: 

609 getter, setter = self.parent._default_getset(collection_class) 

610 

611 if collection_class is list: 

612 return ( 

613 collection_class, 

614 _AssociationList( 

615 lazy_collection, creator, getter, setter, self 

616 ), 

617 ) 

618 elif collection_class is dict: 

619 return ( 

620 collection_class, 

621 _AssociationDict( 

622 lazy_collection, creator, getter, setter, self 

623 ), 

624 ) 

625 elif collection_class is set: 

626 return ( 

627 collection_class, 

628 _AssociationSet( 

629 lazy_collection, creator, getter, setter, self 

630 ), 

631 ) 

632 else: 

633 raise exc.ArgumentError( 

634 "could not guess which interface to use for " 

635 'collection_class "%s" backing "%s"; specify a ' 

636 "proxy_factory and proxy_bulk_set manually" 

637 % (self.collection_class.__name__, self.target_collection) 

638 ) 

639 

640 def _set(self, proxy, values): 

641 if self.parent.proxy_bulk_set: 

642 self.parent.proxy_bulk_set(proxy, values) 

643 elif self.collection_class is list: 

644 proxy.extend(values) 

645 elif self.collection_class is dict: 

646 proxy.update(values) 

647 elif self.collection_class is set: 

648 proxy.update(values) 

649 else: 

650 raise exc.ArgumentError( 

651 "no proxy_bulk_set supplied for custom " 

652 "collection_class implementation" 

653 ) 

654 

655 def _inflate(self, proxy): 

656 creator = ( 

657 self.parent.creator and self.parent.creator or self.target_class 

658 ) 

659 

660 if self.parent.getset_factory: 

661 getter, setter = self.parent.getset_factory( 

662 self.collection_class, self 

663 ) 

664 else: 

665 getter, setter = self.parent._default_getset(self.collection_class) 

666 

667 proxy.creator = creator 

668 proxy.getter = getter 

669 proxy.setter = setter 

670 

671 def _criterion_exists(self, criterion=None, **kwargs): 

672 is_has = kwargs.pop("is_has", None) 

673 

674 target_assoc = self._unwrap_target_assoc_proxy 

675 if target_assoc is not None: 

676 inner = target_assoc._criterion_exists( 

677 criterion=criterion, **kwargs 

678 ) 

679 return self._comparator._criterion_exists(inner) 

680 

681 if self._target_is_object: 

682 prop = getattr(self.target_class, self.value_attr) 

683 value_expr = prop._criterion_exists(criterion, **kwargs) 

684 else: 

685 if kwargs: 

686 raise exc.ArgumentError( 

687 "Can't apply keyword arguments to column-targeted " 

688 "association proxy; use ==" 

689 ) 

690 elif is_has and criterion is not None: 

691 raise exc.ArgumentError( 

692 "Non-empty has() not allowed for " 

693 "column-targeted association proxy; use ==" 

694 ) 

695 

696 value_expr = criterion 

697 

698 return self._comparator._criterion_exists(value_expr) 

699 

700 def any(self, criterion=None, **kwargs): 

701 """Produce a proxied 'any' expression using EXISTS. 

702 

703 This expression will be a composed product 

704 using the :meth:`.RelationshipProperty.Comparator.any` 

705 and/or :meth:`.RelationshipProperty.Comparator.has` 

706 operators of the underlying proxied attributes. 

707 

708 """ 

709 if self._unwrap_target_assoc_proxy is None and ( 

710 self.scalar 

711 and (not self._target_is_object or self._value_is_scalar) 

712 ): 

713 raise exc.InvalidRequestError( 

714 "'any()' not implemented for scalar " "attributes. Use has()." 

715 ) 

716 return self._criterion_exists( 

717 criterion=criterion, is_has=False, **kwargs 

718 ) 

719 

720 def has(self, criterion=None, **kwargs): 

721 """Produce a proxied 'has' expression using EXISTS. 

722 

723 This expression will be a composed product 

724 using the :meth:`.RelationshipProperty.Comparator.any` 

725 and/or :meth:`.RelationshipProperty.Comparator.has` 

726 operators of the underlying proxied attributes. 

727 

728 """ 

729 if self._unwrap_target_assoc_proxy is None and ( 

730 not self.scalar 

731 or (self._target_is_object and not self._value_is_scalar) 

732 ): 

733 raise exc.InvalidRequestError( 

734 "'has()' not implemented for collections. " "Use any()." 

735 ) 

736 return self._criterion_exists( 

737 criterion=criterion, is_has=True, **kwargs 

738 ) 

739 

740 def __repr__(self): 

741 return "%s(%r)" % (self.__class__.__name__, self.parent) 

742 

743 

744class AmbiguousAssociationProxyInstance(AssociationProxyInstance): 

745 """an :class:`.AssociationProxyInstance` where we cannot determine 

746 the type of target object. 

747 """ 

748 

749 _is_canonical = False 

750 

751 def _ambiguous(self): 

752 raise AttributeError( 

753 "Association proxy %s.%s refers to an attribute '%s' that is not " 

754 "directly mapped on class %s; therefore this operation cannot " 

755 "proceed since we don't know what type of object is referred " 

756 "towards" 

757 % ( 

758 self.owning_class.__name__, 

759 self.target_collection, 

760 self.value_attr, 

761 self.target_class, 

762 ) 

763 ) 

764 

765 def get(self, obj): 

766 if obj is None: 

767 return self 

768 else: 

769 return super(AmbiguousAssociationProxyInstance, self).get(obj) 

770 

771 def __eq__(self, obj): 

772 self._ambiguous() 

773 

774 def __ne__(self, obj): 

775 self._ambiguous() 

776 

777 def any(self, criterion=None, **kwargs): 

778 self._ambiguous() 

779 

780 def has(self, criterion=None, **kwargs): 

781 self._ambiguous() 

782 

783 @util.memoized_property 

784 def _lookup_cache(self): 

785 # mapping of <subclass>->AssociationProxyInstance. 

786 # e.g. proxy is A-> A.b -> B -> B.b_attr, but B.b_attr doesn't exist; 

787 # only B1(B) and B2(B) have "b_attr", keys in here would be B1, B2 

788 return {} 

789 

790 def _non_canonical_get_for_object(self, parent_instance): 

791 if parent_instance is not None: 

792 actual_obj = getattr(parent_instance, self.target_collection) 

793 if actual_obj is not None: 

794 try: 

795 insp = inspect(actual_obj) 

796 except exc.NoInspectionAvailable: 

797 pass 

798 else: 

799 mapper = insp.mapper 

800 instance_class = mapper.class_ 

801 if instance_class not in self._lookup_cache: 

802 self._populate_cache(instance_class, mapper) 

803 

804 try: 

805 return self._lookup_cache[instance_class] 

806 except KeyError: 

807 pass 

808 

809 # no object or ambiguous object given, so return "self", which 

810 # is a proxy with generally only instance-level functionality 

811 return self 

812 

813 def _populate_cache(self, instance_class, mapper): 

814 prop = orm.class_mapper(self.owning_class).get_property( 

815 self.target_collection 

816 ) 

817 

818 if mapper.isa(prop.mapper): 

819 target_class = instance_class 

820 try: 

821 target_assoc = self._cls_unwrap_target_assoc_proxy( 

822 target_class, self.value_attr 

823 ) 

824 except AttributeError: 

825 pass 

826 else: 

827 self._lookup_cache[instance_class] = self._construct_for_assoc( 

828 target_assoc, 

829 self.parent, 

830 self.owning_class, 

831 target_class, 

832 self.value_attr, 

833 ) 

834 

835 

836class ObjectAssociationProxyInstance(AssociationProxyInstance): 

837 """an :class:`.AssociationProxyInstance` that has an object as a target. 

838 """ 

839 

840 _target_is_object = True 

841 _is_canonical = True 

842 

843 def contains(self, obj): 

844 """Produce a proxied 'contains' expression using EXISTS. 

845 

846 This expression will be a composed product 

847 using the :meth:`.RelationshipProperty.Comparator.any` 

848 , :meth:`.RelationshipProperty.Comparator.has`, 

849 and/or :meth:`.RelationshipProperty.Comparator.contains` 

850 operators of the underlying proxied attributes. 

851 """ 

852 

853 target_assoc = self._unwrap_target_assoc_proxy 

854 if target_assoc is not None: 

855 return self._comparator._criterion_exists( 

856 target_assoc.contains(obj) 

857 if not target_assoc.scalar 

858 else target_assoc == obj 

859 ) 

860 elif ( 

861 self._target_is_object 

862 and self.scalar 

863 and not self._value_is_scalar 

864 ): 

865 return self._comparator.has( 

866 getattr(self.target_class, self.value_attr).contains(obj) 

867 ) 

868 elif self._target_is_object and self.scalar and self._value_is_scalar: 

869 raise exc.InvalidRequestError( 

870 "contains() doesn't apply to a scalar object endpoint; use ==" 

871 ) 

872 else: 

873 

874 return self._comparator._criterion_exists(**{self.value_attr: obj}) 

875 

876 def __eq__(self, obj): 

877 # note the has() here will fail for collections; eq_() 

878 # is only allowed with a scalar. 

879 if obj is None: 

880 return or_( 

881 self._comparator.has(**{self.value_attr: obj}), 

882 self._comparator == None, 

883 ) 

884 else: 

885 return self._comparator.has(**{self.value_attr: obj}) 

886 

887 def __ne__(self, obj): 

888 # note the has() here will fail for collections; eq_() 

889 # is only allowed with a scalar. 

890 return self._comparator.has( 

891 getattr(self.target_class, self.value_attr) != obj 

892 ) 

893 

894 

895class ColumnAssociationProxyInstance( 

896 ColumnOperators, AssociationProxyInstance 

897): 

898 """an :class:`.AssociationProxyInstance` that has a database column as a 

899 target. 

900 """ 

901 

902 _target_is_object = False 

903 _is_canonical = True 

904 

905 def __eq__(self, other): 

906 # special case "is None" to check for no related row as well 

907 expr = self._criterion_exists( 

908 self.remote_attr.operate(operator.eq, other) 

909 ) 

910 if other is None: 

911 return or_(expr, self._comparator == None) 

912 else: 

913 return expr 

914 

915 def operate(self, op, *other, **kwargs): 

916 return self._criterion_exists( 

917 self.remote_attr.operate(op, *other, **kwargs) 

918 ) 

919 

920 

921class _lazy_collection(object): 

922 def __init__(self, obj, target): 

923 self.parent = obj 

924 self.target = target 

925 

926 def __call__(self): 

927 return getattr(self.parent, self.target) 

928 

929 def __getstate__(self): 

930 return {"obj": self.parent, "target": self.target} 

931 

932 def __setstate__(self, state): 

933 self.parent = state["obj"] 

934 self.target = state["target"] 

935 

936 

937class _AssociationCollection(object): 

938 def __init__(self, lazy_collection, creator, getter, setter, parent): 

939 """Constructs an _AssociationCollection. 

940 

941 This will always be a subclass of either _AssociationList, 

942 _AssociationSet, or _AssociationDict. 

943 

944 lazy_collection 

945 A callable returning a list-based collection of entities (usually an 

946 object attribute managed by a SQLAlchemy relationship()) 

947 

948 creator 

949 A function that creates new target entities. Given one parameter: 

950 value. This assertion is assumed:: 

951 

952 obj = creator(somevalue) 

953 assert getter(obj) == somevalue 

954 

955 getter 

956 A function. Given an associated object, return the 'value'. 

957 

958 setter 

959 A function. Given an associated object and a value, store that 

960 value on the object. 

961 

962 """ 

963 self.lazy_collection = lazy_collection 

964 self.creator = creator 

965 self.getter = getter 

966 self.setter = setter 

967 self.parent = parent 

968 

969 col = property(lambda self: self.lazy_collection()) 

970 

971 def __len__(self): 

972 return len(self.col) 

973 

974 def __bool__(self): 

975 return bool(self.col) 

976 

977 __nonzero__ = __bool__ 

978 

979 def __getstate__(self): 

980 return {"parent": self.parent, "lazy_collection": self.lazy_collection} 

981 

982 def __setstate__(self, state): 

983 self.parent = state["parent"] 

984 self.lazy_collection = state["lazy_collection"] 

985 self.parent._inflate(self) 

986 

987 def _bulk_replace(self, assoc_proxy, values): 

988 self.clear() 

989 assoc_proxy._set(self, values) 

990 

991 

992class _AssociationList(_AssociationCollection): 

993 """Generic, converting, list-to-list proxy.""" 

994 

995 def _create(self, value): 

996 return self.creator(value) 

997 

998 def _get(self, object_): 

999 return self.getter(object_) 

1000 

1001 def _set(self, object_, value): 

1002 return self.setter(object_, value) 

1003 

1004 def __getitem__(self, index): 

1005 if not isinstance(index, slice): 

1006 return self._get(self.col[index]) 

1007 else: 

1008 return [self._get(member) for member in self.col[index]] 

1009 

1010 def __setitem__(self, index, value): 

1011 if not isinstance(index, slice): 

1012 self._set(self.col[index], value) 

1013 else: 

1014 if index.stop is None: 

1015 stop = len(self) 

1016 elif index.stop < 0: 

1017 stop = len(self) + index.stop 

1018 else: 

1019 stop = index.stop 

1020 step = index.step or 1 

1021 

1022 start = index.start or 0 

1023 rng = list(range(index.start or 0, stop, step)) 

1024 if step == 1: 

1025 for i in rng: 

1026 del self[start] 

1027 i = start 

1028 for item in value: 

1029 self.insert(i, item) 

1030 i += 1 

1031 else: 

1032 if len(value) != len(rng): 

1033 raise ValueError( 

1034 "attempt to assign sequence of size %s to " 

1035 "extended slice of size %s" % (len(value), len(rng)) 

1036 ) 

1037 for i, item in zip(rng, value): 

1038 self._set(self.col[i], item) 

1039 

1040 def __delitem__(self, index): 

1041 del self.col[index] 

1042 

1043 def __contains__(self, value): 

1044 for member in self.col: 

1045 # testlib.pragma exempt:__eq__ 

1046 if self._get(member) == value: 

1047 return True 

1048 return False 

1049 

1050 def __getslice__(self, start, end): 

1051 return [self._get(member) for member in self.col[start:end]] 

1052 

1053 def __setslice__(self, start, end, values): 

1054 members = [self._create(v) for v in values] 

1055 self.col[start:end] = members 

1056 

1057 def __delslice__(self, start, end): 

1058 del self.col[start:end] 

1059 

1060 def __iter__(self): 

1061 """Iterate over proxied values. 

1062 

1063 For the actual domain objects, iterate over .col instead or 

1064 just use the underlying collection directly from its property 

1065 on the parent. 

1066 """ 

1067 

1068 for member in self.col: 

1069 yield self._get(member) 

1070 return 

1071 

1072 def append(self, value): 

1073 col = self.col 

1074 item = self._create(value) 

1075 col.append(item) 

1076 

1077 def count(self, value): 

1078 return sum( 

1079 [ 

1080 1 

1081 for _ in util.itertools_filter( 

1082 lambda v: v == value, iter(self) 

1083 ) 

1084 ] 

1085 ) 

1086 

1087 def extend(self, values): 

1088 for v in values: 

1089 self.append(v) 

1090 

1091 def insert(self, index, value): 

1092 self.col[index:index] = [self._create(value)] 

1093 

1094 def pop(self, index=-1): 

1095 return self.getter(self.col.pop(index)) 

1096 

1097 def remove(self, value): 

1098 for i, val in enumerate(self): 

1099 if val == value: 

1100 del self.col[i] 

1101 return 

1102 raise ValueError("value not in list") 

1103 

1104 def reverse(self): 

1105 """Not supported, use reversed(mylist)""" 

1106 

1107 raise NotImplementedError 

1108 

1109 def sort(self): 

1110 """Not supported, use sorted(mylist)""" 

1111 

1112 raise NotImplementedError 

1113 

1114 def clear(self): 

1115 del self.col[0 : len(self.col)] 

1116 

1117 def __eq__(self, other): 

1118 return list(self) == other 

1119 

1120 def __ne__(self, other): 

1121 return list(self) != other 

1122 

1123 def __lt__(self, other): 

1124 return list(self) < other 

1125 

1126 def __le__(self, other): 

1127 return list(self) <= other 

1128 

1129 def __gt__(self, other): 

1130 return list(self) > other 

1131 

1132 def __ge__(self, other): 

1133 return list(self) >= other 

1134 

1135 def __cmp__(self, other): 

1136 return util.cmp(list(self), other) 

1137 

1138 def __add__(self, iterable): 

1139 try: 

1140 other = list(iterable) 

1141 except TypeError: 

1142 return NotImplemented 

1143 return list(self) + other 

1144 

1145 def __radd__(self, iterable): 

1146 try: 

1147 other = list(iterable) 

1148 except TypeError: 

1149 return NotImplemented 

1150 return other + list(self) 

1151 

1152 def __mul__(self, n): 

1153 if not isinstance(n, int): 

1154 return NotImplemented 

1155 return list(self) * n 

1156 

1157 __rmul__ = __mul__ 

1158 

1159 def __iadd__(self, iterable): 

1160 self.extend(iterable) 

1161 return self 

1162 

1163 def __imul__(self, n): 

1164 # unlike a regular list *=, proxied __imul__ will generate unique 

1165 # backing objects for each copy. *= on proxied lists is a bit of 

1166 # a stretch anyhow, and this interpretation of the __imul__ contract 

1167 # is more plausibly useful than copying the backing objects. 

1168 if not isinstance(n, int): 

1169 return NotImplemented 

1170 if n == 0: 

1171 self.clear() 

1172 elif n > 1: 

1173 self.extend(list(self) * (n - 1)) 

1174 return self 

1175 

1176 def index(self, item, *args): 

1177 return list(self).index(item, *args) 

1178 

1179 def copy(self): 

1180 return list(self) 

1181 

1182 def __repr__(self): 

1183 return repr(list(self)) 

1184 

1185 def __hash__(self): 

1186 raise TypeError("%s objects are unhashable" % type(self).__name__) 

1187 

1188 for func_name, func in list(locals().items()): 

1189 if ( 

1190 util.callable(func) 

1191 and func.__name__ == func_name 

1192 and not func.__doc__ 

1193 and hasattr(list, func_name) 

1194 ): 

1195 func.__doc__ = getattr(list, func_name).__doc__ 

1196 del func_name, func 

1197 

1198 

1199_NotProvided = util.symbol("_NotProvided") 

1200 

1201 

1202class _AssociationDict(_AssociationCollection): 

1203 """Generic, converting, dict-to-dict proxy.""" 

1204 

1205 def _create(self, key, value): 

1206 return self.creator(key, value) 

1207 

1208 def _get(self, object_): 

1209 return self.getter(object_) 

1210 

1211 def _set(self, object_, key, value): 

1212 return self.setter(object_, key, value) 

1213 

1214 def __getitem__(self, key): 

1215 return self._get(self.col[key]) 

1216 

1217 def __setitem__(self, key, value): 

1218 if key in self.col: 

1219 self._set(self.col[key], key, value) 

1220 else: 

1221 self.col[key] = self._create(key, value) 

1222 

1223 def __delitem__(self, key): 

1224 del self.col[key] 

1225 

1226 def __contains__(self, key): 

1227 # testlib.pragma exempt:__hash__ 

1228 return key in self.col 

1229 

1230 def has_key(self, key): 

1231 # testlib.pragma exempt:__hash__ 

1232 return key in self.col 

1233 

1234 def __iter__(self): 

1235 return iter(self.col.keys()) 

1236 

1237 def clear(self): 

1238 self.col.clear() 

1239 

1240 def __eq__(self, other): 

1241 return dict(self) == other 

1242 

1243 def __ne__(self, other): 

1244 return dict(self) != other 

1245 

1246 def __lt__(self, other): 

1247 return dict(self) < other 

1248 

1249 def __le__(self, other): 

1250 return dict(self) <= other 

1251 

1252 def __gt__(self, other): 

1253 return dict(self) > other 

1254 

1255 def __ge__(self, other): 

1256 return dict(self) >= other 

1257 

1258 def __cmp__(self, other): 

1259 return util.cmp(dict(self), other) 

1260 

1261 def __repr__(self): 

1262 return repr(dict(self.items())) 

1263 

1264 def get(self, key, default=None): 

1265 try: 

1266 return self[key] 

1267 except KeyError: 

1268 return default 

1269 

1270 def setdefault(self, key, default=None): 

1271 if key not in self.col: 

1272 self.col[key] = self._create(key, default) 

1273 return default 

1274 else: 

1275 return self[key] 

1276 

1277 def keys(self): 

1278 return self.col.keys() 

1279 

1280 if util.py2k: 

1281 

1282 def iteritems(self): 

1283 return ((key, self._get(self.col[key])) for key in self.col) 

1284 

1285 def itervalues(self): 

1286 return (self._get(self.col[key]) for key in self.col) 

1287 

1288 def iterkeys(self): 

1289 return self.col.iterkeys() 

1290 

1291 def values(self): 

1292 return [self._get(member) for member in self.col.values()] 

1293 

1294 def items(self): 

1295 return [(k, self._get(self.col[k])) for k in self] 

1296 

1297 else: 

1298 

1299 def items(self): 

1300 return ((key, self._get(self.col[key])) for key in self.col) 

1301 

1302 def values(self): 

1303 return (self._get(self.col[key]) for key in self.col) 

1304 

1305 def pop(self, key, default=_NotProvided): 

1306 if default is _NotProvided: 

1307 member = self.col.pop(key) 

1308 else: 

1309 member = self.col.pop(key, default) 

1310 return self._get(member) 

1311 

1312 def popitem(self): 

1313 item = self.col.popitem() 

1314 return (item[0], self._get(item[1])) 

1315 

1316 def update(self, *a, **kw): 

1317 if len(a) > 1: 

1318 raise TypeError( 

1319 "update expected at most 1 arguments, got %i" % len(a) 

1320 ) 

1321 elif len(a) == 1: 

1322 seq_or_map = a[0] 

1323 # discern dict from sequence - took the advice from 

1324 # http://www.voidspace.org.uk/python/articles/duck_typing.shtml 

1325 # still not perfect :( 

1326 if hasattr(seq_or_map, "keys"): 

1327 for item in seq_or_map: 

1328 self[item] = seq_or_map[item] 

1329 else: 

1330 try: 

1331 for k, v in seq_or_map: 

1332 self[k] = v 

1333 except ValueError as err: 

1334 util.raise_( 

1335 ValueError( 

1336 "dictionary update sequence " 

1337 "requires 2-element tuples" 

1338 ), 

1339 replace_context=err, 

1340 ) 

1341 

1342 for key, value in kw: 

1343 self[key] = value 

1344 

1345 def _bulk_replace(self, assoc_proxy, values): 

1346 existing = set(self) 

1347 constants = existing.intersection(values or ()) 

1348 additions = set(values or ()).difference(constants) 

1349 removals = existing.difference(constants) 

1350 

1351 for key, member in values.items() or (): 

1352 if key in additions: 

1353 self[key] = member 

1354 elif key in constants: 

1355 self[key] = member 

1356 

1357 for key in removals: 

1358 del self[key] 

1359 

1360 def copy(self): 

1361 return dict(self.items()) 

1362 

1363 def __hash__(self): 

1364 raise TypeError("%s objects are unhashable" % type(self).__name__) 

1365 

1366 for func_name, func in list(locals().items()): 

1367 if ( 

1368 util.callable(func) 

1369 and func.__name__ == func_name 

1370 and not func.__doc__ 

1371 and hasattr(dict, func_name) 

1372 ): 

1373 func.__doc__ = getattr(dict, func_name).__doc__ 

1374 del func_name, func 

1375 

1376 

1377class _AssociationSet(_AssociationCollection): 

1378 """Generic, converting, set-to-set proxy.""" 

1379 

1380 def _create(self, value): 

1381 return self.creator(value) 

1382 

1383 def _get(self, object_): 

1384 return self.getter(object_) 

1385 

1386 def __len__(self): 

1387 return len(self.col) 

1388 

1389 def __bool__(self): 

1390 if self.col: 

1391 return True 

1392 else: 

1393 return False 

1394 

1395 __nonzero__ = __bool__ 

1396 

1397 def __contains__(self, value): 

1398 for member in self.col: 

1399 # testlib.pragma exempt:__eq__ 

1400 if self._get(member) == value: 

1401 return True 

1402 return False 

1403 

1404 def __iter__(self): 

1405 """Iterate over proxied values. 

1406 

1407 For the actual domain objects, iterate over .col instead or just use 

1408 the underlying collection directly from its property on the parent. 

1409 

1410 """ 

1411 for member in self.col: 

1412 yield self._get(member) 

1413 return 

1414 

1415 def add(self, value): 

1416 if value not in self: 

1417 self.col.add(self._create(value)) 

1418 

1419 # for discard and remove, choosing a more expensive check strategy rather 

1420 # than call self.creator() 

1421 def discard(self, value): 

1422 for member in self.col: 

1423 if self._get(member) == value: 

1424 self.col.discard(member) 

1425 break 

1426 

1427 def remove(self, value): 

1428 for member in self.col: 

1429 if self._get(member) == value: 

1430 self.col.discard(member) 

1431 return 

1432 raise KeyError(value) 

1433 

1434 def pop(self): 

1435 if not self.col: 

1436 raise KeyError("pop from an empty set") 

1437 member = self.col.pop() 

1438 return self._get(member) 

1439 

1440 def update(self, other): 

1441 for value in other: 

1442 self.add(value) 

1443 

1444 def _bulk_replace(self, assoc_proxy, values): 

1445 existing = set(self) 

1446 constants = existing.intersection(values or ()) 

1447 additions = set(values or ()).difference(constants) 

1448 removals = existing.difference(constants) 

1449 

1450 appender = self.add 

1451 remover = self.remove 

1452 

1453 for member in values or (): 

1454 if member in additions: 

1455 appender(member) 

1456 elif member in constants: 

1457 appender(member) 

1458 

1459 for member in removals: 

1460 remover(member) 

1461 

1462 def __ior__(self, other): 

1463 if not collections._set_binops_check_strict(self, other): 

1464 return NotImplemented 

1465 for value in other: 

1466 self.add(value) 

1467 return self 

1468 

1469 def _set(self): 

1470 return set(iter(self)) 

1471 

1472 def union(self, other): 

1473 return set(self).union(other) 

1474 

1475 __or__ = union 

1476 

1477 def difference(self, other): 

1478 return set(self).difference(other) 

1479 

1480 __sub__ = difference 

1481 

1482 def difference_update(self, other): 

1483 for value in other: 

1484 self.discard(value) 

1485 

1486 def __isub__(self, other): 

1487 if not collections._set_binops_check_strict(self, other): 

1488 return NotImplemented 

1489 for value in other: 

1490 self.discard(value) 

1491 return self 

1492 

1493 def intersection(self, other): 

1494 return set(self).intersection(other) 

1495 

1496 __and__ = intersection 

1497 

1498 def intersection_update(self, other): 

1499 want, have = self.intersection(other), set(self) 

1500 

1501 remove, add = have - want, want - have 

1502 

1503 for value in remove: 

1504 self.remove(value) 

1505 for value in add: 

1506 self.add(value) 

1507 

1508 def __iand__(self, other): 

1509 if not collections._set_binops_check_strict(self, other): 

1510 return NotImplemented 

1511 want, have = self.intersection(other), set(self) 

1512 

1513 remove, add = have - want, want - have 

1514 

1515 for value in remove: 

1516 self.remove(value) 

1517 for value in add: 

1518 self.add(value) 

1519 return self 

1520 

1521 def symmetric_difference(self, other): 

1522 return set(self).symmetric_difference(other) 

1523 

1524 __xor__ = symmetric_difference 

1525 

1526 def symmetric_difference_update(self, other): 

1527 want, have = self.symmetric_difference(other), set(self) 

1528 

1529 remove, add = have - want, want - have 

1530 

1531 for value in remove: 

1532 self.remove(value) 

1533 for value in add: 

1534 self.add(value) 

1535 

1536 def __ixor__(self, other): 

1537 if not collections._set_binops_check_strict(self, other): 

1538 return NotImplemented 

1539 want, have = self.symmetric_difference(other), set(self) 

1540 

1541 remove, add = have - want, want - have 

1542 

1543 for value in remove: 

1544 self.remove(value) 

1545 for value in add: 

1546 self.add(value) 

1547 return self 

1548 

1549 def issubset(self, other): 

1550 return set(self).issubset(other) 

1551 

1552 def issuperset(self, other): 

1553 return set(self).issuperset(other) 

1554 

1555 def clear(self): 

1556 self.col.clear() 

1557 

1558 def copy(self): 

1559 return set(self) 

1560 

1561 def __eq__(self, other): 

1562 return set(self) == other 

1563 

1564 def __ne__(self, other): 

1565 return set(self) != other 

1566 

1567 def __lt__(self, other): 

1568 return set(self) < other 

1569 

1570 def __le__(self, other): 

1571 return set(self) <= other 

1572 

1573 def __gt__(self, other): 

1574 return set(self) > other 

1575 

1576 def __ge__(self, other): 

1577 return set(self) >= other 

1578 

1579 def __repr__(self): 

1580 return repr(set(self)) 

1581 

1582 def __hash__(self): 

1583 raise TypeError("%s objects are unhashable" % type(self).__name__) 

1584 

1585 for func_name, func in list(locals().items()): 

1586 if ( 

1587 util.callable(func) 

1588 and func.__name__ == func_name 

1589 and not func.__doc__ 

1590 and hasattr(set, func_name) 

1591 ): 

1592 func.__doc__ = getattr(set, func_name).__doc__ 

1593 del func_name, func