Skip to content

Commit 59d5ffb

Browse files
committed
Add backwards compatibility for camelCase
1 parent 66011b6 commit 59d5ffb

File tree

2 files changed

+62
-0
lines changed

2 files changed

+62
-0
lines changed

src/a2a/_base.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import Any, ClassVar
2+
13
from pydantic import BaseModel, ConfigDict
24
from pydantic.alias_generators import to_camel
35

@@ -23,6 +25,9 @@ class A2ABaseModel(BaseModel):
2325
2426
Provides a common configuration (e.g., alias-based population) and
2527
serves as the foundation for future extensions or shared utilities.
28+
29+
This implementation overrides __setattr__ to allow setting fields
30+
using their camelCase alias for backward compatibility.
2631
"""
2732

2833
model_config = ConfigDict(
@@ -32,3 +37,34 @@ class A2ABaseModel(BaseModel):
3237
serialize_by_alias=True,
3338
alias_generator=to_camel_custom,
3439
)
40+
41+
# Cache for the alias -> field_name mapping.
42+
# We use a ClassVar so it's created once per class, not per instance.
43+
# The type hint is now corrected to be `ClassVar[<optional_type>]`.
44+
_alias_to_field_name_map: ClassVar[dict[str, str] | None] = None
45+
46+
def __setattr__(self, name: str, value: Any):
47+
"""Allow setting attributes via their camelCase alias.
48+
49+
This is overridden to provide backward compatibility for code that
50+
sets model fields using aliases after initialization.
51+
"""
52+
# Build the alias-to-name mapping on first use and cache it.
53+
if self.__class__._alias_to_field_name_map is None:
54+
# Using a lock or other mechanism could make this more thread-safe
55+
# for highly concurrent applications, but this is fine for most cases.
56+
self.__class__._alias_to_field_name_map = {
57+
field.alias: field_name
58+
for field_name, field in self.model_fields.items()
59+
if field.alias is not None
60+
}
61+
62+
# If the attribute name is a known alias, redirect the assignment
63+
# to the actual (snake_case) field name.
64+
field_name = self.__class__._alias_to_field_name_map.get(name)
65+
if field_name:
66+
# Use the actual field name for the assignment
67+
super().__setattr__(field_name, value)
68+
else:
69+
# Otherwise, perform a standard attribute assignment
70+
super().__setattr__(name, value)

tests/test_types.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1528,3 +1528,29 @@ def test_use_get_task_push_notification_params_for_request() -> None:
15281528
assert (
15291529
a2a_req_get_push_req.root.method == 'tasks/pushNotificationConfig/get'
15301530
)
1531+
1532+
1533+
def test_camelCase() -> None:
1534+
skill = AgentSkill(
1535+
id='hello_world',
1536+
name='Returns hello world',
1537+
description='just returns hello world',
1538+
tags=['hello world'],
1539+
examples=['hi', 'hello world'],
1540+
)
1541+
1542+
agent_card = AgentCard(
1543+
name='Hello World Agent',
1544+
description='Just a hello world agent',
1545+
url='http://localhost:9999/',
1546+
version='1.0.0',
1547+
defaultInputModes=['text'],
1548+
defaultOutputModes=['text'],
1549+
capabilities=AgentCapabilities(streaming=True),
1550+
skills=[skill],
1551+
supportsAuthenticatedExtendedCard=True,
1552+
)
1553+
1554+
agent_card.supportsAuthenticatedExtendedCard = False
1555+
1556+
assert agent_card

0 commit comments

Comments
 (0)