]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
implement correct errors for Row immutability
authorMike Bayer <mike_mp@zzzcomputing.com>
Thu, 9 Dec 2021 19:23:42 +0000 (14:23 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Thu, 9 Dec 2021 19:24:29 +0000 (14:24 -0500)
Corrected the error message for the ``AttributeError`` that's raised when
attempting to write to an attribute on the :class:`_result.Row` class,
which is immutable. The previous message claimed the column didn't exist
which is misleading.

Fixes: #7432
Change-Id: If0e2cbd3f763dca6c99a18aa42252c69f1207d59
(cherry picked from commit f113e979219e20a22044c4b262e4531ba9993b8a)

doc/build/changelog/unreleased_14/7432.rst [new file with mode: 0644]
lib/sqlalchemy/engine/row.py
test/sql/test_resultset.py

diff --git a/doc/build/changelog/unreleased_14/7432.rst b/doc/build/changelog/unreleased_14/7432.rst
new file mode 100644 (file)
index 0000000..6e3f74c
--- /dev/null
@@ -0,0 +1,8 @@
+.. change::
+    :tags: bug, engine
+    :tickets: 7432
+
+    Corrected the error message for the ``AttributeError`` that's raised when
+    attempting to write to an attribute on the :class:`_result.Row` class,
+    which is immutable. The previous message claimed the column didn't exist
+    which is misleading.
index dc11e354862c4d391924451cfc5796fb1e04bd7c..02fc560ca4d0a071d9e498f65d04b71404f84112 100644 (file)
@@ -66,21 +66,25 @@ except ImportError:
         def __init__(self, parent, processors, keymap, key_style, data):
             """Row objects are constructed by CursorResult objects."""
 
-            self._parent = parent
+            object.__setattr__(self, "_parent", parent)
 
             if processors:
-                self._data = tuple(
-                    [
-                        proc(value) if proc else value
-                        for proc, value in zip(processors, data)
-                    ]
+                object.__setattr__(
+                    self,
+                    "_data",
+                    tuple(
+                        [
+                            proc(value) if proc else value
+                            for proc, value in zip(processors, data)
+                        ]
+                    ),
                 )
             else:
-                self._data = tuple(data)
+                object.__setattr__(self, "_data", tuple(data))
 
-            self._keymap = keymap
+            object.__setattr__(self, "_keymap", keymap)
 
-            self._key_style = key_style
+            object.__setattr__(self, "_key_style", key_style)
 
         def __reduce__(self):
             return (
@@ -211,6 +215,12 @@ class Row(BaseRow, collections_abc.Sequence):
     # in 2.0, this should be KEY_INTEGER_ONLY
     _default_key_style = KEY_OBJECTS_BUT_WARN
 
+    def __setattr__(self, name, value):
+        raise AttributeError("can't set attribute")
+
+    def __delattr__(self, name):
+        raise AttributeError("can't delete attribute")
+
     @property
     def _mapping(self):
         """Return a :class:`.RowMapping` for this :class:`.Row`.
@@ -269,10 +279,11 @@ class Row(BaseRow, collections_abc.Sequence):
         }
 
     def __setstate__(self, state):
-        self._parent = parent = state["_parent"]
-        self._data = state["_data"]
-        self._keymap = parent._keymap
-        self._key_style = state["_key_style"]
+        parent = state["_parent"]
+        object.__setattr__(self, "_parent", parent)
+        object.__setattr__(self, "_data", state["_data"])
+        object.__setattr__(self, "_keymap", parent._keymap)
+        object.__setattr__(self, "_key_style", state["_key_style"])
 
     def _op(self, other, op):
         return (
index 89317b149ffbf0184ca708addaa57be555bd543a..088f580747449af1771d3d2fb8dcbb5e8cc1cee7 100644 (file)
@@ -637,6 +637,80 @@ class CursorResultTest(fixtures.TablesTest):
         eq_(r._mapping[users.c.user_name], "john")
         eq_(r.user_name, "john")
 
+    @testing.fixture
+    def _ab_row_fixture(self, connection):
+        r = connection.execute(
+            select(literal(1).label("a"), literal(2).label("b"))
+        ).first()
+        return r
+
+    def test_named_tuple_access(self, _ab_row_fixture):
+        r = _ab_row_fixture
+        eq_(r.a, 1)
+        eq_(r.b, 2)
+
+    def test_named_tuple_missing_attr(self, _ab_row_fixture):
+        r = _ab_row_fixture
+        with expect_raises_message(
+            AttributeError, "Could not locate column in row for column 'c'"
+        ):
+            r.c
+
+    def test_named_tuple_no_delete_present(self, _ab_row_fixture):
+        r = _ab_row_fixture
+        with expect_raises_message(AttributeError, "can't delete attribute"):
+            del r.a
+
+    def test_named_tuple_no_delete_missing(self, _ab_row_fixture):
+        r = _ab_row_fixture
+        # including for non-existent attributes
+        with expect_raises_message(AttributeError, "can't delete attribute"):
+            del r.c
+
+    def test_named_tuple_no_assign_present(self, _ab_row_fixture):
+        r = _ab_row_fixture
+        with expect_raises_message(AttributeError, "can't set attribute"):
+            r.a = 5
+
+        with expect_raises_message(AttributeError, "can't set attribute"):
+            r.a += 5
+
+    def test_named_tuple_no_assign_missing(self, _ab_row_fixture):
+        r = _ab_row_fixture
+        # including for non-existent attributes
+        with expect_raises_message(AttributeError, "can't set attribute"):
+            r.c = 5
+
+    def test_named_tuple_no_self_assign_missing(self, _ab_row_fixture):
+        r = _ab_row_fixture
+        with expect_raises_message(
+            AttributeError, "Could not locate column in row for column 'c'"
+        ):
+            r.c += 5
+
+    def test_mapping_tuple_readonly_errors(self, connection):
+        r = connection.execute(
+            select(literal(1).label("a"), literal(2).label("b"))
+        ).first()
+        r = r._mapping
+        eq_(r["a"], 1)
+        eq_(r["b"], 2)
+
+        with expect_raises_message(
+            KeyError, "Could not locate column in row for column 'c'"
+        ):
+            r["c"]
+
+        with expect_raises_message(
+            TypeError, "'RowMapping' object does not support item assignment"
+        ):
+            r["a"] = 5
+
+        with expect_raises_message(
+            TypeError, "'RowMapping' object does not support item assignment"
+        ):
+            r["a"] += 5
+
     def test_column_accessor_err(self, connection):
         r = connection.execute(select(1)).first()
         assert_raises_message(