about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/azure/ai/ml/dsl/_dynamic.py
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/azure/ai/ml/dsl/_dynamic.py')
-rw-r--r--.venv/lib/python3.12/site-packages/azure/ai/ml/dsl/_dynamic.py192
1 files changed, 192 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/azure/ai/ml/dsl/_dynamic.py b/.venv/lib/python3.12/site-packages/azure/ai/ml/dsl/_dynamic.py
new file mode 100644
index 00000000..3fcb42fb
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/azure/ai/ml/dsl/_dynamic.py
@@ -0,0 +1,192 @@
+# ---------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# ---------------------------------------------------------
+import logging
+import types
+from inspect import Parameter, Signature
+from typing import Any, Callable, Dict, Sequence, cast
+
+from azure.ai.ml.entities import Component
+from azure.ai.ml.exceptions import ErrorCategory, ErrorTarget, UnexpectedKeywordError, ValidationException
+
+module_logger = logging.getLogger(__name__)
+
+
+class KwParameter(Parameter):
+    """A keyword-only parameter with a default value.
+
+    :param name: The name of the parameter.
+    :type name: str
+    :param default: The default value of the parameter.
+    :param annotation: The annotation type of the parameter, defaults to `Parameter.empty`.
+    :type annotation: Any
+    :param _type: The type of the parameter, defaults to "str".
+    :type _type: str
+    :param _optional: Indicates if the parameter is optional, defaults to False.
+    :type _optional: bool
+    """
+
+    def __init__(
+        self, name: str, default: Any, annotation: Any = Parameter.empty, _type: str = "str", _optional: bool = False
+    ) -> None:
+        super().__init__(name, Parameter.KEYWORD_ONLY, default=default, annotation=annotation)
+        self._type = _type
+        self._optional = _optional
+
+
+def _replace_function_name(func: types.FunctionType, new_name: str) -> types.FunctionType:
+    """Replaces the name of a function with a new name
+
+    :param func: The function to update
+    :type func: types.FunctionType
+    :param new_name: The new function name
+    :type new_name: str
+    :return: The function with a replaced name, but otherwise unchanged body
+    :rtype: types.FunctionType
+    """
+    try:
+        # Use the original code of the function to initialize a new code object for the new function.
+        code_template = func.__code__
+        # For python>=3.8, it is recommended to use `CodeType.replace`, since the interface is change in py3.8
+        # See https://github.com/python/cpython/blob/384621c42f9102e31ba2c47feba144af09c989e5/Objects/codeobject.c#L646
+        # The interface has been changed in py3.8, so the CodeType initializing code is invalid.
+        # See https://github.com/python/cpython/blob/384621c42f9102e31ba2c47feba144af09c989e5/Objects/codeobject.c#L446
+        if hasattr(code_template, "replace"):
+            code = code_template.replace(co_name=new_name)
+        else:
+            # Before python<3.8, replace is not available, we can only initialize the code as following.
+            # https://github.com/python/cpython/blob/v3.7.8/Objects/codeobject.c#L97
+
+            # Bug Item number: 2881688
+            code = types.CodeType(  # type: ignore
+                code_template.co_argcount,
+                code_template.co_kwonlyargcount,
+                code_template.co_nlocals,
+                code_template.co_stacksize,
+                code_template.co_flags,
+                code_template.co_code,  # type: ignore
+                code_template.co_consts,  # type: ignore
+                code_template.co_names,
+                code_template.co_varnames,
+                code_template.co_filename,  # type: ignore
+                new_name,  # Use the new name for the new code object.
+                code_template.co_firstlineno,  # type: ignore
+                code_template.co_lnotab,  # type: ignore
+                # The following two values are required for closures.
+                code_template.co_freevars,  # type: ignore
+                code_template.co_cellvars,  # type: ignore
+            )
+        # Initialize a new function with the code object and the new name, see the following ref for more details.
+        # https://github.com/python/cpython/blob/4901fe274bc82b95dc89bcb3de8802a3dfedab32/Objects/clinic/funcobject.c.h#L30
+        return types.FunctionType(
+            code,
+            globals=func.__globals__,
+            name=new_name,
+            argdefs=func.__defaults__,
+            # Closure must be set to make sure free variables work.
+            closure=func.__closure__,
+        )
+    except BaseException:  # pylint: disable=W0718
+        # If the dynamic replacing failed in corner cases, simply set the two fields.
+        func.__name__ = func.__qualname__ = new_name
+        return func
+
+
+# pylint: disable-next=docstring-missing-param
+def _assert_arg_valid(kwargs: dict, keys: list, func_name: str) -> None:
+    """Assert the arg keys are all in keys."""
+    # pylint: disable=protected-access
+    # validate component input names
+    Component._validate_io_names(kwargs, raise_error=True)
+    lower2original_parameter_names = {x.lower(): x for x in keys}
+    kwargs_need_to_update = []
+    for key in kwargs:
+        if key not in keys:
+            lower_key = key.lower()
+            if lower_key in lower2original_parameter_names:
+                # record key that need to update
+                kwargs_need_to_update.append(key)
+                if key != lower_key:
+                    # raise warning if name not match sanitize version
+                    module_logger.warning(
+                        "Component input name %s, treat it as %s", key, lower2original_parameter_names[lower_key]
+                    )
+            else:
+                raise UnexpectedKeywordError(func_name=func_name, keyword=key, keywords=keys)
+    # update kwargs to align with yaml definition
+    for key in kwargs_need_to_update:
+        kwargs[lower2original_parameter_names[key.lower()]] = kwargs.pop(key)
+
+
+def _update_dct_if_not_exist(dst: Dict, src: Dict) -> None:
+    """Computes the union of `src` and `dst`, in-place within `dst`
+
+    If a key exists in `dst` and `src` the value in `dst` is preserved
+
+    :param dst: The destination to compute the union within
+    :type dst: Dict
+    :param src: A dictionary to include in the union
+    :type src: Dict
+    """
+    for k, v in src.items():
+        if k not in dst:
+            dst[k] = v
+
+
+def create_kw_function_from_parameters(
+    func: Callable,
+    parameters: Sequence[Parameter],
+    flattened_group_keys: list,
+    func_name: str,
+    documentation: str,
+) -> Callable:
+    """Create a new keyword-only function with provided parameters.
+
+    :param func: The original function to be wrapped.
+    :type func: Callable
+    :param parameters: The sequence of parameters for the new function.
+    :type parameters: Sequence[Parameter]
+    :param flattened_group_keys: The list of valid group keys.
+    :type flattened_group_keys: list
+    :param func_name: The name of the new function.
+    :type func_name: str
+    :param documentation: The documentation string for the new function.
+    :type documentation: str
+    :return: The new keyword-only function.
+    :rtype: Callable
+    :raises ValidationException: If the provided function parameters are not keyword-only.
+    """
+    if any(p.default == p.empty or p.kind != Parameter.KEYWORD_ONLY for p in parameters):
+        msg = "This function only accept keyword only parameters."
+        raise ValidationException(
+            message=msg,
+            no_personal_data_message=msg,
+            error_category=ErrorCategory.USER_ERROR,
+            target=ErrorTarget.COMPONENT,
+        )
+    default_kwargs = {p.name: p.default for p in parameters}
+
+    def f(**kwargs: Any) -> Any:
+        # We need to make sure all keys of kwargs are valid.
+        # Merge valid group keys with original keys.
+        _assert_arg_valid(kwargs, [*list(default_kwargs.keys()), *flattened_group_keys], func_name=func_name)
+        # We need to put the default args to the kwargs before invoking the original function.
+        _update_dct_if_not_exist(kwargs, default_kwargs)
+        return func(**kwargs)
+
+    f = _replace_function_name(cast(types.FunctionType, f), func_name)
+    # Set the signature so jupyter notebook could have param hint by calling inspect.signature()
+    # Bug Item number: 2883223
+    f.__signature__ = Signature(parameters)  # type: ignore
+    # Set doc/name/module to make sure help(f) shows following expected result.
+    # Expected help(f):
+    #
+    # Help on function FUNC_NAME:
+    # FUNC_NAME(SIGNATURE)
+    #     FUNC_DOC
+    #
+    f.__doc__ = documentation  # Set documentation to update FUNC_DOC in help.
+    # Set module = None to avoid showing the sentence `in module 'azure.ai.ml.component._dynamic' in help.`
+    # See https://github.com/python/cpython/blob/2145c8c9724287a310bc77a2760d4f1c0ca9eb0c/Lib/pydoc.py#L1757
+    f.__module__ = None  # type: ignore
+    return f