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__(
1144 self, instance: object, value: Union[SQLCoreOperations[_T], _T]
1145 ) -> None:
1146 if self.fset is None:
1147 raise AttributeError("can't set attribute")
1148 self.fset(instance, value) # type: ignore[arg-type]
1149
1150 def __delete__(self, instance: object) -> None:
1151 if self.fdel is None:
1152 raise AttributeError("can't delete attribute")
1153 self.fdel(instance)
1154
1155 def _copy(self, **kw: Any) -> hybrid_property[_T]:
1156 defaults = {
1157 key: value
1158 for key, value in self.__dict__.items()
1159 if not key.startswith("_")
1160 }
1161 defaults.update(**kw)
1162 return type(self)(**defaults)
1163
1164 @property
1165 def overrides(self) -> Self:
1166 """Prefix for a method that is overriding an existing attribute.
1167
1168 The :attr:`.hybrid_property.overrides` accessor just returns
1169 this hybrid object, which when called at the class level from
1170 a parent class, will de-reference the "instrumented attribute"
1171 normally returned at this level, and allow modifying decorators
1172 like :meth:`.hybrid_property.expression` and
1173 :meth:`.hybrid_property.comparator`
1174 to be used without conflicting with the same-named attributes
1175 normally present on the :class:`.QueryableAttribute`::
1176
1177 class SuperClass:
1178 # ...
1179
1180 @hybrid_property
1181 def foobar(self):
1182 return self._foobar
1183
1184
1185 class SubClass(SuperClass):
1186 # ...
1187
1188 @SuperClass.foobar.overrides.expression
1189 def foobar(cls):
1190 return func.subfoobar(self._foobar)
1191
1192 .. versionadded:: 1.2
1193
1194 .. seealso::
1195
1196 :ref:`hybrid_reuse_subclass`
1197
1198 """
1199 return self
1200
1201 class _InPlace(Generic[_TE]):
1202 """A builder helper for .hybrid_property.
1203
1204 .. versionadded:: 2.0.4
1205
1206 """
1207
1208 __slots__ = ("attr",)
1209
1210 def __init__(self, attr: hybrid_property[_TE]):
1211 self.attr = attr
1212
1213 def _set(self, **kw: Any) -> hybrid_property[_TE]:
1214 for k, v in kw.items():
1215 setattr(self.attr, k, _unwrap_classmethod(v))
1216 return self.attr
1217
1218 def getter(self, fget: _HybridGetterType[_TE]) -> hybrid_property[_TE]:
1219 return self._set(fget=fget)
1220
1221 def setter(self, fset: _HybridSetterType[_TE]) -> hybrid_property[_TE]:
1222 return self._set(fset=fset)
1223
1224 def deleter(
1225 self, fdel: _HybridDeleterType[_TE]
1226 ) -> hybrid_property[_TE]:
1227 return self._set(fdel=fdel)
1228
1229 def expression(
1230 self, expr: _HybridExprCallableType[_TE]
1231 ) -> hybrid_property[_TE]:
1232 return self._set(expr=expr)
1233
1234 def comparator(
1235 self, comparator: _HybridComparatorCallableType[_TE]
1236 ) -> hybrid_property[_TE]:
1237 return self._set(custom_comparator=comparator)
1238
1239 def update_expression(
1240 self, meth: _HybridUpdaterType[_TE]
1241 ) -> hybrid_property[_TE]:
1242 return self._set(update_expr=meth)
1243
1244 @property
1245 def inplace(self) -> _InPlace[_T]:
1246 """Return the inplace mutator for this :class:`.hybrid_property`.
1247
1248 This is to allow in-place mutation of the hybrid, allowing the first
1249 hybrid method of a certain name to be re-used in order to add
1250 more methods without having to name those methods the same, e.g.::
1251
1252 class Interval(Base):
1253 # ...
1254
1255 @hybrid_property
1256 def radius(self) -> float:
1257 return abs(self.length) / 2
1258
1259 @radius.inplace.setter
1260 def _radius_setter(self, value: float) -> None:
1261 self.length = value * 2
1262
1263 @radius.inplace.expression
1264 def _radius_expression(cls) -> ColumnElement[float]:
1265 return type_coerce(func.abs(cls.length) / 2, Float)
1266
1267 .. versionadded:: 2.0.4
1268
1269 .. seealso::
1270
1271 :ref:`hybrid_pep484_naming`
1272
1273 """
1274 return hybrid_property._InPlace(self)
1275
1276 def getter(self, fget: _HybridGetterType[_T]) -> hybrid_property[_T]:
1277 """Provide a modifying decorator that defines a getter method.
1278
1279 .. versionadded:: 1.2
1280
1281 """
1282
1283 return self._copy(fget=fget)
1284
1285 def setter(self, fset: _HybridSetterType[_T]) -> hybrid_property[_T]:
1286 """Provide a modifying decorator that defines a setter method."""
1287
1288 return self._copy(fset=fset)
1289
1290 def deleter(self, fdel: _HybridDeleterType[_T]) -> hybrid_property[_T]:
1291 """Provide a modifying decorator that defines a deletion method."""
1292
1293 return self._copy(fdel=fdel)
1294
1295 def expression(
1296 self, expr: _HybridExprCallableType[_T]
1297 ) -> hybrid_property[_T]:
1298 """Provide a modifying decorator that defines a SQL-expression
1299 producing method.
1300
1301 When a hybrid is invoked at the class level, the SQL expression given
1302 here is wrapped inside of a specialized :class:`.QueryableAttribute`,
1303 which is the same kind of object used by the ORM to represent other
1304 mapped attributes. The reason for this is so that other class-level
1305 attributes such as docstrings and a reference to the hybrid itself may
1306 be maintained within the structure that's returned, without any
1307 modifications to the original SQL expression passed in.
1308
1309 .. note::
1310
1311 When referring to a hybrid property from an owning class (e.g.
1312 ``SomeClass.some_hybrid``), an instance of
1313 :class:`.QueryableAttribute` is returned, representing the
1314 expression or comparator object as well as this hybrid object.
1315 However, that object itself has accessors called ``expression`` and
1316 ``comparator``; so when attempting to override these decorators on a
1317 subclass, it may be necessary to qualify it using the
1318 :attr:`.hybrid_property.overrides` modifier first. See that
1319 modifier for details.
1320
1321 .. seealso::
1322
1323 :ref:`hybrid_distinct_expression`
1324
1325 """
1326
1327 return self._copy(expr=expr)
1328
1329 def comparator(
1330 self, comparator: _HybridComparatorCallableType[_T]
1331 ) -> hybrid_property[_T]:
1332 """Provide a modifying decorator that defines a custom
1333 comparator producing method.
1334
1335 The return value of the decorated method should be an instance of
1336 :class:`~.hybrid.Comparator`.
1337
1338 .. note:: The :meth:`.hybrid_property.comparator` decorator
1339 **replaces** the use of the :meth:`.hybrid_property.expression`
1340 decorator. They cannot be used together.
1341
1342 When a hybrid is invoked at the class level, the
1343 :class:`~.hybrid.Comparator` object given here is wrapped inside of a
1344 specialized :class:`.QueryableAttribute`, which is the same kind of
1345 object used by the ORM to represent other mapped attributes. The
1346 reason for this is so that other class-level attributes such as
1347 docstrings and a reference to the hybrid itself may be maintained
1348 within the structure that's returned, without any modifications to the
1349 original comparator object passed in.
1350
1351 .. note::
1352
1353 When referring to a hybrid property from an owning class (e.g.
1354 ``SomeClass.some_hybrid``), an instance of
1355 :class:`.QueryableAttribute` is returned, representing the
1356 expression or comparator object as this hybrid object. However,
1357 that object itself has accessors called ``expression`` and
1358 ``comparator``; so when attempting to override these decorators on a
1359 subclass, it may be necessary to qualify it using the
1360 :attr:`.hybrid_property.overrides` modifier first. See that
1361 modifier for details.
1362
1363 """
1364 return self._copy(custom_comparator=comparator)
1365
1366 def update_expression(
1367 self, meth: _HybridUpdaterType[_T]
1368 ) -> hybrid_property[_T]:
1369 """Provide a modifying decorator that defines an UPDATE tuple
1370 producing method.
1371
1372 The method accepts a single value, which is the value to be
1373 rendered into the SET clause of an UPDATE statement. The method
1374 should then process this value into individual column expressions
1375 that fit into the ultimate SET clause, and return them as a
1376 sequence of 2-tuples. Each tuple
1377 contains a column expression as the key and a value to be rendered.
1378
1379 E.g.::
1380
1381 class Person(Base):
1382 # ...
1383
1384 first_name = Column(String)
1385 last_name = Column(String)
1386
1387 @hybrid_property
1388 def fullname(self):
1389 return first_name + " " + last_name
1390
1391 @fullname.update_expression
1392 def fullname(cls, value):
1393 fname, lname = value.split(" ", 1)
1394 return [(cls.first_name, fname), (cls.last_name, lname)]
1395
1396 .. versionadded:: 1.2
1397
1398 """
1399 return self._copy(update_expr=meth)
1400
1401 @util.memoized_property
1402 def _expr_comparator(
1403 self,
1404 ) -> Callable[[Any], _HybridClassLevelAccessor[_T]]:
1405 if self.custom_comparator is not None:
1406 return self._get_comparator(self.custom_comparator)
1407 elif self.expr is not None:
1408 return self._get_expr(self.expr)
1409 else:
1410 return self._get_expr(cast(_HybridExprCallableType[_T], self.fget))
1411
1412 def _get_expr(
1413 self, expr: _HybridExprCallableType[_T]
1414 ) -> Callable[[Any], _HybridClassLevelAccessor[_T]]:
1415 def _expr(cls: Any) -> ExprComparator[_T]:
1416 return ExprComparator(cls, expr(cls), self)
1417
1418 util.update_wrapper(_expr, expr)
1419
1420 return self._get_comparator(_expr)
1421
1422 def _get_comparator(
1423 self, comparator: Any
1424 ) -> Callable[[Any], _HybridClassLevelAccessor[_T]]:
1425 proxy_attr = attributes.create_proxied_attribute(self)
1426
1427 def expr_comparator(
1428 owner: Type[object],
1429 ) -> _HybridClassLevelAccessor[_T]:
1430 # because this is the descriptor protocol, we don't really know
1431 # what our attribute name is. so search for it through the
1432 # MRO.
1433 for lookup in owner.__mro__:
1434 if self.__name__ in lookup.__dict__:
1435 if lookup.__dict__[self.__name__] is self:
1436 name = self.__name__
1437 break
1438 else:
1439 name = attributes._UNKNOWN_ATTR_KEY # type: ignore[assignment]
1440
1441 return cast(
1442 "_HybridClassLevelAccessor[_T]",
1443 proxy_attr(
1444 owner,
1445 name,
1446 self,
1447 comparator(owner),
1448 doc=comparator.__doc__ or self.__doc__,
1449 ),
1450 )
1451
1452 return expr_comparator
1453
1454
1455class Comparator(interfaces.PropComparator[_T]):
1456 """A helper class that allows easy construction of custom
1457 :class:`~.orm.interfaces.PropComparator`
1458 classes for usage with hybrids."""
1459
1460 def __init__(
1461 self, expression: Union[_HasClauseElement[_T], SQLColumnExpression[_T]]
1462 ):
1463 self.expression = expression
1464
1465 def __clause_element__(self) -> roles.ColumnsClauseRole:
1466 expr = self.expression
1467 if is_has_clause_element(expr):
1468 ret_expr = expr.__clause_element__()
1469 else:
1470 if TYPE_CHECKING:
1471 assert isinstance(expr, ColumnElement)
1472 ret_expr = expr
1473
1474 if TYPE_CHECKING:
1475 # see test_hybrid->test_expression_isnt_clause_element
1476 # that exercises the usual place this is caught if not
1477 # true
1478 assert isinstance(ret_expr, ColumnElement)
1479 return ret_expr
1480
1481 @util.non_memoized_property
1482 def property(self) -> interfaces.MapperProperty[_T]:
1483 raise NotImplementedError()
1484
1485 def adapt_to_entity(
1486 self, adapt_to_entity: AliasedInsp[Any]
1487 ) -> Comparator[_T]:
1488 # interesting....
1489 return self
1490
1491
1492class ExprComparator(Comparator[_T]):
1493 def __init__(
1494 self,
1495 cls: Type[Any],
1496 expression: Union[_HasClauseElement[_T], SQLColumnExpression[_T]],
1497 hybrid: hybrid_property[_T],
1498 ):
1499 self.cls = cls
1500 self.expression = expression
1501 self.hybrid = hybrid
1502
1503 def __getattr__(self, key: str) -> Any:
1504 return getattr(self.expression, key)
1505
1506 @util.ro_non_memoized_property
1507 def info(self) -> _InfoType:
1508 return self.hybrid.info
1509
1510 def _bulk_update_tuples(
1511 self, value: Any
1512 ) -> Sequence[Tuple[_DMLColumnArgument, Any]]:
1513 if isinstance(self.expression, attributes.QueryableAttribute):
1514 return self.expression._bulk_update_tuples(value)
1515 elif self.hybrid.update_expr is not None:
1516 return self.hybrid.update_expr(self.cls, value)
1517 else:
1518 return [(self.expression, value)]
1519
1520 @util.non_memoized_property
1521 def property(self) -> MapperProperty[_T]:
1522 # this accessor is not normally used, however is accessed by things
1523 # like ORM synonyms if the hybrid is used in this context; the
1524 # .property attribute is not necessarily accessible
1525 return self.expression.property # type: ignore
1526
1527 def operate(
1528 self, op: OperatorType, *other: Any, **kwargs: Any
1529 ) -> ColumnElement[Any]:
1530 return op(self.expression, *other, **kwargs)
1531
1532 def reverse_operate(
1533 self, op: OperatorType, other: Any, **kwargs: Any
1534 ) -> ColumnElement[Any]:
1535 return op(other, self.expression, **kwargs) # type: ignore