From 248d232459e38561999c4172acaaddd651c1a933 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Thu, 4 Nov 2021 17:02:24 -0400 Subject: [PATCH] Check for Mapping explicitly in 2.0 params Fixed issue in future :class:`_future.Connection` object where the :meth:`_future.Connection.execute` method would not accept a non-dict mapping object, such as SQLAlchemy's own :class:`.RowMapping` or other ``abc.collections.Mapping`` object as a parameter dictionary. Fixes: #7291 Change-Id: I819f079d86d19d1d81c570e0680f987e51e34b84 --- doc/build/changelog/unreleased_14/7291.rst | 8 ++ lib/sqlalchemy/engine/util.py | 6 +- test/engine/test_execute.py | 104 +++++++++++++++++++++ 3 files changed, 115 insertions(+), 3 deletions(-) create mode 100644 doc/build/changelog/unreleased_14/7291.rst diff --git a/doc/build/changelog/unreleased_14/7291.rst b/doc/build/changelog/unreleased_14/7291.rst new file mode 100644 index 0000000000..add383ee86 --- /dev/null +++ b/doc/build/changelog/unreleased_14/7291.rst @@ -0,0 +1,8 @@ +.. change:: + :tags: bug, engine + :tickets: 7291 + + Fixed issue in future :class:`_future.Connection` object where the + :meth:`_future.Connection.execute` method would not accept a non-dict + mapping object, such as SQLAlchemy's own :class:`.RowMapping` or other + ``abc.collections.Mapping`` object as a parameter dictionary. diff --git a/lib/sqlalchemy/engine/util.py b/lib/sqlalchemy/engine/util.py index 4f2e031ab7..8eb0f18208 100644 --- a/lib/sqlalchemy/engine/util.py +++ b/lib/sqlalchemy/engine/util.py @@ -147,9 +147,9 @@ def _distill_params_20(params): elif isinstance( params, (tuple, dict, immutabledict), - # avoid abc.__instancecheck__ - # (collections_abc.Sequence, collections_abc.Mapping), - ): + # only do abc.__instancecheck__ for Mapping after we've checked + # for plain dictionaries and would otherwise raise + ) or isinstance(params, collections_abc.Mapping): return (params,), _no_kw else: raise exc.ArgumentError("mapping or sequence expected for parameters") diff --git a/test/engine/test_execute.py b/test/engine/test_execute.py index aeb6b48490..23df3b03d2 100644 --- a/test/engine/test_execute.py +++ b/test/engine/test_execute.py @@ -264,6 +264,58 @@ class ExecuteTest(fixtures.TablesTest): (4, "sally"), ] + def test_non_dict_mapping(self, connection): + """ensure arbitrary Mapping works for execute()""" + + class NotADict(collections_abc.Mapping): + def __init__(self, _data): + self._data = _data + + def __iter__(self): + return iter(self._data) + + def __len__(self): + return len(self._data) + + def __getitem__(self, key): + return self._data[key] + + def keys(self): + return self._data.keys() + + nd = NotADict({"a": 10, "b": 15}) + eq_(dict(nd), {"a": 10, "b": 15}) + + result = connection.execute( + select( + bindparam("a", type_=Integer), bindparam("b", type_=Integer) + ), + nd, + ) + eq_(result.first(), (10, 15)) + + def test_row_works_as_mapping(self, connection): + """ensure the RowMapping object works as a parameter dictionary for + execute.""" + + result = connection.execute( + select(literal(10).label("a"), literal(15).label("b")) + ) + row = result.first() + eq_(row, (10, 15)) + eq_(row._mapping, {"a": 10, "b": 15}) + + result = connection.execute( + select( + bindparam("a", type_=Integer).label("a"), + bindparam("b", type_=Integer).label("b"), + ), + row._mapping, + ) + row = result.first() + eq_(row, (10, 15)) + eq_(row._mapping, {"a": 10, "b": 15}) + def test_dialect_has_table_assertion(self): with expect_raises_message( tsa.exc.ArgumentError, @@ -3463,6 +3515,58 @@ class FutureExecuteTest(fixtures.FutureEngineMixin, fixtures.TablesTest): test_needs_acid=True, ) + def test_non_dict_mapping(self, connection): + """ensure arbitrary Mapping works for execute()""" + + class NotADict(collections_abc.Mapping): + def __init__(self, _data): + self._data = _data + + def __iter__(self): + return iter(self._data) + + def __len__(self): + return len(self._data) + + def __getitem__(self, key): + return self._data[key] + + def keys(self): + return self._data.keys() + + nd = NotADict({"a": 10, "b": 15}) + eq_(dict(nd), {"a": 10, "b": 15}) + + result = connection.execute( + select( + bindparam("a", type_=Integer), bindparam("b", type_=Integer) + ), + nd, + ) + eq_(result.first(), (10, 15)) + + def test_row_works_as_mapping(self, connection): + """ensure the RowMapping object works as a parameter dictionary for + execute.""" + + result = connection.execute( + select(literal(10).label("a"), literal(15).label("b")) + ) + row = result.first() + eq_(row, (10, 15)) + eq_(row._mapping, {"a": 10, "b": 15}) + + result = connection.execute( + select( + bindparam("a", type_=Integer).label("a"), + bindparam("b", type_=Integer).label("b"), + ), + row._mapping, + ) + row = result.first() + eq_(row, (10, 15)) + eq_(row._mapping, {"a": 10, "b": 15}) + @testing.combinations( ({}, {}, {}), ({"a": "b"}, {}, {"a": "b"}), -- 2.47.2