Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
hybrid.py1515 linesDownload Raw Back to ext
1# ext/hybrid.py
2# Copyright (C) 2005-2024 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    class Interval(Base):
38        __tablename__ = 'interval'
39
40        id: Mapped[int] = mapped_column(primary_key=True)
41        start: Mapped[int]
42        end: Mapped[int]
43
44        def __init__(self, start: int, end: int):
45            self.start = start
46            self.end = end
47
48        @hybrid_property
49        def length(self) -> int:
50            return self.end - self.start
51
52        @hybrid_method
53        def contains(self, point: int) -> bool:
54            return (self.start <= point) & (point <= self.end)
55
56        @hybrid_method
57        def intersects(self, other: Interval) -> bool:
58            return self.contains(other.start) | self.contains(other.end)
59
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    class Interval(Base):
154        # ...
155
156        @hybrid_property
157        def radius(self) -> float:
158            return abs(self.length) / 2
159
160        @radius.inplace.expression
161        @classmethod
162        def _radius_expression(cls) -> ColumnElement[float]:
163            return type_coerce(func.abs(cls.length) / 2, Float)
164
165In the above example, the :class:`.hybrid_property` first assigned to the
166name ``Interval.radius`` is amended by a subsequent method called
167``Interval._radius_expression``, using the decorator
168``@radius.inplace.expression``, which chains together two modifiers
169:attr:`.hybrid_property.inplace` and :attr:`.hybrid_property.expression`.
170The use of :attr:`.hybrid_property.inplace` indicates that the
171:meth:`.hybrid_property.expression` modifier should mutate the
172existing hybrid object at ``Interval.radius`` in place, without creating a
173new object.   Notes on this modifier and its
174rationale are discussed in the next section :ref:`hybrid_pep484_naming`.
175The use of ``@classmethod`` is optional, and is strictly to give typing
176tools a hint that ``cls`` in this case is expected to be the ``Interval``
177class, and not an instance of ``Interval``.
178
179.. note:: :attr:`.hybrid_property.inplace` as well as the use of ``@classmethod``
180   for proper typing support are available as of SQLAlchemy 2.0.4, and will
181   not work in earlier versions.
182
183With ``Interval.radius`` now including an expression element, the SQL
184function ``ABS()`` is returned when accessing ``Interval.radius``
185at the class level:
186
187.. sourcecode:: pycon+sql
188
189    >>> from sqlalchemy import select
190    >>> print(select(Interval).filter(Interval.radius > 5))
191    {printsql}SELECT interval.id, interval.start, interval."end"
192    FROM interval
193    WHERE abs(interval."end" - interval.start) / :abs_1 > :param_1
194
195
196.. _hybrid_pep484_naming:
197
198Using ``inplace`` to create pep-484 compliant hybrid properties
199---------------------------------------------------------------
200
201In the previous section, a :class:`.hybrid_property` decorator is illustrated
202which includes two separate method-level functions being decorated, both
203to produce a single object attribute referenced as ``Interval.radius``.
204There are actually several different modifiers we can use for
205:class:`.hybrid_property` including :meth:`.hybrid_property.expression`,
206:meth:`.hybrid_property.setter` and :meth:`.hybrid_property.update_expression`.
207
208SQLAlchemy's :class:`.hybrid_property` decorator intends that adding on these
209methods may be done in the identical manner as Python's built-in
210``@property`` decorator, where idiomatic use is to continue to redefine the
211attribute repeatedly, using the **same attribute name** each time, as in the
212example below that illustrates the use of :meth:`.hybrid_property.setter` and
213:meth:`.hybrid_property.expression` for the ``Interval.radius`` descriptor::
214
215    # correct use, however is not accepted by pep-484 tooling
216
217    class Interval(Base):
218        # ...
219
220        @hybrid_property
221        def radius(self):
222            return abs(self.length) / 2
223
224        @radius.setter
225        def radius(self, value):
226            self.length = value * 2
227
228        @radius.expression
229        def radius(cls):
230            return type_coerce(func.abs(cls.length) / 2, Float)
231
232Above, there are three ``Interval.radius`` methods, but as each are decorated,
233first by the :class:`.hybrid_property` decorator and then by the
234``@radius`` name itself, the end effect is that ``Interval.radius`` is
235a single attribute with three different functions contained within it.
236This style of use is taken from `Python's documented use of @property
237<https://docs.python.org/3/library/functions.html#property>`_.
238It is important to note that the way both ``@property`` as well as
239:class:`.hybrid_property` work, a **copy of the descriptor is made each time**.
240That is, each call to ``@radius.expression``, ``@radius.setter`` etc.
241make a new object entirely.  This allows the attribute to be re-defined in
242subclasses without issue (see :ref:`hybrid_reuse_subclass` later in this
243section for how this is used).
244
245However, the above approach is not compatible with typing tools such as
246mypy and pyright.  Python's own ``@property`` decorator does not have this
247limitation only because
248`these tools hardcode the behavior of @property
249<https://github.com/python/typing/discussions/1102>`_, meaning this syntax
250is not available to SQLAlchemy under :pep:`484` compliance.
251
252In order to produce a reasonable syntax while remaining typing compliant,
253the :attr:`.hybrid_property.inplace` decorator allows the same
254decorator to be re-used with different method names, while still producing
255a single decorator under one name::
256
257    # correct use which is also accepted by pep-484 tooling
258
259    class Interval(Base):
260        # ...
261
262        @hybrid_property
263        def radius(self) -> float:
264            return abs(self.length) / 2
265
266        @radius.inplace.setter
267        def _radius_setter(self, value: float) -> None:
268            # for example only
269            self.length = value * 2
270
271        @radius.inplace.expression
272        @classmethod
273        def _radius_expression(cls) -> ColumnElement[float]:
274            return type_coerce(func.abs(cls.length) / 2, Float)
275
276Using :attr:`.hybrid_property.inplace` further qualifies the use of the
277decorator that a new copy should not be made, thereby maintaining the
278``Interval.radius`` name while allowing additional methods
279``Interval._radius_setter`` and ``Interval._radius_expression`` to be
280differently named.
281
282
283.. versionadded:: 2.0.4 Added :attr:`.hybrid_property.inplace` to allow
284   less verbose construction of composite :class:`.hybrid_property` objects
285   while not having to use repeated method names.   Additionally allowed the
286   use of ``@classmethod`` within :attr:`.hybrid_property.expression`,
287   :attr:`.hybrid_property.update_expression`, and
288   :attr:`.hybrid_property.comparator` to allow typing tools to identify
289   ``cls`` as a class and not an instance in the method signature.
290
291
292Defining Setters
293----------------
294
295The :meth:`.hybrid_property.setter` modifier allows the construction of a
296custom setter method, that can modify values on the object::
297
298    class Interval(Base):
299        # ...
300
301        @hybrid_property
302        def length(self) -> int:
303            return self.end - self.start
304
305        @length.inplace.setter
306        def _length_setter(self, value: int) -> None:
307            self.end = self.start + value
308
309The ``length(self, value)`` method is now called upon set::
310
311    >>> i1 = Interval(5, 10)
312    >>> i1.length
313    5
314    >>> i1.length = 12
315    >>> i1.end
316    17
317
318.. _hybrid_bulk_update:
319
320Allowing Bulk ORM Update
321------------------------
322
323A hybrid can define a custom "UPDATE" handler for when using
324ORM-enabled updates, allowing the hybrid to be used in the
325SET clause of the update.
326
327Normally, when using a hybrid with :func:`_sql.update`, the SQL
328expression is used as the column that's the target of the SET.  If our
329``Interval`` class had a hybrid ``start_point`` that linked to
330``Interval.start``, this could be substituted directly::
331
332    from sqlalchemy import update
333    stmt = update(Interval).values({Interval.start_point: 10})
334
335However, when using a composite hybrid like ``Interval.length``, this
336hybrid represents more than one column.   We can set up a handler that will
337accommodate a value passed in the VALUES expression which can affect
338this, using the :meth:`.hybrid_property.update_expression` decorator.
339A handler that works similarly to our setter would be::
340
341    from typing import List, Tuple, Any
342
343    class Interval(Base):
344        # ...
345
346        @hybrid_property
347        def length(self) -> int:
348            return self.end - self.start
349
350        @length.inplace.setter
351        def _length_setter(self, value: int) -> None:
352            self.end = self.start + value
353
354        @length.inplace.update_expression
355        def _length_update_expression(cls, value: Any) -> List[Tuple[Any, Any]]:
356            return [
357                (cls.end, cls.start + value)
358            ]
359
360Above, if we use ``Interval.length`` in an UPDATE expression, we get
361a hybrid SET expression:
362
363.. sourcecode:: pycon+sql
364
365
366    >>> from sqlalchemy import update
367    >>> print(update(Interval).values({Interval.length: 25}))
368    {printsql}UPDATE interval SET "end"=(interval.start + :start_1)
369
370This SET expression is accommodated by the ORM automatically.
371
372.. seealso::
373
374    :ref:`orm_expression_update_delete` - includes background on ORM-enabled
375    UPDATE statements
376
377
378Working with Relationships
379--------------------------
380
381There's no essential difference when creating hybrids that work with
382related objects as opposed to column-based data. The need for distinct
383expressions tends to be greater.  The two variants we'll illustrate
384are the "join-dependent" hybrid, and the "correlated subquery" hybrid.
385
386Join-Dependent Relationship Hybrid
387^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
388
389Consider the following declarative
390mapping which relates a ``User`` to a ``SavingsAccount``::
391
392    from __future__ import annotations
393
394    from decimal import Decimal
395    from typing import cast
396    from typing import List
397    from typing import Optional
398
399    from sqlalchemy import ForeignKey
400    from sqlalchemy import Numeric
401    from sqlalchemy import String
402    from sqlalchemy import SQLColumnExpression
403    from sqlalchemy.ext.hybrid import hybrid_property
404    from sqlalchemy.orm import DeclarativeBase
405    from sqlalchemy.orm import Mapped
406    from sqlalchemy.orm import mapped_column
407    from sqlalchemy.orm import relationship
408
409
410    class Base(DeclarativeBase):
411        pass
412
413
414    class SavingsAccount(Base):
415        __tablename__ = 'account'
416        id: Mapped[int] = mapped_column(primary_key=True)
417        user_id: Mapped[int] = mapped_column(ForeignKey('user.id'))
418        balance: Mapped[Decimal] = mapped_column(Numeric(15, 5))
419
420        owner: Mapped[User] = relationship(back_populates="accounts")
421
422    class User(Base):
423        __tablename__ = 'user'
424        id: Mapped[int] = mapped_column(primary_key=True)
425        name: Mapped[str] = mapped_column(String(100))
426
427        accounts: Mapped[List[SavingsAccount]] = relationship(
428            back_populates="owner", lazy="selectin"
429        )
430
431        @hybrid_property
432        def balance(self) -> Optional[Decimal]:
433            if self.accounts:
434                return self.accounts[0].balance
435            else:
436                return None
437
438        @balance.inplace.setter
439        def _balance_setter(self, value: Optional[Decimal]) -> None:
440            assert value is not None
441
442            if not self.accounts:
443                account = SavingsAccount(owner=self)
444            else:
445                account = self.accounts[0]
446            account.balance = value
447
448        @balance.inplace.expression
449        @classmethod
450        def _balance_expression(cls) -> SQLColumnExpression[Optional[Decimal]]:
451            return cast("SQLColumnExpression[Optional[Decimal]]", SavingsAccount.balance)
452
453The above hybrid property ``balance`` works with the first
454``SavingsAccount`` entry in the list of accounts for this user.   The
455in-Python getter/setter methods can treat ``accounts`` as a Python
456list available on ``self``.
457
458.. tip:: The ``User.balance`` getter in the above example accesses the
459   ``self.acccounts`` collection, which will normally be loaded via the
460   :func:`.selectinload` loader strategy configured on the ``User.balance``
461   :func:`_orm.relationship`. The default loader strategy when not otherwise
462   stated on :func:`_orm.relationship` is :func:`.lazyload`, which emits SQL on
463   demand. When using asyncio, on-demand loaders such as :func:`.lazyload` are
464   not supported, so care should be taken to ensure the ``self.accounts``
465   collection is accessible to this hybrid accessor when using asyncio.
466
467At the expression level, it's expected that the ``User`` class will
468be used in an appropriate context such that an appropriate join to
469``SavingsAccount`` will be present:
470
471.. sourcecode:: pycon+sql
472
473    >>> from sqlalchemy import select
474    >>> print(select(User, User.balance).
475    ...       join(User.accounts).filter(User.balance > 5000))
476    {printsql}SELECT "user".id AS user_id, "user".name AS user_name,
477    account.balance AS account_balance
478    FROM "user" JOIN account ON "user".id = account.user_id
479    WHERE account.balance > :balance_1
480
481Note however, that while the instance level accessors need to worry
482about whether ``self.accounts`` is even present, this issue expresses
483itself differently at the SQL expression level, where we basically
484would use an outer join:
485
486.. sourcecode:: pycon+sql
487
488    >>> from sqlalchemy import select
489    >>> from sqlalchemy import or_
490    >>> print (select(User, User.balance).outerjoin(User.accounts).
491    ...         filter(or_(User.balance < 5000, User.balance == None)))
492    {printsql}SELECT "user".id AS user_id, "user".name AS user_name,
493    account.balance AS account_balance
494    FROM "user" LEFT OUTER JOIN account ON "user".id = account.user_id
495    WHERE account.balance <  :balance_1 OR account.balance IS NULL
496
497Correlated Subquery Relationship Hybrid
498^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
499
500We can, of course, forego being dependent on the enclosing query's usage
501of joins in favor of the correlated subquery, which can portably be packed
502into a single column expression. A correlated subquery is more portable, but
503often performs more poorly at the SQL level. Using the same technique
504illustrated at :ref:`mapper_column_property_sql_expressions`,
505we can adjust our ``SavingsAccount`` example to aggregate the balances for
506*all* accounts, and use a correlated subquery for the column expression::
507
508    from __future__ import annotations
509
510    from decimal import Decimal
511    from typing import List
512
513    from sqlalchemy import ForeignKey
514    from sqlalchemy import func
515    from sqlalchemy import Numeric
516    from sqlalchemy import select
517    from sqlalchemy import SQLColumnExpression
518    from sqlalchemy import String
519    from sqlalchemy.ext.hybrid import hybrid_property
520    from sqlalchemy.orm import DeclarativeBase
521    from sqlalchemy.orm import Mapped
522    from sqlalchemy.orm import mapped_column
523    from sqlalchemy.orm import relationship
524
525
526    class Base(DeclarativeBase):
527        pass
528
529
530    class SavingsAccount(Base):
531        __tablename__ = 'account'
532        id: Mapped[int] = mapped_column(primary_key=True)
533        user_id: Mapped[int] = mapped_column(ForeignKey('user.id'))
534        balance: Mapped[Decimal] = mapped_column(Numeric(15, 5))
535
536        owner: Mapped[User] = relationship(back_populates="accounts")
537
538    class User(Base):
539        __tablename__ = 'user'
540        id: Mapped[int] = mapped_column(primary_key=True)
541        name: Mapped[str] = mapped_column(String(100))
542
543        accounts: Mapped[List[SavingsAccount]] = relationship(
544            back_populates="owner", lazy="selectin"
545        )
546
547        @hybrid_property
548        def balance(self) -> Decimal:
549            return sum((acc.balance for acc in self.accounts), start=Decimal("0"))
550
551        @balance.inplace.expression
552        @classmethod
553        def _balance_expression(cls) -> SQLColumnExpression[Decimal]:
554            return (
555                select(func.sum(SavingsAccount.balance))
556                .where(SavingsAccount.user_id == cls.id)
557                .label("total_balance")
558            )
559
560
561The above recipe will give us the ``balance`` column which renders
562a correlated SELECT:
563
564.. sourcecode:: pycon+sql
565
566    >>> from sqlalchemy import select
567    >>> print(select(User).filter(User.balance > 400))
568    {printsql}SELECT "user".id, "user".name
569    FROM "user"
570    WHERE (
571        SELECT sum(account.balance) AS sum_1 FROM account
572        WHERE account.user_id = "user".id
573    ) > :param_1
574
575
576.. _hybrid_custom_comparators:
577
578Building Custom Comparators
579---------------------------
580
581The hybrid property also includes a helper that allows construction of
582custom comparators. A comparator object allows one to customize the
583behavior of each SQLAlchemy expression operator individually.  They
584are useful when creating custom types that have some highly
585idiosyncratic behavior on the SQL side.
586
587.. note::  The :meth:`.hybrid_property.comparator` decorator introduced
588   in this section **replaces** the use of the
589   :meth:`.hybrid_property.expression` decorator.
590   They cannot be used together.
591
592The example class below allows case-insensitive comparisons on the attribute
593named ``word_insensitive``::
594
595    from __future__ import annotations
596
597    from typing import Any
598
599    from sqlalchemy import ColumnElement
600    from sqlalchemy import func
601    from sqlalchemy.ext.hybrid import Comparator
602    from sqlalchemy.ext.hybrid import hybrid_property
603    from sqlalchemy.orm import DeclarativeBase
604    from sqlalchemy.orm import Mapped
605    from sqlalchemy.orm import mapped_column
606
607    class Base(DeclarativeBase):
608        pass
609
610
611    class CaseInsensitiveComparator(Comparator[str]):
612        def __eq__(self, other: Any) -> ColumnElement[bool]:  # type: ignore[override]  # noqa: E501
613            return func.lower(self.__clause_element__()) == func.lower(other)
614
615    class SearchWord(Base):
616        __tablename__ = 'searchword'
617
618        id: Mapped[int] = mapped_column(primary_key=True)
619        word: Mapped[str]
620
621        @hybrid_property
622        def word_insensitive(self) -> str:
623            return self.word.lower()
624
625        @word_insensitive.inplace.comparator
626        @classmethod
627        def _word_insensitive_comparator(cls) -> CaseInsensitiveComparator:
628            return CaseInsensitiveComparator(cls.word)
629
630Above, SQL expressions against ``word_insensitive`` will apply the ``LOWER()``
631SQL function to both sides:
632
633.. sourcecode:: pycon+sql
634
635    >>> from sqlalchemy import select
636    >>> print(select(SearchWord).filter_by(word_insensitive="Trucks"))
637    {printsql}SELECT searchword.id, searchword.word
638    FROM searchword
639    WHERE lower(searchword.word) = lower(:lower_1)
640
641
642The ``CaseInsensitiveComparator`` above implements part of the
643:class:`.ColumnOperators` interface.   A "coercion" operation like
644lowercasing can be applied to all comparison operations (i.e. ``eq``,
645``lt``, ``gt``, etc.) using :meth:`.Operators.operate`::
646
647    class CaseInsensitiveComparator(Comparator):
648        def operate(self, op, other, **kwargs):
649            return op(
650                func.lower(self.__clause_element__()),
651                func.lower(other),
652                **kwargs,
653            )
654
655.. _hybrid_reuse_subclass:
656
657Reusing Hybrid Properties across Subclasses
658-------------------------------------------
659
660A hybrid can be referred to from a superclass, to allow modifying
661methods like :meth:`.hybrid_property.getter`, :meth:`.hybrid_property.setter`
662to be used to redefine those methods on a subclass.  This is similar to
663how the standard Python ``@property`` object works::
664
665    class FirstNameOnly(Base):
666        # ...
667
668        first_name: Mapped[str]
669
670        @hybrid_property
671        def name(self) -> str:
672            return self.first_name
673
674        @name.inplace.setter
675        def _name_setter(self, value: str) -> None:
676            self.first_name = value
677
678    class FirstNameLastName(FirstNameOnly):
679        # ...
680
681        last_name: Mapped[str]
682
683        # 'inplace' is not used here; calling getter creates a copy
684        # of FirstNameOnly.name that is local to FirstNameLastName
685        @FirstNameOnly.name.getter
686        def name(self) -> str:
687            return self.first_name + ' ' + self.last_name
688
689        @name.inplace.setter
690        def _name_setter(self, value: str) -> None:
691            self.first_name, self.last_name = value.split(' ', 1)
692
693Above, the ``FirstNameLastName`` class refers to the hybrid from
694``FirstNameOnly.name`` to repurpose its getter and setter for the subclass.
695
696When overriding :meth:`.hybrid_property.expression` and
697:meth:`.hybrid_property.comparator` alone as the first reference to the
698superclass, these names conflict with the same-named accessors on the class-
699level :class:`.QueryableAttribute` object returned at the class level.  To
700override these methods when referring directly to the parent class descriptor,
701add the special qualifier :attr:`.hybrid_property.overrides`, which will de-
702reference the instrumented attribute back to the hybrid object::
703
704    class FirstNameLastName(FirstNameOnly):
705        # ...
706
707        last_name: Mapped[str]
708
709        @FirstNameOnly.name.overrides.expression
710        @classmethod
711        def name(cls):
712            return func.concat(cls.first_name, ' ', cls.last_name)
713
714
715Hybrid Value Objects
716--------------------
717
718Note in our previous example, if we were to compare the ``word_insensitive``
719attribute of a ``SearchWord`` instance to a plain Python string, the plain
720Python string would not be coerced to lower case - the
721``CaseInsensitiveComparator`` we built, being returned by
722``@word_insensitive.comparator``, only applies to the SQL side.
723
724A more comprehensive form of the custom comparator is to construct a *Hybrid
725Value Object*. This technique applies the target value or expression to a value
726object which is then returned by the accessor in all cases.   The value object
727allows control of all operations upon the value as well as how compared values
728are treated, both on the SQL expression side as well as the Python value side.
729Replacing the previous ``CaseInsensitiveComparator`` class with a new
730``CaseInsensitiveWord`` class::
731
732    class CaseInsensitiveWord(Comparator):
733        "Hybrid value representing a lower case representation of a word."
734
735        def __init__(self, word):
736            if isinstance(word, basestring):
737                self.word = word.lower()
738            elif isinstance(word, CaseInsensitiveWord):
739                self.word = word.word
740            else:
741                self.word = func.lower(word)
742
743        def operate(self, op, other, **kwargs):
744            if not isinstance(other, CaseInsensitiveWord):
745                other = CaseInsensitiveWord(other)
746            return op(self.word, other.word, **kwargs)
747
748        def __clause_element__(self):
749            return self.word
750
751        def __str__(self):
752            return self.word
753
754        key = 'word'
755        "Label to apply to Query tuple results"
756
757Above, the ``CaseInsensitiveWord`` object represents ``self.word``, which may
758be a SQL function, or may be a Python native.   By overriding ``operate()`` and
759``__clause_element__()`` to work in terms of ``self.word``, all comparison
760operations will work against the "converted" form of ``word``, whether it be
761SQL side or Python side. Our ``SearchWord`` class can now deliver the
762``CaseInsensitiveWord`` object unconditionally from a single hybrid call::
763
764    class SearchWord(Base):
765        __tablename__ = 'searchword'
766        id: Mapped[int] = mapped_column(primary_key=True)
767        word: Mapped[str]
768
769        @hybrid_property
770        def word_insensitive(self) -> CaseInsensitiveWord:
771            return CaseInsensitiveWord(self.word)
772
773The ``word_insensitive`` attribute now has case-insensitive comparison behavior
774universally, including SQL expression vs. Python expression (note the Python
775value is converted to lower case on the Python side here):
776
777.. sourcecode:: pycon+sql
778
779    >>> print(select(SearchWord).filter_by(word_insensitive="Trucks"))
780    {printsql}SELECT searchword.id AS searchword_id, searchword.word AS searchword_word
781    FROM searchword
782    WHERE lower(searchword.word) = :lower_1
783
784SQL expression versus SQL expression:
785
786.. sourcecode:: pycon+sql
787
788    >>> from sqlalchemy.orm import aliased
789    >>> sw1 = aliased(SearchWord)
790    >>> sw2 = aliased(SearchWord)
791    >>> print(
792    ...     select(sw1.word_insensitive, sw2.word_insensitive).filter(
793    ...         sw1.word_insensitive > sw2.word_insensitive
794    ...     )
795    ... )
796    {printsql}SELECT lower(searchword_1.word) AS lower_1,
797    lower(searchword_2.word) AS lower_2
798    FROM searchword AS searchword_1, searchword AS searchword_2
799    WHERE lower(searchword_1.word) > lower(searchword_2.word)
800
801Python only expression::
802
803    >>> ws1 = SearchWord(word="SomeWord")
804    >>> ws1.word_insensitive == "sOmEwOrD"
805    True
806    >>> ws1.word_insensitive == "XOmEwOrX"
807    False
808    >>> print(ws1.word_insensitive)
809    someword
810
811The Hybrid Value pattern is very useful for any kind of value that may have
812multiple representations, such as timestamps, time deltas, units of
813measurement, currencies and encrypted passwords.
814
815.. seealso::
816
817    `Hybrids and Value Agnostic Types
818    <https://techspot.zzzeek.org/2011/10/21/hybrids-and-value-agnostic-types/>`_
819    - on the techspot.zzzeek.org blog
820
821    `Value Agnostic Types, Part II
822    <https://techspot.zzzeek.org/2011/10/29/value-agnostic-types-part-ii/>`_ -
823    on the techspot.zzzeek.org blog
824
825
826"""  # noqa
827
828from __future__ import annotations
829
830from typing import Any
831from typing import Callable
832from typing import cast
833from typing import Generic
834from typing import List
835from typing import Optional
836from typing import overload
837from typing import Sequence
838from typing import Tuple
839from typing import Type
840from typing import TYPE_CHECKING
841from typing import TypeVar
842from typing import Union
843
844from .. import util
845from ..orm import attributes
846from ..orm import InspectionAttrExtensionType
847from ..orm import interfaces
848from ..orm import ORMDescriptor
849from ..orm.attributes import QueryableAttribute
850from ..sql import roles
851from ..sql._typing import is_has_clause_element
852from ..sql.elements import ColumnElement
853from ..sql.elements import SQLCoreOperations
854from ..util.typing import Concatenate
855from ..util.typing import Literal
856from ..util.typing import ParamSpec
857from ..util.typing import Protocol
858from ..util.typing import Self
859
860if TYPE_CHECKING:
861    from ..orm.interfaces import MapperProperty
862    from ..orm.util import AliasedInsp
863    from ..sql import SQLColumnExpression
864    from ..sql._typing import _ColumnExpressionArgument
865    from ..sql._typing import _DMLColumnArgument
866    from ..sql._typing import _HasClauseElement
867    from ..sql._typing import _InfoType
868    from ..sql.operators import OperatorType
869
870_P = ParamSpec("_P")
871_R = TypeVar("_R")
872_T = TypeVar("_T", bound=Any)
873_TE = TypeVar("_TE", bound=Any)
874_T_co = TypeVar("_T_co", bound=Any, covariant=True)
875_T_con = TypeVar("_T_con", bound=Any, contravariant=True)
876
877
878class HybridExtensionType(InspectionAttrExtensionType):
879    HYBRID_METHOD = "HYBRID_METHOD"
880    """Symbol indicating an :class:`InspectionAttr` that's
881    of type :class:`.hybrid_method`.
882
883    Is assigned to the :attr:`.InspectionAttr.extension_type`
884    attribute.
885
886    .. seealso::
887
888        :attr:`_orm.Mapper.all_orm_attributes`
889
890    """
891
892    HYBRID_PROPERTY = "HYBRID_PROPERTY"
893    """Symbol indicating an :class:`InspectionAttr` that's
894        of type :class:`.hybrid_method`.
895
896    Is assigned to the :attr:`.InspectionAttr.extension_type`
897    attribute.
898
899    .. seealso::
900
901        :attr:`_orm.Mapper.all_orm_attributes`
902
903    """
904
905
906class _HybridGetterType(Protocol[_T_co]):
907    def __call__(s, self: Any) -> _T_co: ...
908
909
910class _HybridSetterType(Protocol[_T_con]):
911    def __call__(s, self: Any, value: _T_con) -> None: ...
912
913
914class _HybridUpdaterType(Protocol[_T_con]):
915    def __call__(
916        s,
917        cls: Any,
918        value: Union[_T_con, _ColumnExpressionArgument[_T_con]],
919    ) -> List[Tuple[_DMLColumnArgument, Any]]: ...
920
921
922class _HybridDeleterType(Protocol[_T_co]):
923    def __call__(s, self: Any) -> None: ...
924
925
926class _HybridExprCallableType(Protocol[_T_co]):
927    def __call__(
928        s, cls: Any
929    ) -> Union[_HasClauseElement[_T_co], SQLColumnExpression[_T_co]]: ...
930
931
932class _HybridComparatorCallableType(Protocol[_T]):
933    def __call__(self, cls: Any) -> Comparator[_T]: ...
934
935
936class _HybridClassLevelAccessor(QueryableAttribute[_T]):
937    """Describe the object returned by a hybrid_property() when
938    called as a class-level descriptor.
939
940    """
941
942    if TYPE_CHECKING:
943
944        def getter(
945            self, fget: _HybridGetterType[_T]
946        ) -> hybrid_property[_T]: ...
947
948        def setter(
949            self, fset: _HybridSetterType[_T]
950        ) -> hybrid_property[_T]: ...
951
952        def deleter(
953            self, fdel: _HybridDeleterType[_T]
954        ) -> hybrid_property[_T]: ...
955
956        @property
957        def overrides(self) -> hybrid_property[_T]: ...
958
959        def update_expression(
960            self, meth: _HybridUpdaterType[_T]
961        ) -> hybrid_property[_T]: ...
962
963
964class hybrid_method(interfaces.InspectionAttrInfo, Generic[_P, _R]):
965    """A decorator which allows definition of a Python object method with both
966    instance-level and class-level behavior.
967
968    """
969
970    is_attribute = True
971    extension_type = HybridExtensionType.HYBRID_METHOD
972
973    def __init__(
974        self,
975        func: Callable[Concatenate[Any, _P], _R],
976        expr: Optional[
977            Callable[Concatenate[Any, _P], SQLCoreOperations[_R]]
978        ] = None,
979    ):
980        """Create a new :class:`.hybrid_method`.
981
982        Usage is typically via decorator::
983
984            from sqlalchemy.ext.hybrid import hybrid_method
985
986            class SomeClass:
987                @hybrid_method
988                def value(self, x, y):
989                    return self._value + x + y
990
991                @value.expression
992                @classmethod
993                def value(cls, x, y):
994                    return func.some_function(cls._value, x, y)
995
996        """
997        self.func = func
998        if expr is not None:
999            self.expression(expr)
1000        else:
1001            self.expression(func)  # type: ignore
1002
1003    @property
1004    def inplace(self) -> Self:
1005        """Return the inplace mutator for this :class:`.hybrid_method`.
1006
1007        The :class:`.hybrid_method` class already performs "in place" mutation
1008        when the :meth:`.hybrid_method.expression` decorator is called,
1009        so this attribute returns Self.
1010
1011        .. versionadded:: 2.0.4
1012
1013        .. seealso::
1014
1015            :ref:`hybrid_pep484_naming`
1016
1017        """
1018        return self
1019
1020    @overload
1021    def __get__(
1022        self, instance: Literal[None], owner: Type[object]
1023    ) -> Callable[_P, SQLCoreOperations[_R]]: ...
1024
1025    @overload
1026    def __get__(
1027        self, instance: object, owner: Type[object]
1028    ) -> Callable[_P, _R]: ...
1029
1030    def __get__(
1031        self, instance: Optional[object], owner: Type[object]
1032    ) -> Union[Callable[_P, _R], Callable[_P, SQLCoreOperations[_R]]]:
1033        if instance is None:
1034            return self.expr.__get__(owner, owner)  # type: ignore
1035        else:
1036            return self.func.__get__(instance, owner)  # type: ignore
1037
1038    def expression(
1039        self, expr: Callable[Concatenate[Any, _P], SQLCoreOperations[_R]]
1040    ) -> hybrid_method[_P, _R]:
1041        """Provide a modifying decorator that defines a
1042        SQL-expression producing method."""
1043
1044        self.expr = expr
1045        if not self.expr.__doc__:
1046            self.expr.__doc__ = self.func.__doc__
1047        return self
1048
1049
1050def _unwrap_classmethod(meth: _T) -> _T:
1051    if isinstance(meth, classmethod):
1052        return meth.__func__  # type: ignore
1053    else:
1054        return meth
1055
1056
1057class hybrid_property(interfaces.InspectionAttrInfo, ORMDescriptor[_T]):
1058    """A decorator which allows definition of a Python descriptor with both
1059    instance-level and class-level behavior.
1060
1061    """
1062
1063    is_attribute = True
1064    extension_type = HybridExtensionType.HYBRID_PROPERTY
1065
1066    __name__: str
1067
1068    def __init__(
1069        self,
1070        fget: _HybridGetterType[_T],
1071        fset: Optional[_HybridSetterType[_T]] = None,
1072        fdel: Optional[_HybridDeleterType[_T]] = None,
1073        expr: Optional[_HybridExprCallableType[_T]] = None,
1074        custom_comparator: Optional[Comparator[_T]] = None,
1075        update_expr: Optional[_HybridUpdaterType[_T]] = None,
1076    ):
1077        """Create a new :class:`.hybrid_property`.
1078
1079        Usage is typically via decorator::
1080
1081            from sqlalchemy.ext.hybrid import hybrid_property
1082
1083            class SomeClass:
1084                @hybrid_property
1085                def value(self):
1086                    return self._value
1087
1088                @value.setter
1089                def value(self, value):
1090                    self._value = value
1091
1092        """
1093        self.fget = fget
1094        self.fset = fset
1095        self.fdel = fdel
1096        self.expr = _unwrap_classmethod(expr)
1097        self.custom_comparator = _unwrap_classmethod(custom_comparator)
1098        self.update_expr = _unwrap_classmethod(update_expr)
1099        util.update_wrapper(self, fget)  # type: ignore[arg-type]
1100
1101    @overload
1102    def __get__(self, instance: Any, owner: Literal[None]) -> Self: ...
1103
1104    @overload
1105    def __get__(
1106        self, instance: Literal[None], owner: Type[object]
1107    ) -> _HybridClassLevelAccessor[_T]: ...
1108
1109    @overload
1110    def __get__(self, instance: object, owner: Type[object]) -> _T: ...
1111
1112    def __get__(
1113        self, instance: Optional[object], owner: Optional[Type[object]]
1114    ) -> Union[hybrid_property[_T], _HybridClassLevelAccessor[_T], _T]:
1115        if owner is None:
1116            return self
1117        elif instance is None:
1118            return self._expr_comparator(owner)
1119        else:
1120            return self.fget(instance)
1121
1122    def __set__(self, instance: object, value: Any) -> None:
1123        if self.fset is None:
1124            raise AttributeError("can't set attribute")
1125        self.fset(instance, value)
1126
1127    def __delete__(self, instance: object) -> None:
1128        if self.fdel is None:
1129            raise AttributeError("can't delete attribute")
1130        self.fdel(instance)
1131
1132    def _copy(self, **kw: Any) -> hybrid_property[_T]:
1133        defaults = {
1134            key: value
1135            for key, value in self.__dict__.items()
1136            if not key.startswith("_")
1137        }
1138        defaults.update(**kw)
1139        return type(self)(**defaults)
1140
1141    @property
1142    def overrides(self) -> Self:
1143        """Prefix for a method that is overriding an existing attribute.
1144
1145        The :attr:`.hybrid_property.overrides` accessor just returns
1146        this hybrid object, which when called at the class level from
1147        a parent class, will de-reference the "instrumented attribute"
1148        normally returned at this level, and allow modifying decorators
1149        like :meth:`.hybrid_property.expression` and
1150        :meth:`.hybrid_property.comparator`
1151        to be used without conflicting with the same-named attributes
1152        normally present on the :class:`.QueryableAttribute`::
1153
1154            class SuperClass:
1155                # ...
1156
1157                @hybrid_property
1158                def foobar(self):
1159                    return self._foobar
1160
1161            class SubClass(SuperClass):
1162                # ...
1163
1164                @SuperClass.foobar.overrides.expression
1165                def foobar(cls):
1166                    return func.subfoobar(self._foobar)
1167
1168        .. versionadded:: 1.2
1169
1170        .. seealso::
1171
1172            :ref:`hybrid_reuse_subclass`
1173
1174        """
1175        return self
1176
1177    class _InPlace(Generic[_TE]):
1178        """A builder helper for .hybrid_property.
1179
1180        .. versionadded:: 2.0.4
1181
1182        """
1183
1184        __slots__ = ("attr",)
1185
1186        def __init__(self, attr: hybrid_property[_TE]):
1187            self.attr = attr
1188
1189        def _set(self, **kw: Any) -> hybrid_property[_TE]:
1190            for k, v in kw.items():
1191                setattr(self.attr, k, _unwrap_classmethod(v))
1192            return self.attr
1193
1194        def getter(self, fget: _HybridGetterType[_TE]) -> hybrid_property[_TE]:
1195            return self._set(fget=fget)
1196
1197        def setter(self, fset: _HybridSetterType[_TE]) -> hybrid_property[_TE]:
1198            return self._set(fset=fset)
1199
1200        def deleter(

Showing the first 1,200 of 1515 lines. Download the file for the rest.

codekingpro/portable-devtools · Team Ai