]> 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:23:42 +0000 (14:23 -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

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 1a8c4e5557bb703a27d6da5e6366bd49239f132a..43ef093d614f1af9f3e2637eb2685e4e8128c9d3 100644 (file)
@@ -59,21 +59,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 (
@@ -178,6 +182,12 @@ class Row(BaseRow, collections_abc.Sequence):
 
     _default_key_style = KEY_INTEGER_ONLY
 
+    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`.
@@ -233,10 +243,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 e4f07a75808c7a1a6378fa348017961c8d42caaa..e5b1a0a269824e59cd6807d2342cca23e85639e1 100644 (file)
@@ -746,6 +746,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(