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# orm/descriptor_props.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"""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 properties 

16from . import query 

17from .interfaces import MapperProperty 

18from .interfaces import PropComparator 

19from .util import _none_set 

20from .. import event 

21from .. import exc as sa_exc 

22from .. import schema 

23from .. import sql 

24from .. import util 

25from ..sql import expression 

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 

36 def instrument_class(self, mapper): 

37 prop = self 

38 

39 class _ProxyImpl(object): 

40 accepts_scalar_loader = False 

41 expire_missing = True 

42 collection = False 

43 

44 @property 

45 def uses_objects(self): 

46 return prop.uses_objects 

47 

48 def __init__(self, key): 

49 self.key = key 

50 

51 if hasattr(prop, "get_history"): 

52 

53 def get_history( 

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

55 ): 

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

57 

58 if self.descriptor is None: 

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

60 if mapper._is_userland_descriptor(desc): 

61 self.descriptor = desc 

62 

63 if self.descriptor is None: 

64 

65 def fset(obj, value): 

66 setattr(obj, self.name, value) 

67 

68 def fdel(obj): 

69 delattr(obj, self.name) 

70 

71 def fget(obj): 

72 return getattr(obj, self.name) 

73 

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

75 

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

77 self.parent.class_, 

78 self.key, 

79 self.descriptor, 

80 lambda: self._comparator_factory(mapper), 

81 doc=self.doc, 

82 original_property=self, 

83 ) 

84 proxy_attr.impl = _ProxyImpl(self.key) 

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

86 

87 

88@util.langhelpers.dependency_for("sqlalchemy.orm.properties", add_to_all=True) 

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 @util.deprecated_params( 

103 extension=( 

104 "0.7", 

105 ":class:`.AttributeExtension` is deprecated in favor of the " 

106 ":class:`.AttributeEvents` listener interface. The " 

107 ":paramref:`.composite.extension` parameter will be " 

108 "removed in a future release.", 

109 ) 

110 ) 

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

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

113 

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

115 full usage example. 

116 

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

118 is the :class:`.CompositeProperty`. 

119 

120 :param class\_: 

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

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

123 column values in order. 

124 

125 :param \*cols: 

126 List of Column objects to be mapped. 

127 

128 :param active_history=False: 

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

130 scalar attribute should be loaded when replaced, if not 

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

132 

133 :param group: 

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

135 

136 :param deferred: 

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

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

139 first accessed on an instance. See also 

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

141 

142 :param comparator_factory: a class which extends 

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

144 clause generation for comparison operations. 

145 

146 :param doc: 

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

148 class-bound descriptor. 

149 

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

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

152 

153 :param extension: 

154 an :class:`.AttributeExtension` instance, 

155 or list of extensions, which will be prepended to the list of 

156 attribute listeners for the resulting descriptor placed on the 

157 class. 

158 

159 """ 

160 super(CompositeProperty, self).__init__() 

161 

162 self.attrs = attrs 

163 self.composite_class = class_ 

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

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

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

167 self.comparator_factory = kwargs.pop( 

168 "comparator_factory", self.__class__.Comparator 

169 ) 

170 if "info" in kwargs: 

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

172 

173 util.set_creation_order(self) 

174 self._create_descriptor() 

175 

176 def instrument_class(self, mapper): 

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

178 self._setup_event_handlers() 

179 

180 def do_init(self): 

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

182 has been associated with its parent mapper. 

183 

184 """ 

185 self._setup_arguments_on_columns() 

186 

187 def _create_descriptor(self): 

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

189 the access point on instances of the mapped class. 

190 

191 """ 

192 

193 def fget(instance): 

194 dict_ = attributes.instance_dict(instance) 

195 state = attributes.instance_state(instance) 

196 

197 if self.key not in dict_: 

198 # key not present. Iterate through related 

199 # attributes, retrieve their values. This 

200 # ensures they all load. 

201 values = [ 

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

203 ] 

204 

205 # current expected behavior here is that the composite is 

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

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

208 # if the composite were created unconditionally, 

209 # but that would be a behavioral change. 

210 if self.key not in dict_ and ( 

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

212 ): 

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

214 state.manager.dispatch.refresh(state, None, [self.key]) 

215 

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

217 

218 def fset(instance, value): 

219 dict_ = attributes.instance_dict(instance) 

220 state = attributes.instance_state(instance) 

221 attr = state.manager[self.key] 

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

223 for fn in attr.dispatch.set: 

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

225 dict_[self.key] = value 

226 if value is None: 

227 for key in self._attribute_keys: 

228 setattr(instance, key, None) 

229 else: 

230 for key, value in zip( 

231 self._attribute_keys, value.__composite_values__() 

232 ): 

233 setattr(instance, key, value) 

234 

235 def fdel(instance): 

236 state = attributes.instance_state(instance) 

237 dict_ = attributes.instance_dict(instance) 

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

239 attr = state.manager[self.key] 

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

241 for key in self._attribute_keys: 

242 setattr(instance, key, None) 

243 

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

245 

246 @util.memoized_property 

247 def _comparable_elements(self): 

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

249 

250 @util.memoized_property 

251 def props(self): 

252 props = [] 

253 for attr in self.attrs: 

254 if isinstance(attr, str): 

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

256 elif isinstance(attr, schema.Column): 

257 prop = self.parent._columntoproperty[attr] 

258 elif isinstance(attr, attributes.InstrumentedAttribute): 

259 prop = attr.property 

260 else: 

261 raise sa_exc.ArgumentError( 

262 "Composite expects Column objects or mapped " 

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

264 % (attr,) 

265 ) 

266 props.append(prop) 

267 return props 

268 

269 @property 

270 def columns(self): 

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

272 

273 def _setup_arguments_on_columns(self): 

274 """Propagate configuration arguments made on this composite 

275 to the target columns, for those that apply. 

276 

277 """ 

278 for prop in self.props: 

279 prop.active_history = self.active_history 

280 if self.deferred: 

281 prop.deferred = self.deferred 

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

283 prop.group = self.group 

284 

285 def _setup_event_handlers(self): 

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

287 

288 def load_handler(state, *args): 

289 _load_refresh_handler(state, args, is_refresh=False) 

290 

291 def refresh_handler(state, *args): 

292 _load_refresh_handler(state, args, is_refresh=True) 

293 

294 def _load_refresh_handler(state, args, is_refresh): 

295 dict_ = state.dict 

296 

297 if not is_refresh and self.key in dict_: 

298 return 

299 

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

301 # __get__() will initiate a load for those 

302 # columns 

303 for k in self._attribute_keys: 

304 if k not in dict_: 

305 return 

306 

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

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

309 ) 

310 

311 def expire_handler(state, keys): 

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

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

314 

315 def insert_update_handler(mapper, connection, state): 

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

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

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

319 recreates. 

320 

321 """ 

322 

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

324 

325 event.listen( 

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

327 ) 

328 event.listen( 

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

330 ) 

331 event.listen( 

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

333 ) 

334 event.listen( 

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

336 ) 

337 event.listen( 

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

339 ) 

340 

341 # TODO: need a deserialize hook here 

342 

343 @util.memoized_property 

344 def _attribute_keys(self): 

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

346 

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

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

349 

350 added = [] 

351 deleted = [] 

352 

353 has_history = False 

354 for prop in self.props: 

355 key = prop.key 

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

357 if hist.has_changes(): 

358 has_history = True 

359 

360 non_deleted = hist.non_deleted() 

361 if non_deleted: 

362 added.extend(non_deleted) 

363 else: 

364 added.append(None) 

365 if hist.deleted: 

366 deleted.extend(hist.deleted) 

367 else: 

368 deleted.append(None) 

369 

370 if has_history: 

371 return attributes.History( 

372 [self.composite_class(*added)], 

373 (), 

374 [self.composite_class(*deleted)], 

375 ) 

376 else: 

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

378 

379 def _comparator_factory(self, mapper): 

380 return self.comparator_factory(self, mapper) 

381 

382 class CompositeBundle(query.Bundle): 

383 def __init__(self, property_, expr): 

384 self.property = property_ 

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

386 property_.key, *expr 

387 ) 

388 

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

390 def proc(row): 

391 return self.property.composite_class( 

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

393 ) 

394 

395 return proc 

396 

397 class Comparator(PropComparator): 

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

399 :class:`.CompositeProperty` attributes. 

400 

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

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

403 

404 .. seealso:: 

405 

406 :class:`.PropComparator` 

407 

408 :class:`.ColumnOperators` 

409 

410 :ref:`types_operators` 

411 

412 :attr:`.TypeEngine.comparator_factory` 

413 

414 """ 

415 

416 __hash__ = None 

417 

418 @property 

419 def clauses(self): 

420 return self.__clause_element__() 

421 

422 def __clause_element__(self): 

423 return expression.ClauseList( 

424 group=False, *self._comparable_elements 

425 ) 

426 

427 def _query_clause_element(self): 

428 return CompositeProperty.CompositeBundle( 

429 self.prop, self.__clause_element__() 

430 ) 

431 

432 def _bulk_update_tuples(self, value): 

433 if value is None: 

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

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

436 values = value.__composite_values__() 

437 else: 

438 raise sa_exc.ArgumentError( 

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

440 % (self.prop, value) 

441 ) 

442 

443 return zip(self._comparable_elements, values) 

444 

445 @util.memoized_property 

446 def _comparable_elements(self): 

447 if self._adapt_to_entity: 

448 return [ 

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

450 for prop in self.prop._comparable_elements 

451 ] 

452 else: 

453 return self.prop._comparable_elements 

454 

455 def __eq__(self, other): 

456 if other is None: 

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

458 else: 

459 values = other.__composite_values__() 

460 comparisons = [ 

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

462 ] 

463 if self._adapt_to_entity: 

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

465 return sql.and_(*comparisons) 

466 

467 def __ne__(self, other): 

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

469 

470 def __str__(self): 

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

472 

473 

474@util.langhelpers.dependency_for("sqlalchemy.orm.properties", add_to_all=True) 

475class ConcreteInheritedProperty(DescriptorProperty): 

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

477 an attribute on a concrete subclass that is only present 

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

479 

480 Cases where this occurs include: 

481 

482 * When the superclass mapper is mapped against a 

483 "polymorphic union", which includes all attributes from 

484 all subclasses. 

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

486 but not on the subclass mapper. Concrete mappers require 

487 that relationship() is configured explicitly on each 

488 subclass. 

489 

490 """ 

491 

492 def _comparator_factory(self, mapper): 

493 comparator_callable = None 

494 

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

496 p = m._props[self.key] 

497 if not isinstance(p, ConcreteInheritedProperty): 

498 comparator_callable = p.comparator_factory 

499 break 

500 return comparator_callable 

501 

502 def __init__(self): 

503 super(ConcreteInheritedProperty, self).__init__() 

504 

505 def warn(): 

506 raise AttributeError( 

507 "Concrete %s does not implement " 

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

509 "this property explicitly to %s." 

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

511 ) 

512 

513 class NoninheritedConcreteProp(object): 

514 def __set__(s, obj, value): 

515 warn() 

516 

517 def __delete__(s, obj): 

518 warn() 

519 

520 def __get__(s, obj, owner): 

521 if obj is None: 

522 return self.descriptor 

523 warn() 

524 

525 self.descriptor = NoninheritedConcreteProp() 

526 

527 

528@util.langhelpers.dependency_for("sqlalchemy.orm.properties", add_to_all=True) 

529class SynonymProperty(DescriptorProperty): 

530 def __init__( 

531 self, 

532 name, 

533 map_column=None, 

534 descriptor=None, 

535 comparator_factory=None, 

536 doc=None, 

537 info=None, 

538 ): 

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

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

541 of another attribute. 

542 

543 e.g.:: 

544 

545 class MyClass(Base): 

546 __tablename__ = 'my_table' 

547 

548 id = Column(Integer, primary_key=True) 

549 job_status = Column(String(50)) 

550 

551 status = synonym("job_status") 

552 

553 

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

555 can refer to the string name ORM-mapped attribute 

556 configured on the class, including column-bound attributes 

557 and relationships. 

558 

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

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

561 accessed at the instance level. 

562 

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

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

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

566 object upon the mapped 

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

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

569 maps this :class:`_schema.Column` 

570 to the alternate name given as the "name" 

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

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

573 to be under a different name is 

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

575 :class:`_schema.Column` 

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

577 descriptor, that is, in conjunction with the 

578 :paramref:`.synonym.descriptor` parameter:: 

579 

580 my_table = Table( 

581 "my_table", metadata, 

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

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

584 ) 

585 

586 class MyClass(object): 

587 @property 

588 def _job_status_descriptor(self): 

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

590 

591 

592 mapper( 

593 MyClass, my_table, properties={ 

594 "job_status": synonym( 

595 "_job_status", map_column=True, 

596 descriptor=MyClass._job_status_descriptor) 

597 } 

598 ) 

599 

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

601 mapped to the ``job_status`` column:: 

602 

603 >>> j1 = MyClass() 

604 >>> j1._job_status = "employed" 

605 >>> j1.job_status 

606 Status: employed 

607 

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

609 conjunction with a synonym, use the 

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

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

612 should usually be preferred, particularly when redefining attribute 

613 behavior. 

614 

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

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

617 

618 .. versionadded:: 1.0.0 

619 

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

621 that will provide custom comparison behavior at the SQL expression 

622 level. 

623 

624 .. note:: 

625 

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

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

628 please refer to the Hybrid attribute introduced at 

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

630 

631 .. seealso:: 

632 

633 :ref:`synonyms` - Overview of synonyms 

634 

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

636 

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

638 updated approach to augmenting attribute behavior more flexibly 

639 than can be achieved with synonyms. 

640 

641 """ 

642 super(SynonymProperty, self).__init__() 

643 

644 self.name = name 

645 self.map_column = map_column 

646 self.descriptor = descriptor 

647 self.comparator_factory = comparator_factory 

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

649 if info: 

650 self.info = info 

651 

652 util.set_creation_order(self) 

653 

654 @property 

655 def uses_objects(self): 

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

657 

658 # TODO: when initialized, check _proxied_property, 

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

660 

661 @util.memoized_property 

662 def _proxied_property(self): 

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

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

665 attr.property, MapperProperty 

666 ): 

667 raise sa_exc.InvalidRequestError( 

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

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

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

671 ) 

672 return attr.property 

673 

674 def _comparator_factory(self, mapper): 

675 prop = self._proxied_property 

676 

677 if self.comparator_factory: 

678 comp = self.comparator_factory(prop, mapper) 

679 else: 

680 comp = prop.comparator_factory(prop, mapper) 

681 return comp 

682 

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

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

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

686 

687 def set_parent(self, parent, init): 

688 if self.map_column: 

689 # implement the 'map_column' option. 

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

691 raise sa_exc.ArgumentError( 

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

693 "'%s' named '%s'" 

694 % ( 

695 self.name, 

696 parent.persist_selectable.description, 

697 self.key, 

698 ) 

699 ) 

700 elif ( 

701 parent.persist_selectable.c[self.key] 

702 in parent._columntoproperty 

703 and parent._columntoproperty[ 

704 parent.persist_selectable.c[self.key] 

705 ].key 

706 == self.name 

707 ): 

708 raise sa_exc.ArgumentError( 

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

710 "a ColumnProperty already exists keyed to the name " 

711 "%r for column %r" 

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

713 ) 

714 p = properties.ColumnProperty( 

715 parent.persist_selectable.c[self.key] 

716 ) 

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

718 p._mapped_by_synonym = self.key 

719 

720 self.parent = parent 

721 

722 

723@util.langhelpers.dependency_for("sqlalchemy.orm.properties", add_to_all=True) 

724@util.deprecated_cls( 

725 "0.7", 

726 ":func:`.comparable_property` is deprecated and will be removed in a " 

727 "future release. Please refer to the :mod:`~sqlalchemy.ext.hybrid` " 

728 "extension.", 

729) 

730class ComparableProperty(DescriptorProperty): 

731 """Instruments a Python property for use in query expressions.""" 

732 

733 def __init__( 

734 self, comparator_factory, descriptor=None, doc=None, info=None 

735 ): 

736 """Provides a method of applying a :class:`.PropComparator` 

737 to any Python descriptor attribute. 

738 

739 

740 Allows any Python descriptor to behave like a SQL-enabled 

741 attribute when used at the class level in queries, allowing 

742 redefinition of expression operator behavior. 

743 

744 In the example below we redefine :meth:`.PropComparator.operate` 

745 to wrap both sides of an expression in ``func.lower()`` to produce 

746 case-insensitive comparison:: 

747 

748 from sqlalchemy.orm import comparable_property 

749 from sqlalchemy.orm.interfaces import PropComparator 

750 from sqlalchemy.sql import func 

751 from sqlalchemy import Integer, String, Column 

752 from sqlalchemy.ext.declarative import declarative_base 

753 

754 class CaseInsensitiveComparator(PropComparator): 

755 def __clause_element__(self): 

756 return self.prop 

757 

758 def operate(self, op, other): 

759 return op( 

760 func.lower(self.__clause_element__()), 

761 func.lower(other) 

762 ) 

763 

764 Base = declarative_base() 

765 

766 class SearchWord(Base): 

767 __tablename__ = 'search_word' 

768 id = Column(Integer, primary_key=True) 

769 word = Column(String) 

770 word_insensitive = comparable_property(lambda prop, mapper: 

771 CaseInsensitiveComparator( 

772 mapper.c.word, mapper) 

773 ) 

774 

775 

776 A mapping like the above allows the ``word_insensitive`` attribute 

777 to render an expression like:: 

778 

779 >>> print(SearchWord.word_insensitive == "Trucks") 

780 lower(search_word.word) = lower(:lower_1) 

781 

782 :param comparator_factory: 

783 A PropComparator subclass or factory that defines operator behavior 

784 for this property. 

785 

786 :param descriptor: 

787 Optional when used in a ``properties={}`` declaration. The Python 

788 descriptor or property to layer comparison behavior on top of. 

789 

790 The like-named descriptor will be automatically retrieved from the 

791 mapped class if left blank in a ``properties`` declaration. 

792 

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

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

795 

796 .. versionadded:: 1.0.0 

797 

798 """ 

799 super(ComparableProperty, self).__init__() 

800 self.descriptor = descriptor 

801 self.comparator_factory = comparator_factory 

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

803 if info: 

804 self.info = info 

805 util.set_creation_order(self) 

806 

807 def _comparator_factory(self, mapper): 

808 return self.comparator_factory(self, mapper)