Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/sqlalchemy/ext/hybrid.py: 52%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

237 statements  

1# ext/hybrid.py 

2# Copyright (C) 2005-2025 the SQLAlchemy authors and contributors 

3# <see AUTHORS file> 

4# 

5# This module is part of SQLAlchemy and is released under 

6# the MIT License: https://www.opensource.org/licenses/mit-license.php 

7 

8r"""Define attributes on ORM-mapped classes that have "hybrid" behavior. 

9 

10"hybrid" means the attribute has distinct behaviors defined at the 

11class level and at the instance level. 

12 

13The :mod:`~sqlalchemy.ext.hybrid` extension provides a special form of 

14method decorator and has minimal dependencies on the rest of SQLAlchemy. 

15Its basic theory of operation can work with any descriptor-based expression 

16system. 

17 

18Consider a mapping ``Interval``, representing integer ``start`` and ``end`` 

19values. We can define higher level functions on mapped classes that produce SQL 

20expressions at the class level, and Python expression evaluation at the 

21instance level. Below, each function decorated with :class:`.hybrid_method` or 

22:class:`.hybrid_property` may receive ``self`` as an instance of the class, or 

23may receive the class directly, depending on context:: 

24 

25 from __future__ import annotations 

26 

27 from sqlalchemy.ext.hybrid import hybrid_method 

28 from sqlalchemy.ext.hybrid import hybrid_property 

29 from sqlalchemy.orm import DeclarativeBase 

30 from sqlalchemy.orm import Mapped 

31 from sqlalchemy.orm import mapped_column 

32 

33 

34 class Base(DeclarativeBase): 

35 pass 

36 

37 

38 class Interval(Base): 

39 __tablename__ = "interval" 

40 

41 id: Mapped[int] = mapped_column(primary_key=True) 

42 start: Mapped[int] 

43 end: Mapped[int] 

44 

45 def __init__(self, start: int, end: int): 

46 self.start = start 

47 self.end = end 

48 

49 @hybrid_property 

50 def length(self) -> int: 

51 return self.end - self.start 

52 

53 @hybrid_method 

54 def contains(self, point: int) -> bool: 

55 return (self.start <= point) & (point <= self.end) 

56 

57 @hybrid_method 

58 def intersects(self, other: Interval) -> bool: 

59 return self.contains(other.start) | self.contains(other.end) 

60 

61Above, the ``length`` property returns the difference between the 

62``end`` and ``start`` attributes. With an instance of ``Interval``, 

63this subtraction occurs in Python, using normal Python descriptor 

64mechanics:: 

65 

66 >>> i1 = Interval(5, 10) 

67 >>> i1.length 

68 5 

69 

70When dealing with the ``Interval`` class itself, the :class:`.hybrid_property` 

71descriptor evaluates the function body given the ``Interval`` class as 

72the argument, which when evaluated with SQLAlchemy expression mechanics 

73returns a new SQL expression: 

74 

75.. sourcecode:: pycon+sql 

76 

77 >>> from sqlalchemy import select 

78 >>> print(select(Interval.length)) 

79 {printsql}SELECT interval."end" - interval.start AS length 

80 FROM interval{stop} 

81 

82 

83 >>> print(select(Interval).filter(Interval.length > 10)) 

84 {printsql}SELECT interval.id, interval.start, interval."end" 

85 FROM interval 

86 WHERE interval."end" - interval.start > :param_1 

87 

88Filtering methods such as :meth:`.Select.filter_by` are supported 

89with hybrid attributes as well: 

90 

91.. sourcecode:: pycon+sql 

92 

93 >>> print(select(Interval).filter_by(length=5)) 

94 {printsql}SELECT interval.id, interval.start, interval."end" 

95 FROM interval 

96 WHERE interval."end" - interval.start = :param_1 

97 

98The ``Interval`` class example also illustrates two methods, 

99``contains()`` and ``intersects()``, decorated with 

100:class:`.hybrid_method`. This decorator applies the same idea to 

101methods that :class:`.hybrid_property` applies to attributes. The 

102methods return boolean values, and take advantage of the Python ``|`` 

103and ``&`` bitwise operators to produce equivalent instance-level and 

104SQL expression-level boolean behavior: 

105 

106.. sourcecode:: pycon+sql 

107 

108 >>> i1.contains(6) 

109 True 

110 >>> i1.contains(15) 

111 False 

112 >>> i1.intersects(Interval(7, 18)) 

113 True 

114 >>> i1.intersects(Interval(25, 29)) 

115 False 

116 

117 >>> print(select(Interval).filter(Interval.contains(15))) 

118 {printsql}SELECT interval.id, interval.start, interval."end" 

119 FROM interval 

120 WHERE interval.start <= :start_1 AND interval."end" > :end_1{stop} 

121 

122 >>> ia = aliased(Interval) 

123 >>> print(select(Interval, ia).filter(Interval.intersects(ia))) 

124 {printsql}SELECT interval.id, interval.start, 

125 interval."end", interval_1.id AS interval_1_id, 

126 interval_1.start AS interval_1_start, interval_1."end" AS interval_1_end 

127 FROM interval, interval AS interval_1 

128 WHERE interval.start <= interval_1.start 

129 AND interval."end" > interval_1.start 

130 OR interval.start <= interval_1."end" 

131 AND interval."end" > interval_1."end"{stop} 

132 

133.. _hybrid_distinct_expression: 

134 

135Defining Expression Behavior Distinct from Attribute Behavior 

136-------------------------------------------------------------- 

137 

138In the previous section, our usage of the ``&`` and ``|`` bitwise operators 

139within the ``Interval.contains`` and ``Interval.intersects`` methods was 

140fortunate, considering our functions operated on two boolean values to return a 

141new one. In many cases, the construction of an in-Python function and a 

142SQLAlchemy SQL expression have enough differences that two separate Python 

143expressions should be defined. The :mod:`~sqlalchemy.ext.hybrid` decorator 

144defines a **modifier** :meth:`.hybrid_property.expression` for this purpose. As an 

145example we'll define the radius of the interval, which requires the usage of 

146the absolute value function:: 

147 

148 from sqlalchemy import ColumnElement 

149 from sqlalchemy import Float 

150 from sqlalchemy import func 

151 from sqlalchemy import type_coerce 

152 

153 

154 class Interval(Base): 

155 # ... 

156 

157 @hybrid_property 

158 def radius(self) -> float: 

159 return abs(self.length) / 2 

160 

161 @radius.inplace.expression 

162 @classmethod 

163 def _radius_expression(cls) -> ColumnElement[float]: 

164 return type_coerce(func.abs(cls.length) / 2, Float) 

165 

166In the above example, the :class:`.hybrid_property` first assigned to the 

167name ``Interval.radius`` is amended by a subsequent method called 

168``Interval._radius_expression``, using the decorator 

169``@radius.inplace.expression``, which chains together two modifiers 

170:attr:`.hybrid_property.inplace` and :attr:`.hybrid_property.expression`. 

171The use of :attr:`.hybrid_property.inplace` indicates that the 

172:meth:`.hybrid_property.expression` modifier should mutate the 

173existing hybrid object at ``Interval.radius`` in place, without creating a 

174new object. Notes on this modifier and its 

175rationale are discussed in the next section :ref:`hybrid_pep484_naming`. 

176The use of ``@classmethod`` is optional, and is strictly to give typing 

177tools a hint that ``cls`` in this case is expected to be the ``Interval`` 

178class, and not an instance of ``Interval``. 

179 

180.. note:: :attr:`.hybrid_property.inplace` as well as the use of ``@classmethod`` 

181 for proper typing support are available as of SQLAlchemy 2.0.4, and will 

182 not work in earlier versions. 

183 

184With ``Interval.radius`` now including an expression element, the SQL 

185function ``ABS()`` is returned when accessing ``Interval.radius`` 

186at the class level: 

187 

188.. sourcecode:: pycon+sql 

189 

190 >>> from sqlalchemy import select 

191 >>> print(select(Interval).filter(Interval.radius > 5)) 

192 {printsql}SELECT interval.id, interval.start, interval."end" 

193 FROM interval 

194 WHERE abs(interval."end" - interval.start) / :abs_1 > :param_1 

195 

196 

197.. _hybrid_pep484_naming: 

198 

199Using ``inplace`` to create pep-484 compliant hybrid properties 

200--------------------------------------------------------------- 

201 

202In the previous section, a :class:`.hybrid_property` decorator is illustrated 

203which includes two separate method-level functions being decorated, both 

204to produce a single object attribute referenced as ``Interval.radius``. 

205There are actually several different modifiers we can use for 

206:class:`.hybrid_property` including :meth:`.hybrid_property.expression`, 

207:meth:`.hybrid_property.setter` and :meth:`.hybrid_property.update_expression`. 

208 

209SQLAlchemy's :class:`.hybrid_property` decorator intends that adding on these 

210methods may be done in the identical manner as Python's built-in 

211``@property`` decorator, where idiomatic use is to continue to redefine the 

212attribute repeatedly, using the **same attribute name** each time, as in the 

213example below that illustrates the use of :meth:`.hybrid_property.setter` and 

214:meth:`.hybrid_property.expression` for the ``Interval.radius`` descriptor:: 

215 

216 # correct use, however is not accepted by pep-484 tooling 

217 

218 

219 class Interval(Base): 

220 # ... 

221 

222 @hybrid_property 

223 def radius(self): 

224 return abs(self.length) / 2 

225 

226 @radius.setter 

227 def radius(self, value): 

228 self.length = value * 2 

229 

230 @radius.expression 

231 def radius(cls): 

232 return type_coerce(func.abs(cls.length) / 2, Float) 

233 

234Above, there are three ``Interval.radius`` methods, but as each are decorated, 

235first by the :class:`.hybrid_property` decorator and then by the 

236``@radius`` name itself, the end effect is that ``Interval.radius`` is 

237a single attribute with three different functions contained within it. 

238This style of use is taken from `Python's documented use of @property 

239<https://docs.python.org/3/library/functions.html#property>`_. 

240It is important to note that the way both ``@property`` as well as 

241:class:`.hybrid_property` work, a **copy of the descriptor is made each time**. 

242That is, each call to ``@radius.expression``, ``@radius.setter`` etc. 

243make a new object entirely. This allows the attribute to be re-defined in 

244subclasses without issue (see :ref:`hybrid_reuse_subclass` later in this 

245section for how this is used). 

246 

247However, the above approach is not compatible with typing tools such as 

248mypy and pyright. Python's own ``@property`` decorator does not have this 

249limitation only because 

250`these tools hardcode the behavior of @property 

251<https://github.com/python/typing/discussions/1102>`_, meaning this syntax 

252is not available to SQLAlchemy under :pep:`484` compliance. 

253 

254In order to produce a reasonable syntax while remaining typing compliant, 

255the :attr:`.hybrid_property.inplace` decorator allows the same 

256decorator to be re-used with different method names, while still producing 

257a single decorator under one name:: 

258 

259 # correct use which is also accepted by pep-484 tooling 

260 

261 

262 class Interval(Base): 

263 # ... 

264 

265 @hybrid_property 

266 def radius(self) -> float: 

267 return abs(self.length) / 2 

268 

269 @radius.inplace.setter 

270 def _radius_setter(self, value: float) -> None: 

271 # for example only 

272 self.length = value * 2 

273 

274 @radius.inplace.expression 

275 @classmethod 

276 def _radius_expression(cls) -> ColumnElement[float]: 

277 return type_coerce(func.abs(cls.length) / 2, Float) 

278 

279Using :attr:`.hybrid_property.inplace` further qualifies the use of the 

280decorator that a new copy should not be made, thereby maintaining the 

281``Interval.radius`` name while allowing additional methods 

282``Interval._radius_setter`` and ``Interval._radius_expression`` to be 

283differently named. 

284 

285 

286.. versionadded:: 2.0.4 Added :attr:`.hybrid_property.inplace` to allow 

287 less verbose construction of composite :class:`.hybrid_property` objects 

288 while not having to use repeated method names. Additionally allowed the 

289 use of ``@classmethod`` within :attr:`.hybrid_property.expression`, 

290 :attr:`.hybrid_property.update_expression`, and 

291 :attr:`.hybrid_property.comparator` to allow typing tools to identify 

292 ``cls`` as a class and not an instance in the method signature. 

293 

294 

295Defining Setters 

296---------------- 

297 

298The :meth:`.hybrid_property.setter` modifier allows the construction of a 

299custom setter method, that can modify values on the object:: 

300 

301 class Interval(Base): 

302 # ... 

303 

304 @hybrid_property 

305 def length(self) -> int: 

306 return self.end - self.start 

307 

308 @length.inplace.setter 

309 def _length_setter(self, value: int) -> None: 

310 self.end = self.start + value 

311 

312The ``length(self, value)`` method is now called upon set:: 

313 

314 >>> i1 = Interval(5, 10) 

315 >>> i1.length 

316 5 

317 >>> i1.length = 12 

318 >>> i1.end 

319 17 

320 

321.. _hybrid_bulk_update: 

322 

323Allowing Bulk ORM Update 

324------------------------ 

325 

326A hybrid can define a custom "UPDATE" handler for when using 

327ORM-enabled updates, allowing the hybrid to be used in the 

328SET clause of the update. 

329 

330Normally, when using a hybrid with :func:`_sql.update`, the SQL 

331expression is used as the column that's the target of the SET. If our 

332``Interval`` class had a hybrid ``start_point`` that linked to 

333``Interval.start``, this could be substituted directly:: 

334 

335 from sqlalchemy import update 

336 

337 stmt = update(Interval).values({Interval.start_point: 10}) 

338 

339However, when using a composite hybrid like ``Interval.length``, this 

340hybrid represents more than one column. We can set up a handler that will 

341accommodate a value passed in the VALUES expression which can affect 

342this, using the :meth:`.hybrid_property.update_expression` decorator. 

343A handler that works similarly to our setter would be:: 

344 

345 from typing import List, Tuple, Any 

346 

347 

348 class Interval(Base): 

349 # ... 

350 

351 @hybrid_property 

352 def length(self) -> int: 

353 return self.end - self.start 

354 

355 @length.inplace.setter 

356 def _length_setter(self, value: int) -> None: 

357 self.end = self.start + value 

358 

359 @length.inplace.update_expression 

360 def _length_update_expression( 

361 cls, value: Any 

362 ) -> List[Tuple[Any, Any]]: 

363 return [(cls.end, cls.start + value)] 

364 

365Above, if we use ``Interval.length`` in an UPDATE expression, we get 

366a hybrid SET expression: 

367 

368.. sourcecode:: pycon+sql 

369 

370 

371 >>> from sqlalchemy import update 

372 >>> print(update(Interval).values({Interval.length: 25})) 

373 {printsql}UPDATE interval SET "end"=(interval.start + :start_1) 

374 

375This SET expression is accommodated by the ORM automatically. 

376 

377.. seealso:: 

378 

379 :ref:`orm_expression_update_delete` - includes background on ORM-enabled 

380 UPDATE statements 

381 

382 

383Working with Relationships 

384-------------------------- 

385 

386There's no essential difference when creating hybrids that work with 

387related objects as opposed to column-based data. The need for distinct 

388expressions tends to be greater. The two variants we'll illustrate 

389are the "join-dependent" hybrid, and the "correlated subquery" hybrid. 

390 

391Join-Dependent Relationship Hybrid 

392^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 

393 

394Consider the following declarative 

395mapping which relates a ``User`` to a ``SavingsAccount``:: 

396 

397 from __future__ import annotations 

398 

399 from decimal import Decimal 

400 from typing import cast 

401 from typing import List 

402 from typing import Optional 

403 

404 from sqlalchemy import ForeignKey 

405 from sqlalchemy import Numeric 

406 from sqlalchemy import String 

407 from sqlalchemy import SQLColumnExpression 

408 from sqlalchemy.ext.hybrid import hybrid_property 

409 from sqlalchemy.orm import DeclarativeBase 

410 from sqlalchemy.orm import Mapped 

411 from sqlalchemy.orm import mapped_column 

412 from sqlalchemy.orm import relationship 

413 

414 

415 class Base(DeclarativeBase): 

416 pass 

417 

418 

419 class SavingsAccount(Base): 

420 __tablename__ = "account" 

421 id: Mapped[int] = mapped_column(primary_key=True) 

422 user_id: Mapped[int] = mapped_column(ForeignKey("user.id")) 

423 balance: Mapped[Decimal] = mapped_column(Numeric(15, 5)) 

424 

425 owner: Mapped[User] = relationship(back_populates="accounts") 

426 

427 

428 class User(Base): 

429 __tablename__ = "user" 

430 id: Mapped[int] = mapped_column(primary_key=True) 

431 name: Mapped[str] = mapped_column(String(100)) 

432 

433 accounts: Mapped[List[SavingsAccount]] = relationship( 

434 back_populates="owner", lazy="selectin" 

435 ) 

436 

437 @hybrid_property 

438 def balance(self) -> Optional[Decimal]: 

439 if self.accounts: 

440 return self.accounts[0].balance 

441 else: 

442 return None 

443 

444 @balance.inplace.setter 

445 def _balance_setter(self, value: Optional[Decimal]) -> None: 

446 assert value is not None 

447 

448 if not self.accounts: 

449 account = SavingsAccount(owner=self) 

450 else: 

451 account = self.accounts[0] 

452 account.balance = value 

453 

454 @balance.inplace.expression 

455 @classmethod 

456 def _balance_expression(cls) -> SQLColumnExpression[Optional[Decimal]]: 

457 return cast( 

458 "SQLColumnExpression[Optional[Decimal]]", 

459 SavingsAccount.balance, 

460 ) 

461 

462The above hybrid property ``balance`` works with the first 

463``SavingsAccount`` entry in the list of accounts for this user. The 

464in-Python getter/setter methods can treat ``accounts`` as a Python 

465list available on ``self``. 

466 

467.. tip:: The ``User.balance`` getter in the above example accesses the 

468 ``self.acccounts`` collection, which will normally be loaded via the 

469 :func:`.selectinload` loader strategy configured on the ``User.balance`` 

470 :func:`_orm.relationship`. The default loader strategy when not otherwise 

471 stated on :func:`_orm.relationship` is :func:`.lazyload`, which emits SQL on 

472 demand. When using asyncio, on-demand loaders such as :func:`.lazyload` are 

473 not supported, so care should be taken to ensure the ``self.accounts`` 

474 collection is accessible to this hybrid accessor when using asyncio. 

475 

476At the expression level, it's expected that the ``User`` class will 

477be used in an appropriate context such that an appropriate join to 

478``SavingsAccount`` will be present: 

479 

480.. sourcecode:: pycon+sql 

481 

482 >>> from sqlalchemy import select 

483 >>> print( 

484 ... select(User, User.balance) 

485 ... .join(User.accounts) 

486 ... .filter(User.balance > 5000) 

487 ... ) 

488 {printsql}SELECT "user".id AS user_id, "user".name AS user_name, 

489 account.balance AS account_balance 

490 FROM "user" JOIN account ON "user".id = account.user_id 

491 WHERE account.balance > :balance_1 

492 

493Note however, that while the instance level accessors need to worry 

494about whether ``self.accounts`` is even present, this issue expresses 

495itself differently at the SQL expression level, where we basically 

496would use an outer join: 

497 

498.. sourcecode:: pycon+sql 

499 

500 >>> from sqlalchemy import select 

501 >>> from sqlalchemy import or_ 

502 >>> print( 

503 ... select(User, User.balance) 

504 ... .outerjoin(User.accounts) 

505 ... .filter(or_(User.balance < 5000, User.balance == None)) 

506 ... ) 

507 {printsql}SELECT "user".id AS user_id, "user".name AS user_name, 

508 account.balance AS account_balance 

509 FROM "user" LEFT OUTER JOIN account ON "user".id = account.user_id 

510 WHERE account.balance < :balance_1 OR account.balance IS NULL 

511 

512Correlated Subquery Relationship Hybrid 

513^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 

514 

515We can, of course, forego being dependent on the enclosing query's usage 

516of joins in favor of the correlated subquery, which can portably be packed 

517into a single column expression. A correlated subquery is more portable, but 

518often performs more poorly at the SQL level. Using the same technique 

519illustrated at :ref:`mapper_column_property_sql_expressions`, 

520we can adjust our ``SavingsAccount`` example to aggregate the balances for 

521*all* accounts, and use a correlated subquery for the column expression:: 

522 

523 from __future__ import annotations 

524 

525 from decimal import Decimal 

526 from typing import List 

527 

528 from sqlalchemy import ForeignKey 

529 from sqlalchemy import func 

530 from sqlalchemy import Numeric 

531 from sqlalchemy import select 

532 from sqlalchemy import SQLColumnExpression 

533 from sqlalchemy import String 

534 from sqlalchemy.ext.hybrid import hybrid_property 

535 from sqlalchemy.orm import DeclarativeBase 

536 from sqlalchemy.orm import Mapped 

537 from sqlalchemy.orm import mapped_column 

538 from sqlalchemy.orm import relationship 

539 

540 

541 class Base(DeclarativeBase): 

542 pass 

543 

544 

545 class SavingsAccount(Base): 

546 __tablename__ = "account" 

547 id: Mapped[int] = mapped_column(primary_key=True) 

548 user_id: Mapped[int] = mapped_column(ForeignKey("user.id")) 

549 balance: Mapped[Decimal] = mapped_column(Numeric(15, 5)) 

550 

551 owner: Mapped[User] = relationship(back_populates="accounts") 

552 

553 

554 class User(Base): 

555 __tablename__ = "user" 

556 id: Mapped[int] = mapped_column(primary_key=True) 

557 name: Mapped[str] = mapped_column(String(100)) 

558 

559 accounts: Mapped[List[SavingsAccount]] = relationship( 

560 back_populates="owner", lazy="selectin" 

561 ) 

562 

563 @hybrid_property 

564 def balance(self) -> Decimal: 

565 return sum( 

566 (acc.balance for acc in self.accounts), start=Decimal("0") 

567 ) 

568 

569 @balance.inplace.expression 

570 @classmethod 

571 def _balance_expression(cls) -> SQLColumnExpression[Decimal]: 

572 return ( 

573 select(func.sum(SavingsAccount.balance)) 

574 .where(SavingsAccount.user_id == cls.id) 

575 .label("total_balance") 

576 ) 

577 

578The above recipe will give us the ``balance`` column which renders 

579a correlated SELECT: 

580 

581.. sourcecode:: pycon+sql 

582 

583 >>> from sqlalchemy import select 

584 >>> print(select(User).filter(User.balance > 400)) 

585 {printsql}SELECT "user".id, "user".name 

586 FROM "user" 

587 WHERE ( 

588 SELECT sum(account.balance) AS sum_1 FROM account 

589 WHERE account.user_id = "user".id 

590 ) > :param_1 

591 

592 

593.. _hybrid_custom_comparators: 

594 

595Building Custom Comparators 

596--------------------------- 

597 

598The hybrid property also includes a helper that allows construction of 

599custom comparators. A comparator object allows one to customize the 

600behavior of each SQLAlchemy expression operator individually. They 

601are useful when creating custom types that have some highly 

602idiosyncratic behavior on the SQL side. 

603 

604.. note:: The :meth:`.hybrid_property.comparator` decorator introduced 

605 in this section **replaces** the use of the 

606 :meth:`.hybrid_property.expression` decorator. 

607 They cannot be used together. 

608 

609The example class below allows case-insensitive comparisons on the attribute 

610named ``word_insensitive``:: 

611 

612 from __future__ import annotations 

613 

614 from typing import Any 

615 

616 from sqlalchemy import ColumnElement 

617 from sqlalchemy import func 

618 from sqlalchemy.ext.hybrid import Comparator 

619 from sqlalchemy.ext.hybrid import hybrid_property 

620 from sqlalchemy.orm import DeclarativeBase 

621 from sqlalchemy.orm import Mapped 

622 from sqlalchemy.orm import mapped_column 

623 

624 

625 class Base(DeclarativeBase): 

626 pass 

627 

628 

629 class CaseInsensitiveComparator(Comparator[str]): 

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

631 return func.lower(self.__clause_element__()) == func.lower(other) 

632 

633 

634 class SearchWord(Base): 

635 __tablename__ = "searchword" 

636 

637 id: Mapped[int] = mapped_column(primary_key=True) 

638 word: Mapped[str] 

639 

640 @hybrid_property 

641 def word_insensitive(self) -> str: 

642 return self.word.lower() 

643 

644 @word_insensitive.inplace.comparator 

645 @classmethod 

646 def _word_insensitive_comparator(cls) -> CaseInsensitiveComparator: 

647 return CaseInsensitiveComparator(cls.word) 

648 

649Above, SQL expressions against ``word_insensitive`` will apply the ``LOWER()`` 

650SQL function to both sides: 

651 

652.. sourcecode:: pycon+sql 

653 

654 >>> from sqlalchemy import select 

655 >>> print(select(SearchWord).filter_by(word_insensitive="Trucks")) 

656 {printsql}SELECT searchword.id, searchword.word 

657 FROM searchword 

658 WHERE lower(searchword.word) = lower(:lower_1) 

659 

660 

661The ``CaseInsensitiveComparator`` above implements part of the 

662:class:`.ColumnOperators` interface. A "coercion" operation like 

663lowercasing can be applied to all comparison operations (i.e. ``eq``, 

664``lt``, ``gt``, etc.) using :meth:`.Operators.operate`:: 

665 

666 class CaseInsensitiveComparator(Comparator): 

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

668 return op( 

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

670 func.lower(other), 

671 **kwargs, 

672 ) 

673 

674.. _hybrid_reuse_subclass: 

675 

676Reusing Hybrid Properties across Subclasses 

677------------------------------------------- 

678 

679A hybrid can be referred to from a superclass, to allow modifying 

680methods like :meth:`.hybrid_property.getter`, :meth:`.hybrid_property.setter` 

681to be used to redefine those methods on a subclass. This is similar to 

682how the standard Python ``@property`` object works:: 

683 

684 class FirstNameOnly(Base): 

685 # ... 

686 

687 first_name: Mapped[str] 

688 

689 @hybrid_property 

690 def name(self) -> str: 

691 return self.first_name 

692 

693 @name.inplace.setter 

694 def _name_setter(self, value: str) -> None: 

695 self.first_name = value 

696 

697 

698 class FirstNameLastName(FirstNameOnly): 

699 # ... 

700 

701 last_name: Mapped[str] 

702 

703 # 'inplace' is not used here; calling getter creates a copy 

704 # of FirstNameOnly.name that is local to FirstNameLastName 

705 @FirstNameOnly.name.getter 

706 def name(self) -> str: 

707 return self.first_name + " " + self.last_name 

708 

709 @name.inplace.setter 

710 def _name_setter(self, value: str) -> None: 

711 self.first_name, self.last_name = value.split(" ", 1) 

712 

713Above, the ``FirstNameLastName`` class refers to the hybrid from 

714``FirstNameOnly.name`` to repurpose its getter and setter for the subclass. 

715 

716When overriding :meth:`.hybrid_property.expression` and 

717:meth:`.hybrid_property.comparator` alone as the first reference to the 

718superclass, these names conflict with the same-named accessors on the class- 

719level :class:`.QueryableAttribute` object returned at the class level. To 

720override these methods when referring directly to the parent class descriptor, 

721add the special qualifier :attr:`.hybrid_property.overrides`, which will de- 

722reference the instrumented attribute back to the hybrid object:: 

723 

724 class FirstNameLastName(FirstNameOnly): 

725 # ... 

726 

727 last_name: Mapped[str] 

728 

729 @FirstNameOnly.name.overrides.expression 

730 @classmethod 

731 def name(cls): 

732 return func.concat(cls.first_name, " ", cls.last_name) 

733 

734Hybrid Value Objects 

735-------------------- 

736 

737Note in our previous example, if we were to compare the ``word_insensitive`` 

738attribute of a ``SearchWord`` instance to a plain Python string, the plain 

739Python string would not be coerced to lower case - the 

740``CaseInsensitiveComparator`` we built, being returned by 

741``@word_insensitive.comparator``, only applies to the SQL side. 

742 

743A more comprehensive form of the custom comparator is to construct a *Hybrid 

744Value Object*. This technique applies the target value or expression to a value 

745object which is then returned by the accessor in all cases. The value object 

746allows control of all operations upon the value as well as how compared values 

747are treated, both on the SQL expression side as well as the Python value side. 

748Replacing the previous ``CaseInsensitiveComparator`` class with a new 

749``CaseInsensitiveWord`` class:: 

750 

751 class CaseInsensitiveWord(Comparator): 

752 "Hybrid value representing a lower case representation of a word." 

753 

754 def __init__(self, word): 

755 if isinstance(word, basestring): 

756 self.word = word.lower() 

757 elif isinstance(word, CaseInsensitiveWord): 

758 self.word = word.word 

759 else: 

760 self.word = func.lower(word) 

761 

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

763 if not isinstance(other, CaseInsensitiveWord): 

764 other = CaseInsensitiveWord(other) 

765 return op(self.word, other.word, **kwargs) 

766 

767 def __clause_element__(self): 

768 return self.word 

769 

770 def __str__(self): 

771 return self.word 

772 

773 key = "word" 

774 "Label to apply to Query tuple results" 

775 

776Above, the ``CaseInsensitiveWord`` object represents ``self.word``, which may 

777be a SQL function, or may be a Python native. By overriding ``operate()`` and 

778``__clause_element__()`` to work in terms of ``self.word``, all comparison 

779operations will work against the "converted" form of ``word``, whether it be 

780SQL side or Python side. Our ``SearchWord`` class can now deliver the 

781``CaseInsensitiveWord`` object unconditionally from a single hybrid call:: 

782 

783 class SearchWord(Base): 

784 __tablename__ = "searchword" 

785 id: Mapped[int] = mapped_column(primary_key=True) 

786 word: Mapped[str] 

787 

788 @hybrid_property 

789 def word_insensitive(self) -> CaseInsensitiveWord: 

790 return CaseInsensitiveWord(self.word) 

791 

792The ``word_insensitive`` attribute now has case-insensitive comparison behavior 

793universally, including SQL expression vs. Python expression (note the Python 

794value is converted to lower case on the Python side here): 

795 

796.. sourcecode:: pycon+sql 

797 

798 >>> print(select(SearchWord).filter_by(word_insensitive="Trucks")) 

799 {printsql}SELECT searchword.id AS searchword_id, searchword.word AS searchword_word 

800 FROM searchword 

801 WHERE lower(searchword.word) = :lower_1 

802 

803SQL expression versus SQL expression: 

804 

805.. sourcecode:: pycon+sql 

806 

807 >>> from sqlalchemy.orm import aliased 

808 >>> sw1 = aliased(SearchWord) 

809 >>> sw2 = aliased(SearchWord) 

810 >>> print( 

811 ... select(sw1.word_insensitive, sw2.word_insensitive).filter( 

812 ... sw1.word_insensitive > sw2.word_insensitive 

813 ... ) 

814 ... ) 

815 {printsql}SELECT lower(searchword_1.word) AS lower_1, 

816 lower(searchword_2.word) AS lower_2 

817 FROM searchword AS searchword_1, searchword AS searchword_2 

818 WHERE lower(searchword_1.word) > lower(searchword_2.word) 

819 

820Python only expression:: 

821 

822 >>> ws1 = SearchWord(word="SomeWord") 

823 >>> ws1.word_insensitive == "sOmEwOrD" 

824 True 

825 >>> ws1.word_insensitive == "XOmEwOrX" 

826 False 

827 >>> print(ws1.word_insensitive) 

828 someword 

829 

830The Hybrid Value pattern is very useful for any kind of value that may have 

831multiple representations, such as timestamps, time deltas, units of 

832measurement, currencies and encrypted passwords. 

833 

834.. seealso:: 

835 

836 `Hybrids and Value Agnostic Types 

837 <https://techspot.zzzeek.org/2011/10/21/hybrids-and-value-agnostic-types/>`_ 

838 - on the techspot.zzzeek.org blog 

839 

840 `Value Agnostic Types, Part II 

841 <https://techspot.zzzeek.org/2011/10/29/value-agnostic-types-part-ii/>`_ - 

842 on the techspot.zzzeek.org blog 

843 

844 

845""" # noqa 

846 

847from __future__ import annotations 

848 

849from typing import Any 

850from typing import Callable 

851from typing import cast 

852from typing import Generic 

853from typing import List 

854from typing import Optional 

855from typing import overload 

856from typing import Sequence 

857from typing import Tuple 

858from typing import Type 

859from typing import TYPE_CHECKING 

860from typing import TypeVar 

861from typing import Union 

862 

863from .. import util 

864from ..orm import attributes 

865from ..orm import InspectionAttrExtensionType 

866from ..orm import interfaces 

867from ..orm import ORMDescriptor 

868from ..orm.attributes import QueryableAttribute 

869from ..sql import roles 

870from ..sql._typing import is_has_clause_element 

871from ..sql.elements import ColumnElement 

872from ..sql.elements import SQLCoreOperations 

873from ..util.typing import Concatenate 

874from ..util.typing import Literal 

875from ..util.typing import ParamSpec 

876from ..util.typing import Protocol 

877from ..util.typing import Self 

878 

879if TYPE_CHECKING: 

880 from ..orm.interfaces import MapperProperty 

881 from ..orm.util import AliasedInsp 

882 from ..sql import SQLColumnExpression 

883 from ..sql._typing import _ColumnExpressionArgument 

884 from ..sql._typing import _DMLColumnArgument 

885 from ..sql._typing import _HasClauseElement 

886 from ..sql._typing import _InfoType 

887 from ..sql.operators import OperatorType 

888 

889_P = ParamSpec("_P") 

890_R = TypeVar("_R") 

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

892_TE = TypeVar("_TE", bound=Any) 

893_T_co = TypeVar("_T_co", bound=Any, covariant=True) 

894_T_con = TypeVar("_T_con", bound=Any, contravariant=True) 

895 

896 

897class HybridExtensionType(InspectionAttrExtensionType): 

898 HYBRID_METHOD = "HYBRID_METHOD" 

899 """Symbol indicating an :class:`InspectionAttr` that's 

900 of type :class:`.hybrid_method`. 

901 

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

903 attribute. 

904 

905 .. seealso:: 

906 

907 :attr:`_orm.Mapper.all_orm_attributes` 

908 

909 """ 

910 

911 HYBRID_PROPERTY = "HYBRID_PROPERTY" 

912 """Symbol indicating an :class:`InspectionAttr` that's 

913 of type :class:`.hybrid_method`. 

914 

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

916 attribute. 

917 

918 .. seealso:: 

919 

920 :attr:`_orm.Mapper.all_orm_attributes` 

921 

922 """ 

923 

924 

925class _HybridGetterType(Protocol[_T_co]): 

926 def __call__(s, self: Any) -> _T_co: ... 

927 

928 

929class _HybridSetterType(Protocol[_T_con]): 

930 def __call__(s, self: Any, value: _T_con) -> None: ... 

931 

932 

933class _HybridUpdaterType(Protocol[_T_con]): 

934 def __call__( 

935 s, 

936 cls: Any, 

937 value: Union[_T_con, _ColumnExpressionArgument[_T_con]], 

938 ) -> List[Tuple[_DMLColumnArgument, Any]]: ... 

939 

940 

941class _HybridDeleterType(Protocol[_T_co]): 

942 def __call__(s, self: Any) -> None: ... 

943 

944 

945class _HybridExprCallableType(Protocol[_T_co]): 

946 def __call__( 

947 s, cls: Any 

948 ) -> Union[_HasClauseElement[_T_co], SQLColumnExpression[_T_co]]: ... 

949 

950 

951class _HybridComparatorCallableType(Protocol[_T]): 

952 def __call__(self, cls: Any) -> Comparator[_T]: ... 

953 

954 

955class _HybridClassLevelAccessor(QueryableAttribute[_T]): 

956 """Describe the object returned by a hybrid_property() when 

957 called as a class-level descriptor. 

958 

959 """ 

960 

961 if TYPE_CHECKING: 

962 

963 def getter( 

964 self, fget: _HybridGetterType[_T] 

965 ) -> hybrid_property[_T]: ... 

966 

967 def setter( 

968 self, fset: _HybridSetterType[_T] 

969 ) -> hybrid_property[_T]: ... 

970 

971 def deleter( 

972 self, fdel: _HybridDeleterType[_T] 

973 ) -> hybrid_property[_T]: ... 

974 

975 @property 

976 def overrides(self) -> hybrid_property[_T]: ... 

977 

978 def update_expression( 

979 self, meth: _HybridUpdaterType[_T] 

980 ) -> hybrid_property[_T]: ... 

981 

982 

983class hybrid_method(interfaces.InspectionAttrInfo, Generic[_P, _R]): 

984 """A decorator which allows definition of a Python object method with both 

985 instance-level and class-level behavior. 

986 

987 """ 

988 

989 is_attribute = True 

990 extension_type = HybridExtensionType.HYBRID_METHOD 

991 

992 def __init__( 

993 self, 

994 func: Callable[Concatenate[Any, _P], _R], 

995 expr: Optional[ 

996 Callable[Concatenate[Any, _P], SQLCoreOperations[_R]] 

997 ] = None, 

998 ): 

999 """Create a new :class:`.hybrid_method`. 

1000 

1001 Usage is typically via decorator:: 

1002 

1003 from sqlalchemy.ext.hybrid import hybrid_method 

1004 

1005 

1006 class SomeClass: 

1007 @hybrid_method 

1008 def value(self, x, y): 

1009 return self._value + x + y 

1010 

1011 @value.expression 

1012 @classmethod 

1013 def value(cls, x, y): 

1014 return func.some_function(cls._value, x, y) 

1015 

1016 """ 

1017 self.func = func 

1018 if expr is not None: 

1019 self.expression(expr) 

1020 else: 

1021 self.expression(func) # type: ignore 

1022 

1023 @property 

1024 def inplace(self) -> Self: 

1025 """Return the inplace mutator for this :class:`.hybrid_method`. 

1026 

1027 The :class:`.hybrid_method` class already performs "in place" mutation 

1028 when the :meth:`.hybrid_method.expression` decorator is called, 

1029 so this attribute returns Self. 

1030 

1031 .. versionadded:: 2.0.4 

1032 

1033 .. seealso:: 

1034 

1035 :ref:`hybrid_pep484_naming` 

1036 

1037 """ 

1038 return self 

1039 

1040 @overload 

1041 def __get__( 

1042 self, instance: Literal[None], owner: Type[object] 

1043 ) -> Callable[_P, SQLCoreOperations[_R]]: ... 

1044 

1045 @overload 

1046 def __get__( 

1047 self, instance: object, owner: Type[object] 

1048 ) -> Callable[_P, _R]: ... 

1049 

1050 def __get__( 

1051 self, instance: Optional[object], owner: Type[object] 

1052 ) -> Union[Callable[_P, _R], Callable[_P, SQLCoreOperations[_R]]]: 

1053 if instance is None: 

1054 return self.expr.__get__(owner, owner) # type: ignore 

1055 else: 

1056 return self.func.__get__(instance, owner) # type: ignore 

1057 

1058 def expression( 

1059 self, expr: Callable[Concatenate[Any, _P], SQLCoreOperations[_R]] 

1060 ) -> hybrid_method[_P, _R]: 

1061 """Provide a modifying decorator that defines a 

1062 SQL-expression producing method.""" 

1063 

1064 self.expr = expr 

1065 if not self.expr.__doc__: 

1066 self.expr.__doc__ = self.func.__doc__ 

1067 return self 

1068 

1069 

1070def _unwrap_classmethod(meth: _T) -> _T: 

1071 if isinstance(meth, classmethod): 

1072 return meth.__func__ # type: ignore 

1073 else: 

1074 return meth 

1075 

1076 

1077class hybrid_property(interfaces.InspectionAttrInfo, ORMDescriptor[_T]): 

1078 """A decorator which allows definition of a Python descriptor with both 

1079 instance-level and class-level behavior. 

1080 

1081 """ 

1082 

1083 is_attribute = True 

1084 extension_type = HybridExtensionType.HYBRID_PROPERTY 

1085 

1086 __name__: str 

1087 

1088 def __init__( 

1089 self, 

1090 fget: _HybridGetterType[_T], 

1091 fset: Optional[_HybridSetterType[_T]] = None, 

1092 fdel: Optional[_HybridDeleterType[_T]] = None, 

1093 expr: Optional[_HybridExprCallableType[_T]] = None, 

1094 custom_comparator: Optional[Comparator[_T]] = None, 

1095 update_expr: Optional[_HybridUpdaterType[_T]] = None, 

1096 ): 

1097 """Create a new :class:`.hybrid_property`. 

1098 

1099 Usage is typically via decorator:: 

1100 

1101 from sqlalchemy.ext.hybrid import hybrid_property 

1102 

1103 

1104 class SomeClass: 

1105 @hybrid_property 

1106 def value(self): 

1107 return self._value 

1108 

1109 @value.setter 

1110 def value(self, value): 

1111 self._value = value 

1112 

1113 """ 

1114 self.fget = fget 

1115 self.fset = fset 

1116 self.fdel = fdel 

1117 self.expr = _unwrap_classmethod(expr) 

1118 self.custom_comparator = _unwrap_classmethod(custom_comparator) 

1119 self.update_expr = _unwrap_classmethod(update_expr) 

1120 util.update_wrapper(self, fget) # type: ignore[arg-type] 

1121 

1122 @overload 

1123 def __get__(self, instance: Any, owner: Literal[None]) -> Self: ... 

1124 

1125 @overload 

1126 def __get__( 

1127 self, instance: Literal[None], owner: Type[object] 

1128 ) -> _HybridClassLevelAccessor[_T]: ... 

1129 

1130 @overload 

1131 def __get__(self, instance: object, owner: Type[object]) -> _T: ... 

1132 

1133 def __get__( 

1134 self, instance: Optional[object], owner: Optional[Type[object]] 

1135 ) -> Union[hybrid_property[_T], _HybridClassLevelAccessor[_T], _T]: 

1136 if owner is None: 

1137 return self 

1138 elif instance is None: 

1139 return self._expr_comparator(owner) 

1140 else: 

1141 return self.fget(instance) 

1142 

1143 def __set__(self, instance: object, value: Any) -> None: 

1144 if self.fset is None: 

1145 raise AttributeError("can't set attribute") 

1146 self.fset(instance, value) 

1147 

1148 def __delete__(self, instance: object) -> None: 

1149 if self.fdel is None: 

1150 raise AttributeError("can't delete attribute") 

1151 self.fdel(instance) 

1152 

1153 def _copy(self, **kw: Any) -> hybrid_property[_T]: 

1154 defaults = { 

1155 key: value 

1156 for key, value in self.__dict__.items() 

1157 if not key.startswith("_") 

1158 } 

1159 defaults.update(**kw) 

1160 return type(self)(**defaults) 

1161 

1162 @property 

1163 def overrides(self) -> Self: 

1164 """Prefix for a method that is overriding an existing attribute. 

1165 

1166 The :attr:`.hybrid_property.overrides` accessor just returns 

1167 this hybrid object, which when called at the class level from 

1168 a parent class, will de-reference the "instrumented attribute" 

1169 normally returned at this level, and allow modifying decorators 

1170 like :meth:`.hybrid_property.expression` and 

1171 :meth:`.hybrid_property.comparator` 

1172 to be used without conflicting with the same-named attributes 

1173 normally present on the :class:`.QueryableAttribute`:: 

1174 

1175 class SuperClass: 

1176 # ... 

1177 

1178 @hybrid_property 

1179 def foobar(self): 

1180 return self._foobar 

1181 

1182 

1183 class SubClass(SuperClass): 

1184 # ... 

1185 

1186 @SuperClass.foobar.overrides.expression 

1187 def foobar(cls): 

1188 return func.subfoobar(self._foobar) 

1189 

1190 .. versionadded:: 1.2 

1191 

1192 .. seealso:: 

1193 

1194 :ref:`hybrid_reuse_subclass` 

1195 

1196 """ 

1197 return self 

1198 

1199 class _InPlace(Generic[_TE]): 

1200 """A builder helper for .hybrid_property. 

1201 

1202 .. versionadded:: 2.0.4 

1203 

1204 """ 

1205 

1206 __slots__ = ("attr",) 

1207 

1208 def __init__(self, attr: hybrid_property[_TE]): 

1209 self.attr = attr 

1210 

1211 def _set(self, **kw: Any) -> hybrid_property[_TE]: 

1212 for k, v in kw.items(): 

1213 setattr(self.attr, k, _unwrap_classmethod(v)) 

1214 return self.attr 

1215 

1216 def getter(self, fget: _HybridGetterType[_TE]) -> hybrid_property[_TE]: 

1217 return self._set(fget=fget) 

1218 

1219 def setter(self, fset: _HybridSetterType[_TE]) -> hybrid_property[_TE]: 

1220 return self._set(fset=fset) 

1221 

1222 def deleter( 

1223 self, fdel: _HybridDeleterType[_TE] 

1224 ) -> hybrid_property[_TE]: 

1225 return self._set(fdel=fdel) 

1226 

1227 def expression( 

1228 self, expr: _HybridExprCallableType[_TE] 

1229 ) -> hybrid_property[_TE]: 

1230 return self._set(expr=expr) 

1231 

1232 def comparator( 

1233 self, comparator: _HybridComparatorCallableType[_TE] 

1234 ) -> hybrid_property[_TE]: 

1235 return self._set(custom_comparator=comparator) 

1236 

1237 def update_expression( 

1238 self, meth: _HybridUpdaterType[_TE] 

1239 ) -> hybrid_property[_TE]: 

1240 return self._set(update_expr=meth) 

1241 

1242 @property 

1243 def inplace(self) -> _InPlace[_T]: 

1244 """Return the inplace mutator for this :class:`.hybrid_property`. 

1245 

1246 This is to allow in-place mutation of the hybrid, allowing the first 

1247 hybrid method of a certain name to be re-used in order to add 

1248 more methods without having to name those methods the same, e.g.:: 

1249 

1250 class Interval(Base): 

1251 # ... 

1252 

1253 @hybrid_property 

1254 def radius(self) -> float: 

1255 return abs(self.length) / 2 

1256 

1257 @radius.inplace.setter 

1258 def _radius_setter(self, value: float) -> None: 

1259 self.length = value * 2 

1260 

1261 @radius.inplace.expression 

1262 def _radius_expression(cls) -> ColumnElement[float]: 

1263 return type_coerce(func.abs(cls.length) / 2, Float) 

1264 

1265 .. versionadded:: 2.0.4 

1266 

1267 .. seealso:: 

1268 

1269 :ref:`hybrid_pep484_naming` 

1270 

1271 """ 

1272 return hybrid_property._InPlace(self) 

1273 

1274 def getter(self, fget: _HybridGetterType[_T]) -> hybrid_property[_T]: 

1275 """Provide a modifying decorator that defines a getter method. 

1276 

1277 .. versionadded:: 1.2 

1278 

1279 """ 

1280 

1281 return self._copy(fget=fget) 

1282 

1283 def setter(self, fset: _HybridSetterType[_T]) -> hybrid_property[_T]: 

1284 """Provide a modifying decorator that defines a setter method.""" 

1285 

1286 return self._copy(fset=fset) 

1287 

1288 def deleter(self, fdel: _HybridDeleterType[_T]) -> hybrid_property[_T]: 

1289 """Provide a modifying decorator that defines a deletion method.""" 

1290 

1291 return self._copy(fdel=fdel) 

1292 

1293 def expression( 

1294 self, expr: _HybridExprCallableType[_T] 

1295 ) -> hybrid_property[_T]: 

1296 """Provide a modifying decorator that defines a SQL-expression 

1297 producing method. 

1298 

1299 When a hybrid is invoked at the class level, the SQL expression given 

1300 here is wrapped inside of a specialized :class:`.QueryableAttribute`, 

1301 which is the same kind of object used by the ORM to represent other 

1302 mapped attributes. The reason for this is so that other class-level 

1303 attributes such as docstrings and a reference to the hybrid itself may 

1304 be maintained within the structure that's returned, without any 

1305 modifications to the original SQL expression passed in. 

1306 

1307 .. note:: 

1308 

1309 When referring to a hybrid property from an owning class (e.g. 

1310 ``SomeClass.some_hybrid``), an instance of 

1311 :class:`.QueryableAttribute` is returned, representing the 

1312 expression or comparator object as well as this hybrid object. 

1313 However, that object itself has accessors called ``expression`` and 

1314 ``comparator``; so when attempting to override these decorators on a 

1315 subclass, it may be necessary to qualify it using the 

1316 :attr:`.hybrid_property.overrides` modifier first. See that 

1317 modifier for details. 

1318 

1319 .. seealso:: 

1320 

1321 :ref:`hybrid_distinct_expression` 

1322 

1323 """ 

1324 

1325 return self._copy(expr=expr) 

1326 

1327 def comparator( 

1328 self, comparator: _HybridComparatorCallableType[_T] 

1329 ) -> hybrid_property[_T]: 

1330 """Provide a modifying decorator that defines a custom 

1331 comparator producing method. 

1332 

1333 The return value of the decorated method should be an instance of 

1334 :class:`~.hybrid.Comparator`. 

1335 

1336 .. note:: The :meth:`.hybrid_property.comparator` decorator 

1337 **replaces** the use of the :meth:`.hybrid_property.expression` 

1338 decorator. They cannot be used together. 

1339 

1340 When a hybrid is invoked at the class level, the 

1341 :class:`~.hybrid.Comparator` object given here is wrapped inside of a 

1342 specialized :class:`.QueryableAttribute`, which is the same kind of 

1343 object used by the ORM to represent other mapped attributes. The 

1344 reason for this is so that other class-level attributes such as 

1345 docstrings and a reference to the hybrid itself may be maintained 

1346 within the structure that's returned, without any modifications to the 

1347 original comparator object passed in. 

1348 

1349 .. note:: 

1350 

1351 When referring to a hybrid property from an owning class (e.g. 

1352 ``SomeClass.some_hybrid``), an instance of 

1353 :class:`.QueryableAttribute` is returned, representing the 

1354 expression or comparator object as this hybrid object. However, 

1355 that object itself has accessors called ``expression`` and 

1356 ``comparator``; so when attempting to override these decorators on a 

1357 subclass, it may be necessary to qualify it using the 

1358 :attr:`.hybrid_property.overrides` modifier first. See that 

1359 modifier for details. 

1360 

1361 """ 

1362 return self._copy(custom_comparator=comparator) 

1363 

1364 def update_expression( 

1365 self, meth: _HybridUpdaterType[_T] 

1366 ) -> hybrid_property[_T]: 

1367 """Provide a modifying decorator that defines an UPDATE tuple 

1368 producing method. 

1369 

1370 The method accepts a single value, which is the value to be 

1371 rendered into the SET clause of an UPDATE statement. The method 

1372 should then process this value into individual column expressions 

1373 that fit into the ultimate SET clause, and return them as a 

1374 sequence of 2-tuples. Each tuple 

1375 contains a column expression as the key and a value to be rendered. 

1376 

1377 E.g.:: 

1378 

1379 class Person(Base): 

1380 # ... 

1381 

1382 first_name = Column(String) 

1383 last_name = Column(String) 

1384 

1385 @hybrid_property 

1386 def fullname(self): 

1387 return first_name + " " + last_name 

1388 

1389 @fullname.update_expression 

1390 def fullname(cls, value): 

1391 fname, lname = value.split(" ", 1) 

1392 return [(cls.first_name, fname), (cls.last_name, lname)] 

1393 

1394 .. versionadded:: 1.2 

1395 

1396 """ 

1397 return self._copy(update_expr=meth) 

1398 

1399 @util.memoized_property 

1400 def _expr_comparator( 

1401 self, 

1402 ) -> Callable[[Any], _HybridClassLevelAccessor[_T]]: 

1403 if self.custom_comparator is not None: 

1404 return self._get_comparator(self.custom_comparator) 

1405 elif self.expr is not None: 

1406 return self._get_expr(self.expr) 

1407 else: 

1408 return self._get_expr(cast(_HybridExprCallableType[_T], self.fget)) 

1409 

1410 def _get_expr( 

1411 self, expr: _HybridExprCallableType[_T] 

1412 ) -> Callable[[Any], _HybridClassLevelAccessor[_T]]: 

1413 def _expr(cls: Any) -> ExprComparator[_T]: 

1414 return ExprComparator(cls, expr(cls), self) 

1415 

1416 util.update_wrapper(_expr, expr) 

1417 

1418 return self._get_comparator(_expr) 

1419 

1420 def _get_comparator( 

1421 self, comparator: Any 

1422 ) -> Callable[[Any], _HybridClassLevelAccessor[_T]]: 

1423 proxy_attr = attributes.create_proxied_attribute(self) 

1424 

1425 def expr_comparator( 

1426 owner: Type[object], 

1427 ) -> _HybridClassLevelAccessor[_T]: 

1428 # because this is the descriptor protocol, we don't really know 

1429 # what our attribute name is. so search for it through the 

1430 # MRO. 

1431 for lookup in owner.__mro__: 

1432 if self.__name__ in lookup.__dict__: 

1433 if lookup.__dict__[self.__name__] is self: 

1434 name = self.__name__ 

1435 break 

1436 else: 

1437 name = attributes._UNKNOWN_ATTR_KEY # type: ignore[assignment] 

1438 

1439 return cast( 

1440 "_HybridClassLevelAccessor[_T]", 

1441 proxy_attr( 

1442 owner, 

1443 name, 

1444 self, 

1445 comparator(owner), 

1446 doc=comparator.__doc__ or self.__doc__, 

1447 ), 

1448 ) 

1449 

1450 return expr_comparator 

1451 

1452 

1453class Comparator(interfaces.PropComparator[_T]): 

1454 """A helper class that allows easy construction of custom 

1455 :class:`~.orm.interfaces.PropComparator` 

1456 classes for usage with hybrids.""" 

1457 

1458 def __init__( 

1459 self, expression: Union[_HasClauseElement[_T], SQLColumnExpression[_T]] 

1460 ): 

1461 self.expression = expression 

1462 

1463 def __clause_element__(self) -> roles.ColumnsClauseRole: 

1464 expr = self.expression 

1465 if is_has_clause_element(expr): 

1466 ret_expr = expr.__clause_element__() 

1467 else: 

1468 if TYPE_CHECKING: 

1469 assert isinstance(expr, ColumnElement) 

1470 ret_expr = expr 

1471 

1472 if TYPE_CHECKING: 

1473 # see test_hybrid->test_expression_isnt_clause_element 

1474 # that exercises the usual place this is caught if not 

1475 # true 

1476 assert isinstance(ret_expr, ColumnElement) 

1477 return ret_expr 

1478 

1479 @util.non_memoized_property 

1480 def property(self) -> interfaces.MapperProperty[_T]: 

1481 raise NotImplementedError() 

1482 

1483 def adapt_to_entity( 

1484 self, adapt_to_entity: AliasedInsp[Any] 

1485 ) -> Comparator[_T]: 

1486 # interesting.... 

1487 return self 

1488 

1489 

1490class ExprComparator(Comparator[_T]): 

1491 def __init__( 

1492 self, 

1493 cls: Type[Any], 

1494 expression: Union[_HasClauseElement[_T], SQLColumnExpression[_T]], 

1495 hybrid: hybrid_property[_T], 

1496 ): 

1497 self.cls = cls 

1498 self.expression = expression 

1499 self.hybrid = hybrid 

1500 

1501 def __getattr__(self, key: str) -> Any: 

1502 return getattr(self.expression, key) 

1503 

1504 @util.ro_non_memoized_property 

1505 def info(self) -> _InfoType: 

1506 return self.hybrid.info 

1507 

1508 def _bulk_update_tuples( 

1509 self, value: Any 

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

1511 if isinstance(self.expression, attributes.QueryableAttribute): 

1512 return self.expression._bulk_update_tuples(value) 

1513 elif self.hybrid.update_expr is not None: 

1514 return self.hybrid.update_expr(self.cls, value) 

1515 else: 

1516 return [(self.expression, value)] 

1517 

1518 @util.non_memoized_property 

1519 def property(self) -> MapperProperty[_T]: 

1520 # this accessor is not normally used, however is accessed by things 

1521 # like ORM synonyms if the hybrid is used in this context; the 

1522 # .property attribute is not necessarily accessible 

1523 return self.expression.property # type: ignore 

1524 

1525 def operate( 

1526 self, op: OperatorType, *other: Any, **kwargs: Any 

1527 ) -> ColumnElement[Any]: 

1528 return op(self.expression, *other, **kwargs) 

1529 

1530 def reverse_operate( 

1531 self, op: OperatorType, other: Any, **kwargs: Any 

1532 ) -> ColumnElement[Any]: 

1533 return op(other, self.expression, **kwargs) # type: ignore