Skip to content

Commit

Permalink
Add msgspec.structs.force_setattr
Browse files Browse the repository at this point in the history
This is an advanced (and potentially unsafe) function that allows for
mutating a frozen struct. The primary use case for this is mutating the
struct in a `__post_init__` method before returning. Most users
shouldn't ever use this.
  • Loading branch information
jcrist committed Dec 4, 2023
1 parent dea8056 commit 5593670
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 0 deletions.
2 changes: 2 additions & 0 deletions docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ Structs

.. autofunction:: msgspec.structs.astuple

.. autofunction:: msgspec.structs.force_setattr

.. autofunction:: msgspec.structs.fields

.. autoclass:: msgspec.structs.FieldInfo
Expand Down
43 changes: 43 additions & 0 deletions msgspec/_core.c
Original file line number Diff line number Diff line change
Expand Up @@ -7568,6 +7568,45 @@ struct_astuple(PyObject *self, PyObject *const *args, Py_ssize_t nargs)
return NULL;
}

PyDoc_STRVAR(struct_force_setattr__doc__,
"force_setattr(struct, name, value)\n"
"--\n"
"\n"
"Set an attribute on a struct, even if the struct is frozen.\n"
"\n"
"The main use case for this is modifying a frozen struct in a ``__post_init__``\n"
"method before returning.\n"
"\n"
".. warning::\n\n"
" This function violates the guarantees of a frozen struct, and is potentially\n"
" unsafe. Only use it if you know what you're doing!\n"
"\n"
"Parameters\n"
"----------\n"
"struct: Struct\n"
" The struct instance.\n"
"name: str\n"
" The attribute name.\n"
"value: Any\n"
" The attribute value."
);
static PyObject*
struct_force_setattr(PyObject *self, PyObject *const *args, Py_ssize_t nargs)
{
if (!check_positional_nargs(nargs, 3, 3)) return NULL;
PyObject *obj = args[0];
PyObject *name = args[1];
PyObject *value = args[2];
if (Py_TYPE(Py_TYPE(obj)) != &StructMetaType) {
PyErr_SetString(PyExc_TypeError, "`struct` must be a `msgspec.Struct`");
return NULL;
}
if (PyObject_GenericSetAttr(obj, name, value) < 0) {
return NULL;
}
Py_RETURN_NONE;
}

static PyObject *
Struct_reduce(PyObject *self, PyObject *args)
{
Expand Down Expand Up @@ -20886,6 +20925,10 @@ static struct PyMethodDef msgspec_methods[] = {
"defstruct", (PyCFunction) msgspec_defstruct, METH_VARARGS | METH_KEYWORDS,
msgspec_defstruct__doc__,
},
{
"force_setattr", (PyCFunction) struct_force_setattr, METH_FASTCALL,
struct_force_setattr__doc__,
},
{
"msgpack_encode", (PyCFunction) msgspec_msgpack_encode, METH_FASTCALL | METH_KEYWORDS,
msgspec_msgpack_encode__doc__,
Expand Down
2 changes: 2 additions & 0 deletions msgspec/structs.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
asdict,
astuple,
replace,
force_setattr,
)
from ._utils import get_class_annotations as _get_class_annotations

Expand All @@ -18,6 +19,7 @@
"asdict",
"astuple",
"fields",
"force_setattr",
"replace",
)

Expand Down
1 change: 1 addition & 0 deletions msgspec/structs.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ S = TypeVar("S", bound=Struct)
def replace(struct: S, /, **changes: Any) -> S: ...
def asdict(struct: Struct) -> dict[str, Any]: ...
def astuple(struct: Struct) -> tuple[Any, ...]: ...
def force_setattr(struct: Struct, name: str, value: Any) -> None: ...

class StructConfig:
frozen: bool
Expand Down
9 changes: 9 additions & 0 deletions tests/basic_typing_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,15 @@ class Test(msgspec.Struct):
reveal_type(o[0]) # assert "Any" in typ


def check_force_setattr() -> None:
class Point(msgspec.Struct, frozen=True):
x: int
y: int

obj = Point(1, 2)
msgspec.structs.force_setattr(obj, "x", 3)


def check_fields() -> None:
class Test(msgspec.Struct):
x: int
Expand Down
16 changes: 16 additions & 0 deletions tests/test_struct.py
Original file line number Diff line number Diff line change
Expand Up @@ -1600,6 +1600,22 @@ class Test(Base, gc=has_gc):
t.x = [1]
assert gc.is_tracked(t)

def test_force_setattr(self):
class Ex(Struct, frozen=True):
x: Any

obj = Ex(1)

res = msgspec.structs.force_setattr(obj, "x", 2)
assert res is None
assert obj.x == 2

with pytest.raises(AttributeError):
msgspec.structs.force_setattr(obj, "oops", 3)

with pytest.raises(TypeError):
msgspec.structs.force_setattr(1, "oops", 3)


class TestOrderAndEq:
@staticmethod
Expand Down

0 comments on commit 5593670

Please sign in to comment.