From d313cc3f0fc18fb2c3968bebfe9f82789ef137d4 Mon Sep 17 00:00:00 2001 From: Akanksha Trehun <146705736+magic-peach@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:43:44 +0000 Subject: [PATCH] Reject None for required tool call parameters (#1225) Signed-off-by: Akanksha Trehun Co-authored-by: Sam Kendziora Co-authored-by: Dapr Bot <56698301+dapr-bot@users.noreply.github.com> (cherry picked from commit 27f115816c56382a82a27e484b37116a17981273) Signed-off-by: dapr-bot --- dapr/clients/grpc/_conversation_helpers.py | 5 ++- tests/clients/test_conversation_helpers.py | 36 ++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/dapr/clients/grpc/_conversation_helpers.py b/dapr/clients/grpc/_conversation_helpers.py index dae006bb4..d50055afe 100644 --- a/dapr/clients/grpc/_conversation_helpers.py +++ b/dapr/clients/grpc/_conversation_helpers.py @@ -880,6 +880,9 @@ def _coerce_and_validate(value: Any, expected_type: Any) -> Any: str(last_err) if last_err else f'Cannot coerce {value!r} to {expected_type}' ) + if value is None: + raise ValueError(f'None is not a valid value for {expected_type}') + origin = get_origin(expected_type) # Literal @@ -912,7 +915,7 @@ def _coerce_and_validate(value: Any, expected_type: Any) -> Any: # Dataclasses if inspect.isclass(expected_type) and is_dataclass(expected_type): - if isinstance(value, expected_type) or value is None: + if isinstance(value, expected_type): return value raise ValueError( f'Expected {expected_type.__name__} dataclass instance, got {type(value).__name__}' diff --git a/tests/clients/test_conversation_helpers.py b/tests/clients/test_conversation_helpers.py index e7c69b30e..473dd7bff 100644 --- a/tests/clients/test_conversation_helpers.py +++ b/tests/clients/test_conversation_helpers.py @@ -1836,6 +1836,42 @@ def k(p: P) -> str: with self.assertRaises(ToolArgumentError): bind_params_to_func(k, {'p': {'x': 1, 'y': 'nope'}}) + def test_none_rejected_for_required_str(self): + def f(name: str) -> str: + return name + + with self.assertRaises(ToolArgumentError): + bind_params_to_func(f, {'name': None}) + + def test_none_rejected_for_required_dataclass(self): + @dataclass + class P: + x: int + + def k(p: P) -> int: + return p.x + + with self.assertRaises(ToolArgumentError): + bind_params_to_func(k, {'p': None}) + + def test_none_rejected_for_required_plain_class(self): + class C: + def __init__(self, level: str): + self.level = level + + def apply(config: C) -> str: + return config.level + + with self.assertRaises(ToolArgumentError): + bind_params_to_func(apply, {'config': None}) + + def test_none_still_allowed_for_optional(self): + def f(name: Optional[str]) -> Optional[str]: + return name + + bound = bind_params_to_func(f, {'name': None}) + self.assertIsNone(f(*bound.args, **bound.kwargs)) + class TestPlainClassSchema(unittest.TestCase): def test_plain_class_init_signature(self):