--- /dev/null
+.. change::
+ :tags: bug, orm
+ :tickets: 10784
+
+ Fixed 2.0 regression in :class:`.MutableList` where a routine that detects
+ sequences would not correctly filter out string or bytes instances, making
+ it impossible to assign a string value to a specific index (while
+ non-sequence values would work fine).
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
self[:] = state
def is_scalar(self, value: _T | Iterable[_T]) -> TypeGuard[_T]:
- return not isinstance(value, Iterable)
+ return not util.is_non_string_iterable(value)
def is_iterable(self, value: _T | Iterable[_T]) -> TypeGuard[Iterable[_T]]:
- return isinstance(value, Iterable)
+ return util.is_non_string_iterable(value)
def __setitem__(
self, index: SupportsIndex | slice, value: _T | Iterable[_T]
)
def _literal_coercion(self, element, expr, operator, **kw):
- if isinstance(element, collections_abc.Iterable) and not isinstance(
- element, str
- ):
+ if util.is_non_string_iterable(element):
non_literal_expressions: Dict[
Optional[operators.ColumnOperators],
operators.ColumnOperators,
from .langhelpers import warn_limited as warn_limited
from .langhelpers import wrap_callable as wrap_callable
from .preloaded import preload_module as preload_module
+from .typing import is_non_string_iterable as is_non_string_iterable
"""Collection classes and helpers."""
from __future__ import annotations
-import collections.abc as collections_abc
import operator
import threading
import types
import weakref
from ._has_cy import HAS_CYEXTENSION
+from .typing import is_non_string_iterable
from .typing import Literal
if typing.TYPE_CHECKING or not HAS_CYEXTENSION:
def to_list(x: Any, default: Optional[List[Any]] = None) -> List[Any]:
if x is None:
return default # type: ignore
- if not isinstance(x, collections_abc.Iterable) or isinstance(
- x, (str, bytes)
- ):
+ if not is_non_string_iterable(x):
return [x]
elif isinstance(x, list):
return x
from __future__ import annotations
import builtins
+import collections.abc as collections_abc
import re
import sys
from typing import Any
return type_ is not None and typing_get_origin(type_) is Annotated
+def is_non_string_iterable(obj: Any) -> TypeGuard[Iterable[Any]]:
+ return isinstance(obj, collections_abc.Iterable) and not isinstance(
+ obj, (str, bytes)
+ )
+
+
def is_literal(type_: _AnnotationScanType) -> bool:
return get_origin(type_) is Literal
import copy
+from decimal import Decimal
import inspect
from pathlib import Path
import pickle
from sqlalchemy.util import compat
from sqlalchemy.util import FastIntFlag
from sqlalchemy.util import get_callable_argspec
+from sqlalchemy.util import is_non_string_iterable
from sqlalchemy.util import langhelpers
from sqlalchemy.util import preloaded
from sqlalchemy.util import WeakSequence
return True
+class MiscTest(fixtures.TestBase):
+ @testing.combinations(
+ (["one", "two", "three"], True),
+ (("one", "two", "three"), True),
+ ((), True),
+ ("four", False),
+ (252, False),
+ (Decimal("252"), False),
+ (b"four", False),
+ (iter("four"), True),
+ (b"", False),
+ ("", False),
+ (None, False),
+ ({"dict": "value"}, True),
+ ({}, True),
+ ({"set", "two"}, True),
+ (set(), True),
+ (util.immutabledict(), True),
+ (util.immutabledict({"key": "value"}), True),
+ )
+ def test_non_string_iterable_check(self, fixture, expected):
+ is_(is_non_string_iterable(fixture), expected)
+
+
class IdentitySetTest(fixtures.TestBase):
obj_type = object
data={1, 2, 3},
)
- def test_in_place_mutation(self):
+ def test_in_place_mutation_int(self):
sess = fixture_session()
f1 = Foo(data=[1, 2])
eq_(f1.data, [3, 2])
- def test_in_place_slice_mutation(self):
+ def test_in_place_mutation_str(self):
+ sess = fixture_session()
+
+ f1 = Foo(data=["one", "two"])
+ sess.add(f1)
+ sess.commit()
+
+ f1.data[0] = "three"
+ sess.commit()
+
+ eq_(f1.data, ["three", "two"])
+
+ def test_in_place_slice_mutation_int(self):
sess = fixture_session()
f1 = Foo(data=[1, 2, 3, 4])
eq_(f1.data, [1, 5, 6, 4])
+ def test_in_place_slice_mutation_str(self):
+ sess = fixture_session()
+
+ f1 = Foo(data=["one", "two", "three", "four"])
+ sess.add(f1)
+ sess.commit()
+
+ f1.data[1:3] = "five", "six"
+ sess.commit()
+
+ eq_(f1.data, ["one", "five", "six", "four"])
+
def test_del_slice(self):
sess = fixture_session()
__tablename__ = "foo"
id = Column(Integer, primary_key=True)
+ def test_in_place_mutation_str(self):
+ """this test is hardcoded to integer, skip strings"""
+
+ def test_in_place_slice_mutation_str(self):
+ """this test is hardcoded to integer, skip strings"""
+
class MutableListWithScalarPickleTest(
_MutableListTestBase, fixtures.MappedTest