Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 25 additions & 11 deletions msgspec/_core.c
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ ms_popcount(uint64_t i) { \
/* Capacity of a list */
#define LIST_CAPACITY(x) (((PyListObject *)x)->allocated)

/* Get the raw items pointer for a list and tuple */
#define LIST_ITEMS(x) (((PyListObject *)(x))->ob_item)
#define TUPLE_ITEMS(x) (((PyTupleObject *)(x))->ob_item)

/* Fast shrink of bytes & bytearray objects. This doesn't do any memory
* allocations, it just shrinks the size of the view presented to Python. Since
* outputs of `encode` should be short lived (immediately written to a
Expand Down Expand Up @@ -18241,10 +18245,10 @@ convert_seq_to_struct_array_union(
}

static PyObject *
convert_seq(ConvertState *self, PyObject *obj, TypeNode *type, PathNode *path) {
PyObject **items = PySequence_Fast_ITEMS(obj);
Py_ssize_t size = PySequence_Fast_GET_SIZE(obj);

convert_seq(
ConvertState *self, PyObject **items, Py_ssize_t size,
TypeNode *type, PathNode *path
) {
if (!ms_passes_array_constraints(size, type, path)) return NULL;

if (type->types & MS_TYPE_LIST) {
Expand Down Expand Up @@ -18824,7 +18828,13 @@ convert_other(
}
}

/* No luck. Next try converting from a mapping or by attribute */
/* No luck. Next check if it's a tuple subclass (standard tuples are
* handled earlier), and if so try converting it as a sequence */
if (PyTuple_Check(obj)) {
return convert_seq(self, TUPLE_ITEMS(obj), PyTuple_GET_SIZE(obj), type, path);
}

/* Next try converting from a mapping or by attribute */
bool is_mapping = PyMapping_Check(obj);
if (is_mapping && type->types & MS_TYPE_DICT) {
return convert_mapping_to_dict(self, obj, type, path);
Expand Down Expand Up @@ -18886,10 +18896,14 @@ convert(
else if (pytype == &PyFloat_Type) {
return convert_float(self, obj, type, path);
}
else if (pytype == &PyList_Type || pytype == &PyTuple_Type) {
return convert_seq(self, obj, type, path);
else if (PyList_Check(obj)) {
return convert_seq(self, LIST_ITEMS(obj), PyList_GET_SIZE(obj), type, path);
}
else if (pytype == &PyDict_Type) {
else if (pytype == &PyTuple_Type) {
/* Tuple subclasses are handled later on */
return convert_seq(self, TUPLE_ITEMS(obj), PyTuple_GET_SIZE(obj), type, path);
}
else if (PyDict_Check(obj)) {
return convert_dict(self, obj, type, path);
}
else if (obj == Py_None) {
Expand All @@ -18910,9 +18924,6 @@ convert(
else if (pytype == PyDateTimeAPI->DateType) {
return convert_immutable(self, MS_TYPE_DATE, "date", obj, type, path);
}
else if (pytype == &PySet_Type || pytype == &PyFrozenSet_Type) {
return convert_any_set(self, obj, type, path);
}
else if (pytype == (PyTypeObject *)self->mod->UUIDType) {
return convert_immutable(self, MS_TYPE_UUID, "uuid", obj, type, path);
}
Expand All @@ -18925,6 +18936,9 @@ convert(
else if (pytype == &Ext_Type) {
return convert_immutable(self, MS_TYPE_EXT, "ext", obj, type, path);
}
else if (PyAnySet_Check(obj)) {
return convert_any_set(self, obj, type, path);
}
else {
return convert_other(self, obj, type, path);
}
Expand Down
63 changes: 54 additions & 9 deletions tests/test_convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,25 +97,66 @@ def KWList(**kwargs):
return list(kwargs.values())


class SubList(list):
pass


class SubTuple(tuple):
pass


class SubSet(set):
pass


class SubFrozenSet(frozenset):
pass


class SubDict(dict):
pass


mapcls_and_from_attributes = pytest.mark.parametrize(
"mapcls, from_attributes", [(dict, False), (GetAttrObj, True), (GetItemObj, False)]
"mapcls, from_attributes",
[(dict, False), (SubDict, False), (GetAttrObj, True), (GetItemObj, False)],
)

mapcls_from_attributes_and_array_like = pytest.mark.parametrize(
"mapcls, from_attributes, array_like",
[
(dict, False, False),
(SubDict, False, False),
(KWList, False, True),
(GetAttrObj, True, True),
(GetAttrObj, True, False),
(GetItemObj, False, False),
],
)

seq_in_type = pytest.mark.parametrize(
"in_type",
[
list,
tuple,
set,
frozenset,
SubList,
SubTuple,
SubSet,
SubFrozenSet,
],
)


@pytest.fixture(params=["dict", "mapping"])
@pytest.fixture(params=["dict", "subclass", "mapping"])
def dictcls(request):
return dict if request.param == "dict" else GetItemObj
if request.param == "dict":
return dict
elif request.param == "subclass":
return SubDict
else:
return GetItemObj


def assert_eq(x, y):
Expand Down Expand Up @@ -773,12 +814,12 @@ def test_any_sequence(self):
msg = (1, 2, 3)
assert convert(msg, Any) is msg

@pytest.mark.parametrize("in_type", [list, tuple, set, frozenset])
@seq_in_type
@pytest.mark.parametrize("out_type", [list, tuple, set, frozenset])
def test_empty_sequence(self, in_type, out_type):
assert convert(in_type(), out_type) == out_type()

@pytest.mark.parametrize("in_type", [list, tuple, set, frozenset])
@seq_in_type
@pytest.mark.parametrize(
"out_type_annot",
[(list, List), (tuple, Tuple), (set, Set), (frozenset, FrozenSet)],
Expand All @@ -796,7 +837,7 @@ def test_sequence(self, in_type, out_type_annot, item_annot):
assert res == sol
assert isinstance(res, out_type)

@pytest.mark.parametrize("in_type", [list, tuple, set, frozenset])
@seq_in_type
@pytest.mark.parametrize(
"out_annot", [List[int], Tuple[int, ...], Set[int], FrozenSet[int]]
)
Expand Down Expand Up @@ -959,11 +1000,15 @@ class Ex1(NamedTuple):
class Ex2(NamedTuple):
x: int

class Ex3(NamedTuple):
x: str

msg = Ex1(1)
assert convert(msg, Ex1) is msg
assert convert(msg, Ex2) == Ex2(1)

with pytest.raises(ValidationError, match="got `Ex1`"):
convert(msg, Ex2)
with pytest.raises(ValidationError, match="Expected `str`, got `int`"):
convert(msg, Ex3)


class TestDict:
Expand Down Expand Up @@ -1542,7 +1587,7 @@ class Ex(Struct, forbid_unknown_fields=forbid_unknown_fields):
b: int

msg = mapcls(x=1, a=2, y=3, b=4, z=5)
if forbid_unknown_fields and mapcls is dict:
if forbid_unknown_fields and issubclass(mapcls, dict):
with pytest.raises(ValidationError, match="unknown field `x`"):
convert(msg, Ex, from_attributes=from_attributes)
else:
Expand Down