From 9f5f50ba546da655e21ef6126100bfbda83c1cee Mon Sep 17 00:00:00 2001 From: Jim Crist-Harif Date: Tue, 17 Oct 2023 00:01:41 -0500 Subject: [PATCH] Fix bug handling default values for frozen & slotted dataclasses Previously there was a bug where dataclasses or attrs classes with `slots=True, frozen=True` wouldn't be able to properly use default values for optional fields. --- msgspec/_core.c | 2 +- tests/test_common.py | 15 +++++++++------ tests/test_convert.py | 10 ++++++---- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/msgspec/_core.c b/msgspec/_core.c index 1582695f..6403d398 100644 --- a/msgspec/_core.c +++ b/msgspec/_core.c @@ -8049,7 +8049,7 @@ DataclassInfo_post_decode(DataclassInfo *self, PyObject *obj, PathNode *path) { default_value = CALL_NO_ARGS(default_value); if (default_value == NULL) return -1; } - int status = PyObject_SetAttr(obj, name, default_value); + int status = PyObject_GenericSetAttr(obj, name, default_value); if (is_factory) { Py_DECREF(default_value); } diff --git a/tests/test_common.py b/tests/test_common.py index 4daa23a4..d422ad44 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -2694,8 +2694,9 @@ class Example: ): dec.decode(proto.encode({"a": "bad"})) + @pytest.mark.parametrize("frozen", [False, True]) @pytest.mark.parametrize("slots", [False, True]) - def test_decode_dataclass_defaults(self, proto, slots): + def test_decode_dataclass_defaults(self, proto, frozen, slots): if slots: if not PY310: pytest.skip(reason="Python 3.10+ required") @@ -2703,7 +2704,7 @@ def test_decode_dataclass_defaults(self, proto, slots): else: kws = {} - @dataclass(**kws) + @dataclass(frozen=frozen, **kws) class Example: a: int b: int @@ -2904,9 +2905,10 @@ class Example: ): dec.decode(proto.encode({"a": "bad"})) + @pytest.mark.parametrize("frozen", [False, True]) @pytest.mark.parametrize("slots", [False, True]) - def test_decode_attrs_defaults(self, proto, slots): - @attrs.define(slots=slots) + def test_decode_attrs_defaults(self, proto, frozen, slots): + @attrs.define(frozen=frozen, slots=slots) class Example: a: int b: int @@ -2916,9 +2918,10 @@ class Example: dec = proto.Decoder(Example) for args in [(1, 2), (1, 2, 3), (1, 2, 3, 4), (1, 2, 3, 4, 5)]: - msg = Example(*args) + sol = Example(*args) + msg = dict(zip("abcde", args)) res = dec.decode(proto.encode(msg)) - assert res == msg + assert res == sol # Missing fields error with pytest.raises(ValidationError, match="missing required field `a`"): diff --git a/tests/test_convert.py b/tests/test_convert.py index d1d9f7b0..9b862f9f 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -1388,9 +1388,10 @@ class Ex: assert convert(msg, Ex, from_attributes=True) == Ex(1) + @pytest.mark.parametrize("frozen", [False, True]) @pytest.mark.parametrize("slots", [False, True]) @mapcls_and_from_attributes - def test_dataclass_defaults(self, slots, mapcls, from_attributes): + def test_dataclass_defaults(self, frozen, slots, mapcls, from_attributes): if slots: if not PY310: pytest.skip(reason="Python 3.10+ required") @@ -1398,7 +1399,7 @@ def test_dataclass_defaults(self, slots, mapcls, from_attributes): else: kws = {} - @dataclass(**kws) + @dataclass(frozen=frozen, **kws) class Example: a: int b: int @@ -1557,10 +1558,11 @@ class Ex: assert convert(msg, Ex, from_attributes=True) == Ex(1) + @pytest.mark.parametrize("frozen", [False, True]) @pytest.mark.parametrize("slots", [False, True]) @mapcls_and_from_attributes - def test_attrs_defaults(self, slots, mapcls, from_attributes): - @attrs.define(slots=slots) + def test_attrs_defaults(self, frozen, slots, mapcls, from_attributes): + @attrs.define(frozen=frozen, slots=slots) class Example: a: int b: int