From 1b356b3012f6887bf52081448f368885f35df848 Mon Sep 17 00:00:00 2001 From: Holt Skinner Date: Mon, 21 Jul 2025 20:37:22 +0100 Subject: [PATCH 1/5] Add deprecation warning for camelCase Alias --- src/a2a/_base.py | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/a2a/_base.py b/src/a2a/_base.py index deb8b334..f48d34ab 100644 --- a/src/a2a/_base.py +++ b/src/a2a/_base.py @@ -1,3 +1,5 @@ +import warnings + from typing import Any, ClassVar from pydantic import BaseModel, ConfigDict @@ -27,13 +29,13 @@ class A2ABaseModel(BaseModel): serves as the foundation for future extensions or shared utilities. This implementation provides backward compatibility for camelCase aliases - by lazy-loading an alias map upon first use. + by lazy-loading an alias map upon first use. Accessing or setting + attributes via their camelCase alias will raise a DeprecationWarning. """ model_config = ConfigDict( # SEE: https://docs.pydantic.dev/latest/api/config/#pydantic.config.ConfigDict.populate_by_name - validate_by_name=True, - validate_by_alias=True, + populate_by_name=True, # Recommended over validate_by_name/validate_by_alias serialize_by_alias=True, alias_generator=to_camel_custom, ) @@ -60,6 +62,18 @@ def __setattr__(self, name: str, value: Any) -> None: """Allow setting attributes via their camelCase alias.""" # Get the map and find the corresponding snake_case field name. field_name = type(self)._get_alias_map().get(name) # noqa: SLF001 + + if field_name: + # An alias was used, issue a warning. + warnings.warn( + ( + f"Setting field '{name}' via its camelCase alias is deprecated and will be removed in version 0.3.0 " + f"Use the snake_case name '{field_name}' instead." + ), + DeprecationWarning, + stacklevel=2, + ) + # If an alias was used, field_name will be set; otherwise, use the original name. super().__setattr__(field_name or name, value) @@ -67,7 +81,18 @@ def __getattr__(self, name: str) -> Any: """Allow getting attributes via their camelCase alias.""" # Get the map and find the corresponding snake_case field name. field_name = type(self)._get_alias_map().get(name) # noqa: SLF001 + if field_name: + # An alias was used, issue a warning. + warnings.warn( + ( + f"Accessing field '{name}' via its camelCase alias is deprecated and will be removed in version 0.3.0 " + f"Use the snake_case name '{field_name}' instead." + ), + DeprecationWarning, + stacklevel=2, + ) + # If an alias was used, retrieve the actual snake_case attribute. return getattr(self, field_name) From 3dd5ea8e3156e3c82a59f1f5d7662f7a231d71c9 Mon Sep 17 00:00:00 2001 From: Holt Skinner Date: Mon, 21 Jul 2025 20:37:45 +0100 Subject: [PATCH 2/5] Convert remaining camelCase usage to snake_case --- .../server/request_handlers/default_request_handler.py | 4 ++-- .../request_handlers/test_default_request_handler.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/a2a/server/request_handlers/default_request_handler.py b/src/a2a/server/request_handlers/default_request_handler.py index e27aedf0..8c5f58a3 100644 --- a/src/a2a/server/request_handlers/default_request_handler.py +++ b/src/a2a/server/request_handlers/default_request_handler.py @@ -200,10 +200,10 @@ async def _setup_message_execution( ) task = task_manager.update_with_message(params.message, task) - elif params.message.taskId: + elif params.message.task_id: raise ServerError( error=TaskNotFoundError( - message=f'Task {params.message.taskId} was specified but does not exist' + message=f'Task {params.message.task_id} was specified but does not exist' ) ) diff --git a/tests/server/request_handlers/test_default_request_handler.py b/tests/server/request_handlers/test_default_request_handler.py index 0bcae844..865bae2e 100644 --- a/tests/server/request_handlers/test_default_request_handler.py +++ b/tests/server/request_handlers/test_default_request_handler.py @@ -1787,7 +1787,7 @@ async def test_on_resubscribe_to_task_in_terminal_state(terminal_state): @pytest.mark.asyncio async def test_on_message_send_task_id_provided_but_task_not_found(): - """Test on_message_send when taskId is provided but task doesn't exist.""" + """Test on_message_send when task_id is provided but task doesn't exist.""" task_id = 'nonexistent_task' mock_task_store = AsyncMock(spec=TaskStore) @@ -1800,7 +1800,7 @@ async def test_on_message_send_task_id_provided_but_task_not_found(): role=Role.user, messageId='msg_nonexistent', parts=[Part(root=TextPart(text='Hello'))], - taskId=task_id, + task_id=task_id, contextId='ctx1', ) ) @@ -1827,7 +1827,7 @@ async def test_on_message_send_task_id_provided_but_task_not_found(): @pytest.mark.asyncio async def test_on_message_send_stream_task_id_provided_but_task_not_found(): - """Test on_message_send_stream when taskId is provided but task doesn't exist.""" + """Test on_message_send_stream when task_id is provided but task doesn't exist.""" task_id = 'nonexistent_stream_task' mock_task_store = AsyncMock(spec=TaskStore) @@ -1840,7 +1840,7 @@ async def test_on_message_send_stream_task_id_provided_but_task_not_found(): role=Role.user, messageId='msg_nonexistent_stream', parts=[Part(root=TextPart(text='Hello'))], - taskId=task_id, + task_id=task_id, contextId='ctx1', ) ) From 4f5674ba23ecdeed4c32e96ae51ad01d4b48a7e3 Mon Sep 17 00:00:00 2001 From: Holt Skinner Date: Mon, 21 Jul 2025 20:39:16 +0100 Subject: [PATCH 3/5] Re-add validate_by_name, validate_by_alias --- src/a2a/_base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/a2a/_base.py b/src/a2a/_base.py index f48d34ab..81ce827d 100644 --- a/src/a2a/_base.py +++ b/src/a2a/_base.py @@ -35,7 +35,8 @@ class A2ABaseModel(BaseModel): model_config = ConfigDict( # SEE: https://docs.pydantic.dev/latest/api/config/#pydantic.config.ConfigDict.populate_by_name - populate_by_name=True, # Recommended over validate_by_name/validate_by_alias + validate_by_name=True, + validate_by_alias=True, serialize_by_alias=True, alias_generator=to_camel_custom, ) From ddae0891203f43ded35c4b0a753780fd05b455d7 Mon Sep 17 00:00:00 2001 From: Holt Skinner Date: Mon, 21 Jul 2025 20:42:01 +0100 Subject: [PATCH 4/5] Add checks for deprecationwarning in tests --- tests/test_types.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/tests/test_types.py b/tests/test_types.py index b02963bc..c9d20209 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -1551,8 +1551,22 @@ def test_camelCase() -> None: supportsAuthenticatedExtendedCard=True, ) - agent_card.supportsAuthenticatedExtendedCard = False - - default_input_modes = agent_card.defaultInputModes - assert agent_card + # Test setting an attribute via camelCase alias + # We expect a DeprecationWarning with a specific message + with pytest.warns( + DeprecationWarning, + match="Setting field 'supportsAuthenticatedExtendedCard'", + ): + agent_card.supportsAuthenticatedExtendedCard = False + + # Test getting an attribute via camelCase alias + # We expect another DeprecationWarning with a specific message + with pytest.warns( + DeprecationWarning, match="Accessing field 'defaultInputModes'" + ): + default_input_modes = agent_card.defaultInputModes + + # Assert the functionality still works as expected + assert agent_card.supports_authenticated_extended_card is False assert default_input_modes == ['text'] + assert agent_card.default_input_modes == ['text'] From 8327315b710330e08ba6a8eb6a868dbed02ba14b Mon Sep 17 00:00:00 2001 From: Holt Skinner Date: Mon, 21 Jul 2025 20:43:52 +0100 Subject: [PATCH 5/5] Fix remaining camelCase usage --- .../request_handlers/test_default_request_handler.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/server/request_handlers/test_default_request_handler.py b/tests/server/request_handlers/test_default_request_handler.py index 865bae2e..08b91b29 100644 --- a/tests/server/request_handlers/test_default_request_handler.py +++ b/tests/server/request_handlers/test_default_request_handler.py @@ -1798,10 +1798,10 @@ async def test_on_message_send_task_id_provided_but_task_not_found(): params = MessageSendParams( message=Message( role=Role.user, - messageId='msg_nonexistent', + message_id='msg_nonexistent', parts=[Part(root=TextPart(text='Hello'))], task_id=task_id, - contextId='ctx1', + context_id='ctx1', ) ) @@ -1838,10 +1838,10 @@ async def test_on_message_send_stream_task_id_provided_but_task_not_found(): params = MessageSendParams( message=Message( role=Role.user, - messageId='msg_nonexistent_stream', + message_id='msg_nonexistent_stream', parts=[Part(root=TextPart(text='Hello'))], task_id=task_id, - contextId='ctx1', + context_id='ctx1', ) )