| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533 |
- # ext/hybrid.py
- # Copyright (C) 2005-2025 the SQLAlchemy authors and contributors
- # <see AUTHORS file>
- #
- # This module is part of SQLAlchemy and is released under
- # the MIT License: https://www.opensource.org/licenses/mit-license.php
- r"""Define attributes on ORM-mapped classes that have "hybrid" behavior.
- "hybrid" means the attribute has distinct behaviors defined at the
- class level and at the instance level.
- The :mod:`~sqlalchemy.ext.hybrid` extension provides a special form of
- method decorator and has minimal dependencies on the rest of SQLAlchemy.
- Its basic theory of operation can work with any descriptor-based expression
- system.
- Consider a mapping ``Interval``, representing integer ``start`` and ``end``
- values. We can define higher level functions on mapped classes that produce SQL
- expressions at the class level, and Python expression evaluation at the
- instance level. Below, each function decorated with :class:`.hybrid_method` or
- :class:`.hybrid_property` may receive ``self`` as an instance of the class, or
- may receive the class directly, depending on context::
- from __future__ import annotations
- from sqlalchemy.ext.hybrid import hybrid_method
- from sqlalchemy.ext.hybrid import hybrid_property
- from sqlalchemy.orm import DeclarativeBase
- from sqlalchemy.orm import Mapped
- from sqlalchemy.orm import mapped_column
- class Base(DeclarativeBase):
- pass
- class Interval(Base):
- __tablename__ = "interval"
- id: Mapped[int] = mapped_column(primary_key=True)
- start: Mapped[int]
- end: Mapped[int]
- def __init__(self, start: int, end: int):
- self.start = start
- self.end = end
- @hybrid_property
- def length(self) -> int:
- return self.end - self.start
- @hybrid_method
- def contains(self, point: int) -> bool:
- return (self.start <= point) & (point <= self.end)
- @hybrid_method
- def intersects(self, other: Interval) -> bool:
- return self.contains(other.start) | self.contains(other.end)
- Above, the ``length`` property returns the difference between the
- ``end`` and ``start`` attributes. With an instance of ``Interval``,
- this subtraction occurs in Python, using normal Python descriptor
- mechanics::
- >>> i1 = Interval(5, 10)
- >>> i1.length
- 5
- When dealing with the ``Interval`` class itself, the :class:`.hybrid_property`
- descriptor evaluates the function body given the ``Interval`` class as
- the argument, which when evaluated with SQLAlchemy expression mechanics
- returns a new SQL expression:
- .. sourcecode:: pycon+sql
- >>> from sqlalchemy import select
- >>> print(select(Interval.length))
- {printsql}SELECT interval."end" - interval.start AS length
- FROM interval{stop}
- >>> print(select(Interval).filter(Interval.length > 10))
- {printsql}SELECT interval.id, interval.start, interval."end"
- FROM interval
- WHERE interval."end" - interval.start > :param_1
- Filtering methods such as :meth:`.Select.filter_by` are supported
- with hybrid attributes as well:
- .. sourcecode:: pycon+sql
- >>> print(select(Interval).filter_by(length=5))
- {printsql}SELECT interval.id, interval.start, interval."end"
- FROM interval
- WHERE interval."end" - interval.start = :param_1
- The ``Interval`` class example also illustrates two methods,
- ``contains()`` and ``intersects()``, decorated with
- :class:`.hybrid_method`. This decorator applies the same idea to
- methods that :class:`.hybrid_property` applies to attributes. The
- methods return boolean values, and take advantage of the Python ``|``
- and ``&`` bitwise operators to produce equivalent instance-level and
- SQL expression-level boolean behavior:
- .. sourcecode:: pycon+sql
- >>> i1.contains(6)
- True
- >>> i1.contains(15)
- False
- >>> i1.intersects(Interval(7, 18))
- True
- >>> i1.intersects(Interval(25, 29))
- False
- >>> print(select(Interval).filter(Interval.contains(15)))
- {printsql}SELECT interval.id, interval.start, interval."end"
- FROM interval
- WHERE interval.start <= :start_1 AND interval."end" > :end_1{stop}
- >>> ia = aliased(Interval)
- >>> print(select(Interval, ia).filter(Interval.intersects(ia)))
- {printsql}SELECT interval.id, interval.start,
- interval."end", interval_1.id AS interval_1_id,
- interval_1.start AS interval_1_start, interval_1."end" AS interval_1_end
- FROM interval, interval AS interval_1
- WHERE interval.start <= interval_1.start
- AND interval."end" > interval_1.start
- OR interval.start <= interval_1."end"
- AND interval."end" > interval_1."end"{stop}
- .. _hybrid_distinct_expression:
- Defining Expression Behavior Distinct from Attribute Behavior
- --------------------------------------------------------------
- In the previous section, our usage of the ``&`` and ``|`` bitwise operators
- within the ``Interval.contains`` and ``Interval.intersects`` methods was
- fortunate, considering our functions operated on two boolean values to return a
- new one. In many cases, the construction of an in-Python function and a
- SQLAlchemy SQL expression have enough differences that two separate Python
- expressions should be defined. The :mod:`~sqlalchemy.ext.hybrid` decorator
- defines a **modifier** :meth:`.hybrid_property.expression` for this purpose. As an
- example we'll define the radius of the interval, which requires the usage of
- the absolute value function::
- from sqlalchemy import ColumnElement
- from sqlalchemy import Float
- from sqlalchemy import func
- from sqlalchemy import type_coerce
- class Interval(Base):
- # ...
- @hybrid_property
- def radius(self) -> float:
- return abs(self.length) / 2
- @radius.inplace.expression
- @classmethod
- def _radius_expression(cls) -> ColumnElement[float]:
- return type_coerce(func.abs(cls.length) / 2, Float)
- In the above example, the :class:`.hybrid_property` first assigned to the
- name ``Interval.radius`` is amended by a subsequent method called
- ``Interval._radius_expression``, using the decorator
- ``@radius.inplace.expression``, which chains together two modifiers
- :attr:`.hybrid_property.inplace` and :attr:`.hybrid_property.expression`.
- The use of :attr:`.hybrid_property.inplace` indicates that the
- :meth:`.hybrid_property.expression` modifier should mutate the
- existing hybrid object at ``Interval.radius`` in place, without creating a
- new object. Notes on this modifier and its
- rationale are discussed in the next section :ref:`hybrid_pep484_naming`.
- The use of ``@classmethod`` is optional, and is strictly to give typing
- tools a hint that ``cls`` in this case is expected to be the ``Interval``
- class, and not an instance of ``Interval``.
- .. note:: :attr:`.hybrid_property.inplace` as well as the use of ``@classmethod``
- for proper typing support are available as of SQLAlchemy 2.0.4, and will
- not work in earlier versions.
- With ``Interval.radius`` now including an expression element, the SQL
- function ``ABS()`` is returned when accessing ``Interval.radius``
- at the class level:
- .. sourcecode:: pycon+sql
- >>> from sqlalchemy import select
- >>> print(select(Interval).filter(Interval.radius > 5))
- {printsql}SELECT interval.id, interval.start, interval."end"
- FROM interval
- WHERE abs(interval."end" - interval.start) / :abs_1 > :param_1
- .. _hybrid_pep484_naming:
- Using ``inplace`` to create pep-484 compliant hybrid properties
- ---------------------------------------------------------------
- In the previous section, a :class:`.hybrid_property` decorator is illustrated
- which includes two separate method-level functions being decorated, both
- to produce a single object attribute referenced as ``Interval.radius``.
- There are actually several different modifiers we can use for
- :class:`.hybrid_property` including :meth:`.hybrid_property.expression`,
- :meth:`.hybrid_property.setter` and :meth:`.hybrid_property.update_expression`.
- SQLAlchemy's :class:`.hybrid_property` decorator intends that adding on these
- methods may be done in the identical manner as Python's built-in
- ``@property`` decorator, where idiomatic use is to continue to redefine the
- attribute repeatedly, using the **same attribute name** each time, as in the
- example below that illustrates the use of :meth:`.hybrid_property.setter` and
- :meth:`.hybrid_property.expression` for the ``Interval.radius`` descriptor::
- # correct use, however is not accepted by pep-484 tooling
- class Interval(Base):
- # ...
- @hybrid_property
- def radius(self):
- return abs(self.length) / 2
- @radius.setter
- def radius(self, value):
- self.length = value * 2
- @radius.expression
- def radius(cls):
- return type_coerce(func.abs(cls.length) / 2, Float)
- Above, there are three ``Interval.radius`` methods, but as each are decorated,
- first by the :class:`.hybrid_property` decorator and then by the
- ``@radius`` name itself, the end effect is that ``Interval.radius`` is
- a single attribute with three different functions contained within it.
- This style of use is taken from `Python's documented use of @property
- <https://docs.python.org/3/library/functions.html#property>`_.
- It is important to note that the way both ``@property`` as well as
- :class:`.hybrid_property` work, a **copy of the descriptor is made each time**.
- That is, each call to ``@radius.expression``, ``@radius.setter`` etc.
- make a new object entirely. This allows the attribute to be re-defined in
- subclasses without issue (see :ref:`hybrid_reuse_subclass` later in this
- section for how this is used).
- However, the above approach is not compatible with typing tools such as
- mypy and pyright. Python's own ``@property`` decorator does not have this
- limitation only because
- `these tools hardcode the behavior of @property
- <https://github.com/python/typing/discussions/1102>`_, meaning this syntax
- is not available to SQLAlchemy under :pep:`484` compliance.
- In order to produce a reasonable syntax while remaining typing compliant,
- the :attr:`.hybrid_property.inplace` decorator allows the same
- decorator to be re-used with different method names, while still producing
- a single decorator under one name::
- # correct use which is also accepted by pep-484 tooling
- class Interval(Base):
- # ...
- @hybrid_property
- def radius(self) -> float:
- return abs(self.length) / 2
- @radius.inplace.setter
- def _radius_setter(self, value: float) -> None:
- # for example only
- self.length = value * 2
- @radius.inplace.expression
- @classmethod
- def _radius_expression(cls) -> ColumnElement[float]:
- return type_coerce(func.abs(cls.length) / 2, Float)
- Using :attr:`.hybrid_property.inplace` further qualifies the use of the
- decorator that a new copy should not be made, thereby maintaining the
- ``Interval.radius`` name while allowing additional methods
- ``Interval._radius_setter`` and ``Interval._radius_expression`` to be
- differently named.
- .. versionadded:: 2.0.4 Added :attr:`.hybrid_property.inplace` to allow
- less verbose construction of composite :class:`.hybrid_property` objects
- while not having to use repeated method names. Additionally allowed the
- use of ``@classmethod`` within :attr:`.hybrid_property.expression`,
- :attr:`.hybrid_property.update_expression`, and
- :attr:`.hybrid_property.comparator` to allow typing tools to identify
- ``cls`` as a class and not an instance in the method signature.
- Defining Setters
- ----------------
- The :meth:`.hybrid_property.setter` modifier allows the construction of a
- custom setter method, that can modify values on the object::
- class Interval(Base):
- # ...
- @hybrid_property
- def length(self) -> int:
- return self.end - self.start
- @length.inplace.setter
- def _length_setter(self, value: int) -> None:
- self.end = self.start + value
- The ``length(self, value)`` method is now called upon set::
- >>> i1 = Interval(5, 10)
- >>> i1.length
- 5
- >>> i1.length = 12
- >>> i1.end
- 17
- .. _hybrid_bulk_update:
- Allowing Bulk ORM Update
- ------------------------
- A hybrid can define a custom "UPDATE" handler for when using
- ORM-enabled updates, allowing the hybrid to be used in the
- SET clause of the update.
- Normally, when using a hybrid with :func:`_sql.update`, the SQL
- expression is used as the column that's the target of the SET. If our
- ``Interval`` class had a hybrid ``start_point`` that linked to
- ``Interval.start``, this could be substituted directly::
- from sqlalchemy import update
- stmt = update(Interval).values({Interval.start_point: 10})
- However, when using a composite hybrid like ``Interval.length``, this
- hybrid represents more than one column. We can set up a handler that will
- accommodate a value passed in the VALUES expression which can affect
- this, using the :meth:`.hybrid_property.update_expression` decorator.
- A handler that works similarly to our setter would be::
- from typing import List, Tuple, Any
- class Interval(Base):
- # ...
- @hybrid_property
- def length(self) -> int:
- return self.end - self.start
- @length.inplace.setter
- def _length_setter(self, value: int) -> None:
- self.end = self.start + value
- @length.inplace.update_expression
- def _length_update_expression(
- cls, value: Any
- ) -> List[Tuple[Any, Any]]:
- return [(cls.end, cls.start + value)]
- Above, if we use ``Interval.length`` in an UPDATE expression, we get
- a hybrid SET expression:
- .. sourcecode:: pycon+sql
- >>> from sqlalchemy import update
- >>> print(update(Interval).values({Interval.length: 25}))
- {printsql}UPDATE interval SET "end"=(interval.start + :start_1)
- This SET expression is accommodated by the ORM automatically.
- .. seealso::
- :ref:`orm_expression_update_delete` - includes background on ORM-enabled
- UPDATE statements
- Working with Relationships
- --------------------------
- There's no essential difference when creating hybrids that work with
- related objects as opposed to column-based data. The need for distinct
- expressions tends to be greater. The two variants we'll illustrate
- are the "join-dependent" hybrid, and the "correlated subquery" hybrid.
- Join-Dependent Relationship Hybrid
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Consider the following declarative
- mapping which relates a ``User`` to a ``SavingsAccount``::
- from __future__ import annotations
- from decimal import Decimal
- from typing import cast
- from typing import List
- from typing import Optional
- from sqlalchemy import ForeignKey
- from sqlalchemy import Numeric
- from sqlalchemy import String
- from sqlalchemy import SQLColumnExpression
- from sqlalchemy.ext.hybrid import hybrid_property
- from sqlalchemy.orm import DeclarativeBase
- from sqlalchemy.orm import Mapped
- from sqlalchemy.orm import mapped_column
- from sqlalchemy.orm import relationship
- class Base(DeclarativeBase):
- pass
- class SavingsAccount(Base):
- __tablename__ = "account"
- id: Mapped[int] = mapped_column(primary_key=True)
- user_id: Mapped[int] = mapped_column(ForeignKey("user.id"))
- balance: Mapped[Decimal] = mapped_column(Numeric(15, 5))
- owner: Mapped[User] = relationship(back_populates="accounts")
- class User(Base):
- __tablename__ = "user"
- id: Mapped[int] = mapped_column(primary_key=True)
- name: Mapped[str] = mapped_column(String(100))
- accounts: Mapped[List[SavingsAccount]] = relationship(
- back_populates="owner", lazy="selectin"
- )
- @hybrid_property
- def balance(self) -> Optional[Decimal]:
- if self.accounts:
- return self.accounts[0].balance
- else:
- return None
- @balance.inplace.setter
- def _balance_setter(self, value: Optional[Decimal]) -> None:
- assert value is not None
- if not self.accounts:
- account = SavingsAccount(owner=self)
- else:
- account = self.accounts[0]
- account.balance = value
- @balance.inplace.expression
- @classmethod
- def _balance_expression(cls) -> SQLColumnExpression[Optional[Decimal]]:
- return cast(
- "SQLColumnExpression[Optional[Decimal]]",
- SavingsAccount.balance,
- )
- The above hybrid property ``balance`` works with the first
- ``SavingsAccount`` entry in the list of accounts for this user. The
- in-Python getter/setter methods can treat ``accounts`` as a Python
- list available on ``self``.
- .. tip:: The ``User.balance`` getter in the above example accesses the
- ``self.acccounts`` collection, which will normally be loaded via the
- :func:`.selectinload` loader strategy configured on the ``User.balance``
- :func:`_orm.relationship`. The default loader strategy when not otherwise
- stated on :func:`_orm.relationship` is :func:`.lazyload`, which emits SQL on
- demand. When using asyncio, on-demand loaders such as :func:`.lazyload` are
- not supported, so care should be taken to ensure the ``self.accounts``
- collection is accessible to this hybrid accessor when using asyncio.
- At the expression level, it's expected that the ``User`` class will
- be used in an appropriate context such that an appropriate join to
- ``SavingsAccount`` will be present:
- .. sourcecode:: pycon+sql
- >>> from sqlalchemy import select
- >>> print(
- ... select(User, User.balance)
- ... .join(User.accounts)
- ... .filter(User.balance > 5000)
- ... )
- {printsql}SELECT "user".id AS user_id, "user".name AS user_name,
- account.balance AS account_balance
- FROM "user" JOIN account ON "user".id = account.user_id
- WHERE account.balance > :balance_1
- Note however, that while the instance level accessors need to worry
- about whether ``self.accounts`` is even present, this issue expresses
- itself differently at the SQL expression level, where we basically
- would use an outer join:
- .. sourcecode:: pycon+sql
- >>> from sqlalchemy import select
- >>> from sqlalchemy import or_
- >>> print(
- ... select(User, User.balance)
- ... .outerjoin(User.accounts)
- ... .filter(or_(User.balance < 5000, User.balance == None))
- ... )
- {printsql}SELECT "user".id AS user_id, "user".name AS user_name,
- account.balance AS account_balance
- FROM "user" LEFT OUTER JOIN account ON "user".id = account.user_id
- WHERE account.balance < :balance_1 OR account.balance IS NULL
- Correlated Subquery Relationship Hybrid
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- We can, of course, forego being dependent on the enclosing query's usage
- of joins in favor of the correlated subquery, which can portably be packed
- into a single column expression. A correlated subquery is more portable, but
- often performs more poorly at the SQL level. Using the same technique
- illustrated at :ref:`mapper_column_property_sql_expressions`,
- we can adjust our ``SavingsAccount`` example to aggregate the balances for
- *all* accounts, and use a correlated subquery for the column expression::
- from __future__ import annotations
- from decimal import Decimal
- from typing import List
- from sqlalchemy import ForeignKey
- from sqlalchemy import func
- from sqlalchemy import Numeric
- from sqlalchemy import select
- from sqlalchemy import SQLColumnExpression
- from sqlalchemy import String
- from sqlalchemy.ext.hybrid import hybrid_property
- from sqlalchemy.orm import DeclarativeBase
- from sqlalchemy.orm import Mapped
- from sqlalchemy.orm import mapped_column
- from sqlalchemy.orm import relationship
- class Base(DeclarativeBase):
- pass
- class SavingsAccount(Base):
- __tablename__ = "account"
- id: Mapped[int] = mapped_column(primary_key=True)
- user_id: Mapped[int] = mapped_column(ForeignKey("user.id"))
- balance: Mapped[Decimal] = mapped_column(Numeric(15, 5))
- owner: Mapped[User] = relationship(back_populates="accounts")
- class User(Base):
- __tablename__ = "user"
- id: Mapped[int] = mapped_column(primary_key=True)
- name: Mapped[str] = mapped_column(String(100))
- accounts: Mapped[List[SavingsAccount]] = relationship(
- back_populates="owner", lazy="selectin"
- )
- @hybrid_property
- def balance(self) -> Decimal:
- return sum(
- (acc.balance for acc in self.accounts), start=Decimal("0")
- )
- @balance.inplace.expression
- @classmethod
- def _balance_expression(cls) -> SQLColumnExpression[Decimal]:
- return (
- select(func.sum(SavingsAccount.balance))
- .where(SavingsAccount.user_id == cls.id)
- .label("total_balance")
- )
- The above recipe will give us the ``balance`` column which renders
- a correlated SELECT:
- .. sourcecode:: pycon+sql
- >>> from sqlalchemy import select
- >>> print(select(User).filter(User.balance > 400))
- {printsql}SELECT "user".id, "user".name
- FROM "user"
- WHERE (
- SELECT sum(account.balance) AS sum_1 FROM account
- WHERE account.user_id = "user".id
- ) > :param_1
- .. _hybrid_custom_comparators:
- Building Custom Comparators
- ---------------------------
- The hybrid property also includes a helper that allows construction of
- custom comparators. A comparator object allows one to customize the
- behavior of each SQLAlchemy expression operator individually. They
- are useful when creating custom types that have some highly
- idiosyncratic behavior on the SQL side.
- .. note:: The :meth:`.hybrid_property.comparator` decorator introduced
- in this section **replaces** the use of the
- :meth:`.hybrid_property.expression` decorator.
- They cannot be used together.
- The example class below allows case-insensitive comparisons on the attribute
- named ``word_insensitive``::
- from __future__ import annotations
- from typing import Any
- from sqlalchemy import ColumnElement
- from sqlalchemy import func
- from sqlalchemy.ext.hybrid import Comparator
- from sqlalchemy.ext.hybrid import hybrid_property
- from sqlalchemy.orm import DeclarativeBase
- from sqlalchemy.orm import Mapped
- from sqlalchemy.orm import mapped_column
- class Base(DeclarativeBase):
- pass
- class CaseInsensitiveComparator(Comparator[str]):
- def __eq__(self, other: Any) -> ColumnElement[bool]: # type: ignore[override] # noqa: E501
- return func.lower(self.__clause_element__()) == func.lower(other)
- class SearchWord(Base):
- __tablename__ = "searchword"
- id: Mapped[int] = mapped_column(primary_key=True)
- word: Mapped[str]
- @hybrid_property
- def word_insensitive(self) -> str:
- return self.word.lower()
- @word_insensitive.inplace.comparator
- @classmethod
- def _word_insensitive_comparator(cls) -> CaseInsensitiveComparator:
- return CaseInsensitiveComparator(cls.word)
- Above, SQL expressions against ``word_insensitive`` will apply the ``LOWER()``
- SQL function to both sides:
- .. sourcecode:: pycon+sql
- >>> from sqlalchemy import select
- >>> print(select(SearchWord).filter_by(word_insensitive="Trucks"))
- {printsql}SELECT searchword.id, searchword.word
- FROM searchword
- WHERE lower(searchword.word) = lower(:lower_1)
- The ``CaseInsensitiveComparator`` above implements part of the
- :class:`.ColumnOperators` interface. A "coercion" operation like
- lowercasing can be applied to all comparison operations (i.e. ``eq``,
- ``lt``, ``gt``, etc.) using :meth:`.Operators.operate`::
- class CaseInsensitiveComparator(Comparator):
- def operate(self, op, other, **kwargs):
- return op(
- func.lower(self.__clause_element__()),
- func.lower(other),
- **kwargs,
- )
- .. _hybrid_reuse_subclass:
- Reusing Hybrid Properties across Subclasses
- -------------------------------------------
- A hybrid can be referred to from a superclass, to allow modifying
- methods like :meth:`.hybrid_property.getter`, :meth:`.hybrid_property.setter`
- to be used to redefine those methods on a subclass. This is similar to
- how the standard Python ``@property`` object works::
- class FirstNameOnly(Base):
- # ...
- first_name: Mapped[str]
- @hybrid_property
- def name(self) -> str:
- return self.first_name
- @name.inplace.setter
- def _name_setter(self, value: str) -> None:
- self.first_name = value
- class FirstNameLastName(FirstNameOnly):
- # ...
- last_name: Mapped[str]
- # 'inplace' is not used here; calling getter creates a copy
- # of FirstNameOnly.name that is local to FirstNameLastName
- @FirstNameOnly.name.getter
- def name(self) -> str:
- return self.first_name + " " + self.last_name
- @name.inplace.setter
- def _name_setter(self, value: str) -> None:
- self.first_name, self.last_name = value.split(" ", 1)
- Above, the ``FirstNameLastName`` class refers to the hybrid from
- ``FirstNameOnly.name`` to repurpose its getter and setter for the subclass.
- When overriding :meth:`.hybrid_property.expression` and
- :meth:`.hybrid_property.comparator` alone as the first reference to the
- superclass, these names conflict with the same-named accessors on the class-
- level :class:`.QueryableAttribute` object returned at the class level. To
- override these methods when referring directly to the parent class descriptor,
- add the special qualifier :attr:`.hybrid_property.overrides`, which will de-
- reference the instrumented attribute back to the hybrid object::
- class FirstNameLastName(FirstNameOnly):
- # ...
- last_name: Mapped[str]
- @FirstNameOnly.name.overrides.expression
- @classmethod
- def name(cls):
- return func.concat(cls.first_name, " ", cls.last_name)
- Hybrid Value Objects
- --------------------
- Note in our previous example, if we were to compare the ``word_insensitive``
- attribute of a ``SearchWord`` instance to a plain Python string, the plain
- Python string would not be coerced to lower case - the
- ``CaseInsensitiveComparator`` we built, being returned by
- ``@word_insensitive.comparator``, only applies to the SQL side.
- A more comprehensive form of the custom comparator is to construct a *Hybrid
- Value Object*. This technique applies the target value or expression to a value
- object which is then returned by the accessor in all cases. The value object
- allows control of all operations upon the value as well as how compared values
- are treated, both on the SQL expression side as well as the Python value side.
- Replacing the previous ``CaseInsensitiveComparator`` class with a new
- ``CaseInsensitiveWord`` class::
- class CaseInsensitiveWord(Comparator):
- "Hybrid value representing a lower case representation of a word."
- def __init__(self, word):
- if isinstance(word, basestring):
- self.word = word.lower()
- elif isinstance(word, CaseInsensitiveWord):
- self.word = word.word
- else:
- self.word = func.lower(word)
- def operate(self, op, other, **kwargs):
- if not isinstance(other, CaseInsensitiveWord):
- other = CaseInsensitiveWord(other)
- return op(self.word, other.word, **kwargs)
- def __clause_element__(self):
- return self.word
- def __str__(self):
- return self.word
- key = "word"
- "Label to apply to Query tuple results"
- Above, the ``CaseInsensitiveWord`` object represents ``self.word``, which may
- be a SQL function, or may be a Python native. By overriding ``operate()`` and
- ``__clause_element__()`` to work in terms of ``self.word``, all comparison
- operations will work against the "converted" form of ``word``, whether it be
- SQL side or Python side. Our ``SearchWord`` class can now deliver the
- ``CaseInsensitiveWord`` object unconditionally from a single hybrid call::
- class SearchWord(Base):
- __tablename__ = "searchword"
- id: Mapped[int] = mapped_column(primary_key=True)
- word: Mapped[str]
- @hybrid_property
- def word_insensitive(self) -> CaseInsensitiveWord:
- return CaseInsensitiveWord(self.word)
- The ``word_insensitive`` attribute now has case-insensitive comparison behavior
- universally, including SQL expression vs. Python expression (note the Python
- value is converted to lower case on the Python side here):
- .. sourcecode:: pycon+sql
- >>> print(select(SearchWord).filter_by(word_insensitive="Trucks"))
- {printsql}SELECT searchword.id AS searchword_id, searchword.word AS searchword_word
- FROM searchword
- WHERE lower(searchword.word) = :lower_1
- SQL expression versus SQL expression:
- .. sourcecode:: pycon+sql
- >>> from sqlalchemy.orm import aliased
- >>> sw1 = aliased(SearchWord)
- >>> sw2 = aliased(SearchWord)
- >>> print(
- ... select(sw1.word_insensitive, sw2.word_insensitive).filter(
- ... sw1.word_insensitive > sw2.word_insensitive
- ... )
- ... )
- {printsql}SELECT lower(searchword_1.word) AS lower_1,
- lower(searchword_2.word) AS lower_2
- FROM searchword AS searchword_1, searchword AS searchword_2
- WHERE lower(searchword_1.word) > lower(searchword_2.word)
- Python only expression::
- >>> ws1 = SearchWord(word="SomeWord")
- >>> ws1.word_insensitive == "sOmEwOrD"
- True
- >>> ws1.word_insensitive == "XOmEwOrX"
- False
- >>> print(ws1.word_insensitive)
- someword
- The Hybrid Value pattern is very useful for any kind of value that may have
- multiple representations, such as timestamps, time deltas, units of
- measurement, currencies and encrypted passwords.
- .. seealso::
- `Hybrids and Value Agnostic Types
- <https://techspot.zzzeek.org/2011/10/21/hybrids-and-value-agnostic-types/>`_
- - on the techspot.zzzeek.org blog
- `Value Agnostic Types, Part II
- <https://techspot.zzzeek.org/2011/10/29/value-agnostic-types-part-ii/>`_ -
- on the techspot.zzzeek.org blog
- """ # noqa
- from __future__ import annotations
- from typing import Any
- from typing import Callable
- from typing import cast
- from typing import Generic
- from typing import List
- from typing import Optional
- from typing import overload
- from typing import Sequence
- from typing import Tuple
- from typing import Type
- from typing import TYPE_CHECKING
- from typing import TypeVar
- from typing import Union
- from .. import util
- from ..orm import attributes
- from ..orm import InspectionAttrExtensionType
- from ..orm import interfaces
- from ..orm import ORMDescriptor
- from ..orm.attributes import QueryableAttribute
- from ..sql import roles
- from ..sql._typing import is_has_clause_element
- from ..sql.elements import ColumnElement
- from ..sql.elements import SQLCoreOperations
- from ..util.typing import Concatenate
- from ..util.typing import Literal
- from ..util.typing import ParamSpec
- from ..util.typing import Protocol
- from ..util.typing import Self
- if TYPE_CHECKING:
- from ..orm.interfaces import MapperProperty
- from ..orm.util import AliasedInsp
- from ..sql import SQLColumnExpression
- from ..sql._typing import _ColumnExpressionArgument
- from ..sql._typing import _DMLColumnArgument
- from ..sql._typing import _HasClauseElement
- from ..sql._typing import _InfoType
- from ..sql.operators import OperatorType
- _P = ParamSpec("_P")
- _R = TypeVar("_R")
- _T = TypeVar("_T", bound=Any)
- _TE = TypeVar("_TE", bound=Any)
- _T_co = TypeVar("_T_co", bound=Any, covariant=True)
- _T_con = TypeVar("_T_con", bound=Any, contravariant=True)
- class HybridExtensionType(InspectionAttrExtensionType):
- HYBRID_METHOD = "HYBRID_METHOD"
- """Symbol indicating an :class:`InspectionAttr` that's
- of type :class:`.hybrid_method`.
- Is assigned to the :attr:`.InspectionAttr.extension_type`
- attribute.
- .. seealso::
- :attr:`_orm.Mapper.all_orm_attributes`
- """
- HYBRID_PROPERTY = "HYBRID_PROPERTY"
- """Symbol indicating an :class:`InspectionAttr` that's
- of type :class:`.hybrid_method`.
- Is assigned to the :attr:`.InspectionAttr.extension_type`
- attribute.
- .. seealso::
- :attr:`_orm.Mapper.all_orm_attributes`
- """
- class _HybridGetterType(Protocol[_T_co]):
- def __call__(s, self: Any) -> _T_co: ...
- class _HybridSetterType(Protocol[_T_con]):
- def __call__(s, self: Any, value: _T_con) -> None: ...
- class _HybridUpdaterType(Protocol[_T_con]):
- def __call__(
- s,
- cls: Any,
- value: Union[_T_con, _ColumnExpressionArgument[_T_con]],
- ) -> List[Tuple[_DMLColumnArgument, Any]]: ...
- class _HybridDeleterType(Protocol[_T_co]):
- def __call__(s, self: Any) -> None: ...
- class _HybridExprCallableType(Protocol[_T_co]):
- def __call__(
- s, cls: Any
- ) -> Union[_HasClauseElement[_T_co], SQLColumnExpression[_T_co]]: ...
- class _HybridComparatorCallableType(Protocol[_T]):
- def __call__(self, cls: Any) -> Comparator[_T]: ...
- class _HybridClassLevelAccessor(QueryableAttribute[_T]):
- """Describe the object returned by a hybrid_property() when
- called as a class-level descriptor.
- """
- if TYPE_CHECKING:
- def getter(
- self, fget: _HybridGetterType[_T]
- ) -> hybrid_property[_T]: ...
- def setter(
- self, fset: _HybridSetterType[_T]
- ) -> hybrid_property[_T]: ...
- def deleter(
- self, fdel: _HybridDeleterType[_T]
- ) -> hybrid_property[_T]: ...
- @property
- def overrides(self) -> hybrid_property[_T]: ...
- def update_expression(
- self, meth: _HybridUpdaterType[_T]
- ) -> hybrid_property[_T]: ...
- class hybrid_method(interfaces.InspectionAttrInfo, Generic[_P, _R]):
- """A decorator which allows definition of a Python object method with both
- instance-level and class-level behavior.
- """
- is_attribute = True
- extension_type = HybridExtensionType.HYBRID_METHOD
- def __init__(
- self,
- func: Callable[Concatenate[Any, _P], _R],
- expr: Optional[
- Callable[Concatenate[Any, _P], SQLCoreOperations[_R]]
- ] = None,
- ):
- """Create a new :class:`.hybrid_method`.
- Usage is typically via decorator::
- from sqlalchemy.ext.hybrid import hybrid_method
- class SomeClass:
- @hybrid_method
- def value(self, x, y):
- return self._value + x + y
- @value.expression
- @classmethod
- def value(cls, x, y):
- return func.some_function(cls._value, x, y)
- """
- self.func = func
- if expr is not None:
- self.expression(expr)
- else:
- self.expression(func) # type: ignore
- @property
- def inplace(self) -> Self:
- """Return the inplace mutator for this :class:`.hybrid_method`.
- The :class:`.hybrid_method` class already performs "in place" mutation
- when the :meth:`.hybrid_method.expression` decorator is called,
- so this attribute returns Self.
- .. versionadded:: 2.0.4
- .. seealso::
- :ref:`hybrid_pep484_naming`
- """
- return self
- @overload
- def __get__(
- self, instance: Literal[None], owner: Type[object]
- ) -> Callable[_P, SQLCoreOperations[_R]]: ...
- @overload
- def __get__(
- self, instance: object, owner: Type[object]
- ) -> Callable[_P, _R]: ...
- def __get__(
- self, instance: Optional[object], owner: Type[object]
- ) -> Union[Callable[_P, _R], Callable[_P, SQLCoreOperations[_R]]]:
- if instance is None:
- return self.expr.__get__(owner, owner) # type: ignore
- else:
- return self.func.__get__(instance, owner) # type: ignore
- def expression(
- self, expr: Callable[Concatenate[Any, _P], SQLCoreOperations[_R]]
- ) -> hybrid_method[_P, _R]:
- """Provide a modifying decorator that defines a
- SQL-expression producing method."""
- self.expr = expr
- if not self.expr.__doc__:
- self.expr.__doc__ = self.func.__doc__
- return self
- def _unwrap_classmethod(meth: _T) -> _T:
- if isinstance(meth, classmethod):
- return meth.__func__ # type: ignore
- else:
- return meth
- class hybrid_property(interfaces.InspectionAttrInfo, ORMDescriptor[_T]):
- """A decorator which allows definition of a Python descriptor with both
- instance-level and class-level behavior.
- """
- is_attribute = True
- extension_type = HybridExtensionType.HYBRID_PROPERTY
- __name__: str
- def __init__(
- self,
- fget: _HybridGetterType[_T],
- fset: Optional[_HybridSetterType[_T]] = None,
- fdel: Optional[_HybridDeleterType[_T]] = None,
- expr: Optional[_HybridExprCallableType[_T]] = None,
- custom_comparator: Optional[Comparator[_T]] = None,
- update_expr: Optional[_HybridUpdaterType[_T]] = None,
- ):
- """Create a new :class:`.hybrid_property`.
- Usage is typically via decorator::
- from sqlalchemy.ext.hybrid import hybrid_property
- class SomeClass:
- @hybrid_property
- def value(self):
- return self._value
- @value.setter
- def value(self, value):
- self._value = value
- """
- self.fget = fget
- self.fset = fset
- self.fdel = fdel
- self.expr = _unwrap_classmethod(expr)
- self.custom_comparator = _unwrap_classmethod(custom_comparator)
- self.update_expr = _unwrap_classmethod(update_expr)
- util.update_wrapper(self, fget) # type: ignore[arg-type]
- @overload
- def __get__(self, instance: Any, owner: Literal[None]) -> Self: ...
- @overload
- def __get__(
- self, instance: Literal[None], owner: Type[object]
- ) -> _HybridClassLevelAccessor[_T]: ...
- @overload
- def __get__(self, instance: object, owner: Type[object]) -> _T: ...
- def __get__(
- self, instance: Optional[object], owner: Optional[Type[object]]
- ) -> Union[hybrid_property[_T], _HybridClassLevelAccessor[_T], _T]:
- if owner is None:
- return self
- elif instance is None:
- return self._expr_comparator(owner)
- else:
- return self.fget(instance)
- def __set__(self, instance: object, value: Any) -> None:
- if self.fset is None:
- raise AttributeError("can't set attribute")
- self.fset(instance, value)
- def __delete__(self, instance: object) -> None:
- if self.fdel is None:
- raise AttributeError("can't delete attribute")
- self.fdel(instance)
- def _copy(self, **kw: Any) -> hybrid_property[_T]:
- defaults = {
- key: value
- for key, value in self.__dict__.items()
- if not key.startswith("_")
- }
- defaults.update(**kw)
- return type(self)(**defaults)
- @property
- def overrides(self) -> Self:
- """Prefix for a method that is overriding an existing attribute.
- The :attr:`.hybrid_property.overrides` accessor just returns
- this hybrid object, which when called at the class level from
- a parent class, will de-reference the "instrumented attribute"
- normally returned at this level, and allow modifying decorators
- like :meth:`.hybrid_property.expression` and
- :meth:`.hybrid_property.comparator`
- to be used without conflicting with the same-named attributes
- normally present on the :class:`.QueryableAttribute`::
- class SuperClass:
- # ...
- @hybrid_property
- def foobar(self):
- return self._foobar
- class SubClass(SuperClass):
- # ...
- @SuperClass.foobar.overrides.expression
- def foobar(cls):
- return func.subfoobar(self._foobar)
- .. versionadded:: 1.2
- .. seealso::
- :ref:`hybrid_reuse_subclass`
- """
- return self
- class _InPlace(Generic[_TE]):
- """A builder helper for .hybrid_property.
- .. versionadded:: 2.0.4
- """
- __slots__ = ("attr",)
- def __init__(self, attr: hybrid_property[_TE]):
- self.attr = attr
- def _set(self, **kw: Any) -> hybrid_property[_TE]:
- for k, v in kw.items():
- setattr(self.attr, k, _unwrap_classmethod(v))
- return self.attr
- def getter(self, fget: _HybridGetterType[_TE]) -> hybrid_property[_TE]:
- return self._set(fget=fget)
- def setter(self, fset: _HybridSetterType[_TE]) -> hybrid_property[_TE]:
- return self._set(fset=fset)
- def deleter(
- self, fdel: _HybridDeleterType[_TE]
- ) -> hybrid_property[_TE]:
- return self._set(fdel=fdel)
- def expression(
- self, expr: _HybridExprCallableType[_TE]
- ) -> hybrid_property[_TE]:
- return self._set(expr=expr)
- def comparator(
- self, comparator: _HybridComparatorCallableType[_TE]
- ) -> hybrid_property[_TE]:
- return self._set(custom_comparator=comparator)
- def update_expression(
- self, meth: _HybridUpdaterType[_TE]
- ) -> hybrid_property[_TE]:
- return self._set(update_expr=meth)
- @property
- def inplace(self) -> _InPlace[_T]:
- """Return the inplace mutator for this :class:`.hybrid_property`.
- This is to allow in-place mutation of the hybrid, allowing the first
- hybrid method of a certain name to be re-used in order to add
- more methods without having to name those methods the same, e.g.::
- class Interval(Base):
- # ...
- @hybrid_property
- def radius(self) -> float:
- return abs(self.length) / 2
- @radius.inplace.setter
- def _radius_setter(self, value: float) -> None:
- self.length = value * 2
- @radius.inplace.expression
- def _radius_expression(cls) -> ColumnElement[float]:
- return type_coerce(func.abs(cls.length) / 2, Float)
- .. versionadded:: 2.0.4
- .. seealso::
- :ref:`hybrid_pep484_naming`
- """
- return hybrid_property._InPlace(self)
- def getter(self, fget: _HybridGetterType[_T]) -> hybrid_property[_T]:
- """Provide a modifying decorator that defines a getter method.
- .. versionadded:: 1.2
- """
- return self._copy(fget=fget)
- def setter(self, fset: _HybridSetterType[_T]) -> hybrid_property[_T]:
- """Provide a modifying decorator that defines a setter method."""
- return self._copy(fset=fset)
- def deleter(self, fdel: _HybridDeleterType[_T]) -> hybrid_property[_T]:
- """Provide a modifying decorator that defines a deletion method."""
- return self._copy(fdel=fdel)
- def expression(
- self, expr: _HybridExprCallableType[_T]
- ) -> hybrid_property[_T]:
- """Provide a modifying decorator that defines a SQL-expression
- producing method.
- When a hybrid is invoked at the class level, the SQL expression given
- here is wrapped inside of a specialized :class:`.QueryableAttribute`,
- which is the same kind of object used by the ORM to represent other
- mapped attributes. The reason for this is so that other class-level
- attributes such as docstrings and a reference to the hybrid itself may
- be maintained within the structure that's returned, without any
- modifications to the original SQL expression passed in.
- .. note::
- When referring to a hybrid property from an owning class (e.g.
- ``SomeClass.some_hybrid``), an instance of
- :class:`.QueryableAttribute` is returned, representing the
- expression or comparator object as well as this hybrid object.
- However, that object itself has accessors called ``expression`` and
- ``comparator``; so when attempting to override these decorators on a
- subclass, it may be necessary to qualify it using the
- :attr:`.hybrid_property.overrides` modifier first. See that
- modifier for details.
- .. seealso::
- :ref:`hybrid_distinct_expression`
- """
- return self._copy(expr=expr)
- def comparator(
- self, comparator: _HybridComparatorCallableType[_T]
- ) -> hybrid_property[_T]:
- """Provide a modifying decorator that defines a custom
- comparator producing method.
- The return value of the decorated method should be an instance of
- :class:`~.hybrid.Comparator`.
- .. note:: The :meth:`.hybrid_property.comparator` decorator
- **replaces** the use of the :meth:`.hybrid_property.expression`
- decorator. They cannot be used together.
- When a hybrid is invoked at the class level, the
- :class:`~.hybrid.Comparator` object given here is wrapped inside of a
- specialized :class:`.QueryableAttribute`, which is the same kind of
- object used by the ORM to represent other mapped attributes. The
- reason for this is so that other class-level attributes such as
- docstrings and a reference to the hybrid itself may be maintained
- within the structure that's returned, without any modifications to the
- original comparator object passed in.
- .. note::
- When referring to a hybrid property from an owning class (e.g.
- ``SomeClass.some_hybrid``), an instance of
- :class:`.QueryableAttribute` is returned, representing the
- expression or comparator object as this hybrid object. However,
- that object itself has accessors called ``expression`` and
- ``comparator``; so when attempting to override these decorators on a
- subclass, it may be necessary to qualify it using the
- :attr:`.hybrid_property.overrides` modifier first. See that
- modifier for details.
- """
- return self._copy(custom_comparator=comparator)
- def update_expression(
- self, meth: _HybridUpdaterType[_T]
- ) -> hybrid_property[_T]:
- """Provide a modifying decorator that defines an UPDATE tuple
- producing method.
- The method accepts a single value, which is the value to be
- rendered into the SET clause of an UPDATE statement. The method
- should then process this value into individual column expressions
- that fit into the ultimate SET clause, and return them as a
- sequence of 2-tuples. Each tuple
- contains a column expression as the key and a value to be rendered.
- E.g.::
- class Person(Base):
- # ...
- first_name = Column(String)
- last_name = Column(String)
- @hybrid_property
- def fullname(self):
- return first_name + " " + last_name
- @fullname.update_expression
- def fullname(cls, value):
- fname, lname = value.split(" ", 1)
- return [(cls.first_name, fname), (cls.last_name, lname)]
- .. versionadded:: 1.2
- """
- return self._copy(update_expr=meth)
- @util.memoized_property
- def _expr_comparator(
- self,
- ) -> Callable[[Any], _HybridClassLevelAccessor[_T]]:
- if self.custom_comparator is not None:
- return self._get_comparator(self.custom_comparator)
- elif self.expr is not None:
- return self._get_expr(self.expr)
- else:
- return self._get_expr(cast(_HybridExprCallableType[_T], self.fget))
- def _get_expr(
- self, expr: _HybridExprCallableType[_T]
- ) -> Callable[[Any], _HybridClassLevelAccessor[_T]]:
- def _expr(cls: Any) -> ExprComparator[_T]:
- return ExprComparator(cls, expr(cls), self)
- util.update_wrapper(_expr, expr)
- return self._get_comparator(_expr)
- def _get_comparator(
- self, comparator: Any
- ) -> Callable[[Any], _HybridClassLevelAccessor[_T]]:
- proxy_attr = attributes.create_proxied_attribute(self)
- def expr_comparator(
- owner: Type[object],
- ) -> _HybridClassLevelAccessor[_T]:
- # because this is the descriptor protocol, we don't really know
- # what our attribute name is. so search for it through the
- # MRO.
- for lookup in owner.__mro__:
- if self.__name__ in lookup.__dict__:
- if lookup.__dict__[self.__name__] is self:
- name = self.__name__
- break
- else:
- name = attributes._UNKNOWN_ATTR_KEY # type: ignore[assignment]
- return cast(
- "_HybridClassLevelAccessor[_T]",
- proxy_attr(
- owner,
- name,
- self,
- comparator(owner),
- doc=comparator.__doc__ or self.__doc__,
- ),
- )
- return expr_comparator
- class Comparator(interfaces.PropComparator[_T]):
- """A helper class that allows easy construction of custom
- :class:`~.orm.interfaces.PropComparator`
- classes for usage with hybrids."""
- def __init__(
- self, expression: Union[_HasClauseElement[_T], SQLColumnExpression[_T]]
- ):
- self.expression = expression
- def __clause_element__(self) -> roles.ColumnsClauseRole:
- expr = self.expression
- if is_has_clause_element(expr):
- ret_expr = expr.__clause_element__()
- else:
- if TYPE_CHECKING:
- assert isinstance(expr, ColumnElement)
- ret_expr = expr
- if TYPE_CHECKING:
- # see test_hybrid->test_expression_isnt_clause_element
- # that exercises the usual place this is caught if not
- # true
- assert isinstance(ret_expr, ColumnElement)
- return ret_expr
- @util.non_memoized_property
- def property(self) -> interfaces.MapperProperty[_T]:
- raise NotImplementedError()
- def adapt_to_entity(
- self, adapt_to_entity: AliasedInsp[Any]
- ) -> Comparator[_T]:
- # interesting....
- return self
- class ExprComparator(Comparator[_T]):
- def __init__(
- self,
- cls: Type[Any],
- expression: Union[_HasClauseElement[_T], SQLColumnExpression[_T]],
- hybrid: hybrid_property[_T],
- ):
- self.cls = cls
- self.expression = expression
- self.hybrid = hybrid
- def __getattr__(self, key: str) -> Any:
- return getattr(self.expression, key)
- @util.ro_non_memoized_property
- def info(self) -> _InfoType:
- return self.hybrid.info
- def _bulk_update_tuples(
- self, value: Any
- ) -> Sequence[Tuple[_DMLColumnArgument, Any]]:
- if isinstance(self.expression, attributes.QueryableAttribute):
- return self.expression._bulk_update_tuples(value)
- elif self.hybrid.update_expr is not None:
- return self.hybrid.update_expr(self.cls, value)
- else:
- return [(self.expression, value)]
- @util.non_memoized_property
- def property(self) -> MapperProperty[_T]:
- # this accessor is not normally used, however is accessed by things
- # like ORM synonyms if the hybrid is used in this context; the
- # .property attribute is not necessarily accessible
- return self.expression.property # type: ignore
- def operate(
- self, op: OperatorType, *other: Any, **kwargs: Any
- ) -> ColumnElement[Any]:
- return op(self.expression, *other, **kwargs)
- def reverse_operate(
- self, op: OperatorType, other: Any, **kwargs: Any
- ) -> ColumnElement[Any]:
- return op(other, self.expression, **kwargs) # type: ignore
|