Skip to content

Commit 51858e5

Browse files
committed
Validate type annotation separately for input and output bindings
Input and output bindings have different requirements for the type annotation. Input bindings are usually passed as a specific `azure.functions` type, whereas the output type may vary. This corrects converters that implement both In and Out interfaces.
1 parent fca7283 commit 51858e5

File tree

7 files changed

+61
-24
lines changed

7 files changed

+61
-24
lines changed

azure/worker/bindings/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from .context import Context
2-
from .meta import check_type_annotation
2+
from .meta import check_input_type_annotation
3+
from .meta import check_output_type_annotation
34
from .meta import is_binding, is_trigger_binding
45
from .meta import from_incoming_proto, to_outgoing_proto
56
from .out import Out
@@ -15,6 +16,6 @@
1516
__all__ = (
1617
'Out', 'Context',
1718
'is_binding', 'is_trigger_binding',
18-
'check_type_annotation',
19+
'check_input_type_annotation', 'check_output_type_annotation',
1920
'from_incoming_proto', 'to_outgoing_proto',
2021
)

azure/worker/bindings/blob.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,11 @@ class BlobConverter(meta.InConverter,
4747
binding='blob'):
4848

4949
@classmethod
50-
def check_python_type(cls, pytype: type) -> bool:
50+
def check_input_type_annotation(cls, pytype: type) -> bool:
51+
return issubclass(pytype, azf_abc.InputStream)
52+
53+
@classmethod
54+
def check_output_type_annotation(cls, pytype: type) -> bool:
5155
return (issubclass(pytype, (str, bytes, bytearray,
5256
azf_abc.InputStream) or
5357
callable(getattr(pytype, 'read', None))))

azure/worker/bindings/http.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def get_json(self) -> typing.Any:
6868
class HttpResponseConverter(meta.OutConverter, binding='http'):
6969

7070
@classmethod
71-
def check_python_type(cls, pytype: type) -> bool:
71+
def check_output_type_annotation(cls, pytype: type) -> bool:
7272
return issubclass(pytype, (azf_abc.HttpResponse, str))
7373

7474
@classmethod
@@ -107,7 +107,7 @@ class HttpRequestConverter(meta.InConverter,
107107
binding='httpTrigger', trigger=True):
108108

109109
@classmethod
110-
def check_python_type(cls, pytype: type) -> bool:
110+
def check_input_type_annotation(cls, pytype: type) -> bool:
111111
return issubclass(pytype, azf_abc.HttpRequest)
112112

113113
@classmethod

azure/worker/bindings/meta.py

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ class TypedDataKind(enum.Enum):
1919

2020
class _ConverterMeta(abc.ABCMeta):
2121

22-
_check_py_type: typing.Mapping[str, typing.Callable[[type], bool]] = {}
22+
_check_in_typeann: typing.Mapping[str, typing.Callable[[type], bool]] = {}
23+
_check_out_typeann: typing.Mapping[str, typing.Callable[[type], bool]] = {}
2324
_from_proto: typing.Mapping[str, typing.Callable] = {}
2425
_to_proto: typing.Mapping[str, typing.Callable] = {}
2526
_binding_types: typing.Mapping[str, bool] = {}
@@ -37,11 +38,23 @@ def __new__(mcls, name, bases, dct, *,
3738
f'registered')
3839
mcls._binding_types[binding] = trigger
3940

40-
if binding in mcls._check_py_type:
41-
raise RuntimeError(
42-
f'cannot register a second check_python_type implementation '
43-
f'for {binding!r} binding')
44-
mcls._check_py_type[binding] = getattr(cls, 'check_python_type')
41+
check_in_typesig = getattr(cls, 'check_input_type_annotation', None)
42+
if (check_in_typesig is not None and
43+
not getattr(check_in_typesig, '__isabstractmethod__', False)):
44+
if binding in mcls._check_in_typeann:
45+
raise RuntimeError(
46+
f'cannot register a second check_input_type_annotation '
47+
f'implementation for {binding!r} binding')
48+
mcls._check_in_typeann[binding] = check_in_typesig
49+
50+
check_out_typesig = getattr(cls, 'check_output_type_annotation', None)
51+
if (check_out_typesig is not None and
52+
not getattr(check_out_typesig, '__isabstractmethod__', False)):
53+
if binding in mcls._check_out_typeann:
54+
raise RuntimeError(
55+
f'cannot register a second check_output_type_annotation '
56+
f'implementation for {binding!r} binding')
57+
mcls._check_out_typeann[binding] = check_out_typesig
4558

4659
if issubclass(cls, InConverter):
4760
if binding in mcls._from_proto:
@@ -62,10 +75,6 @@ def __new__(mcls, name, bases, dct, *,
6275

6376
class _BaseConverter(metaclass=_ConverterMeta, binding=None):
6477

65-
@abc.abstractclassmethod
66-
def check_python_type(cls, pytype: type) -> bool:
67-
pass
68-
6978
@classmethod
7079
def _decode_typed_data(
7180
cls, data: typing.Optional[protos.TypedData], *,
@@ -125,6 +134,10 @@ def _decode_trigger_metadata_field(
125134

126135
class InConverter(_BaseConverter, binding=None):
127136

137+
@abc.abstractclassmethod
138+
def check_input_type_annotation(cls, pytype: type) -> bool:
139+
pass
140+
128141
@abc.abstractclassmethod
129142
def from_proto(cls, data: protos.TypedData, *,
130143
pytype: typing.Optional[type],
@@ -134,6 +147,10 @@ def from_proto(cls, data: protos.TypedData, *,
134147

135148
class OutConverter(_BaseConverter, binding=None):
136149

150+
@abc.abstractclassmethod
151+
def check_output_type_annotation(cls, pytype: type) -> bool:
152+
pass
153+
137154
@abc.abstractclassmethod
138155
def to_proto(cls, obj: typing.Any, *,
139156
pytype: typing.Optional[type]) -> protos.TypedData:
@@ -151,12 +168,23 @@ def is_trigger_binding(bind_name: str) -> bool:
151168
raise ValueError(f'unsupported binding type {bind_name!r}')
152169

153170

154-
def check_type_annotation(binding: str, pytype: type) -> bool:
171+
def check_input_type_annotation(binding: str, pytype: type) -> bool:
172+
try:
173+
checker = _ConverterMeta._check_in_typeann[binding]
174+
except KeyError:
175+
raise TypeError(
176+
f'{binding!r} input binding does not have '
177+
f'a corresponding Python type') from None
178+
179+
return checker(pytype)
180+
181+
182+
def check_output_type_annotation(binding: str, pytype: type) -> bool:
155183
try:
156-
checker = _ConverterMeta._check_py_type[binding]
184+
checker = _ConverterMeta._check_out_typeann[binding]
157185
except KeyError:
158186
raise TypeError(
159-
f'bind type {binding!r} does not have '
187+
f'{binding!r} output binding does not have '
160188
f'a corresponding Python type') from None
161189

162190
return checker(pytype)

azure/worker/bindings/queue.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ class QueueMessageInConverter(meta.InConverter,
5555
binding='queueTrigger', trigger=True):
5656

5757
@classmethod
58-
def check_python_type(cls, pytype: type) -> bool:
58+
def check_input_type_annotation(cls, pytype: type) -> bool:
5959
return issubclass(pytype, azf_abc.QueueMessage)
6060

6161
@classmethod
@@ -114,7 +114,7 @@ def _parse_datetime_metadata(
114114
class QueueMessageOutConverter(meta.OutConverter, binding='queue'):
115115

116116
@classmethod
117-
def check_python_type(cls, pytype: type) -> bool:
117+
def check_output_type_annotation(cls, pytype: type) -> bool:
118118
return issubclass(pytype, (azf_abc.QueueMessage, str, bytes))
119119

120120
@classmethod

azure/worker/bindings/timer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class TimerRequestConverter(meta.InConverter,
2121
binding='timerTrigger', trigger=True):
2222

2323
@classmethod
24-
def check_python_type(cls, pytype: type) -> bool:
24+
def check_input_type_annotation(cls, pytype: type) -> bool:
2525
return issubclass(pytype, azf_abc.TimerRequest)
2626

2727
@classmethod

azure/worker/functions.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,12 @@ def add_function(self, function_id: str,
157157
else:
158158
param_py_type = param.annotation
159159
if param_py_type:
160-
if not bindings.check_type_annotation(
161-
param_bind_type, param_py_type):
160+
if is_param_out:
161+
checker = bindings.check_output_type_annotation
162+
else:
163+
checker = bindings.check_input_type_annotation
164+
165+
if not checker(param_bind_type, param_py_type):
162166
raise FunctionLoadError(
163167
func_name,
164168
f'type of {param.name} binding in function.json '
@@ -186,7 +190,7 @@ def add_function(self, function_id: str,
186190
func_name,
187191
f'return annotation should not be azure.functions.Out')
188192

189-
if not bindings.check_type_annotation(
193+
if not bindings.check_output_type_annotation(
190194
return_binding_name, return_pytype):
191195
raise FunctionLoadError(
192196
func_name,

0 commit comments

Comments
 (0)