Загрузить файлы в «venv/Lib/site-packages/sqlalchemy/ext»
This commit is contained in:
479
venv/Lib/site-packages/sqlalchemy/ext/horizontal_shard.py
Normal file
479
venv/Lib/site-packages/sqlalchemy/ext/horizontal_shard.py
Normal file
@@ -0,0 +1,479 @@
|
|||||||
|
# ext/horizontal_shard.py
|
||||||
|
# Copyright (C) 2005-2026 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
|
||||||
|
|
||||||
|
"""Horizontal sharding support.
|
||||||
|
|
||||||
|
Defines a rudimental 'horizontal sharding' system which allows a Session to
|
||||||
|
distribute queries and persistence operations across multiple databases.
|
||||||
|
|
||||||
|
For a usage example, see the :ref:`examples_sharding` example included in
|
||||||
|
the source distribution.
|
||||||
|
|
||||||
|
.. deepalchemy:: The horizontal sharding extension is an advanced feature,
|
||||||
|
involving a complex statement -> database interaction as well as
|
||||||
|
use of semi-public APIs for non-trivial cases. Simpler approaches to
|
||||||
|
referring to multiple database "shards", most commonly using a distinct
|
||||||
|
:class:`_orm.Session` per "shard", should always be considered first
|
||||||
|
before using this more complex and less-production-tested system.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
from typing import Callable
|
||||||
|
from typing import Dict
|
||||||
|
from typing import Iterable
|
||||||
|
from typing import Optional
|
||||||
|
from typing import Tuple
|
||||||
|
from typing import Type
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
from typing import TypeVar
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
from .. import event
|
||||||
|
from .. import exc
|
||||||
|
from .. import inspect
|
||||||
|
from .. import util
|
||||||
|
from ..orm import PassiveFlag
|
||||||
|
from ..orm._typing import OrmExecuteOptionsParameter
|
||||||
|
from ..orm.interfaces import ORMOption
|
||||||
|
from ..orm.mapper import Mapper
|
||||||
|
from ..orm.query import Query
|
||||||
|
from ..orm.session import _BindArguments
|
||||||
|
from ..orm.session import _PKIdentityArgument
|
||||||
|
from ..orm.session import Session
|
||||||
|
from ..util.typing import Protocol
|
||||||
|
from ..util.typing import Self
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..engine.base import Connection
|
||||||
|
from ..engine.base import Engine
|
||||||
|
from ..engine.base import OptionEngine
|
||||||
|
from ..engine.result import IteratorResult
|
||||||
|
from ..engine.result import Result
|
||||||
|
from ..orm import LoaderCallableStatus
|
||||||
|
from ..orm._typing import _O
|
||||||
|
from ..orm.bulk_persistence import BulkUDCompileState
|
||||||
|
from ..orm.context import QueryContext
|
||||||
|
from ..orm.session import _EntityBindKey
|
||||||
|
from ..orm.session import _SessionBind
|
||||||
|
from ..orm.session import ORMExecuteState
|
||||||
|
from ..orm.state import InstanceState
|
||||||
|
from ..sql import Executable
|
||||||
|
from ..sql._typing import _TP
|
||||||
|
from ..sql.elements import ClauseElement
|
||||||
|
|
||||||
|
__all__ = ["ShardedSession", "ShardedQuery"]
|
||||||
|
|
||||||
|
_T = TypeVar("_T", bound=Any)
|
||||||
|
|
||||||
|
|
||||||
|
ShardIdentifier = str
|
||||||
|
|
||||||
|
|
||||||
|
class ShardChooser(Protocol):
|
||||||
|
def __call__(
|
||||||
|
self,
|
||||||
|
mapper: Optional[Mapper[_T]],
|
||||||
|
instance: Any,
|
||||||
|
clause: Optional[ClauseElement],
|
||||||
|
) -> Any: ...
|
||||||
|
|
||||||
|
|
||||||
|
class IdentityChooser(Protocol):
|
||||||
|
def __call__(
|
||||||
|
self,
|
||||||
|
mapper: Mapper[_T],
|
||||||
|
primary_key: _PKIdentityArgument,
|
||||||
|
*,
|
||||||
|
lazy_loaded_from: Optional[InstanceState[Any]],
|
||||||
|
execution_options: OrmExecuteOptionsParameter,
|
||||||
|
bind_arguments: _BindArguments,
|
||||||
|
**kw: Any,
|
||||||
|
) -> Any: ...
|
||||||
|
|
||||||
|
|
||||||
|
class ShardedQuery(Query[_T]):
|
||||||
|
"""Query class used with :class:`.ShardedSession`.
|
||||||
|
|
||||||
|
.. legacy:: The :class:`.ShardedQuery` is a subclass of the legacy
|
||||||
|
:class:`.Query` class. The :class:`.ShardedSession` now supports
|
||||||
|
2.0 style execution via the :meth:`.ShardedSession.execute` method.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
assert isinstance(self.session, ShardedSession)
|
||||||
|
|
||||||
|
self.identity_chooser = self.session.identity_chooser
|
||||||
|
self.execute_chooser = self.session.execute_chooser
|
||||||
|
self._shard_id = None
|
||||||
|
|
||||||
|
def set_shard(self, shard_id: ShardIdentifier) -> Self:
|
||||||
|
"""Return a new query, limited to a single shard ID.
|
||||||
|
|
||||||
|
All subsequent operations with the returned query will
|
||||||
|
be against the single shard regardless of other state.
|
||||||
|
|
||||||
|
The shard_id can be passed for a 2.0 style execution to the
|
||||||
|
bind_arguments dictionary of :meth:`.Session.execute`::
|
||||||
|
|
||||||
|
results = session.execute(stmt, bind_arguments={"shard_id": "my_shard"})
|
||||||
|
|
||||||
|
""" # noqa: E501
|
||||||
|
return self.execution_options(_sa_shard_id=shard_id)
|
||||||
|
|
||||||
|
|
||||||
|
class ShardedSession(Session):
|
||||||
|
shard_chooser: ShardChooser
|
||||||
|
identity_chooser: IdentityChooser
|
||||||
|
execute_chooser: Callable[[ORMExecuteState], Iterable[Any]]
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
shard_chooser: ShardChooser,
|
||||||
|
identity_chooser: Optional[IdentityChooser] = None,
|
||||||
|
execute_chooser: Optional[
|
||||||
|
Callable[[ORMExecuteState], Iterable[Any]]
|
||||||
|
] = None,
|
||||||
|
shards: Optional[Dict[str, Any]] = None,
|
||||||
|
query_cls: Type[Query[_T]] = ShardedQuery,
|
||||||
|
*,
|
||||||
|
id_chooser: Optional[
|
||||||
|
Callable[[Query[_T], Iterable[_T]], Iterable[Any]]
|
||||||
|
] = None,
|
||||||
|
query_chooser: Optional[Callable[[Executable], Iterable[Any]]] = None,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> None:
|
||||||
|
"""Construct a ShardedSession.
|
||||||
|
|
||||||
|
:param shard_chooser: A callable which, passed a Mapper, a mapped
|
||||||
|
instance, and possibly a SQL clause, returns a shard ID. This id
|
||||||
|
may be based off of the attributes present within the object, or on
|
||||||
|
some round-robin scheme. If the scheme is based on a selection, it
|
||||||
|
should set whatever state on the instance to mark it in the future as
|
||||||
|
participating in that shard.
|
||||||
|
|
||||||
|
:param identity_chooser: A callable, passed a Mapper and primary key
|
||||||
|
argument, which should return a list of shard ids where this
|
||||||
|
primary key might reside.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.0 The ``identity_chooser`` parameter
|
||||||
|
supersedes the ``id_chooser`` parameter.
|
||||||
|
|
||||||
|
:param execute_chooser: For a given :class:`.ORMExecuteState`,
|
||||||
|
returns the list of shard_ids
|
||||||
|
where the query should be issued. Results from all shards returned
|
||||||
|
will be combined together into a single listing.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.4 The ``execute_chooser`` parameter
|
||||||
|
supersedes the ``query_chooser`` parameter.
|
||||||
|
|
||||||
|
:param shards: A dictionary of string shard names
|
||||||
|
to :class:`~sqlalchemy.engine.Engine` objects.
|
||||||
|
|
||||||
|
"""
|
||||||
|
super().__init__(query_cls=query_cls, **kwargs)
|
||||||
|
|
||||||
|
event.listen(
|
||||||
|
self, "do_orm_execute", execute_and_instances, retval=True
|
||||||
|
)
|
||||||
|
self.shard_chooser = shard_chooser
|
||||||
|
|
||||||
|
if id_chooser:
|
||||||
|
_id_chooser = id_chooser
|
||||||
|
util.warn_deprecated(
|
||||||
|
"The ``id_chooser`` parameter is deprecated; "
|
||||||
|
"please use ``identity_chooser``.",
|
||||||
|
"2.0",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _legacy_identity_chooser(
|
||||||
|
mapper: Mapper[_T],
|
||||||
|
primary_key: _PKIdentityArgument,
|
||||||
|
*,
|
||||||
|
lazy_loaded_from: Optional[InstanceState[Any]],
|
||||||
|
execution_options: OrmExecuteOptionsParameter,
|
||||||
|
bind_arguments: _BindArguments,
|
||||||
|
**kw: Any,
|
||||||
|
) -> Any:
|
||||||
|
q = self.query(mapper)
|
||||||
|
if lazy_loaded_from:
|
||||||
|
q = q._set_lazyload_from(lazy_loaded_from)
|
||||||
|
return _id_chooser(q, primary_key)
|
||||||
|
|
||||||
|
self.identity_chooser = _legacy_identity_chooser
|
||||||
|
elif identity_chooser:
|
||||||
|
self.identity_chooser = identity_chooser
|
||||||
|
else:
|
||||||
|
raise exc.ArgumentError(
|
||||||
|
"identity_chooser or id_chooser is required"
|
||||||
|
)
|
||||||
|
|
||||||
|
if query_chooser:
|
||||||
|
_query_chooser = query_chooser
|
||||||
|
util.warn_deprecated(
|
||||||
|
"The ``query_chooser`` parameter is deprecated; "
|
||||||
|
"please use ``execute_chooser``.",
|
||||||
|
"1.4",
|
||||||
|
)
|
||||||
|
if execute_chooser:
|
||||||
|
raise exc.ArgumentError(
|
||||||
|
"Can't pass query_chooser and execute_chooser "
|
||||||
|
"at the same time."
|
||||||
|
)
|
||||||
|
|
||||||
|
def _default_execute_chooser(
|
||||||
|
orm_context: ORMExecuteState,
|
||||||
|
) -> Iterable[Any]:
|
||||||
|
return _query_chooser(orm_context.statement)
|
||||||
|
|
||||||
|
if execute_chooser is None:
|
||||||
|
execute_chooser = _default_execute_chooser
|
||||||
|
|
||||||
|
if execute_chooser is None:
|
||||||
|
raise exc.ArgumentError(
|
||||||
|
"execute_chooser or query_chooser is required"
|
||||||
|
)
|
||||||
|
self.execute_chooser = execute_chooser
|
||||||
|
self.__shards: Dict[ShardIdentifier, _SessionBind] = {}
|
||||||
|
if shards is not None:
|
||||||
|
for k in shards:
|
||||||
|
self.bind_shard(k, shards[k])
|
||||||
|
|
||||||
|
def _identity_lookup(
|
||||||
|
self,
|
||||||
|
mapper: Mapper[_O],
|
||||||
|
primary_key_identity: Union[Any, Tuple[Any, ...]],
|
||||||
|
identity_token: Optional[Any] = None,
|
||||||
|
passive: PassiveFlag = PassiveFlag.PASSIVE_OFF,
|
||||||
|
lazy_loaded_from: Optional[InstanceState[Any]] = None,
|
||||||
|
execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT,
|
||||||
|
bind_arguments: Optional[_BindArguments] = None,
|
||||||
|
**kw: Any,
|
||||||
|
) -> Union[Optional[_O], LoaderCallableStatus]:
|
||||||
|
"""override the default :meth:`.Session._identity_lookup` method so
|
||||||
|
that we search for a given non-token primary key identity across all
|
||||||
|
possible identity tokens (e.g. shard ids).
|
||||||
|
|
||||||
|
.. versionchanged:: 1.4 Moved :meth:`.Session._identity_lookup` from
|
||||||
|
the :class:`_query.Query` object to the :class:`.Session`.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
if identity_token is not None:
|
||||||
|
obj = super()._identity_lookup(
|
||||||
|
mapper,
|
||||||
|
primary_key_identity,
|
||||||
|
identity_token=identity_token,
|
||||||
|
**kw,
|
||||||
|
)
|
||||||
|
|
||||||
|
return obj
|
||||||
|
else:
|
||||||
|
for shard_id in self.identity_chooser(
|
||||||
|
mapper,
|
||||||
|
primary_key_identity,
|
||||||
|
lazy_loaded_from=lazy_loaded_from,
|
||||||
|
execution_options=execution_options,
|
||||||
|
bind_arguments=dict(bind_arguments) if bind_arguments else {},
|
||||||
|
):
|
||||||
|
obj2 = super()._identity_lookup(
|
||||||
|
mapper,
|
||||||
|
primary_key_identity,
|
||||||
|
identity_token=shard_id,
|
||||||
|
lazy_loaded_from=lazy_loaded_from,
|
||||||
|
**kw,
|
||||||
|
)
|
||||||
|
if obj2 is not None:
|
||||||
|
return obj2
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _choose_shard_and_assign(
|
||||||
|
self,
|
||||||
|
mapper: Optional[_EntityBindKey[_O]],
|
||||||
|
instance: Any,
|
||||||
|
**kw: Any,
|
||||||
|
) -> Any:
|
||||||
|
if instance is not None:
|
||||||
|
state = inspect(instance)
|
||||||
|
if state.key:
|
||||||
|
token = state.key[2]
|
||||||
|
assert token is not None
|
||||||
|
return token
|
||||||
|
elif state.identity_token:
|
||||||
|
return state.identity_token
|
||||||
|
|
||||||
|
assert isinstance(mapper, Mapper)
|
||||||
|
shard_id = self.shard_chooser(mapper, instance, **kw)
|
||||||
|
if instance is not None:
|
||||||
|
state.identity_token = shard_id
|
||||||
|
return shard_id
|
||||||
|
|
||||||
|
def connection_callable(
|
||||||
|
self,
|
||||||
|
mapper: Optional[Mapper[_T]] = None,
|
||||||
|
instance: Optional[Any] = None,
|
||||||
|
shard_id: Optional[ShardIdentifier] = None,
|
||||||
|
**kw: Any,
|
||||||
|
) -> Connection:
|
||||||
|
"""Provide a :class:`_engine.Connection` to use in the unit of work
|
||||||
|
flush process.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
if shard_id is None:
|
||||||
|
shard_id = self._choose_shard_and_assign(mapper, instance)
|
||||||
|
|
||||||
|
if self.in_transaction():
|
||||||
|
trans = self.get_transaction()
|
||||||
|
assert trans is not None
|
||||||
|
return trans.connection(mapper, shard_id=shard_id)
|
||||||
|
else:
|
||||||
|
bind = self.get_bind(
|
||||||
|
mapper=mapper, shard_id=shard_id, instance=instance
|
||||||
|
)
|
||||||
|
|
||||||
|
if isinstance(bind, Engine):
|
||||||
|
return bind.connect(**kw)
|
||||||
|
else:
|
||||||
|
assert isinstance(bind, Connection)
|
||||||
|
return bind
|
||||||
|
|
||||||
|
def get_bind(
|
||||||
|
self,
|
||||||
|
mapper: Optional[_EntityBindKey[_O]] = None,
|
||||||
|
*,
|
||||||
|
shard_id: Optional[ShardIdentifier] = None,
|
||||||
|
instance: Optional[Any] = None,
|
||||||
|
clause: Optional[ClauseElement] = None,
|
||||||
|
**kw: Any,
|
||||||
|
) -> _SessionBind:
|
||||||
|
if shard_id is None:
|
||||||
|
shard_id = self._choose_shard_and_assign(
|
||||||
|
mapper, instance=instance, clause=clause
|
||||||
|
)
|
||||||
|
assert shard_id is not None
|
||||||
|
return self.__shards[shard_id]
|
||||||
|
|
||||||
|
def bind_shard(
|
||||||
|
self, shard_id: ShardIdentifier, bind: Union[Engine, OptionEngine]
|
||||||
|
) -> None:
|
||||||
|
self.__shards[shard_id] = bind
|
||||||
|
|
||||||
|
|
||||||
|
class set_shard_id(ORMOption):
|
||||||
|
"""a loader option for statements to apply a specific shard id to the
|
||||||
|
primary query as well as for additional relationship and column
|
||||||
|
loaders.
|
||||||
|
|
||||||
|
The :class:`_horizontal.set_shard_id` option may be applied using
|
||||||
|
the :meth:`_sql.Executable.options` method of any executable statement::
|
||||||
|
|
||||||
|
stmt = (
|
||||||
|
select(MyObject)
|
||||||
|
.where(MyObject.name == "some name")
|
||||||
|
.options(set_shard_id("shard1"))
|
||||||
|
)
|
||||||
|
|
||||||
|
Above, the statement when invoked will limit to the "shard1" shard
|
||||||
|
identifier for the primary query as well as for all relationship and
|
||||||
|
column loading strategies, including eager loaders such as
|
||||||
|
:func:`_orm.selectinload`, deferred column loaders like :func:`_orm.defer`,
|
||||||
|
and the lazy relationship loader :func:`_orm.lazyload`.
|
||||||
|
|
||||||
|
In this way, the :class:`_horizontal.set_shard_id` option has much wider
|
||||||
|
scope than using the "shard_id" argument within the
|
||||||
|
:paramref:`_orm.Session.execute.bind_arguments` dictionary.
|
||||||
|
|
||||||
|
|
||||||
|
.. versionadded:: 2.0.0
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("shard_id", "propagate_to_loaders")
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, shard_id: ShardIdentifier, propagate_to_loaders: bool = True
|
||||||
|
):
|
||||||
|
"""Construct a :class:`_horizontal.set_shard_id` option.
|
||||||
|
|
||||||
|
:param shard_id: shard identifier
|
||||||
|
:param propagate_to_loaders: if left at its default of ``True``, the
|
||||||
|
shard option will take place for lazy loaders such as
|
||||||
|
:func:`_orm.lazyload` and :func:`_orm.defer`; if False, the option
|
||||||
|
will not be propagated to loaded objects. Note that :func:`_orm.defer`
|
||||||
|
always limits to the shard_id of the parent row in any case, so the
|
||||||
|
parameter only has a net effect on the behavior of the
|
||||||
|
:func:`_orm.lazyload` strategy.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.shard_id = shard_id
|
||||||
|
self.propagate_to_loaders = propagate_to_loaders
|
||||||
|
|
||||||
|
|
||||||
|
def execute_and_instances(
|
||||||
|
orm_context: ORMExecuteState,
|
||||||
|
) -> Union[Result[_T], IteratorResult[_TP]]:
|
||||||
|
active_options: Union[
|
||||||
|
None,
|
||||||
|
QueryContext.default_load_options,
|
||||||
|
Type[QueryContext.default_load_options],
|
||||||
|
BulkUDCompileState.default_update_options,
|
||||||
|
Type[BulkUDCompileState.default_update_options],
|
||||||
|
]
|
||||||
|
|
||||||
|
if orm_context.is_select:
|
||||||
|
active_options = orm_context.load_options
|
||||||
|
|
||||||
|
elif orm_context.is_update or orm_context.is_delete:
|
||||||
|
active_options = orm_context.update_delete_options
|
||||||
|
else:
|
||||||
|
active_options = None
|
||||||
|
|
||||||
|
session = orm_context.session
|
||||||
|
assert isinstance(session, ShardedSession)
|
||||||
|
|
||||||
|
def iter_for_shard(
|
||||||
|
shard_id: ShardIdentifier,
|
||||||
|
) -> Union[Result[_T], IteratorResult[_TP]]:
|
||||||
|
bind_arguments = dict(orm_context.bind_arguments)
|
||||||
|
bind_arguments["shard_id"] = shard_id
|
||||||
|
|
||||||
|
orm_context.update_execution_options(identity_token=shard_id)
|
||||||
|
return orm_context.invoke_statement(bind_arguments=bind_arguments)
|
||||||
|
|
||||||
|
for orm_opt in orm_context._non_compile_orm_options:
|
||||||
|
# TODO: if we had an ORMOption that gets applied at ORM statement
|
||||||
|
# execution time, that would allow this to be more generalized.
|
||||||
|
# for now just iterate and look for our options
|
||||||
|
if isinstance(orm_opt, set_shard_id):
|
||||||
|
shard_id = orm_opt.shard_id
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if active_options and active_options._identity_token is not None:
|
||||||
|
shard_id = active_options._identity_token
|
||||||
|
elif "_sa_shard_id" in orm_context.execution_options:
|
||||||
|
shard_id = orm_context.execution_options["_sa_shard_id"]
|
||||||
|
elif "shard_id" in orm_context.bind_arguments:
|
||||||
|
shard_id = orm_context.bind_arguments["shard_id"]
|
||||||
|
else:
|
||||||
|
shard_id = None
|
||||||
|
|
||||||
|
if shard_id is not None:
|
||||||
|
return iter_for_shard(shard_id)
|
||||||
|
else:
|
||||||
|
partial = []
|
||||||
|
for shard_id in session.execute_chooser(orm_context):
|
||||||
|
result_ = iter_for_shard(shard_id)
|
||||||
|
partial.append(result_)
|
||||||
|
return partial[0].merge(*partial[1:])
|
||||||
1535
venv/Lib/site-packages/sqlalchemy/ext/hybrid.py
Normal file
1535
venv/Lib/site-packages/sqlalchemy/ext/hybrid.py
Normal file
File diff suppressed because it is too large
Load Diff
364
venv/Lib/site-packages/sqlalchemy/ext/indexable.py
Normal file
364
venv/Lib/site-packages/sqlalchemy/ext/indexable.py
Normal file
@@ -0,0 +1,364 @@
|
|||||||
|
# ext/indexable.py
|
||||||
|
# Copyright (C) 2005-2026 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
|
||||||
|
|
||||||
|
"""Define attributes on ORM-mapped classes that have "index" attributes for
|
||||||
|
columns with :class:`_types.Indexable` types.
|
||||||
|
|
||||||
|
"index" means the attribute is associated with an element of an
|
||||||
|
:class:`_types.Indexable` column with the predefined index to access it.
|
||||||
|
The :class:`_types.Indexable` types include types such as
|
||||||
|
:class:`_types.ARRAY`, :class:`_types.JSON` and
|
||||||
|
:class:`_postgresql.HSTORE`.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
The :mod:`~sqlalchemy.ext.indexable` extension provides
|
||||||
|
:class:`_schema.Column`-like interface for any element of an
|
||||||
|
:class:`_types.Indexable` typed column. In simple cases, it can be
|
||||||
|
treated as a :class:`_schema.Column` - mapped attribute.
|
||||||
|
|
||||||
|
Synopsis
|
||||||
|
========
|
||||||
|
|
||||||
|
Given ``Person`` as a model with a primary key and JSON data field.
|
||||||
|
While this field may have any number of elements encoded within it,
|
||||||
|
we would like to refer to the element called ``name`` individually
|
||||||
|
as a dedicated attribute which behaves like a standalone column::
|
||||||
|
|
||||||
|
from sqlalchemy import Column, JSON, Integer
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.ext.indexable import index_property
|
||||||
|
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
|
||||||
|
class Person(Base):
|
||||||
|
__tablename__ = "person"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
data = Column(JSON)
|
||||||
|
|
||||||
|
name = index_property("data", "name")
|
||||||
|
|
||||||
|
Above, the ``name`` attribute now behaves like a mapped column. We
|
||||||
|
can compose a new ``Person`` and set the value of ``name``::
|
||||||
|
|
||||||
|
>>> person = Person(name="Alchemist")
|
||||||
|
|
||||||
|
The value is now accessible::
|
||||||
|
|
||||||
|
>>> person.name
|
||||||
|
'Alchemist'
|
||||||
|
|
||||||
|
Behind the scenes, the JSON field was initialized to a new blank dictionary
|
||||||
|
and the field was set::
|
||||||
|
|
||||||
|
>>> person.data
|
||||||
|
{'name': 'Alchemist'}
|
||||||
|
|
||||||
|
The field is mutable in place::
|
||||||
|
|
||||||
|
>>> person.name = "Renamed"
|
||||||
|
>>> person.name
|
||||||
|
'Renamed'
|
||||||
|
>>> person.data
|
||||||
|
{'name': 'Renamed'}
|
||||||
|
|
||||||
|
When using :class:`.index_property`, the change that we make to the indexable
|
||||||
|
structure is also automatically tracked as history; we no longer need
|
||||||
|
to use :class:`~.mutable.MutableDict` in order to track this change
|
||||||
|
for the unit of work.
|
||||||
|
|
||||||
|
Deletions work normally as well::
|
||||||
|
|
||||||
|
>>> del person.name
|
||||||
|
>>> person.data
|
||||||
|
{}
|
||||||
|
|
||||||
|
Above, deletion of ``person.name`` deletes the value from the dictionary,
|
||||||
|
but not the dictionary itself.
|
||||||
|
|
||||||
|
A missing key will produce ``AttributeError``::
|
||||||
|
|
||||||
|
>>> person = Person()
|
||||||
|
>>> person.name
|
||||||
|
AttributeError: 'name'
|
||||||
|
|
||||||
|
Unless you set a default value::
|
||||||
|
|
||||||
|
>>> class Person(Base):
|
||||||
|
... __tablename__ = "person"
|
||||||
|
...
|
||||||
|
... id = Column(Integer, primary_key=True)
|
||||||
|
... data = Column(JSON)
|
||||||
|
...
|
||||||
|
... name = index_property("data", "name", default=None) # See default
|
||||||
|
|
||||||
|
>>> person = Person()
|
||||||
|
>>> print(person.name)
|
||||||
|
None
|
||||||
|
|
||||||
|
|
||||||
|
The attributes are also accessible at the class level.
|
||||||
|
Below, we illustrate ``Person.name`` used to generate
|
||||||
|
an indexed SQL criteria::
|
||||||
|
|
||||||
|
>>> from sqlalchemy.orm import Session
|
||||||
|
>>> session = Session()
|
||||||
|
>>> query = session.query(Person).filter(Person.name == "Alchemist")
|
||||||
|
|
||||||
|
The above query is equivalent to::
|
||||||
|
|
||||||
|
>>> query = session.query(Person).filter(Person.data["name"] == "Alchemist")
|
||||||
|
|
||||||
|
Multiple :class:`.index_property` objects can be chained to produce
|
||||||
|
multiple levels of indexing::
|
||||||
|
|
||||||
|
from sqlalchemy import Column, JSON, Integer
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.ext.indexable import index_property
|
||||||
|
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
|
||||||
|
class Person(Base):
|
||||||
|
__tablename__ = "person"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
data = Column(JSON)
|
||||||
|
|
||||||
|
birthday = index_property("data", "birthday")
|
||||||
|
year = index_property("birthday", "year")
|
||||||
|
month = index_property("birthday", "month")
|
||||||
|
day = index_property("birthday", "day")
|
||||||
|
|
||||||
|
Above, a query such as::
|
||||||
|
|
||||||
|
q = session.query(Person).filter(Person.year == "1980")
|
||||||
|
|
||||||
|
On a PostgreSQL backend, the above query will render as:
|
||||||
|
|
||||||
|
.. sourcecode:: sql
|
||||||
|
|
||||||
|
SELECT person.id, person.data
|
||||||
|
FROM person
|
||||||
|
WHERE person.data -> %(data_1)s -> %(param_1)s = %(param_2)s
|
||||||
|
|
||||||
|
Default Values
|
||||||
|
==============
|
||||||
|
|
||||||
|
:class:`.index_property` includes special behaviors for when the indexed
|
||||||
|
data structure does not exist, and a set operation is called:
|
||||||
|
|
||||||
|
* For an :class:`.index_property` that is given an integer index value,
|
||||||
|
the default data structure will be a Python list of ``None`` values,
|
||||||
|
at least as long as the index value; the value is then set at its
|
||||||
|
place in the list. This means for an index value of zero, the list
|
||||||
|
will be initialized to ``[None]`` before setting the given value,
|
||||||
|
and for an index value of five, the list will be initialized to
|
||||||
|
``[None, None, None, None, None]`` before setting the fifth element
|
||||||
|
to the given value. Note that an existing list is **not** extended
|
||||||
|
in place to receive a value.
|
||||||
|
|
||||||
|
* for an :class:`.index_property` that is given any other kind of index
|
||||||
|
value (e.g. strings usually), a Python dictionary is used as the
|
||||||
|
default data structure.
|
||||||
|
|
||||||
|
* The default data structure can be set to any Python callable using the
|
||||||
|
:paramref:`.index_property.datatype` parameter, overriding the previous
|
||||||
|
rules.
|
||||||
|
|
||||||
|
|
||||||
|
Subclassing
|
||||||
|
===========
|
||||||
|
|
||||||
|
:class:`.index_property` can be subclassed, in particular for the common
|
||||||
|
use case of providing coercion of values or SQL expressions as they are
|
||||||
|
accessed. Below is a common recipe for use with a PostgreSQL JSON type,
|
||||||
|
where we want to also include automatic casting plus ``astext()``::
|
||||||
|
|
||||||
|
class pg_json_property(index_property):
|
||||||
|
def __init__(self, attr_name, index, cast_type):
|
||||||
|
super(pg_json_property, self).__init__(attr_name, index)
|
||||||
|
self.cast_type = cast_type
|
||||||
|
|
||||||
|
def expr(self, model):
|
||||||
|
expr = super(pg_json_property, self).expr(model)
|
||||||
|
return expr.astext.cast(self.cast_type)
|
||||||
|
|
||||||
|
The above subclass can be used with the PostgreSQL-specific
|
||||||
|
version of :class:`_postgresql.JSON`::
|
||||||
|
|
||||||
|
from sqlalchemy import Column, Integer
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.dialects.postgresql import JSON
|
||||||
|
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
|
||||||
|
class Person(Base):
|
||||||
|
__tablename__ = "person"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
data = Column(JSON)
|
||||||
|
|
||||||
|
age = pg_json_property("data", "age", Integer)
|
||||||
|
|
||||||
|
The ``age`` attribute at the instance level works as before; however
|
||||||
|
when rendering SQL, PostgreSQL's ``->>`` operator will be used
|
||||||
|
for indexed access, instead of the usual index operator of ``->``::
|
||||||
|
|
||||||
|
>>> query = session.query(Person).filter(Person.age < 20)
|
||||||
|
|
||||||
|
The above query will render:
|
||||||
|
|
||||||
|
.. sourcecode:: sql
|
||||||
|
|
||||||
|
SELECT person.id, person.data
|
||||||
|
FROM person
|
||||||
|
WHERE CAST(person.data ->> %(data_1)s AS INTEGER) < %(param_1)s
|
||||||
|
|
||||||
|
""" # noqa
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
from typing import Callable
|
||||||
|
from typing import cast
|
||||||
|
from typing import Optional
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
from typing import TypeVar
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
from .. import inspect
|
||||||
|
from ..ext.hybrid import hybrid_property
|
||||||
|
from ..orm.attributes import flag_modified
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..sql import SQLColumnExpression
|
||||||
|
from ..sql._typing import _HasClauseElement
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["index_property"]
|
||||||
|
|
||||||
|
_T = TypeVar("_T")
|
||||||
|
|
||||||
|
|
||||||
|
class index_property(hybrid_property[_T]):
|
||||||
|
"""A property generator. The generated property describes an object
|
||||||
|
attribute that corresponds to an :class:`_types.Indexable`
|
||||||
|
column.
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
:mod:`sqlalchemy.ext.indexable`
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
_NO_DEFAULT_ARGUMENT = cast(_T, object())
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
attr_name: str,
|
||||||
|
index: Union[int, str],
|
||||||
|
default: _T = _NO_DEFAULT_ARGUMENT,
|
||||||
|
datatype: Optional[Callable[[], Any]] = None,
|
||||||
|
mutable: bool = True,
|
||||||
|
onebased: bool = True,
|
||||||
|
):
|
||||||
|
"""Create a new :class:`.index_property`.
|
||||||
|
|
||||||
|
:param attr_name:
|
||||||
|
An attribute name of an `Indexable` typed column, or other
|
||||||
|
attribute that returns an indexable structure.
|
||||||
|
:param index:
|
||||||
|
The index to be used for getting and setting this value. This
|
||||||
|
should be the Python-side index value for integers.
|
||||||
|
:param default:
|
||||||
|
A value which will be returned instead of `AttributeError`
|
||||||
|
when there is not a value at given index.
|
||||||
|
:param datatype: default datatype to use when the field is empty.
|
||||||
|
By default, this is derived from the type of index used; a
|
||||||
|
Python list for an integer index, or a Python dictionary for
|
||||||
|
any other style of index. For a list, the list will be
|
||||||
|
initialized to a list of None values that is at least
|
||||||
|
``index`` elements long.
|
||||||
|
:param mutable: if False, writes and deletes to the attribute will
|
||||||
|
be disallowed.
|
||||||
|
:param onebased: assume the SQL representation of this value is
|
||||||
|
one-based; that is, the first index in SQL is 1, not zero.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if mutable:
|
||||||
|
super().__init__(self.fget, self.fset, self.fdel, self.expr)
|
||||||
|
else:
|
||||||
|
super().__init__(self.fget, None, None, self.expr)
|
||||||
|
self.attr_name = attr_name
|
||||||
|
self.index = index
|
||||||
|
self.default = default
|
||||||
|
is_numeric = isinstance(index, int)
|
||||||
|
onebased = is_numeric and onebased
|
||||||
|
|
||||||
|
if datatype is not None:
|
||||||
|
self.datatype = datatype
|
||||||
|
else:
|
||||||
|
if is_numeric:
|
||||||
|
self.datatype = lambda: [None for x in range(index + 1)] # type: ignore[operator] # noqa: E501
|
||||||
|
else:
|
||||||
|
self.datatype = dict
|
||||||
|
self.onebased = onebased
|
||||||
|
|
||||||
|
def _fget_default(self, err: Optional[BaseException] = None) -> _T:
|
||||||
|
if self.default == self._NO_DEFAULT_ARGUMENT:
|
||||||
|
raise AttributeError(self.attr_name) from err
|
||||||
|
else:
|
||||||
|
return self.default
|
||||||
|
|
||||||
|
def fget(self, __instance: Any) -> _T:
|
||||||
|
attr_name = self.attr_name
|
||||||
|
column_value = getattr(__instance, attr_name)
|
||||||
|
if column_value is None:
|
||||||
|
return self._fget_default()
|
||||||
|
try:
|
||||||
|
value = column_value[self.index]
|
||||||
|
except (KeyError, IndexError) as err:
|
||||||
|
return self._fget_default(err)
|
||||||
|
else:
|
||||||
|
return value # type: ignore[no-any-return]
|
||||||
|
|
||||||
|
def fset(self, instance: Any, value: _T) -> None:
|
||||||
|
attr_name = self.attr_name
|
||||||
|
column_value = getattr(instance, attr_name, None)
|
||||||
|
if column_value is None:
|
||||||
|
column_value = self.datatype()
|
||||||
|
setattr(instance, attr_name, column_value)
|
||||||
|
column_value[self.index] = value
|
||||||
|
setattr(instance, attr_name, column_value)
|
||||||
|
if attr_name in inspect(instance).mapper.attrs:
|
||||||
|
flag_modified(instance, attr_name)
|
||||||
|
|
||||||
|
def fdel(self, instance: Any) -> None:
|
||||||
|
attr_name = self.attr_name
|
||||||
|
column_value = getattr(instance, attr_name)
|
||||||
|
if column_value is None:
|
||||||
|
raise AttributeError(self.attr_name)
|
||||||
|
try:
|
||||||
|
del column_value[self.index]
|
||||||
|
except KeyError as err:
|
||||||
|
raise AttributeError(self.attr_name) from err
|
||||||
|
else:
|
||||||
|
setattr(instance, attr_name, column_value)
|
||||||
|
flag_modified(instance, attr_name)
|
||||||
|
|
||||||
|
def expr(
|
||||||
|
self, model: Any
|
||||||
|
) -> Union[_HasClauseElement[_T], SQLColumnExpression[_T]]:
|
||||||
|
column = getattr(model, self.attr_name)
|
||||||
|
index = self.index
|
||||||
|
if self.onebased:
|
||||||
|
index += 1 # type: ignore[operator]
|
||||||
|
return column[index] # type: ignore[no-any-return]
|
||||||
450
venv/Lib/site-packages/sqlalchemy/ext/instrumentation.py
Normal file
450
venv/Lib/site-packages/sqlalchemy/ext/instrumentation.py
Normal file
@@ -0,0 +1,450 @@
|
|||||||
|
# ext/instrumentation.py
|
||||||
|
# Copyright (C) 2005-2026 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
|
||||||
|
# mypy: ignore-errors
|
||||||
|
|
||||||
|
"""Extensible class instrumentation.
|
||||||
|
|
||||||
|
The :mod:`sqlalchemy.ext.instrumentation` package provides for alternate
|
||||||
|
systems of class instrumentation within the ORM. Class instrumentation
|
||||||
|
refers to how the ORM places attributes on the class which maintain
|
||||||
|
data and track changes to that data, as well as event hooks installed
|
||||||
|
on the class.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
The extension package is provided for the benefit of integration
|
||||||
|
with other object management packages, which already perform
|
||||||
|
their own instrumentation. It is not intended for general use.
|
||||||
|
|
||||||
|
For examples of how the instrumentation extension is used,
|
||||||
|
see the example :ref:`examples_instrumentation`.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import weakref
|
||||||
|
|
||||||
|
from .. import util
|
||||||
|
from ..orm import attributes
|
||||||
|
from ..orm import base as orm_base
|
||||||
|
from ..orm import collections
|
||||||
|
from ..orm import exc as orm_exc
|
||||||
|
from ..orm import instrumentation as orm_instrumentation
|
||||||
|
from ..orm import util as orm_util
|
||||||
|
from ..orm.instrumentation import _default_dict_getter
|
||||||
|
from ..orm.instrumentation import _default_manager_getter
|
||||||
|
from ..orm.instrumentation import _default_opt_manager_getter
|
||||||
|
from ..orm.instrumentation import _default_state_getter
|
||||||
|
from ..orm.instrumentation import ClassManager
|
||||||
|
from ..orm.instrumentation import InstrumentationFactory
|
||||||
|
|
||||||
|
INSTRUMENTATION_MANAGER = "__sa_instrumentation_manager__"
|
||||||
|
"""Attribute, elects custom instrumentation when present on a mapped class.
|
||||||
|
|
||||||
|
Allows a class to specify a slightly or wildly different technique for
|
||||||
|
tracking changes made to mapped attributes and collections.
|
||||||
|
|
||||||
|
Only one instrumentation implementation is allowed in a given object
|
||||||
|
inheritance hierarchy.
|
||||||
|
|
||||||
|
The value of this attribute must be a callable and will be passed a class
|
||||||
|
object. The callable must return one of:
|
||||||
|
|
||||||
|
- An instance of an :class:`.InstrumentationManager` or subclass
|
||||||
|
- An object implementing all or some of InstrumentationManager (TODO)
|
||||||
|
- A dictionary of callables, implementing all or some of the above (TODO)
|
||||||
|
- An instance of a :class:`.ClassManager` or subclass
|
||||||
|
|
||||||
|
This attribute is consulted by SQLAlchemy instrumentation
|
||||||
|
resolution, once the :mod:`sqlalchemy.ext.instrumentation` module
|
||||||
|
has been imported. If custom finders are installed in the global
|
||||||
|
instrumentation_finders list, they may or may not choose to honor this
|
||||||
|
attribute.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def find_native_user_instrumentation_hook(cls):
|
||||||
|
"""Find user-specified instrumentation management for a class."""
|
||||||
|
return getattr(cls, INSTRUMENTATION_MANAGER, None)
|
||||||
|
|
||||||
|
|
||||||
|
instrumentation_finders = [find_native_user_instrumentation_hook]
|
||||||
|
"""An extensible sequence of callables which return instrumentation
|
||||||
|
implementations
|
||||||
|
|
||||||
|
When a class is registered, each callable will be passed a class object.
|
||||||
|
If None is returned, the
|
||||||
|
next finder in the sequence is consulted. Otherwise the return must be an
|
||||||
|
instrumentation factory that follows the same guidelines as
|
||||||
|
sqlalchemy.ext.instrumentation.INSTRUMENTATION_MANAGER.
|
||||||
|
|
||||||
|
By default, the only finder is find_native_user_instrumentation_hook, which
|
||||||
|
searches for INSTRUMENTATION_MANAGER. If all finders return None, standard
|
||||||
|
ClassManager instrumentation is used.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class ExtendedInstrumentationRegistry(InstrumentationFactory):
|
||||||
|
"""Extends :class:`.InstrumentationFactory` with additional
|
||||||
|
bookkeeping, to accommodate multiple types of
|
||||||
|
class managers.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
_manager_finders = weakref.WeakKeyDictionary()
|
||||||
|
_state_finders = weakref.WeakKeyDictionary()
|
||||||
|
_dict_finders = weakref.WeakKeyDictionary()
|
||||||
|
_extended = False
|
||||||
|
|
||||||
|
def _locate_extended_factory(self, class_):
|
||||||
|
for finder in instrumentation_finders:
|
||||||
|
factory = finder(class_)
|
||||||
|
if factory is not None:
|
||||||
|
manager = self._extended_class_manager(class_, factory)
|
||||||
|
return manager, factory
|
||||||
|
else:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
def _check_conflicts(self, class_, factory):
|
||||||
|
existing_factories = self._collect_management_factories_for(
|
||||||
|
class_
|
||||||
|
).difference([factory])
|
||||||
|
if existing_factories:
|
||||||
|
raise TypeError(
|
||||||
|
"multiple instrumentation implementations specified "
|
||||||
|
"in %s inheritance hierarchy: %r"
|
||||||
|
% (class_.__name__, list(existing_factories))
|
||||||
|
)
|
||||||
|
|
||||||
|
def _extended_class_manager(self, class_, factory):
|
||||||
|
manager = factory(class_)
|
||||||
|
if not isinstance(manager, ClassManager):
|
||||||
|
manager = _ClassInstrumentationAdapter(class_, manager)
|
||||||
|
|
||||||
|
if factory != ClassManager and not self._extended:
|
||||||
|
# somebody invoked a custom ClassManager.
|
||||||
|
# reinstall global "getter" functions with the more
|
||||||
|
# expensive ones.
|
||||||
|
self._extended = True
|
||||||
|
_install_instrumented_lookups()
|
||||||
|
|
||||||
|
self._manager_finders[class_] = manager.manager_getter()
|
||||||
|
self._state_finders[class_] = manager.state_getter()
|
||||||
|
self._dict_finders[class_] = manager.dict_getter()
|
||||||
|
return manager
|
||||||
|
|
||||||
|
def _collect_management_factories_for(self, cls):
|
||||||
|
"""Return a collection of factories in play or specified for a
|
||||||
|
hierarchy.
|
||||||
|
|
||||||
|
Traverses the entire inheritance graph of a cls and returns a
|
||||||
|
collection of instrumentation factories for those classes. Factories
|
||||||
|
are extracted from active ClassManagers, if available, otherwise
|
||||||
|
instrumentation_finders is consulted.
|
||||||
|
|
||||||
|
"""
|
||||||
|
hierarchy = util.class_hierarchy(cls)
|
||||||
|
factories = set()
|
||||||
|
for member in hierarchy:
|
||||||
|
manager = self.opt_manager_of_class(member)
|
||||||
|
if manager is not None:
|
||||||
|
factories.add(manager.factory)
|
||||||
|
else:
|
||||||
|
for finder in instrumentation_finders:
|
||||||
|
factory = finder(member)
|
||||||
|
if factory is not None:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
factory = None
|
||||||
|
factories.add(factory)
|
||||||
|
factories.discard(None)
|
||||||
|
return factories
|
||||||
|
|
||||||
|
def unregister(self, class_):
|
||||||
|
super().unregister(class_)
|
||||||
|
if class_ in self._manager_finders:
|
||||||
|
del self._manager_finders[class_]
|
||||||
|
del self._state_finders[class_]
|
||||||
|
del self._dict_finders[class_]
|
||||||
|
|
||||||
|
def opt_manager_of_class(self, cls):
|
||||||
|
try:
|
||||||
|
finder = self._manager_finders.get(
|
||||||
|
cls, _default_opt_manager_getter
|
||||||
|
)
|
||||||
|
except TypeError:
|
||||||
|
# due to weakref lookup on invalid object
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return finder(cls)
|
||||||
|
|
||||||
|
def manager_of_class(self, cls):
|
||||||
|
try:
|
||||||
|
finder = self._manager_finders.get(cls, _default_manager_getter)
|
||||||
|
except TypeError:
|
||||||
|
# due to weakref lookup on invalid object
|
||||||
|
raise orm_exc.UnmappedClassError(
|
||||||
|
cls, f"Can't locate an instrumentation manager for class {cls}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
manager = finder(cls)
|
||||||
|
if manager is None:
|
||||||
|
raise orm_exc.UnmappedClassError(
|
||||||
|
cls,
|
||||||
|
f"Can't locate an instrumentation manager for class {cls}",
|
||||||
|
)
|
||||||
|
return manager
|
||||||
|
|
||||||
|
def state_of(self, instance):
|
||||||
|
if instance is None:
|
||||||
|
raise AttributeError("None has no persistent state.")
|
||||||
|
return self._state_finders.get(
|
||||||
|
instance.__class__, _default_state_getter
|
||||||
|
)(instance)
|
||||||
|
|
||||||
|
def dict_of(self, instance):
|
||||||
|
if instance is None:
|
||||||
|
raise AttributeError("None has no persistent state.")
|
||||||
|
return self._dict_finders.get(
|
||||||
|
instance.__class__, _default_dict_getter
|
||||||
|
)(instance)
|
||||||
|
|
||||||
|
|
||||||
|
orm_instrumentation._instrumentation_factory = _instrumentation_factory = (
|
||||||
|
ExtendedInstrumentationRegistry()
|
||||||
|
)
|
||||||
|
orm_instrumentation.instrumentation_finders = instrumentation_finders
|
||||||
|
|
||||||
|
|
||||||
|
class InstrumentationManager:
|
||||||
|
"""User-defined class instrumentation extension.
|
||||||
|
|
||||||
|
:class:`.InstrumentationManager` can be subclassed in order
|
||||||
|
to change
|
||||||
|
how class instrumentation proceeds. This class exists for
|
||||||
|
the purposes of integration with other object management
|
||||||
|
frameworks which would like to entirely modify the
|
||||||
|
instrumentation methodology of the ORM, and is not intended
|
||||||
|
for regular usage. For interception of class instrumentation
|
||||||
|
events, see :class:`.InstrumentationEvents`.
|
||||||
|
|
||||||
|
The API for this class should be considered as semi-stable,
|
||||||
|
and may change slightly with new releases.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# r4361 added a mandatory (cls) constructor to this interface.
|
||||||
|
# given that, perhaps class_ should be dropped from all of these
|
||||||
|
# signatures.
|
||||||
|
|
||||||
|
def __init__(self, class_):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def manage(self, class_, manager):
|
||||||
|
setattr(class_, "_default_class_manager", manager)
|
||||||
|
|
||||||
|
def unregister(self, class_, manager):
|
||||||
|
delattr(class_, "_default_class_manager")
|
||||||
|
|
||||||
|
def manager_getter(self, class_):
|
||||||
|
def get(cls):
|
||||||
|
return cls._default_class_manager
|
||||||
|
|
||||||
|
return get
|
||||||
|
|
||||||
|
def instrument_attribute(self, class_, key, inst):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def post_configure_attribute(self, class_, key, inst):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def install_descriptor(self, class_, key, inst):
|
||||||
|
setattr(class_, key, inst)
|
||||||
|
|
||||||
|
def uninstall_descriptor(self, class_, key):
|
||||||
|
delattr(class_, key)
|
||||||
|
|
||||||
|
def install_member(self, class_, key, implementation):
|
||||||
|
setattr(class_, key, implementation)
|
||||||
|
|
||||||
|
def uninstall_member(self, class_, key):
|
||||||
|
delattr(class_, key)
|
||||||
|
|
||||||
|
def instrument_collection_class(self, class_, key, collection_class):
|
||||||
|
return collections.prepare_instrumentation(collection_class)
|
||||||
|
|
||||||
|
def get_instance_dict(self, class_, instance):
|
||||||
|
return instance.__dict__
|
||||||
|
|
||||||
|
def initialize_instance_dict(self, class_, instance):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def install_state(self, class_, instance, state):
|
||||||
|
setattr(instance, "_default_state", state)
|
||||||
|
|
||||||
|
def remove_state(self, class_, instance):
|
||||||
|
delattr(instance, "_default_state")
|
||||||
|
|
||||||
|
def state_getter(self, class_):
|
||||||
|
return lambda instance: getattr(instance, "_default_state")
|
||||||
|
|
||||||
|
def dict_getter(self, class_):
|
||||||
|
return lambda inst: self.get_instance_dict(class_, inst)
|
||||||
|
|
||||||
|
|
||||||
|
class _ClassInstrumentationAdapter(ClassManager):
|
||||||
|
"""Adapts a user-defined InstrumentationManager to a ClassManager."""
|
||||||
|
|
||||||
|
def __init__(self, class_, override):
|
||||||
|
self._adapted = override
|
||||||
|
self._get_state = self._adapted.state_getter(class_)
|
||||||
|
self._get_dict = self._adapted.dict_getter(class_)
|
||||||
|
|
||||||
|
ClassManager.__init__(self, class_)
|
||||||
|
|
||||||
|
def manage(self):
|
||||||
|
self._adapted.manage(self.class_, self)
|
||||||
|
|
||||||
|
def unregister(self):
|
||||||
|
self._adapted.unregister(self.class_, self)
|
||||||
|
|
||||||
|
def manager_getter(self):
|
||||||
|
return self._adapted.manager_getter(self.class_)
|
||||||
|
|
||||||
|
def instrument_attribute(self, key, inst, propagated=False):
|
||||||
|
ClassManager.instrument_attribute(self, key, inst, propagated)
|
||||||
|
if not propagated:
|
||||||
|
self._adapted.instrument_attribute(self.class_, key, inst)
|
||||||
|
|
||||||
|
def post_configure_attribute(self, key):
|
||||||
|
super().post_configure_attribute(key)
|
||||||
|
self._adapted.post_configure_attribute(self.class_, key, self[key])
|
||||||
|
|
||||||
|
def install_descriptor(self, key, inst):
|
||||||
|
self._adapted.install_descriptor(self.class_, key, inst)
|
||||||
|
|
||||||
|
def uninstall_descriptor(self, key):
|
||||||
|
self._adapted.uninstall_descriptor(self.class_, key)
|
||||||
|
|
||||||
|
def install_member(self, key, implementation):
|
||||||
|
self._adapted.install_member(self.class_, key, implementation)
|
||||||
|
|
||||||
|
def uninstall_member(self, key):
|
||||||
|
self._adapted.uninstall_member(self.class_, key)
|
||||||
|
|
||||||
|
def instrument_collection_class(self, key, collection_class):
|
||||||
|
return self._adapted.instrument_collection_class(
|
||||||
|
self.class_, key, collection_class
|
||||||
|
)
|
||||||
|
|
||||||
|
def initialize_collection(self, key, state, factory):
|
||||||
|
delegate = getattr(self._adapted, "initialize_collection", None)
|
||||||
|
if delegate:
|
||||||
|
return delegate(key, state, factory)
|
||||||
|
else:
|
||||||
|
return ClassManager.initialize_collection(
|
||||||
|
self, key, state, factory
|
||||||
|
)
|
||||||
|
|
||||||
|
def new_instance(self, state=None):
|
||||||
|
instance = self.class_.__new__(self.class_)
|
||||||
|
self.setup_instance(instance, state)
|
||||||
|
return instance
|
||||||
|
|
||||||
|
def _new_state_if_none(self, instance):
|
||||||
|
"""Install a default InstanceState if none is present.
|
||||||
|
|
||||||
|
A private convenience method used by the __init__ decorator.
|
||||||
|
"""
|
||||||
|
if self.has_state(instance):
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return self.setup_instance(instance)
|
||||||
|
|
||||||
|
def setup_instance(self, instance, state=None):
|
||||||
|
self._adapted.initialize_instance_dict(self.class_, instance)
|
||||||
|
|
||||||
|
if state is None:
|
||||||
|
state = self._state_constructor(instance, self)
|
||||||
|
|
||||||
|
# the given instance is assumed to have no state
|
||||||
|
self._adapted.install_state(self.class_, instance, state)
|
||||||
|
return state
|
||||||
|
|
||||||
|
def teardown_instance(self, instance):
|
||||||
|
self._adapted.remove_state(self.class_, instance)
|
||||||
|
|
||||||
|
def has_state(self, instance):
|
||||||
|
try:
|
||||||
|
self._get_state(instance)
|
||||||
|
except orm_exc.NO_STATE:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def state_getter(self):
|
||||||
|
return self._get_state
|
||||||
|
|
||||||
|
def dict_getter(self):
|
||||||
|
return self._get_dict
|
||||||
|
|
||||||
|
|
||||||
|
def _install_instrumented_lookups():
|
||||||
|
"""Replace global class/object management functions
|
||||||
|
with ExtendedInstrumentationRegistry implementations, which
|
||||||
|
allow multiple types of class managers to be present,
|
||||||
|
at the cost of performance.
|
||||||
|
|
||||||
|
This function is called only by ExtendedInstrumentationRegistry
|
||||||
|
and unit tests specific to this behavior.
|
||||||
|
|
||||||
|
The _reinstall_default_lookups() function can be called
|
||||||
|
after this one to re-establish the default functions.
|
||||||
|
|
||||||
|
"""
|
||||||
|
_install_lookups(
|
||||||
|
dict(
|
||||||
|
instance_state=_instrumentation_factory.state_of,
|
||||||
|
instance_dict=_instrumentation_factory.dict_of,
|
||||||
|
manager_of_class=_instrumentation_factory.manager_of_class,
|
||||||
|
opt_manager_of_class=_instrumentation_factory.opt_manager_of_class,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _reinstall_default_lookups():
|
||||||
|
"""Restore simplified lookups."""
|
||||||
|
_install_lookups(
|
||||||
|
dict(
|
||||||
|
instance_state=_default_state_getter,
|
||||||
|
instance_dict=_default_dict_getter,
|
||||||
|
manager_of_class=_default_manager_getter,
|
||||||
|
opt_manager_of_class=_default_opt_manager_getter,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
_instrumentation_factory._extended = False
|
||||||
|
|
||||||
|
|
||||||
|
def _install_lookups(lookups):
|
||||||
|
global instance_state, instance_dict
|
||||||
|
global manager_of_class, opt_manager_of_class
|
||||||
|
instance_state = lookups["instance_state"]
|
||||||
|
instance_dict = lookups["instance_dict"]
|
||||||
|
manager_of_class = lookups["manager_of_class"]
|
||||||
|
opt_manager_of_class = lookups["opt_manager_of_class"]
|
||||||
|
orm_base.instance_state = attributes.instance_state = (
|
||||||
|
orm_instrumentation.instance_state
|
||||||
|
) = instance_state
|
||||||
|
orm_base.instance_dict = attributes.instance_dict = (
|
||||||
|
orm_instrumentation.instance_dict
|
||||||
|
) = instance_dict
|
||||||
|
orm_base.manager_of_class = attributes.manager_of_class = (
|
||||||
|
orm_instrumentation.manager_of_class
|
||||||
|
) = manager_of_class
|
||||||
|
orm_base.opt_manager_of_class = orm_util.opt_manager_of_class = (
|
||||||
|
attributes.opt_manager_of_class
|
||||||
|
) = orm_instrumentation.opt_manager_of_class = opt_manager_of_class
|
||||||
1085
venv/Lib/site-packages/sqlalchemy/ext/mutable.py
Normal file
1085
venv/Lib/site-packages/sqlalchemy/ext/mutable.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user