Skip to content

Commit 546f21d

Browse files
apoorvkhnightsailerofek
authored
Expose StructMeta in Python API (#890)
Co-authored-by: Pan Fan <[email protected]> Co-authored-by: Ofek Lev <[email protected]>
1 parent 7acceff commit 546f21d

6 files changed

Lines changed: 522 additions & 18 deletions

File tree

‎docs/source/api.rst‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ Structs
88

99
.. autoclass:: Struct
1010

11+
.. autoclass:: StructMeta(name, bases, namespace, /, *, **struct_config)
12+
1113
.. autofunction:: field
1214

1315
.. autofunction:: defstruct

‎docs/source/structs.rst‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -931,6 +931,58 @@ has a signature similar to `dataclasses.make_dataclass`. See
931931
Point(x=1.0, y=2.0)
932932
933933
934+
Metaclasses
935+
-----------
936+
937+
You can define project-wide :class:`msgspec.Struct` policies at class-creation
938+
time by extending the :class:`msgspec.StructMeta` metaclass.
939+
940+
In the following example, we flip the default value of ``kw_only`` to ``True``
941+
in all subclasses of ``KwOnlyStruct``.
942+
943+
.. code-block:: python
944+
945+
>>> from msgspec import Struct, StructMeta
946+
947+
>>> class KwOnlyStructMeta(StructMeta):
948+
... def __new__(mcls, name, bases, namespace, **struct_config):
949+
... struct_config.setdefault("kw_only", True)
950+
... return super().__new__(mcls, name, bases, namespace, **struct_config)
951+
952+
>>> class KwOnlyStruct(Struct, metaclass=KwOnlyStructMeta): ...
953+
954+
>>> class Example(KwOnlyStruct):
955+
... a: str = ""
956+
... b: int
957+
958+
>>> Example()
959+
Traceback (most recent call last):
960+
File "<python-input-3>", line 1, in <module>
961+
Example()
962+
~~~~~~~^^
963+
TypeError: Missing required argument 'b'
964+
965+
>>> Example(b=123)
966+
Example(a='', b=123)
967+
968+
You can also mix :class:`msgspec.StructMeta` with other metaclasses. For
969+
example, here we create an abstract :class:`msgspec.Struct` class:
970+
971+
.. code-block:: python
972+
973+
>>> from msgspec import Struct, StructMeta
974+
975+
>>> from abc import ABCMeta
976+
977+
>>> class ABCStructMeta(StructMeta, ABCMeta): ...
978+
979+
>>> class StructABC(Struct, metaclass=ABCStructMeta): ...
980+
981+
.. note::
982+
983+
When inheriting from multiple metaclasses, put :class:`msgspec.StructMeta` first.
984+
985+
934986
.. _struct-gc:
935987

936988
Disabling Garbage Collection (Advanced)
@@ -1013,3 +1065,4 @@ collected (leading to a memory leak).
10131065
.. _rich: https://rich.readthedocs.io/en/stable/pretty.html
10141066
.. _keyword-only parameters: https://docs.python.org/3/glossary.html#term-parameter
10151067
.. _lambda: https://docs.python.org/3/tutorial/controlflow.html#lambda-expressions
1068+
.. _metaclass: https://docs.python.org/3/reference/datamodel.html#metaclasses

‎msgspec/__init__.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
MsgspecError,
77
Raw,
88
Struct,
9+
StructMeta,
910
UnsetType,
1011
UNSET,
1112
NODEFAULT,

‎msgspec/__init__.pyi‎

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import enum
2+
from inspect import Signature
23
from typing import (
34
Any,
45
Callable,
@@ -20,6 +21,51 @@ from typing_extensions import dataclass_transform, Buffer
2021

2122
from . import inspect, json, msgpack, structs, toml, yaml
2223

24+
# PEP 673 explicitly rejects using Self in metaclass definitions:
25+
# https://peps.python.org/pep-0673/#valid-locations-for-self
26+
#
27+
# Typeshed works around this by using a type variable as well:
28+
# https://github.com/python/typeshed/blob/17bde1bd5e556de001adde3c2f340ba1c3581bd2/stdlib/abc.pyi#L14-L19
29+
_SM = TypeVar("_SM", bound="StructMeta")
30+
31+
class StructMeta(type):
32+
__struct_fields__: ClassVar[Tuple[str, ...]]
33+
__struct_defaults__: ClassVar[Tuple[Any, ...]]
34+
__struct_encode_fields__: ClassVar[Tuple[str, ...]]
35+
__match_args__: ClassVar[Tuple[str, ...]]
36+
@property
37+
def __signature__(self) -> Signature: ...
38+
@property
39+
def __struct_config__(self) -> structs.StructConfig: ...
40+
def __new__(
41+
mcls: Type[_SM],
42+
name: str,
43+
bases: Tuple[type, ...],
44+
namespace: Dict[str, Any],
45+
/,
46+
*,
47+
tag: Union[None, bool, str, int, Callable[[str], Union[str, int]]] = None,
48+
tag_field: Union[None, str] = None,
49+
rename: Union[
50+
None,
51+
Literal["lower", "upper", "camel", "pascal", "kebab"],
52+
Callable[[str], Optional[str]],
53+
Mapping[str, str],
54+
] = None,
55+
omit_defaults: bool = False,
56+
forbid_unknown_fields: bool = False,
57+
frozen: bool = False,
58+
eq: bool = True,
59+
order: bool = False,
60+
kw_only: bool = False,
61+
repr_omit_defaults: bool = False,
62+
array_like: bool = False,
63+
gc: bool = True,
64+
weakref: bool = False,
65+
dict: bool = False,
66+
cache_hash: bool = False,
67+
) -> _SM: ...
68+
2369
T = TypeVar("T")
2470

2571
class UnsetType(enum.Enum):
@@ -39,7 +85,7 @@ def field(*, default_factory: Callable[[], T], name: Optional[str] = None) -> T:
3985
@overload
4086
def field(*, name: Optional[str] = None) -> Any: ...
4187
@dataclass_transform(field_specifiers=(field,))
42-
class Struct:
88+
class Struct(metaclass=StructMeta):
4389
__struct_fields__: ClassVar[Tuple[str, ...]]
4490
__struct_config__: ClassVar[structs.StructConfig]
4591
__match_args__: ClassVar[Tuple[str, ...]]

‎msgspec/_core.c‎

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4943,8 +4943,8 @@ typenode_collect_type(TypeNodeCollectState *state, PyObject *obj) {
49434943
out = typenode_collect_typevar(state, t);
49444944
}
49454945
else if (
4946-
Py_TYPE(t) == &StructMetaType ||
4947-
(origin != NULL && Py_TYPE(origin) == &StructMetaType)
4946+
PyType_IsSubtype(Py_TYPE(t), &StructMetaType) ||
4947+
(origin != NULL && PyType_IsSubtype(Py_TYPE(origin), &StructMetaType))
49484948
) {
49494949
out = typenode_collect_struct(state, t);
49504950
}
@@ -5598,7 +5598,7 @@ structmeta_collect_base(StructMetaInfo *info, MsgspecState *mod, PyObject *base)
55985598
return -1;
55995599
}
56005600

5601-
if (Py_TYPE(base) != &StructMetaType) {
5601+
if (!PyType_IsSubtype(Py_TYPE(base), &StructMetaType)) {
56025602
if (((PyTypeObject *)base)->tp_dictoffset) {
56035603
info->has_non_slots_bases = true;
56045604
}
@@ -5805,7 +5805,7 @@ structmeta_process_default(StructMetaInfo *info, PyObject *name) {
58055805
if (default_val == NULL) return -1;
58065806
}
58075807
else if (
5808-
(Py_TYPE(type) == &StructMetaType) &&
5808+
(PyType_IsSubtype(Py_TYPE(type), &StructMetaType)) &&
58095809
((StructMetaObject *)type)->frozen != OPT_TRUE
58105810
) {
58115811
goto error_mutable_struct;
@@ -6800,7 +6800,7 @@ StructInfo_Convert_lock_held(PyObject *obj) {
68006800
PyObject *annotations = NULL;
68016801
StructInfo *info = NULL;
68026802
bool cache_set = false;
6803-
bool is_struct = Py_TYPE(obj) == &StructMetaType;
6803+
bool is_struct = PyType_IsSubtype(Py_TYPE(obj), &StructMetaType);
68046804

68056805
/* Check for a cached StructInfo, and return if one exists */
68066806
if (MS_LIKELY(is_struct)) {
@@ -6818,7 +6818,7 @@ StructInfo_Convert_lock_held(PyObject *obj) {
68186818
}
68196819
PyObject *origin = PyObject_GetAttr(obj, mod->str___origin__);
68206820
if (origin == NULL) return NULL;
6821-
if (Py_TYPE(origin) != &StructMetaType) {
6821+
if (!PyType_IsSubtype(Py_TYPE(origin), &StructMetaType)) {
68226822
Py_DECREF(origin);
68236823
PyErr_SetString(
68246824
PyExc_RuntimeError, "Expected __origin__ to be a Struct type"
@@ -7271,12 +7271,44 @@ static PyGetSetDef StructMeta_getset[] = {
72717271
{NULL},
72727272
};
72737273

7274+
PyDoc_STRVAR(StructMeta__doc__,
7275+
"StructMeta(name, bases, namespace, /, *, **struct_config)\n"
7276+
"--\n"
7277+
"\n"
7278+
"The metaclass for creating `Struct` types. See its documentation for the\n"
7279+
"available configuration options when subclassing.\n"
7280+
"\n"
7281+
"Examples\n"
7282+
"--------\n"
7283+
"Here we define a metaclass that modifies the default configuration and use\n"
7284+
"it to create a new `Struct` base class.\n"
7285+
"\n"
7286+
">>> from msgspec import Struct, StructMeta\n"
7287+
">>> class KwOnlyStructMeta(StructMeta):\n"
7288+
"... def __new__(mcls, name, bases, namespace, **struct_config):\n"
7289+
"... struct_config.setdefault(\"kw_only\", True)\n"
7290+
"... return super().__new__(mcls, name, bases, namespace, **struct_config)\n"
7291+
"...\n"
7292+
">>> class KwOnlyStruct(Struct, metaclass=KwOnlyStructMeta): ...\n"
7293+
"\n"
7294+
"Any subclass of ``KwOnlyStruct`` will have ``kw_only`` set to ``True`` by\n"
7295+
"default.\n"
7296+
"\n"
7297+
">>> class Example(KwOnlyStruct):\n"
7298+
"... a: str = ""\n"
7299+
"... b: int\n"
7300+
"...\n"
7301+
">>> Example(b=123)\n"
7302+
"Example(a='', b=123)\n"
7303+
);
7304+
72747305
static PyTypeObject StructMetaType = {
72757306
PyVarObject_HEAD_INIT(NULL, 0)
72767307
.tp_name = "msgspec._core.StructMeta",
7308+
.tp_doc = StructMeta__doc__,
72777309
.tp_basicsize = sizeof(StructMetaObject),
72787310
.tp_itemsize = 0,
7279-
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_TYPE_SUBCLASS | Py_TPFLAGS_HAVE_GC | _Py_TPFLAGS_HAVE_VECTORCALL,
7311+
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_TYPE_SUBCLASS | Py_TPFLAGS_HAVE_GC | _Py_TPFLAGS_HAVE_VECTORCALL | Py_TPFLAGS_BASETYPE,
72807312
.tp_new = StructMeta_new,
72817313
.tp_dealloc = (destructor) StructMeta_dealloc,
72827314
.tp_clear = (inquiry) StructMeta_clear,
@@ -7915,7 +7947,7 @@ struct_replace(PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyObject
79157947

79167948
if (!check_positional_nargs(nargs, 1, 1)) return NULL;
79177949
PyObject *obj = args[0];
7918-
if (Py_TYPE(Py_TYPE(obj)) != &StructMetaType) {
7950+
if (!PyType_IsSubtype(Py_TYPE(Py_TYPE(obj)), &StructMetaType)) {
79197951
PyErr_SetString(PyExc_TypeError, "`struct` must be a `msgspec.Struct`");
79207952
return NULL;
79217953
}
@@ -7956,7 +7988,7 @@ struct_asdict(PyObject *self, PyObject *const *args, Py_ssize_t nargs)
79567988
{
79577989
if (!check_positional_nargs(nargs, 1, 1)) return NULL;
79587990
PyObject *obj = args[0];
7959-
if (Py_TYPE(Py_TYPE(obj)) != &StructMetaType) {
7991+
if (!PyType_IsSubtype(Py_TYPE(Py_TYPE(obj)), &StructMetaType)) {
79607992
PyErr_SetString(PyExc_TypeError, "`struct` must be a `msgspec.Struct`");
79617993
return NULL;
79627994
}
@@ -8014,7 +8046,7 @@ struct_astuple(PyObject *self, PyObject *const *args, Py_ssize_t nargs)
80148046
{
80158047
if (!check_positional_nargs(nargs, 1, 1)) return NULL;
80168048
PyObject *obj = args[0];
8017-
if (Py_TYPE(Py_TYPE(obj)) != &StructMetaType) {
8049+
if (!PyType_IsSubtype(Py_TYPE(Py_TYPE(obj)), &StructMetaType)) {
80188050
PyErr_SetString(PyExc_TypeError, "`struct` must be a `msgspec.Struct`");
80198051
return NULL;
80208052
}
@@ -8066,7 +8098,7 @@ struct_force_setattr(PyObject *self, PyObject *const *args, Py_ssize_t nargs)
80668098
PyObject *obj = args[0];
80678099
PyObject *name = args[1];
80688100
PyObject *value = args[2];
8069-
if (Py_TYPE(Py_TYPE(obj)) != &StructMetaType) {
8101+
if (!PyType_IsSubtype(Py_TYPE(Py_TYPE(obj)), &StructMetaType)) {
80708102
PyErr_SetString(PyExc_TypeError, "`struct` must be a `msgspec.Struct`");
80718103
return NULL;
80728104
}
@@ -13245,7 +13277,7 @@ mpack_encode_uncommon(EncoderState *self, PyTypeObject *type, PyObject *obj)
1324513277
else if (type == &PyBool_Type) {
1324613278
return mpack_encode_bool(self, obj);
1324713279
}
13248-
else if (Py_TYPE(type) == &StructMetaType) {
13280+
else if (PyType_IsSubtype(Py_TYPE(type), &StructMetaType)) {
1324913281
return mpack_encode_struct(self, obj);
1325013282
}
1325113283
else if (type == &PyBytes_Type) {
@@ -14352,7 +14384,7 @@ json_encode_uncommon(EncoderState *self, PyTypeObject *type, PyObject *obj) {
1435214384
else if (obj == Py_False) {
1435314385
return ms_write(self, "false", 5);
1435414386
}
14355-
else if (Py_TYPE(type) == &StructMetaType) {
14387+
else if (PyType_IsSubtype(Py_TYPE(type), &StructMetaType)) {
1435614388
return json_encode_struct(self, obj);
1435714389
}
1435814390
else if (PyTuple_Check(obj)) {
@@ -16481,7 +16513,7 @@ msgspec_msgpack_decode(PyObject *self, PyObject *const *args, Py_ssize_t nargs,
1648116513
if (type == NULL || type == mod->typing_any) {
1648216514
state.type = &typenode_any;
1648316515
}
16484-
else if (Py_TYPE(type) == &StructMetaType) {
16516+
else if (PyType_IsSubtype(Py_TYPE(type), &StructMetaType)) {
1648516517
PyObject *info = StructInfo_Convert(type);
1648616518
if (info == NULL) return NULL;
1648716519
bool array_like = ((StructMetaObject *)type)->array_like == OPT_TRUE;
@@ -19516,7 +19548,7 @@ msgspec_json_decode(PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyO
1951619548
if (type == NULL || type == mod->typing_any) {
1951719549
state.type = &typenode_any;
1951819550
}
19519-
else if (Py_TYPE(type) == &StructMetaType) {
19551+
else if (PyType_IsSubtype(Py_TYPE(type), &StructMetaType)) {
1952019552
PyObject *info = StructInfo_Convert(type);
1952119553
if (info == NULL) return NULL;
1952219554
bool array_like = ((StructMetaObject *)type)->array_like == OPT_TRUE;
@@ -20084,7 +20116,7 @@ to_builtins(ToBuiltinsState *self, PyObject *obj, bool is_key) {
2008420116
else if (PyDict_Check(obj)) {
2008520117
return to_builtins_dict(self, obj);
2008620118
}
20087-
else if (Py_TYPE(type) == &StructMetaType) {
20119+
else if (PyType_IsSubtype(Py_TYPE(type), &StructMetaType)) {
2008820120
return to_builtins_struct(self, obj, is_key);
2008920121
}
2009020122
else if (Py_TYPE(type) == self->mod->EnumMetaType) {
@@ -21983,7 +22015,7 @@ msgspec_convert(PyObject *self, PyObject *args, PyObject *kwargs)
2198322015
state.dec_hook = dec_hook;
2198422016

2198522017
/* Avoid allocating a new TypeNode for struct types */
21986-
if (Py_TYPE(pytype) == &StructMetaType) {
22018+
if (PyType_IsSubtype(Py_TYPE(pytype), &StructMetaType)) {
2198722019
PyObject *info = StructInfo_Convert(pytype);
2198822020
if (info == NULL) return NULL;
2198922021
bool array_like = ((StructMetaObject *)pytype)->array_like == OPT_TRUE;
@@ -22306,6 +22338,12 @@ PyInit__core(void)
2230622338
Py_INCREF(&Unset_Type);
2230722339
if (PyModule_AddObject(m, "UnsetType", (PyObject *)&Unset_Type) < 0)
2230822340
return NULL;
22341+
Py_INCREF((PyObject *)&StructMetaType);
22342+
if (PyModule_AddObject(m, "StructMeta", (PyObject *)&StructMetaType) < 0) {
22343+
Py_DECREF((PyObject *)&StructMetaType);
22344+
Py_DECREF(m);
22345+
return NULL;
22346+
}
2230922347

2231022348
st = msgspec_get_state(m);
2231122349

0 commit comments

Comments
 (0)