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 and field_name != 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 and field_name != 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}'"
)
6 changes: 6 additions & 0 deletions src/a2a/utils/proto_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,12 @@ def security_scheme(
flows=cls.oauth2_flows(scheme.root.flows),
)
)
if isinstance(scheme.root, types.MutualTLSSecurityScheme):
return a2a_pb2.SecurityScheme(
mtls_security_scheme=a2a_pb2.MutualTlsSecurityScheme(
description=scheme.root.description,
)
)
return a2a_pb2.SecurityScheme(
open_id_connect_security_scheme=a2a_pb2.OpenIdConnectSecurityScheme(
description=scheme.root.description,
Expand Down
37 changes: 27 additions & 10 deletions tests/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -1556,7 +1556,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 @@ -1565,6 +1569,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 @@ -1577,21 +1582,33 @@ def test_camelCase() -> None:
supportsAuthenticatedExtendedCard=True, # type: ignore
)

# Test setting an attribute via camelCase alias
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(
ValueError,
match='"AgentCard" object has no field "supportsAuthenticatedExtendedCard"',
):
agent_card.supportsAuthenticatedExtendedCard = False

# Test getting an attribute via camelCase alias
with pytest.warns(
DeprecationWarning, match="Accessing field 'defaultInputModes'"
# Test getting an attribute via camelCase alias raises AttributeError
with pytest.raises(
AttributeError,
match="'AgentCard' object has no attribute 'defaultInputModes'",
):
default_input_modes = agent_card.defaultInputModes
_ = agent_card.defaultInputModes

# 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']

Expand Down
Loading