Skip to content

Commit dbdee05

Browse files
author
aleksandr.tseluyko
committed
Support Literal[True] and Literal[False] types
Add MS_TYPE_BOOLLITERAL_TRUE/FALSE flags and handle PyBool_Type before PyLong_Type in typenode_collect_literal. Update decode paths (JSON, msgpack, convert) to validate against specific bool literal values. Closes #859
1 parent a670c34 commit dbdee05

3 files changed

Lines changed: 73 additions & 11 deletions

File tree

‎src/msgspec/_core.c‎

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2820,6 +2820,8 @@ AssocList_Sort(AssocList* list) {
28202820
#define MS_TYPE_TYPEDDICT (1ull << 33)
28212821
#define MS_TYPE_DATACLASS (1ull << 34)
28222822
#define MS_TYPE_NAMEDTUPLE (1ull << 35)
2823+
#define MS_TYPE_BOOLLITERAL_TRUE (1ull << 36)
2824+
#define MS_TYPE_BOOLLITERAL_FALSE (1ull << 37)
28232825
/* Constraints */
28242826
#define MS_CONSTR_INT_MIN (1ull << 42)
28252827
#define MS_CONSTR_INT_MAX (1ull << 43)
@@ -2943,6 +2945,8 @@ typedef struct {
29432945
PyObject *int_lookup;
29442946
PyObject *str_lookup;
29452947
bool literal_none;
2948+
bool literal_bool_true;
2949+
bool literal_bool_false;
29462950
} LiteralInfo;
29472951

29482952
typedef struct {
@@ -3543,6 +3547,8 @@ typedef struct {
35433547
PyObject *literal_str_values;
35443548
PyObject *literal_str_lookup;
35453549
bool literal_none;
3550+
bool literal_bool_true;
3551+
bool literal_bool_false;
35463552
/* Constraints */
35473553
int64_t c_int_min;
35483554
int64_t c_int_max;
@@ -4433,6 +4439,14 @@ typenode_collect_literal(TypeNodeCollectState *state, PyObject *literal) {
44334439
if (obj == Py_None || obj == NONE_TYPE) {
44344440
state->literal_none = true;
44354441
}
4442+
else if (type == &PyBool_Type) {
4443+
if (obj == Py_True) {
4444+
state->literal_bool_true = true;
4445+
}
4446+
else {
4447+
state->literal_bool_false = true;
4448+
}
4449+
}
44364450
else if (type == &PyLong_Type) {
44374451
if (state->literal_int_values == NULL) {
44384452
state->literal_int_values = PySet_New(NULL);
@@ -4469,7 +4483,7 @@ typenode_collect_literal(TypeNodeCollectState *state, PyObject *literal) {
44694483
invalid:
44704484
PyErr_Format(
44714485
PyExc_TypeError,
4472-
"Literal may only contain None/integers/strings - %R is not supported",
4486+
"Literal may only contain None/booleans/integers/strings - %R is not supported",
44734487
literal
44744488
);
44754489

@@ -4507,6 +4521,12 @@ typenode_collect_convert_literals(TypeNodeCollectState *state) {
45074521
if (info->literal_none) {
45084522
state->types |= MS_TYPE_NONE;
45094523
}
4524+
if (info->literal_bool_true) {
4525+
state->types |= MS_TYPE_BOOLLITERAL_TRUE;
4526+
}
4527+
if (info->literal_bool_false) {
4528+
state->types |= MS_TYPE_BOOLLITERAL_FALSE;
4529+
}
45104530
Py_DECREF(cached);
45114531
return 0;
45124532
}
@@ -4535,6 +4555,12 @@ typenode_collect_convert_literals(TypeNodeCollectState *state) {
45354555
if (state->literal_none) {
45364556
state->types |= MS_TYPE_NONE;
45374557
}
4558+
if (state->literal_bool_true) {
4559+
state->types |= MS_TYPE_BOOLLITERAL_TRUE;
4560+
}
4561+
if (state->literal_bool_false) {
4562+
state->types |= MS_TYPE_BOOLLITERAL_FALSE;
4563+
}
45384564

45394565
if (n == 1) {
45404566
/* A single `Literal` object, cache the lookups on it */
@@ -4545,6 +4571,8 @@ typenode_collect_convert_literals(TypeNodeCollectState *state) {
45454571
Py_XINCREF(state->literal_str_lookup);
45464572
info->str_lookup = state->literal_str_lookup;
45474573
info->literal_none = state->literal_none;
4574+
info->literal_bool_true = state->literal_bool_true;
4575+
info->literal_bool_false = state->literal_bool_false;
45484576
PyObject_GC_Track(info);
45494577
PyObject *literal = PyList_GET_ITEM(state->literals, 0);
45504578
int status = PyObject_SetAttr(
@@ -15338,6 +15366,14 @@ mpack_decode_bool(DecoderState *self, PyObject *val, TypeNode *type, PathNode *p
1533815366
Py_INCREF(val);
1533915367
return val;
1534015368
}
15369+
if (val == Py_True && (type->types & MS_TYPE_BOOLLITERAL_TRUE)) {
15370+
Py_INCREF(Py_True);
15371+
return Py_True;
15372+
}
15373+
if (val == Py_False && (type->types & MS_TYPE_BOOLLITERAL_FALSE)) {
15374+
Py_INCREF(Py_False);
15375+
return Py_False;
15376+
}
1534115377
return ms_validation_error("bool", type, path);
1534215378
}
1534315379

@@ -16963,7 +16999,7 @@ json_decode_true(JSONDecoderState *self, TypeNode *type, PathNode *path) {
1696316999
if (MS_UNLIKELY(c1 != 'r' || c2 != 'u' || c3 != 'e')) {
1696417000
return json_err_invalid(self, "invalid character");
1696517001
}
16966-
if (type->types & (MS_TYPE_ANY | MS_TYPE_BOOL)) {
17002+
if (type->types & (MS_TYPE_ANY | MS_TYPE_BOOL | MS_TYPE_BOOLLITERAL_TRUE)) {
1696717003
Py_INCREF(Py_True);
1696817004
return Py_True;
1696917005
}
@@ -16984,7 +17020,7 @@ json_decode_false(JSONDecoderState *self, TypeNode *type, PathNode *path) {
1698417020
if (MS_UNLIKELY(c1 != 'a' || c2 != 'l' || c3 != 's' || c4 != 'e')) {
1698517021
return json_err_invalid(self, "invalid character");
1698617022
}
16987-
if (type->types & (MS_TYPE_ANY | MS_TYPE_BOOL)) {
17023+
if (type->types & (MS_TYPE_ANY | MS_TYPE_BOOL | MS_TYPE_BOOLLITERAL_FALSE)) {
1698817024
Py_INCREF(Py_False);
1698917025
return Py_False;
1699017026
}
@@ -20635,6 +20671,14 @@ convert_bool(
2063520671
Py_INCREF(obj);
2063620672
return obj;
2063720673
}
20674+
if (obj == Py_True && (type->types & MS_TYPE_BOOLLITERAL_TRUE)) {
20675+
Py_INCREF(Py_True);
20676+
return Py_True;
20677+
}
20678+
if (obj == Py_False && (type->types & MS_TYPE_BOOLLITERAL_FALSE)) {
20679+
Py_INCREF(Py_False);
20680+
return Py_False;
20681+
}
2063820682
return ms_validation_error("bool", type, path);
2063920683
}
2064020684

‎src/msgspec/inspect.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -315,11 +315,11 @@ class LiteralType(Type):
315315
Parameters
316316
----------
317317
values: tuple
318-
A tuple of possible values for this literal instance. Only `str` or
319-
`int` literals are supported.
318+
A tuple of possible values for this literal instance. Only `bool`,
319+
`str`, or `int` literals are supported.
320320
"""
321321

322-
values: Union[Tuple[str, ...], Tuple[int, ...]]
322+
values: Union[Tuple[bool, ...], Tuple[str, ...], Tuple[int, ...]]
323323

324324

325325
class CustomType(Type):

‎tests/unit/test_common.py‎

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -849,13 +849,8 @@ def test_int_literal_values_out_of_range(self, values):
849849
@pytest.mark.parametrize(
850850
"typ",
851851
[
852-
Literal[1, False],
853852
Literal["ok", b"bad"],
854853
Literal[1, object()],
855-
Union[Literal[1, 2], Literal[3, False]],
856-
Union[Literal["one", "two"], Literal[3, False]],
857-
Literal[Literal[1, 2], Literal[3, False]],
858-
Literal[Literal["one", "two"], Literal[3, False]],
859854
Literal[1, 2, List[int]],
860855
Literal[1, 2, List],
861856
],
@@ -932,6 +927,29 @@ def test_nested_literals(self):
932927
with pytest.raises(ValidationError, match="Invalid enum value 'carrot'"):
933928
dec.decode(msgspec.msgpack.encode("carrot"))
934929

930+
@pytest.mark.parametrize(
931+
"typ, good, bad",
932+
[
933+
(Literal[True], [True], [False]),
934+
(Literal[False], [False], [True]),
935+
(Literal[True, False], [True, False], []),
936+
(Literal[1, False], [1, False], [True]),
937+
(Literal[True, "yes", None], [True, "yes", None], [False]),
938+
],
939+
)
940+
def test_literal_bool(self, typ, good, bad):
941+
dec = msgspec.msgpack.Decoder(typ)
942+
for val in good:
943+
assert dec.decode(msgspec.msgpack.encode(val)) == val
944+
for val in bad:
945+
with pytest.raises(ValidationError):
946+
dec.decode(msgspec.msgpack.encode(val))
947+
948+
def test_mix_bool_and_bool_literal(self):
949+
dec = msgspec.msgpack.Decoder(Union[Literal[True], bool])
950+
assert dec.decode(msgspec.msgpack.encode(True)) is True
951+
assert dec.decode(msgspec.msgpack.encode(False)) is False
952+
935953
def test_mix_int_and_int_literal(self):
936954
dec = msgspec.msgpack.Decoder(Union[Literal[-1, 1], int])
937955
for x in [-1, 1, 10]:

0 commit comments

Comments
 (0)