From 82f79155e785beed0f9b10d5722d52dc5cf2ffdf Mon Sep 17 00:00:00 2001 From: Federico Caselli Date: Fri, 15 Aug 2025 00:05:07 +0200 Subject: [PATCH] restore functionality in list Fixed issue caused by an unwanted functional change while typing the :class:`.MutableList` class. This change also reverts all other functional changes done in the same change, commit ba0e508141206efc55cdab91df21c18e7dd63c80 Fixes: #12802 Change-Id: I007aa86aec881241ea42ce59d1b078cf8c6829bb (cherry picked from commit a93d18357f7080b8e52f4e02983b6e50e33212ed) --- doc/build/changelog/unreleased_20/12802.rst | 8 +++ lib/sqlalchemy/ext/mutable.py | 63 +++++++++------------ test/ext/test_mutable.py | 26 +++++++++ 3 files changed, 60 insertions(+), 37 deletions(-) create mode 100644 doc/build/changelog/unreleased_20/12802.rst diff --git a/doc/build/changelog/unreleased_20/12802.rst b/doc/build/changelog/unreleased_20/12802.rst new file mode 100644 index 0000000000..752326b8af --- /dev/null +++ b/doc/build/changelog/unreleased_20/12802.rst @@ -0,0 +1,8 @@ +.. change:: + :tags: bug, ext + :tickets: 12802 + + Fixed issue caused by an unwanted functional change while typing + the :class:`.MutableList` class. + This change also reverts all other functional changes done in + the same change. diff --git a/lib/sqlalchemy/ext/mutable.py b/lib/sqlalchemy/ext/mutable.py index 3d568fc989..3a1fa4d6da 100644 --- a/lib/sqlalchemy/ext/mutable.py +++ b/lib/sqlalchemy/ext/mutable.py @@ -397,7 +397,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 @@ -416,7 +415,6 @@ from ..sql.schema import Column from ..sql.type_api import TypeEngine from ..util import memoized_property from ..util.typing import SupportsIndex -from ..util.typing import TypeGuard _KT = TypeVar("_KT") # Key type. _VT = TypeVar("_VT") # Value type. @@ -814,7 +812,7 @@ class MutableDict(Mutable, Dict[_KT, _VT]): def __setitem__(self, key: _KT, value: _VT) -> None: """Detect dictionary set events and emit change events.""" - super().__setitem__(key, value) + dict.__setitem__(self, key, value) self.changed() if TYPE_CHECKING: @@ -833,17 +831,17 @@ class MutableDict(Mutable, Dict[_KT, _VT]): else: def setdefault(self, *arg): # noqa: F811 - result = super().setdefault(*arg) + result = dict.setdefault(self, *arg) self.changed() return result def __delitem__(self, key: _KT) -> None: """Detect dictionary del events and emit change events.""" - super().__delitem__(key) + dict.__delitem__(self, key) self.changed() def update(self, *a: Any, **kw: _VT) -> None: - super().update(*a, **kw) + dict.update(self, *a, **kw) self.changed() if TYPE_CHECKING: @@ -861,17 +859,17 @@ class MutableDict(Mutable, Dict[_KT, _VT]): else: def pop(self, *arg): # noqa: F811 - result = super().pop(*arg) + result = dict.pop(self, *arg) self.changed() return result def popitem(self) -> Tuple[_KT, _VT]: - result = super().popitem() + result = dict.popitem(self) self.changed() return result def clear(self) -> None: - super().clear() + dict.clear(self) self.changed() @classmethod @@ -926,38 +924,29 @@ class MutableList(Mutable, List[_T]): def __setstate__(self, state: Iterable[_T]) -> None: self[:] = state - def is_scalar(self, value: _T | Iterable[_T]) -> TypeGuard[_T]: - return not util.is_non_string_iterable(value) - - def is_iterable(self, value: _T | Iterable[_T]) -> TypeGuard[Iterable[_T]]: - return util.is_non_string_iterable(value) - def __setitem__( self, index: SupportsIndex | slice, value: _T | Iterable[_T] ) -> None: """Detect list set events and emit change events.""" - if isinstance(index, SupportsIndex) and self.is_scalar(value): - super().__setitem__(index, value) - elif isinstance(index, slice) and self.is_iterable(value): - super().__setitem__(index, value) + list.__setitem__(self, index, value) self.changed() def __delitem__(self, index: SupportsIndex | slice) -> None: """Detect list del events and emit change events.""" - super().__delitem__(index) + list.__delitem__(self, index) self.changed() def pop(self, *arg: SupportsIndex) -> _T: - result = super().pop(*arg) + result = list.pop(self, *arg) self.changed() return result def append(self, x: _T) -> None: - super().append(x) + list.append(self, x) self.changed() def extend(self, x: Iterable[_T]) -> None: - super().extend(x) + list.extend(self, x) self.changed() def __iadd__(self, x: Iterable[_T]) -> MutableList[_T]: # type: ignore[override,misc] # noqa: E501 @@ -965,23 +954,23 @@ class MutableList(Mutable, List[_T]): return self def insert(self, i: SupportsIndex, x: _T) -> None: - super().insert(i, x) + list.insert(self, i, x) self.changed() def remove(self, i: _T) -> None: - super().remove(i) + list.remove(self, i) self.changed() def clear(self) -> None: - super().clear() + list.clear(self) self.changed() def sort(self, **kw: Any) -> None: - super().sort(**kw) + list.sort(self, **kw) self.changed() def reverse(self) -> None: - super().reverse() + list.reverse(self) self.changed() @classmethod @@ -1022,19 +1011,19 @@ class MutableSet(Mutable, Set[_T]): """ def update(self, *arg: Iterable[_T]) -> None: - super().update(*arg) + set.update(self, *arg) self.changed() def intersection_update(self, *arg: Iterable[Any]) -> None: - super().intersection_update(*arg) + set.intersection_update(self, *arg) self.changed() def difference_update(self, *arg: Iterable[Any]) -> None: - super().difference_update(*arg) + set.difference_update(self, *arg) self.changed() def symmetric_difference_update(self, *arg: Iterable[_T]) -> None: - super().symmetric_difference_update(*arg) + set.symmetric_difference_update(self, *arg) self.changed() def __ior__(self, other: AbstractSet[_T]) -> MutableSet[_T]: # type: ignore[override,misc] # noqa: E501 @@ -1054,24 +1043,24 @@ class MutableSet(Mutable, Set[_T]): return self def add(self, elem: _T) -> None: - super().add(elem) + set.add(self, elem) self.changed() def remove(self, elem: _T) -> None: - super().remove(elem) + set.remove(self, elem) self.changed() def discard(self, elem: _T) -> None: - super().discard(elem) + set.discard(self, elem) self.changed() def pop(self, *arg: Any) -> _T: - result = super().pop(*arg) + result = set.pop(self, *arg) self.changed() return result def clear(self) -> None: - super().clear() + set.clear(self) self.changed() @classmethod diff --git a/test/ext/test_mutable.py b/test/ext/test_mutable.py index 4237847778..e83550aecd 100644 --- a/test/ext/test_mutable.py +++ b/test/ext/test_mutable.py @@ -566,6 +566,11 @@ class _MutableListTestBase(_MutableListTestFixture): eq_(f1.data, ["three", "two"]) + # test 12802 + f1.data[1] = ["four", "two"] + sess.commit() + eq_(f1.data, ["three", ["four", "two"]]) + def test_in_place_slice_mutation_int(self): sess = fixture_session() @@ -578,6 +583,11 @@ class _MutableListTestBase(_MutableListTestFixture): eq_(f1.data, [1, 5, 6, 4]) + # test 12802 + f1.data[1:3] = [9, 8, 7] + sess.commit() + eq_(f1.data, [1, 9, 8, 7, 4]) + def test_in_place_slice_mutation_str(self): sess = fixture_session() @@ -680,6 +690,19 @@ class _MutableListTestBase(_MutableListTestFixture): eq_(f1.data, [1, 5, 2]) + def test_insert2(self): + # test #12802 + sess = fixture_session() + + f1 = Foo(data=[1, 2]) + sess.add(f1) + sess.commit() + + f1.data.insert(1, [4, 2]) + sess.commit() + + eq_(f1.data, [1, [4, 2], 2]) + def test_remove(self): sess = fixture_session() @@ -1270,6 +1293,9 @@ class MutableColumnCopyArrayTest(_MutableListTestBase, fixtures.MappedTest): def test_in_place_slice_mutation_str(self): """this test is hardcoded to integer, skip strings""" + def test_insert2(self): + """This test does not work with arrays, skip it""" + class MutableListWithScalarPickleTest( _MutableListTestBase, fixtures.MappedTest -- 2.47.3