azure_functions_worker/functions.py (390 lines of code) (raw):
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
import inspect
import operator
import pathlib
import typing
import uuid
from . import bindings as bindings_utils
from . import protos
from ._thirdparty import typing_inspect
from .constants import HTTP_TRIGGER
from .protos import BindingInfo
class ParamTypeInfo(typing.NamedTuple):
binding_name: str
pytype: typing.Optional[type]
deferred_bindings_enabled: typing.Optional[bool] = False
class FunctionInfo(typing.NamedTuple):
func: typing.Callable
name: str
directory: str
function_id: str
requires_context: bool
is_async: bool
has_return: bool
is_http_func: bool
deferred_bindings_enabled: bool
input_types: typing.Mapping[str, ParamTypeInfo]
output_types: typing.Mapping[str, ParamTypeInfo]
return_type: typing.Optional[ParamTypeInfo]
trigger_metadata: typing.Optional[typing.Dict[str, typing.Any]]
class FunctionLoadError(RuntimeError):
def __init__(self, function_name: str, msg: str) -> None:
super().__init__(
f'cannot load the {function_name} function: {msg}')
class Registry:
_functions: typing.MutableMapping[str, FunctionInfo]
_deferred_bindings_enabled: bool = False
def __init__(self) -> None:
self._functions = {}
def get_function(self, function_id: str) -> FunctionInfo:
if function_id in self._functions:
return self._functions[function_id]
return None
def deferred_bindings_enabled(self) -> bool:
return self._deferred_bindings_enabled
@staticmethod
def get_explicit_and_implicit_return(binding_name: str,
binding: BindingInfo,
explicit_return: bool,
implicit_return: bool,
bound_params: dict) -> \
typing.Tuple[bool, bool]:
if binding_name == '$return':
explicit_return = True
elif bindings_utils.has_implicit_output(
binding.type):
implicit_return = True
bound_params[binding_name] = binding
else:
bound_params[binding_name] = binding
return explicit_return, implicit_return
@staticmethod
def get_return_binding(binding_name: str,
binding_type: str,
return_binding_name: str,
explicit_return_val_set: bool) \
-> typing.Tuple[str, bool]:
# prioritize explicit return value
if explicit_return_val_set:
return return_binding_name, explicit_return_val_set
if binding_name == "$return":
return_binding_name = binding_type
assert return_binding_name is not None
explicit_return_val_set = True
elif bindings_utils.has_implicit_output(binding_type):
return_binding_name = binding_type
return return_binding_name, explicit_return_val_set
@staticmethod
def validate_binding_direction(binding_name: str,
binding_direction: str,
func_name: str):
if binding_direction == protos.BindingInfo.inout:
raise FunctionLoadError(
func_name,
'"inout" bindings are not supported')
if binding_name == '$return' and \
binding_direction != protos.BindingInfo.out:
raise FunctionLoadError(
func_name,
'"$return" binding must have direction set to "out"')
@staticmethod
def is_context_required(params, bound_params: dict,
annotations: dict,
func_name: str) -> bool:
requires_context = False
if 'context' in params and 'context' not in bound_params:
requires_context = True
params.pop('context')
if 'context' in annotations:
ctx_anno = annotations.get('context')
if (not isinstance(ctx_anno, type)
or ctx_anno.__name__ != 'Context'):
raise FunctionLoadError(
func_name,
'the "context" parameter is expected to be of '
'type azure.functions.Context, got '
f'{ctx_anno!r}')
return requires_context
@staticmethod
def validate_function_params(params: dict, bound_params: dict,
annotations: dict, func_name: str):
if set(params) - set(bound_params):
raise FunctionLoadError(
func_name,
'the following parameters are declared in Python but '
f'not in function.json: {set(params) - set(bound_params)!r}')
if set(bound_params) - set(params):
raise FunctionLoadError(
func_name,
f'the following parameters are declared in function.json but '
f'not in Python: {set(bound_params) - set(params)!r}')
input_types: typing.Dict[str, ParamTypeInfo] = {}
output_types: typing.Dict[str, ParamTypeInfo] = {}
fx_deferred_bindings_enabled = False
for param in params.values():
binding = bound_params[param.name]
param_has_anno = param.name in annotations
param_anno = annotations.get(param.name)
# Check if deferred bindings is enabled
fx_deferred_bindings_enabled, is_deferred_binding = (
bindings_utils.check_deferred_bindings_enabled(
param_anno,
fx_deferred_bindings_enabled))
if param_has_anno:
if typing_inspect.is_generic_type(param_anno):
param_anno_origin = typing_inspect.get_origin(param_anno)
if param_anno_origin is not None:
is_param_out = (
isinstance(param_anno_origin, type)
and param_anno_origin.__name__ == 'Out'
)
else:
is_param_out = (
isinstance(param_anno, type)
and param_anno.__name__ == 'Out'
)
else:
is_param_out = (
isinstance(param_anno, type)
and param_anno.__name__ == 'Out'
)
else:
is_param_out = False
is_binding_out = binding.direction == protos.BindingInfo.out
if is_param_out:
param_anno_args = typing_inspect.get_args(param_anno)
if len(param_anno_args) != 1:
raise FunctionLoadError(
func_name,
f'binding {param.name} has invalid Out annotation '
f'{param_anno!r}')
param_py_type = param_anno_args[0]
# typing_inspect.get_args() returns a flat list,
# so if the annotation was func.Out[typing.List[foo]],
# we need to reconstruct it.
if (isinstance(param_py_type, tuple)
and typing_inspect.is_generic_type(param_py_type[0])):
param_py_type = operator.getitem(
param_py_type[0], *param_py_type[1:])
else:
param_py_type = param_anno
if (param_has_anno and not isinstance(param_py_type, type)
and not typing_inspect.is_generic_type(param_py_type)):
raise FunctionLoadError(
func_name,
f'binding {param.name} has invalid non-type annotation '
f'{param_anno!r}')
if is_binding_out and param_has_anno and not is_param_out:
raise FunctionLoadError(
func_name,
f'binding {param.name} is declared to have the "out" '
'direction, but its annotation in Python is not '
'a subclass of azure.functions.Out')
if not is_binding_out and is_param_out:
raise FunctionLoadError(
func_name,
f'binding {param.name} is declared to have the "in" '
'direction in function.json, but its annotation '
'is azure.functions.Out in Python')
if param_has_anno and param_py_type in (str, bytes) and (
not bindings_utils.has_implicit_output(binding.type)):
param_bind_type = 'generic'
else:
param_bind_type = binding.type
if param_has_anno:
if is_param_out:
checks_out = bindings_utils.check_output_type_annotation(
param_bind_type, param_py_type)
else:
checks_out = bindings_utils.check_input_type_annotation(
param_bind_type, param_py_type, is_deferred_binding)
if not checks_out:
if binding.data_type is not protos.BindingInfo.undefined:
raise FunctionLoadError(
func_name,
f'{param.name!r} binding type "{binding.type}" '
f'and dataType "{binding.data_type}" in '
f'function.json do not match the corresponding '
f'function parameter\'s Python type '
f'annotation "{param_py_type.__name__}"')
else:
raise FunctionLoadError(
func_name,
f'type of {param.name} binding in function.json '
f'"{binding.type}" does not match its Python '
f'annotation "{param_py_type.__name__}"')
param_type_info = ParamTypeInfo(param_bind_type,
param_py_type,
is_deferred_binding)
if is_binding_out:
output_types[param.name] = param_type_info
else:
input_types[param.name] = param_type_info
return input_types, output_types, fx_deferred_bindings_enabled
@staticmethod
def get_function_return_type(annotations: dict, has_explicit_return: bool,
has_implicit_return: bool, binding_name: str,
func_name: str):
return_pytype = None
if has_explicit_return and 'return' in annotations:
return_anno = annotations.get('return')
if typing_inspect.is_generic_type(
return_anno) and typing_inspect.get_origin(
return_anno).__name__ == 'Out':
raise FunctionLoadError(
func_name,
'return annotation should not be azure.functions.Out')
return_pytype = return_anno
if not isinstance(return_pytype, type):
raise FunctionLoadError(
func_name,
f'has invalid non-type return '
f'annotation {return_pytype!r}')
if return_pytype is (str, bytes):
binding_name = 'generic'
if not bindings_utils.check_output_type_annotation(
binding_name, return_pytype):
raise FunctionLoadError(
func_name,
f'Python return annotation "{return_pytype.__name__}" '
f'does not match binding type "{binding_name}"')
if has_implicit_return and 'return' in annotations:
return_pytype = annotations.get('return')
return_type = None
if has_explicit_return or has_implicit_return:
return_type = ParamTypeInfo(binding_name, return_pytype)
return return_type
def add_func_to_registry_and_return_funcinfo(
self, function,
function_name: str,
function_id: str,
directory: str,
requires_context: bool,
has_explicit_return: bool,
has_implicit_return: bool,
deferred_bindings_enabled: bool,
input_types: typing.Dict[str, ParamTypeInfo],
output_types: typing.Dict[str, ParamTypeInfo],
return_type: str):
http_trigger_param_name = self._get_http_trigger_param_name(input_types)
trigger_metadata = None
is_http_func = False
if http_trigger_param_name is not None:
trigger_metadata = {
"type": HTTP_TRIGGER,
"param_name": http_trigger_param_name
}
is_http_func = True
function_info = FunctionInfo(
func=function,
name=function_name,
directory=directory,
function_id=function_id,
requires_context=requires_context,
is_async=inspect.iscoroutinefunction(function),
has_return=has_explicit_return or has_implicit_return,
is_http_func=is_http_func,
deferred_bindings_enabled=deferred_bindings_enabled,
input_types=input_types,
output_types=output_types,
return_type=return_type,
trigger_metadata=trigger_metadata)
self._functions[function_id] = function_info
if not self._deferred_bindings_enabled:
self._deferred_bindings_enabled = deferred_bindings_enabled
return function_info
def _get_http_trigger_param_name(self, input_types):
http_trigger_param_name = next(
(input_type for input_type, type_info in input_types.items()
if type_info.binding_name == HTTP_TRIGGER),
None
)
return http_trigger_param_name
def add_function(self, function_id: str,
func: typing.Callable,
metadata: protos.RpcFunctionMetadata):
func_name = metadata.name
sig = inspect.signature(func)
params = dict(sig.parameters)
annotations = typing.get_type_hints(func)
return_binding_name: typing.Optional[str] = None
explicit_return_val_set = False
has_explicit_return = False
has_implicit_return = False
bound_params = {}
for binding_name, binding_info in metadata.bindings.items():
self.validate_binding_direction(binding_name,
binding_info.direction, func_name)
has_explicit_return, has_implicit_return = \
self.get_explicit_and_implicit_return(
binding_name, binding_info, has_explicit_return,
has_implicit_return, bound_params)
return_binding_name, explicit_return_val_set = \
self.get_return_binding(binding_name,
binding_info.type,
return_binding_name,
explicit_return_val_set)
requires_context = self.is_context_required(params, bound_params,
annotations,
func_name)
input_types, output_types, _ = self.validate_function_params(
params, bound_params, annotations, func_name)
return_type = \
self.get_function_return_type(annotations,
has_explicit_return,
has_implicit_return,
return_binding_name,
func_name)
self.add_func_to_registry_and_return_funcinfo(func, func_name,
function_id,
metadata.directory,
requires_context,
has_explicit_return,
has_implicit_return,
_,
input_types,
output_types,
return_type)
def add_indexed_function(self, function):
func = function.get_user_function()
func_name = function.get_function_name()
function_id = str(uuid.uuid5(namespace=uuid.NAMESPACE_OID,
name=func_name))
return_binding_name: typing.Optional[str] = None
explicit_return_val_set = False
has_explicit_return = False
has_implicit_return = False
sig = inspect.signature(func)
params = dict(sig.parameters)
annotations = typing.get_type_hints(func)
func_dir = str(pathlib.Path(inspect.getfile(func)).parent)
bound_params = {}
for binding in function.get_bindings():
self.validate_binding_direction(binding.name,
binding.direction,
func_name)
has_explicit_return, has_implicit_return = \
self.get_explicit_and_implicit_return(
binding.name, binding, has_explicit_return,
has_implicit_return, bound_params)
return_binding_name, explicit_return_val_set = \
self.get_return_binding(binding.name,
binding.type,
return_binding_name,
explicit_return_val_set)
requires_context = self.is_context_required(params, bound_params,
annotations,
func_name)
(input_types, output_types,
deferred_bindings_enabled) = self.validate_function_params(
params,
bound_params,
annotations,
func_name)
return_type = \
self.get_function_return_type(annotations,
has_explicit_return,
has_implicit_return,
return_binding_name,
func_name)
return \
self.add_func_to_registry_and_return_funcinfo(
func, func_name, function_id, func_dir,
requires_context, has_explicit_return,
has_implicit_return, deferred_bindings_enabled,
input_types, output_types,
return_type)