From 851efcef79618f0443bb1223686fbe4bc6293b02 Mon Sep 17 00:00:00 2001 From: Manohar Paturi <186662190+ManoharPaturi@users.noreply.github.com> Date: Sun, 13 Sep 2026 22:08:02 +0530 Subject: [PATCH] Python: fix kernel_function decorator mutating shared Annotated metadata dicts _parse_parameter() called meta.pop("description") on dict metadata used in typing.Annotated, mutating the user-provided dict. When the same metadata dict is reused across multiple kernel functions (shared parameter docs), every function after the first silently lost its parameter description, and the dict itself was permanently modified. Use meta.get() instead of meta.pop() and filter the description key out when applying the remaining metadata keys, so semantics are unchanged but the caller's dict is left untouched. --- .../functions/kernel_function_decorator.py | 6 ++- .../test_kernel_function_decorators.py | 41 +++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/python/semantic_kernel/functions/kernel_function_decorator.py b/python/semantic_kernel/functions/kernel_function_decorator.py index 4d103a1a9093..3176ed2bb4ef 100644 --- a/python/semantic_kernel/functions/kernel_function_decorator.py +++ b/python/semantic_kernel/functions/kernel_function_decorator.py @@ -152,9 +152,11 @@ def _parse_parameter(name: str, param: Any, default: Any) -> dict[str, Any]: ret["description"] = meta elif isinstance(meta, dict): # only override from the metadata if it is not already set - if "description" not in ret and (description := meta.pop("description", None)): + # note: do not mutate the metadata dict itself, it is user-provided + # and can be shared between multiple functions/parameters + if "description" not in ret and (description := meta.get("description", None)): ret["description"] = description - ret.update(meta) + ret.update({key: value for key, value in meta.items() if key != "description"}) else: logger.debug(f"Unknown metadata type: {meta}") if hasattr(param, "__origin__"): diff --git a/python/tests/unit/functions/test_kernel_function_decorators.py b/python/tests/unit/functions/test_kernel_function_decorators.py index 803e0d2ffc7a..e47615fffc94 100644 --- a/python/tests/unit/functions/test_kernel_function_decorators.py +++ b/python/tests/unit/functions/test_kernel_function_decorators.py @@ -280,3 +280,44 @@ def test_annotation_parsing(name, annotation, description, type_, is_required): assert description == annotation_dict.get("description") assert type_ == annotation_dict["type_"] assert is_required == annotation_dict["is_required"] + + +def test_shared_annotation_metadata_dict_not_mutated(): + """A dict used as Annotated metadata must not be mutated when parsed. + + The same metadata dict can be reused across multiple kernel functions + (e.g. shared parameter docs); parsing one function's signature must not + remove the "description" key for the next function. + """ + shared_meta = {"description": "shared description"} + + @kernel_function + def func_one(arg: Annotated[int, shared_meta]) -> str: + return "one" + + @kernel_function + def func_two(arg: Annotated[int, shared_meta]) -> str: + return "two" + + # the user-provided dict is unchanged + assert shared_meta == {"description": "shared description"} + + params_one = {p["name"]: p.get("description") for p in func_one.__kernel_function_parameters__} + params_two = {p["name"]: p.get("description") for p in func_two.__kernel_function_parameters__} + + assert params_one["arg"] == "shared description" + assert params_two["arg"] == "shared description" + + +def test_annotation_metadata_dict_other_keys_still_applied(): + """Keys other than 'description' in a metadata dict must still be applied, without mutation.""" + meta = {"description": "the description", "default_value": 42} + + @kernel_function + def func(arg: Annotated[int, meta]) -> str: + return "ok" + + assert meta == {"description": "the description", "default_value": 42} + param = func.__kernel_function_parameters__[0] + assert param["description"] == "the description" + assert param["default_value"] == 42