Skip to content

Commit d9d34d1

Browse files
Remove read_optional in favor of is_null/read_null
This removes read_optional in favor of is_null and read_null on shape deserializers. This lets us reduce the number of lambdas and gives us more flexibility. This allows, for example, for skipping nulls in sparse collections.
1 parent 7e543d8 commit d9d34d1

File tree

8 files changed

+101
-70
lines changed

8 files changed

+101
-70
lines changed

codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/generators/ListGenerator.java

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -81,15 +81,13 @@ private void generateDeserializer() {
8181
${?includeSchema}
8282
member_schema = schema.members["member"]
8383
${/includeSchema}
84-
deserializer.read_list(
85-
schema,
86-
${?sparse}
87-
lambda d: result.append(d.read_optional(member_schema, lambda s: ${3C|}))
88-
${/sparse}
89-
${^sparse}
90-
lambda d: result.append(${3C|})
91-
${/sparse}
92-
)
84+
def _read_value(d: ShapeDeserializer):
85+
if d.is_null():
86+
d.read_null()
87+
${?sparse}result.append(None)${/sparse}
88+
else:
89+
result.append(${3C|})
90+
deserializer.read_list(schema, _read_value)
9391
return result
9492
""", deserializerSymbol.getName(), listSymbol,
9593
writer.consumer(w -> memberTarget.accept(

codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/generators/MapGenerator.java

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -82,15 +82,13 @@ private void generateDeserializer() {
8282
def $1L(deserializer: ShapeDeserializer, schema: Schema) -> $2T:
8383
result: $2T = {}
8484
value_schema = schema.members["value"]
85-
deserializer.read_map(
86-
schema,
87-
${?sparse}
88-
lambda k, d: result.__setitem__(k, d.read_optional(value_schema, lambda s: ${3C|}))
89-
${/sparse}
90-
${^sparse}
91-
lambda k, d: result.__setitem__(k, ${3C|})
92-
${/sparse}
93-
)
85+
def _read_value(k: str, d: ShapeDeserializer):
86+
if d.is_null():
87+
d.read_null()
88+
${?sparse}result[k] = None${/sparse}
89+
else:
90+
result[k] = ${3C|}
91+
deserializer.read_map(schema, _read_value)
9492
return result
9593
""", deserializerSymbol.getName(), listSymbol,
9694
writer.consumer(w -> valueTarget.accept(

python-packages/smithy-core/smithy_core/deserializers.py

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -53,24 +53,15 @@ def read_map(
5353
"""
5454
...
5555

56-
def read_null(self, schema: "Schema") -> None:
57-
"""Read a null value from the underlying data.
56+
def is_null(self) -> bool:
57+
"""Returns whether the next value in the underlying data represents null.
5858
5959
:param schema: The shape's schema.
6060
"""
6161
...
6262

63-
def read_optional[
64-
T
65-
](self, schema: "Schema", optional: Callable[["Schema"], T]) -> T | None:
66-
"""Read an optional value from the underlying data.
67-
68-
This is intended to be used with sparse lists or maps.
69-
70-
:param schema: The shape's schema.
71-
:param optional: A callable that takes a schema and reads a non-nullable value
72-
from the underlying data.
73-
"""
63+
def read_null(self) -> None:
64+
"""Read a null value from the underlying data."""
7465
...
7566

7667
def read_boolean(self, schema: "Schema") -> bool:

python-packages/smithy-core/smithy_core/documents.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -578,20 +578,16 @@ def read_map(
578578
consumer(k, _DocumentDeserializer(v))
579579

580580
@override
581-
def read_null(self, schema: "Schema") -> None:
581+
def is_null(self) -> bool:
582+
return self._value.is_none()
583+
584+
@override
585+
def read_null(self) -> None:
582586
if (value := self._value.as_value()) is not None:
583587
raise ExpectationNotMetException(
584588
f"Expected document value to be None, but was: {value}"
585589
)
586590

587-
@override
588-
def read_optional[
589-
T
590-
](self, schema: "Schema", optional: Callable[["Schema"], T]) -> T | None:
591-
if self._value.is_none():
592-
return None
593-
return optional(schema)
594-
595591
@override
596592
def read_boolean(self, schema: "Schema") -> bool:
597593
return self._value.as_boolean()

python-packages/smithy-core/tests/unit/test_documents.py

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -707,20 +707,32 @@ def _consumer(schema: Schema, de: ShapeDeserializer) -> None:
707707
kwargs["struct_member"] = DocumentSerdeShape.deserialize(de)
708708
case 11:
709709
sparse_list_value: list[str | None] = []
710+
711+
def _read_optional_list(d: ShapeDeserializer):
712+
if d.is_null():
713+
d.read_null()
714+
sparse_list_value.append(None)
715+
else:
716+
sparse_list_value.append(d.read_string(STRING))
717+
710718
de.read_list(
711719
SCHEMA.members["sparseListMember"],
712-
lambda d: sparse_list_value.append(
713-
d.read_optional(STRING, d.read_string)
714-
),
720+
_read_optional_list,
715721
)
716722
kwargs["list_member"] = sparse_list_value
717723
case 12:
718724
sparse_map_value: dict[str, str | None] = {}
725+
726+
def _read_optional_map(k: str, d: ShapeDeserializer):
727+
if d.is_null():
728+
d.read_null()
729+
sparse_map_value[k] = None
730+
else:
731+
sparse_map_value[k] = d.read_string(STRING)
732+
719733
de.read_map(
720734
SCHEMA.members["sparseMapMember"],
721-
lambda k, d: sparse_map_value.__setitem__(
722-
k, d.read_optional(STRING, d.read_string)
723-
),
735+
_read_optional_map,
724736
)
725737
kwargs["map_member"] = sparse_map_value
726738
case _:
@@ -891,18 +903,32 @@ def test_document_deserializer(given: Document, expected: Any):
891903
case list():
892904
actual = []
893905
deserializer = _DocumentDeserializer(given)
906+
907+
def _read_optional_list(d: ShapeDeserializer):
908+
if d.is_null():
909+
d.read_null()
910+
actual.append(None)
911+
else:
912+
actual.append(d.read_string(STRING))
913+
894914
deserializer.read_list(
895915
SCHEMA.members["sparseListMember"],
896-
lambda d: actual.append(d.read_optional(STRING, d.read_string)),
916+
_read_optional_list,
897917
)
898918
case dict():
899919
actual = {}
900920
deserializer = _DocumentDeserializer(given)
921+
922+
def _read_optional_map(k: str, d: ShapeDeserializer):
923+
if d.is_null():
924+
d.read_null()
925+
actual[k] = None
926+
else:
927+
actual[k] = d.read_string(STRING)
928+
901929
deserializer.read_map(
902930
SCHEMA.members["sparseMapMember"],
903-
lambda k, d: actual.__setitem__(
904-
k, d.read_optional(STRING, d.read_string)
905-
),
931+
_read_optional_map,
906932
)
907933
case DocumentSerdeShape():
908934
actual = given.as_shape(DocumentSerdeShape)

python-packages/smithy-json/smithy_json/_private/deserializers.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -106,20 +106,15 @@ def __init__(
106106
# populated on an as-needed basis.
107107
self._json_names: dict[ShapeID, dict[str, str]] = {}
108108

109-
def read_null(self, schema: Schema) -> None:
109+
def is_null(self) -> bool:
110+
return self._stream.peek().type == "null"
111+
112+
def read_null(self) -> None:
110113
event = next(self._stream)
111114
if event.value is not None:
112115
raise JSONTokenError("null", event)
113116
return None
114117

115-
def read_optional[
116-
T
117-
](self, schema: "Schema", optional: Callable[["Schema"], T]) -> T | None:
118-
if self._stream.peek().value is None:
119-
next(self._stream)
120-
return None
121-
return optional(schema)
122-
123118
def read_boolean(self, schema: Schema) -> bool:
124119
event = next(self._stream)
125120
if not isinstance(event.value, bool):

python-packages/smithy-json/tests/unit/__init__.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -270,20 +270,32 @@ def _consumer(schema: Schema, de: ShapeDeserializer) -> None:
270270
kwargs["struct_member"] = SerdeShape.deserialize(de)
271271
case 15:
272272
sparse_list_value: list[str | None] = []
273+
274+
def _read_optional_list(d: ShapeDeserializer):
275+
if d.is_null():
276+
d.read_null()
277+
sparse_list_value.append(None)
278+
else:
279+
sparse_list_value.append(d.read_string(STRING))
280+
273281
de.read_list(
274282
SCHEMA.members["sparseListMember"],
275-
lambda d: sparse_list_value.append(
276-
d.read_optional(STRING, d.read_string)
277-
),
283+
_read_optional_list,
278284
)
279285
kwargs["sparse_list_member"] = sparse_list_value
280286
case 16:
281287
sparse_map_value: dict[str, str | None] = {}
288+
289+
def _read_optional_map(k: str, d: ShapeDeserializer):
290+
if d.is_null():
291+
d.read_null()
292+
sparse_map_value[k] = None
293+
else:
294+
sparse_map_value[k] = d.read_string(STRING)
295+
282296
de.read_map(
283297
SCHEMA.members["sparseMapMember"],
284-
lambda k, d: sparse_map_value.__setitem__(
285-
k, d.read_optional(STRING, d.read_string)
286-
),
298+
_read_optional_map,
287299
)
288300
kwargs["sparse_map_member"] = sparse_map_value
289301
case _:

python-packages/smithy-json/tests/unit/test_deserializers.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import Any
44

55
import pytest
6+
from smithy_core.deserializers import ShapeDeserializer
67
from smithy_core.documents import Document
78
from smithy_core.prelude import (
89
BIG_DECIMAL,
@@ -30,7 +31,7 @@ def test_json_deserializer(expected: Any, given: bytes) -> None:
3031
deserializer = codec.create_deserializer(given)
3132
match expected:
3233
case None:
33-
actual = deserializer.read_null(STRING)
34+
actual = deserializer.read_null()
3435
case bool():
3536
actual = deserializer.read_boolean(BOOLEAN)
3637
case int():
@@ -49,18 +50,32 @@ def test_json_deserializer(expected: Any, given: bytes) -> None:
4950
actual = deserializer.read_document(expected._schema) # type: ignore
5051
case list():
5152
actual_list: list[str | None] = []
53+
54+
def _read_optional_list(d: ShapeDeserializer):
55+
if d.is_null():
56+
d.read_null()
57+
actual_list.append(None)
58+
else:
59+
actual_list.append(d.read_string(STRING))
60+
5261
deserializer.read_list(
5362
SPARSE_STRING_LIST_SCHEMA,
54-
lambda d: actual_list.append(d.read_optional(STRING, d.read_string)),
63+
_read_optional_list,
5564
)
5665
actual = actual_list
5766
case dict():
5867
actual_map: dict[str, str | None] = {}
68+
69+
def _read_optional_map(k: str, d: ShapeDeserializer):
70+
if d.is_null():
71+
d.read_null()
72+
actual_map[k] = None
73+
else:
74+
actual_map[k] = d.read_string(STRING)
75+
5976
deserializer.read_map(
6077
SPARSE_STRING_MAP_SCHEMA,
61-
lambda k, d: actual_map.__setitem__(
62-
k, d.read_optional(STRING, d.read_string)
63-
),
78+
_read_optional_map,
6479
)
6580
actual = actual_map
6681
case SerdeShape():

0 commit comments

Comments
 (0)