Skip to content

Commit 28656a0

Browse files
committed
Add support for camelCase __getattr__
1 parent 04f105a commit 28656a0

File tree

2 files changed

+39
-19
lines changed

2 files changed

+39
-19
lines changed

src/a2a/_base.py

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ class A2ABaseModel(BaseModel):
2626
Provides a common configuration (e.g., alias-based population) and
2727
serves as the foundation for future extensions or shared utilities.
2828
29-
This implementation overrides __setattr__ to allow setting fields
30-
using their camelCase alias for backward compatibility.
29+
This implementation overrides __setattr__ and __getattr__ to allow
30+
getting and setting fields using their camelCase alias for backward
31+
compatibility.
3132
"""
3233

3334
model_config = ConfigDict(
@@ -40,31 +41,48 @@ class A2ABaseModel(BaseModel):
4041

4142
# Cache for the alias -> field_name mapping.
4243
# 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>]`.
4444
_alias_to_field_name_map: ClassVar[dict[str, str] | None] = None
4545

46-
def __setattr__(self, name: str, value: Any) -> None:
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: # noqa: SLF001
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 = { # noqa: SLF001
46+
@classmethod
47+
def _initialize_alias_map(cls) -> None:
48+
"""Build and cache the alias-to-field-name mapping."""
49+
if cls._alias_to_field_name_map is None:
50+
cls._alias_to_field_name_map = {
5751
field.alias: field_name
58-
for field_name, field in self.model_fields.items()
52+
for field_name, field in cls.model_fields.items()
5953
if field.alias is not None
6054
}
6155

62-
# If the attribute name is a known alias, redirect the assignment
63-
# to the actual (snake_case) field name.
56+
def __setattr__(self, name: str, value: Any) -> None:
57+
"""Allow setting attributes via their camelCase alias."""
58+
self.__class__._initialize_alias_map() # noqa: SLF001
59+
assert self.__class__._alias_to_field_name_map is not None # noqa: SLF001
60+
6461
field_name = self.__class__._alias_to_field_name_map.get(name) # noqa: SLF001
6562
if field_name:
66-
# Use the actual field name for the assignment
63+
# If the name is an alias, set the actual (snake_case) attribute.
6764
super().__setattr__(field_name, value)
6865
else:
69-
# Otherwise, perform a standard attribute assignment
66+
# Otherwise, perform a standard attribute assignment.
7067
super().__setattr__(name, value)
68+
69+
def __getattr__(self, name: str) -> Any:
70+
"""Allow getting attributes via their camelCase alias.
71+
72+
This method is called as a fallback when the attribute 'name' is
73+
not found through normal mechanisms.
74+
"""
75+
self.__class__._initialize_alias_map() # noqa: SLF001
76+
# The map must exist at this point, so we can assert it for type checkers
77+
assert self.__class__._alias_to_field_name_map is not None # noqa: SLF001
78+
79+
field_name = self.__class__._alias_to_field_name_map.get(name) # noqa: SLF001
80+
if field_name:
81+
# If the name is an alias, get the actual (snake_case) attribute.
82+
return getattr(self, field_name)
83+
84+
# If the name is not a known alias, it's a genuine missing attribute.
85+
# It is crucial to raise AttributeError to maintain normal Python behavior.
86+
raise AttributeError(
87+
f"'{self.__class__.__name__}' object has no attribute '{name}'"
88+
)

tests/test_types.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1553,4 +1553,6 @@ def test_camelCase() -> None:
15531553

15541554
agent_card.supportsAuthenticatedExtendedCard = False
15551555

1556+
default_input_modes = agent_card.defaultInputModes
15561557
assert agent_card
1558+
assert default_input_modes == ['text']

0 commit comments

Comments
 (0)