This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# ext/mutable.py
|
||||
# Copyright (C) 2005-2025 the SQLAlchemy authors and contributors
|
||||
# Copyright (C) 2005-2023 the SQLAlchemy authors and contributors
|
||||
# <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
@@ -21,7 +21,6 @@ JSON strings before being persisted::
|
||||
from sqlalchemy.types import TypeDecorator, VARCHAR
|
||||
import json
|
||||
|
||||
|
||||
class JSONEncodedDict(TypeDecorator):
|
||||
"Represents an immutable structure as a json-encoded string."
|
||||
|
||||
@@ -49,7 +48,6 @@ the :class:`.Mutable` mixin to a plain Python dictionary::
|
||||
|
||||
from sqlalchemy.ext.mutable import Mutable
|
||||
|
||||
|
||||
class MutableDict(Mutable, dict):
|
||||
@classmethod
|
||||
def coerce(cls, key, value):
|
||||
@@ -103,11 +101,9 @@ attribute. Such as, with classical table metadata::
|
||||
|
||||
from sqlalchemy import Table, Column, Integer
|
||||
|
||||
my_data = Table(
|
||||
"my_data",
|
||||
metadata,
|
||||
Column("id", Integer, primary_key=True),
|
||||
Column("data", MutableDict.as_mutable(JSONEncodedDict)),
|
||||
my_data = Table('my_data', metadata,
|
||||
Column('id', Integer, primary_key=True),
|
||||
Column('data', MutableDict.as_mutable(JSONEncodedDict))
|
||||
)
|
||||
|
||||
Above, :meth:`~.Mutable.as_mutable` returns an instance of ``JSONEncodedDict``
|
||||
@@ -119,17 +115,13 @@ mapping against the ``my_data`` table::
|
||||
from sqlalchemy.orm import Mapped
|
||||
from sqlalchemy.orm import mapped_column
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
class MyDataClass(Base):
|
||||
__tablename__ = "my_data"
|
||||
__tablename__ = 'my_data'
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
data: Mapped[dict[str, str]] = mapped_column(
|
||||
MutableDict.as_mutable(JSONEncodedDict)
|
||||
)
|
||||
data: Mapped[dict[str, str]] = mapped_column(MutableDict.as_mutable(JSONEncodedDict))
|
||||
|
||||
The ``MyDataClass.data`` member will now be notified of in place changes
|
||||
to its value.
|
||||
@@ -140,11 +132,11 @@ will flag the attribute as "dirty" on the parent object::
|
||||
>>> from sqlalchemy.orm import Session
|
||||
|
||||
>>> sess = Session(some_engine)
|
||||
>>> m1 = MyDataClass(data={"value1": "foo"})
|
||||
>>> m1 = MyDataClass(data={'value1':'foo'})
|
||||
>>> sess.add(m1)
|
||||
>>> sess.commit()
|
||||
|
||||
>>> m1.data["value1"] = "bar"
|
||||
>>> m1.data['value1'] = 'bar'
|
||||
>>> assert m1 in sess.dirty
|
||||
True
|
||||
|
||||
@@ -161,16 +153,15 @@ the need to declare it individually::
|
||||
|
||||
MutableDict.associate_with(JSONEncodedDict)
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
class MyDataClass(Base):
|
||||
__tablename__ = "my_data"
|
||||
__tablename__ = 'my_data'
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
data: Mapped[dict[str, str]] = mapped_column(JSONEncodedDict)
|
||||
|
||||
|
||||
Supporting Pickling
|
||||
--------------------
|
||||
|
||||
@@ -189,7 +180,7 @@ stream::
|
||||
class MyMutableType(Mutable):
|
||||
def __getstate__(self):
|
||||
d = self.__dict__.copy()
|
||||
d.pop("_parents", None)
|
||||
d.pop('_parents', None)
|
||||
return d
|
||||
|
||||
With our dictionary example, we need to return the contents of the dict itself
|
||||
@@ -222,18 +213,13 @@ from within the mutable extension::
|
||||
from sqlalchemy.orm import mapped_column
|
||||
from sqlalchemy import event
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
class MyDataClass(Base):
|
||||
__tablename__ = "my_data"
|
||||
__tablename__ = 'my_data'
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
data: Mapped[dict[str, str]] = mapped_column(
|
||||
MutableDict.as_mutable(JSONEncodedDict)
|
||||
)
|
||||
|
||||
data: Mapped[dict[str, str]] = mapped_column(MutableDict.as_mutable(JSONEncodedDict))
|
||||
|
||||
@event.listens_for(MyDataClass.data, "modified")
|
||||
def modified_json(instance, initiator):
|
||||
@@ -261,7 +247,6 @@ class introduced in :ref:`mapper_composite` to include
|
||||
import dataclasses
|
||||
from sqlalchemy.ext.mutable import MutableComposite
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Point(MutableComposite):
|
||||
x: int
|
||||
@@ -276,6 +261,7 @@ class introduced in :ref:`mapper_composite` to include
|
||||
# alert all parents to the change
|
||||
self.changed()
|
||||
|
||||
|
||||
The :class:`.MutableComposite` class makes use of class mapping events to
|
||||
automatically establish listeners for any usage of :func:`_orm.composite` that
|
||||
specifies our ``Point`` type. Below, when ``Point`` is mapped to the ``Vertex``
|
||||
@@ -285,7 +271,6 @@ objects to each of the ``Vertex.start`` and ``Vertex.end`` attributes::
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped
|
||||
from sqlalchemy.orm import composite, mapped_column
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
@@ -295,12 +280,8 @@ objects to each of the ``Vertex.start`` and ``Vertex.end`` attributes::
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
|
||||
start: Mapped[Point] = composite(
|
||||
mapped_column("x1"), mapped_column("y1")
|
||||
)
|
||||
end: Mapped[Point] = composite(
|
||||
mapped_column("x2"), mapped_column("y2")
|
||||
)
|
||||
start: Mapped[Point] = composite(mapped_column("x1"), mapped_column("y1"))
|
||||
end: Mapped[Point] = composite(mapped_column("x2"), mapped_column("y2"))
|
||||
|
||||
def __repr__(self):
|
||||
return f"Vertex(start={self.start}, end={self.end})"
|
||||
@@ -397,7 +378,6 @@ from weakref import WeakKeyDictionary
|
||||
from .. import event
|
||||
from .. import inspect
|
||||
from .. import types
|
||||
from .. import util
|
||||
from ..orm import Mapper
|
||||
from ..orm._typing import _ExternalEntityType
|
||||
from ..orm._typing import _O
|
||||
@@ -410,7 +390,6 @@ from ..orm.context import QueryContext
|
||||
from ..orm.decl_api import DeclarativeAttributeIntercept
|
||||
from ..orm.state import InstanceState
|
||||
from ..orm.unitofwork import UOWTransaction
|
||||
from ..sql._typing import _TypeEngineArgument
|
||||
from ..sql.base import SchemaEventTarget
|
||||
from ..sql.schema import Column
|
||||
from ..sql.type_api import TypeEngine
|
||||
@@ -524,7 +503,6 @@ class MutableBase:
|
||||
if val is not None:
|
||||
if coerce:
|
||||
val = cls.coerce(key, val)
|
||||
assert val is not None
|
||||
state.dict[key] = val
|
||||
val._parents[state] = key
|
||||
|
||||
@@ -659,7 +637,7 @@ class Mutable(MutableBase):
|
||||
event.listen(Mapper, "mapper_configured", listen_for_type)
|
||||
|
||||
@classmethod
|
||||
def as_mutable(cls, sqltype: _TypeEngineArgument[_T]) -> TypeEngine[_T]:
|
||||
def as_mutable(cls, sqltype: TypeEngine[_T]) -> TypeEngine[_T]:
|
||||
"""Associate a SQL type with this mutable Python type.
|
||||
|
||||
This establishes listeners that will detect ORM mappings against
|
||||
@@ -668,11 +646,9 @@ class Mutable(MutableBase):
|
||||
The type is returned, unconditionally as an instance, so that
|
||||
:meth:`.as_mutable` can be used inline::
|
||||
|
||||
Table(
|
||||
"mytable",
|
||||
metadata,
|
||||
Column("id", Integer, primary_key=True),
|
||||
Column("data", MyMutableType.as_mutable(PickleType)),
|
||||
Table('mytable', metadata,
|
||||
Column('id', Integer, primary_key=True),
|
||||
Column('data', MyMutableType.as_mutable(PickleType))
|
||||
)
|
||||
|
||||
Note that the returned type is always an instance, even if a class
|
||||
@@ -823,12 +799,15 @@ class MutableDict(Mutable, Dict[_KT, _VT]):
|
||||
@overload
|
||||
def setdefault(
|
||||
self: MutableDict[_KT, Optional[_T]], key: _KT, value: None = None
|
||||
) -> Optional[_T]: ...
|
||||
) -> Optional[_T]:
|
||||
...
|
||||
|
||||
@overload
|
||||
def setdefault(self, key: _KT, value: _VT) -> _VT: ...
|
||||
def setdefault(self, key: _KT, value: _VT) -> _VT:
|
||||
...
|
||||
|
||||
def setdefault(self, key: _KT, value: object = None) -> object: ...
|
||||
def setdefault(self, key: _KT, value: object = None) -> object:
|
||||
...
|
||||
|
||||
else:
|
||||
|
||||
@@ -849,14 +828,17 @@ class MutableDict(Mutable, Dict[_KT, _VT]):
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@overload
|
||||
def pop(self, __key: _KT) -> _VT: ...
|
||||
def pop(self, __key: _KT) -> _VT:
|
||||
...
|
||||
|
||||
@overload
|
||||
def pop(self, __key: _KT, __default: _VT | _T) -> _VT | _T: ...
|
||||
def pop(self, __key: _KT, __default: _VT | _T) -> _VT | _T:
|
||||
...
|
||||
|
||||
def pop(
|
||||
self, __key: _KT, __default: _VT | _T | None = None
|
||||
) -> _VT | _T: ...
|
||||
) -> _VT | _T:
|
||||
...
|
||||
|
||||
else:
|
||||
|
||||
@@ -927,10 +909,10 @@ class MutableList(Mutable, List[_T]):
|
||||
self[:] = state
|
||||
|
||||
def is_scalar(self, value: _T | Iterable[_T]) -> TypeGuard[_T]:
|
||||
return not util.is_non_string_iterable(value)
|
||||
return not isinstance(value, Iterable)
|
||||
|
||||
def is_iterable(self, value: _T | Iterable[_T]) -> TypeGuard[Iterable[_T]]:
|
||||
return util.is_non_string_iterable(value)
|
||||
return isinstance(value, Iterable)
|
||||
|
||||
def __setitem__(
|
||||
self, index: SupportsIndex | slice, value: _T | Iterable[_T]
|
||||
|
||||
Reference in New Issue
Block a user