Skip to content

Commit a0277c7

Browse files
committed
Pass Python type to type converters.
This allows to implement custom type encoding/decoding logic depending on parameter annotatition type. Closes issue #67.
1 parent 0b37f3e commit a0277c7

File tree

7 files changed

+83
-48
lines changed

7 files changed

+83
-48
lines changed

azure/worker/bindings/blob.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ def check_python_type(cls, pytype: type) -> bool:
3535
callable(getattr(pytype, 'read', None))))
3636

3737
@classmethod
38-
def to_proto(cls, obj: typing.Any) -> protos.TypedData:
38+
def to_proto(cls, obj: typing.Any, *,
39+
pytype: typing.Optional[type]) -> protos.TypedData:
3940
if callable(getattr(obj, 'read', None)):
4041
# file-like object
4142
obj = obj.read()
@@ -50,7 +51,8 @@ def to_proto(cls, obj: typing.Any) -> protos.TypedData:
5051
raise NotImplementedError
5152

5253
@classmethod
53-
def from_proto(cls, data: protos.TypedData,
54+
def from_proto(cls, data: protos.TypedData, *,
55+
pytype: typing.Optional[type],
5456
trigger_metadata) -> typing.Any:
5557
data_type = data.WhichOneof('data')
5658
if data_type == 'string':

azure/worker/bindings/http.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ def check_python_type(cls, pytype: type) -> bool:
7272
return issubclass(pytype, (azf_abc.HttpResponse, str))
7373

7474
@classmethod
75-
def to_proto(cls, obj: typing.Any) -> protos.TypedData:
75+
def to_proto(cls, obj: typing.Any, *,
76+
pytype: typing.Optional[type]) -> protos.TypedData:
7677
if isinstance(obj, str):
7778
return protos.TypedData(string=obj)
7879

@@ -110,7 +111,8 @@ def check_python_type(cls, pytype: type) -> bool:
110111
return issubclass(pytype, azf_abc.HttpRequest)
111112

112113
@classmethod
113-
def from_proto(cls, data: protos.TypedData,
114+
def from_proto(cls, data: protos.TypedData, *,
115+
pytype: typing.Optional[type],
114116
trigger_metadata) -> typing.Any:
115117
if data.WhichOneof('data') != 'http':
116118
raise NotImplementedError

azure/worker/bindings/meta.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -129,15 +129,17 @@ def _decode_trigger_metadata_field(
129129
class InConverter(_BaseConverter, binding=None):
130130

131131
@abc.abstractclassmethod
132-
def from_proto(cls, data: protos.TypedData,
132+
def from_proto(cls, data: protos.TypedData, *,
133+
pytype: typing.Optional[type],
133134
trigger_metadata) -> typing.Any:
134135
pass
135136

136137

137138
class OutConverter(_BaseConverter, binding=None):
138139

139140
@abc.abstractclassmethod
140-
def to_proto(cls, obj: typing.Any) -> protos.TypedData:
141+
def to_proto(cls, obj: typing.Any, *,
142+
pytype: typing.Optional[type]) -> protos.TypedData:
141143
pass
142144

143145

@@ -164,7 +166,8 @@ def check_type_annotation(binding: str, pytype: type) -> bool:
164166

165167

166168
def from_incoming_proto(
167-
binding: str, val: protos.TypedData,
169+
binding: str, val: protos.TypedData, *,
170+
pytype: typing.Optional[type],
168171
trigger_metadata: typing.Optional[typing.Dict[str, protos.TypedData]])\
169172
-> typing.Any:
170173
converter = _ConverterMeta._from_proto.get(binding)
@@ -175,7 +178,8 @@ def from_incoming_proto(
175178
except KeyError:
176179
raise NotImplementedError
177180
else:
178-
return converter(val, trigger_metadata)
181+
return converter(val, pytype=pytype,
182+
trigger_metadata=trigger_metadata)
179183
except NotImplementedError:
180184
# Either there's no converter or a converter has failed.
181185
dt = val.WhichOneof('data')
@@ -186,7 +190,8 @@ def from_incoming_proto(
186190
f'and expected binding type {binding}')
187191

188192

189-
def to_outgoing_proto(binding: str, obj: typing.Any) -> protos.TypedData:
193+
def to_outgoing_proto(binding: str, obj: typing.Any, *,
194+
pytype: typing.Optional[type]) -> protos.TypedData:
190195
converter = _ConverterMeta._to_proto.get(binding)
191196

192197
try:
@@ -195,7 +200,7 @@ def to_outgoing_proto(binding: str, obj: typing.Any) -> protos.TypedData:
195200
except KeyError:
196201
raise NotImplementedError
197202
else:
198-
return converter(obj)
203+
return converter(obj, pytype=pytype)
199204
except NotImplementedError:
200205
# Either there's no converter or a converter has failed.
201206
raise TypeError(

azure/worker/bindings/queue.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,9 @@ def check_python_type(cls, pytype: type) -> bool:
5959
return issubclass(pytype, azf_abc.QueueMessage)
6060

6161
@classmethod
62-
def from_proto(cls, data: protos.TypedData,
63-
trigger_metadata) -> azf_abc.QueueMessage:
62+
def from_proto(cls, data: protos.TypedData, *,
63+
pytype: typing.Optional[type],
64+
trigger_metadata) -> typing.Any:
6465
data_type = data.WhichOneof('data')
6566

6667
if data_type == 'string':
@@ -117,7 +118,8 @@ def check_python_type(cls, pytype: type) -> bool:
117118
return issubclass(pytype, (azf_abc.QueueMessage, str, bytes))
118119

119120
@classmethod
120-
def to_proto(cls, obj: typing.Any) -> protos.TypedData:
121+
def to_proto(cls, obj: typing.Any, *,
122+
pytype: typing.Optional[type]) -> protos.TypedData:
121123
if isinstance(obj, str):
122124
return protos.TypedData(string=obj)
123125

azure/worker/bindings/timer.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ def check_python_type(cls, pytype: type) -> bool:
2525
return issubclass(pytype, azf_abc.TimerRequest)
2626

2727
@classmethod
28-
def from_proto(cls, data: protos.TypedData,
28+
def from_proto(cls, data: protos.TypedData, *,
29+
pytype: typing.Optional[type],
2930
trigger_metadata) -> typing.Any:
3031
if data.WhichOneof('data') != 'json':
3132
raise NotImplementedError

azure/worker/dispatcher.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -235,20 +235,22 @@ async def _handle__invocation_request(self, req):
235235

236236
args = {}
237237
for pb in invoc_request.input_data:
238-
pb_type = fi.input_binding_types[pb.name]
239-
if bindings.is_trigger_binding(pb_type):
238+
pb_type_info = fi.input_types[pb.name]
239+
if bindings.is_trigger_binding(pb_type_info.binding_name):
240240
trigger_metadata = invoc_request.trigger_metadata
241241
else:
242242
trigger_metadata = None
243243
args[pb.name] = bindings.from_incoming_proto(
244-
pb_type, pb.data, trigger_metadata)
244+
pb_type_info.binding_name, pb.data,
245+
trigger_metadata=trigger_metadata,
246+
pytype=pb_type_info.pytype)
245247

246248
if fi.requires_context:
247249
args['context'] = bindings.Context(
248250
fi.name, fi.directory, invocation_id)
249251

250-
if fi.output_binding_types:
251-
for name in fi.output_binding_types:
252+
if fi.output_types:
253+
for name in fi.output_types:
252254
args[name] = bindings.Out()
253255

254256
if fi.is_async:
@@ -259,15 +261,17 @@ async def _handle__invocation_request(self, req):
259261
self.__run_sync_func, invocation_id, fi.func, args)
260262

261263
output_data = []
262-
if fi.output_binding_types:
263-
for out_name, out_type in fi.output_binding_types.items():
264+
if fi.output_types:
265+
for out_name, out_type_info in fi.output_types.items():
264266
val = args[name].get()
265267
if val is None:
266268
# TODO: is the "Out" parameter optional?
267269
# Can "None" be marshaled into protos.TypedData?
268270
continue
269271

270-
rpc_val = bindings.to_outgoing_proto(out_type, val)
272+
rpc_val = bindings.to_outgoing_proto(
273+
out_type_info.binding_name, val,
274+
pytype=out_type_info.pytype)
271275
assert rpc_val is not None
272276

273277
output_data.append(
@@ -276,9 +280,10 @@ async def _handle__invocation_request(self, req):
276280
data=rpc_val))
277281

278282
return_value = None
279-
if fi.return_binding_type is not None:
283+
if fi.return_type is not None:
280284
return_value = bindings.to_outgoing_proto(
281-
fi.return_binding_type, call_result)
285+
fi.return_type.binding_name, call_result,
286+
pytype=fi.return_type.pytype)
282287

283288
return protos.StreamingMessage(
284289
request_id=self.request_id,

azure/worker/functions.py

Lines changed: 42 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77
from . import protos
88

99

10+
class ParamTypeInfo(typing.NamedTuple):
11+
12+
binding_name: str
13+
pytype: typing.Optional[type]
14+
15+
1016
class FunctionInfo(typing.NamedTuple):
1117

1218
func: typing.Callable
@@ -16,9 +22,9 @@ class FunctionInfo(typing.NamedTuple):
1622
requires_context: bool
1723
is_async: bool
1824

19-
input_binding_types: typing.Mapping[str, str]
20-
output_binding_types: typing.Mapping[str, str]
21-
return_binding_type: typing.Optional[str]
25+
input_types: typing.Mapping[str, ParamTypeInfo]
26+
output_types: typing.Mapping[str, ParamTypeInfo]
27+
return_type: typing.Optional[ParamTypeInfo]
2228

2329

2430
class FunctionLoadError(RuntimeError):
@@ -49,9 +55,10 @@ def add_function(self, function_id: str,
4955
sig = inspect.signature(func)
5056
params = dict(sig.parameters)
5157

52-
input_types = {}
53-
output_types = {}
54-
return_type = None
58+
input_types: typing.Dict[str, ParamTypeInfo] = {}
59+
output_types: typing.Dict[str, ParamTypeInfo] = {}
60+
return_binding_name: typing.Optional[str] = None
61+
return_pytype: typing.Optional[type] = None
5562

5663
requires_context = False
5764

@@ -68,8 +75,10 @@ def add_function(self, function_id: str,
6875
func_name,
6976
f'"$return" binding must have direction set to "out"')
7077

71-
return_type = desc.type
72-
if not bindings.is_binding(return_type):
78+
return_binding_name = desc.type
79+
assert return_binding_name is not None
80+
81+
if not bindings.is_binding(return_binding_name):
7382
raise FunctionLoadError(
7483
func_name,
7584
f'unknown type for $return binding: "{desc.type}"')
@@ -134,11 +143,7 @@ def add_function(self, function_id: str,
134143
func_name,
135144
f'unknown type for {param.name} binding: "{desc.type}"')
136145

137-
if is_binding_out:
138-
output_types[param.name] = param_bind_type
139-
else:
140-
input_types[param.name] = param_bind_type
141-
146+
param_py_type = None
142147
if param_has_anno:
143148
if is_param_out:
144149
assert issubclass(param.annotation, azf.Out)
@@ -156,31 +161,44 @@ def add_function(self, function_id: str,
156161
f'"{param_bind_type}" does not match its Python '
157162
f'annotation "{param_py_type.__name__}"')
158163

159-
if return_type is not None and sig.return_annotation is not sig.empty:
160-
ra = sig.return_annotation
161-
if not isinstance(ra, type):
164+
param_type_info = ParamTypeInfo(param_bind_type, param_py_type)
165+
if is_binding_out:
166+
output_types[param.name] = param_type_info
167+
else:
168+
input_types[param.name] = param_type_info
169+
170+
return_pytype = None
171+
if (return_binding_name is not None and
172+
sig.return_annotation is not sig.empty):
173+
return_pytype = sig.return_annotation
174+
if not isinstance(return_pytype, type):
162175
raise FunctionLoadError(
163176
func_name,
164-
f'has invalid non-type return annotation {ra!r}')
177+
f'has invalid non-type return '
178+
f'annotation {return_pytype!r}')
165179

166-
if issubclass(ra, azf.Out):
180+
if issubclass(return_pytype, azf.Out):
167181
raise FunctionLoadError(
168182
func_name,
169183
f'return annotation should not be azure.functions.Out')
170184

171185
if not bindings.check_type_annotation(
172-
return_type, ra):
186+
return_binding_name, return_pytype):
173187
raise FunctionLoadError(
174188
func_name,
175-
f'Python return annotation "{ra.__name__}" does not match '
176-
f'binding type "{return_type}"')
189+
f'Python return annotation "{return_pytype.__name__}" '
190+
f'does not match binding type "{return_binding_name}"')
191+
192+
return_type = None
193+
if return_binding_name is not None:
194+
return_type = ParamTypeInfo(return_binding_name, return_pytype)
177195

178196
self._functions[function_id] = FunctionInfo(
179197
func=func,
180198
name=func_name,
181199
directory=metadata.directory,
182200
requires_context=requires_context,
183201
is_async=inspect.iscoroutinefunction(func),
184-
input_binding_types=input_types,
185-
output_binding_types=output_types,
186-
return_binding_type=return_type)
202+
input_types=input_types,
203+
output_types=output_types,
204+
return_type=return_type)

0 commit comments

Comments
 (0)