summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormike bayer <mike_mp@zzzcomputing.com>2021-12-12 23:29:11 +0000
committerGerrit Code Review <gerrit@ci3.zzzcomputing.com>2021-12-12 23:29:11 +0000
commitf34542b37bfc60330a96891c2271d3f43f42b62f (patch)
treeb26ea67fd053c3b65c5fd71a2dadf4704167d2b2
parent04421c8bed9e93a625b7164e99eb1ee0395bebfe (diff)
parentf113e979219e20a22044c4b262e4531ba9993b8a (diff)
downloadsqlalchemy-f34542b37bfc60330a96891c2271d3f43f42b62f.tar.gz
Merge "implement correct errors for Row immutability" into main
-rw-r--r--doc/build/changelog/unreleased_14/7432.rst8
-rw-r--r--lib/sqlalchemy/engine/row.py37
-rw-r--r--test/sql/test_resultset.py74
3 files changed, 106 insertions, 13 deletions
diff --git a/doc/build/changelog/unreleased_14/7432.rst b/doc/build/changelog/unreleased_14/7432.rst
new file mode 100644
index 000000000..6e3f74c67
--- /dev/null
+++ b/doc/build/changelog/unreleased_14/7432.rst
@@ -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.
diff --git a/lib/sqlalchemy/engine/row.py b/lib/sqlalchemy/engine/row.py
index 1a8c4e555..43ef093d6 100644
--- a/lib/sqlalchemy/engine/row.py
+++ b/lib/sqlalchemy/engine/row.py
@@ -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 (
diff --git a/test/sql/test_resultset.py b/test/sql/test_resultset.py
index e4f07a758..e5b1a0a26 100644
--- a/test/sql/test_resultset.py
+++ b/test/sql/test_resultset.py
@@ -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(