Skip to content
65 changes: 0 additions & 65 deletions src/a2a/_base.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
import warnings

from typing import Any, ClassVar

from pydantic import BaseModel, ConfigDict
from pydantic.alias_generators import to_camel

Expand Down Expand Up @@ -40,64 +36,3 @@ class A2ABaseModel(BaseModel):
serialize_by_alias=True,
alias_generator=to_camel_custom,
)

# Cache for the alias -> field_name mapping.
# It starts as None and is populated on first access.
_alias_to_field_name_map: ClassVar[dict[str, str] | None] = None

@classmethod
def _get_alias_map(cls) -> dict[str, str]:
"""Lazily builds and returns the alias-to-field-name mapping for the class.

The map is cached on the class object to avoid re-computation.
"""
if cls._alias_to_field_name_map is None:
cls._alias_to_field_name_map = {
field.alias: field_name
for field_name, field in cls.model_fields.items()
if field.alias is not None
}
return cls._alias_to_field_name_map

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)

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)

# If it's not a known alias, it's a genuine missing attribute.
raise AttributeError(
f"'{type(self).__name__}' object has no attribute '{name}'"
)
40 changes: 27 additions & 13 deletions tests/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -1530,7 +1530,11 @@ def test_use_get_task_push_notification_params_for_request() -> None:
)


def test_camelCase() -> None:
def test_camelCase_access_raises_attribute_error() -> None:
"""
Tests that accessing or setting fields via their camelCase alias
raises an AttributeError.
"""
skill = AgentSkill(
id='hello_world',
name='Returns hello world',
Expand All @@ -1539,6 +1543,7 @@ def test_camelCase() -> None:
examples=['hi', 'hello world'],
)

# Initialization with camelCase still works due to Pydantic's populate_by_name config
agent_card = AgentCard(
name='Hello World Agent',
description='Just a hello world agent',
Expand All @@ -1551,22 +1556,31 @@ def test_camelCase() -> None:
supportsAuthenticatedExtendedCard=True,
)

# Test setting an attribute via camelCase alias
# We expect a DeprecationWarning with a specific message
with pytest.warns(
DeprecationWarning,
match="Setting field 'supportsAuthenticatedExtendedCard'",
# --- Test that using camelCase aliases raises errors ---

# Test setting an attribute via camelCase alias raises AttributeError
with pytest.raises(
AttributeError,
match="Setting field 'supportsAuthenticatedExtendedCard' via its camelCase alias is not allowed. Use the snake_case name 'supports_authenticated_extended_card' instead.",
):
agent_card.supportsAuthenticatedExtendedCard = False
Comment thread
holtskinner marked this conversation as resolved.

# Test getting an attribute via camelCase alias
# We expect another DeprecationWarning with a specific message
with pytest.warns(
DeprecationWarning, match="Accessing field 'defaultInputModes'"
# Test getting an attribute via camelCase alias raises AttributeError
with pytest.raises(
AttributeError,
match="Accessing field 'defaultInputModes' via its camelCase alias is not allowed. Use the snake_case name 'default_input_modes' instead.",
):
default_input_modes = agent_card.defaultInputModes
_ = agent_card.defaultInputModes
Comment thread
holtskinner marked this conversation as resolved.

# Assert the functionality still works as expected
# --- Test that using snake_case names works correctly ---

# The value should be unchanged because the camelCase setattr failed
assert agent_card.supports_authenticated_extended_card is True

# Now, set it correctly using the snake_case name
agent_card.supports_authenticated_extended_card = False
assert agent_card.supports_authenticated_extended_card is False

# Get the attribute correctly using the snake_case name
default_input_modes = agent_card.default_input_modes
assert default_input_modes == ['text']
assert agent_card.default_input_modes == ['text']
Loading