Skip to content

Commit 8d01abb

Browse files
🐛 Improve handling null and collection parameters.
1 parent abb5fb3 commit 8d01abb

File tree

4 files changed

+53
-37
lines changed

4 files changed

+53
-37
lines changed

src/lapidary/runtime/metattype.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,7 @@ def make_not_optional(typ: typing.Any) -> typing.Any:
1818

1919
def is_array_like(typ: typing.Any) -> bool:
2020
return inspect.isclass(typ) and issubclass(typ, Iterable) and not (typ in (str, bytes) or issubclass(typ, Mapping))
21+
22+
23+
def unwrap_origin(typ: typing.Any) -> typing.Any:
24+
return typing.get_origin(typ) or typ

src/lapidary/runtime/model/param_serialization.py

Lines changed: 16 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
import typing_extensions as typing
1212

13-
from ..metattype import make_not_optional
13+
from ..metattype import make_not_optional, unwrap_origin
1414

1515
# Some basic scalar types that we're sure are passed unchanged by pydantic
1616
PYTHON_SCALARS = (
@@ -103,29 +103,29 @@ def serialize_scalar(cls, name: str, value: ScalarType) -> Multimap:
103103

104104
@classmethod
105105
def deserialize(cls, value: typing.Any, target: type) -> ValueType:
106-
target = make_not_optional(target)
106+
target = unwrap_origin(make_not_optional(target))
107107
if target in PYTHON_SCALARS:
108-
return cls.deserialize_scalar(value)
108+
return cls.deserialize_scalar(value, target)
109109
elif issubclass(target, Mapping):
110-
return cls.deserialize_object(value)
110+
return cls.deserialize_object(value, target)
111111
elif issubclass(target, Iterable):
112-
return cls.deserialize_array(value)
112+
return cls.deserialize_array(value, target)
113113
else:
114114
raise SerializationError(type(value), target)
115115

116116
@classmethod
117-
@abc.abstractmethod
118-
def deserialize_scalar(cls, value: str) -> str:
119-
pass
117+
def deserialize_scalar(cls, value: str, _) -> ScalarType:
118+
# leave handling to pydantic
119+
return value
120120

121121
@classmethod
122122
@abc.abstractmethod
123-
def deserialize_object(cls, value: str) -> ValueType:
123+
def deserialize_object(cls, value: str, _) -> ValueType:
124124
pass
125125

126126
@classmethod
127127
@abc.abstractmethod
128-
def deserialize_array(cls, value: str) -> ArrayType:
128+
def deserialize_array(cls, value: str, _) -> ArrayType:
129129
pass
130130

131131

@@ -140,7 +140,7 @@ def serialize_array(cls, name: str, value: ArrayType) -> Multimap:
140140

141141
@classmethod
142142
def serialize_scalar(cls, name: str, value: ScalarType) -> Multimap:
143-
return [cls._scalar_as_entry(name, value)]
143+
return [cls._scalar_as_entry(name, value)] if value else ()
144144

145145
@staticmethod
146146
def _serialize_scalar(value: ScalarType) -> str:
@@ -151,29 +151,13 @@ def _scalar_as_entry(cls, name: str, value: ScalarType) -> Entry:
151151
return name, cls._serialize_scalar(value)
152152

153153
@classmethod
154-
def deserialize(cls, value: typing.Any, target: type) -> ValueType:
155-
target = make_not_optional(target)
156-
if target in PYTHON_SCALARS:
157-
return cls.deserialize_scalar(value)
158-
elif issubclass(target, Mapping):
159-
return cls.deserialize_object(value)
160-
elif issubclass(target, Iterable):
161-
return cls.deserialize_array(value)
162-
else:
163-
raise SerializationError(type(value), target)
164-
165-
@classmethod
166-
def deserialize_scalar(cls, value: str) -> str:
167-
return value
168-
169-
@classmethod
170-
def deserialize_object(cls, value: str) -> ValueType:
154+
def deserialize_object(cls, value: str, _) -> ValueType:
171155
tokens = value.split(',')
172-
return dict((k, cls.deserialize_scalar(v)) for k, v in zip(tokens[::2], tokens[1::2]))
156+
return dict((k, cls.deserialize_scalar(v, None)) for k, v in zip(tokens[::2], tokens[1::2]))
173157

174158
@classmethod
175-
def deserialize_array(cls, value: str) -> ArrayType:
176-
return [cls.deserialize_scalar(scalar) for scalar in value.split(',')]
159+
def deserialize_array(cls, value: str, _) -> ArrayType:
160+
return [cls.deserialize_scalar(scalar, None) for scalar in value.split(',')]
177161

178162

179163
class SimpleString(StringSerializationStyle):
@@ -208,11 +192,7 @@ def serialize_object(cls, _name: str, value: ObjectType) -> Multimap:
208192
return itertools.chain.from_iterable(cls.serialize_scalar(key, item) for key, item in value.items() if item)
209193

210194
@classmethod
211-
def deserialize_scalar(cls, value: str) -> str:
212-
return value
213-
214-
@classmethod
215-
def deserialize_array(cls, value: str) -> ArrayType:
195+
def deserialize_array(cls, value: str, _) -> ArrayType:
216196
raise NotImplementedError
217197

218198

src/lapidary/runtime/model/response.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ def handle_response(self, response: 'httpx.Response') -> typing.Any:
6666
value = part[self.http_name()]
6767
if value is None:
6868
return None
69-
return self._deserialize(value)
69+
return self._deserialize(value, make_not_optional(self.python_type))
7070

7171
@staticmethod
7272
@abc.abstractmethod

tests/test_param_serialization.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ def test_simple_multimap_string():
77
assert SimpleMultimap.serialize_scalar('name', 'value') == [('name', 'value')]
88

99

10+
def test_simple_multimap_scalar_none():
11+
assert list(SimpleMultimap.serialize_scalar('name', None)) == []
12+
13+
14+
def test_simple_multimap_none():
15+
assert list(SimpleMultimap.serialize('name', None)) == []
16+
17+
1018
def test_simple_multimap_int():
1119
assert SimpleMultimap.serialize_scalar('name', 1) == [('name', '1')]
1220

@@ -19,6 +27,30 @@ def test_simple_multimap_object():
1927
assert SimpleMultimap.serialize_object('name', {'key1': 'value', 'key2': 1}) == [('name', 'key1,value,key2,1')]
2028

2129

30+
def test_deser_simple_multimap_str():
31+
assert SimpleMultimap.deserialize_scalar('value', str) == 'value'
32+
33+
34+
def test_deser_simple_multimap_str_generic():
35+
assert SimpleMultimap.deserialize('value', str) == 'value'
36+
37+
38+
def test_deser_simple_multimap_array():
39+
assert SimpleMultimap.deserialize_array('value1,value2', list[str]) == ['value1', 'value2']
40+
41+
42+
def test_deser_simple_multimap_array_generic():
43+
assert SimpleMultimap.deserialize('value1,value2', list[str]) == ['value1', 'value2']
44+
45+
46+
def test_deser_simple_multimap_object():
47+
assert SimpleMultimap.deserialize_object('name1,value1,name2,value2', dict[str, str]) == {'name1': 'value1', 'name2': 'value2'}
48+
49+
50+
def test_deser_simple_multimap_object_generic():
51+
assert SimpleMultimap.deserialize('name1,value1,name2,value2', dict[str, str]) == {'name1': 'value1', 'name2': 'value2'}
52+
53+
2254
# simple no-explode, string version
2355

2456

0 commit comments

Comments
 (0)