|
| 1 | +import warnings |
| 2 | + |
| 3 | +from typing import Any, ClassVar |
| 4 | + |
1 | 5 | from pydantic import BaseModel, ConfigDict |
| 6 | +from pydantic.alias_generators import to_camel |
| 7 | + |
| 8 | + |
| 9 | +def to_camel_custom(snake: str) -> str: |
| 10 | + """Convert a snake_case string to camelCase. |
| 11 | +
|
| 12 | + Args: |
| 13 | + snake: The string to convert. |
| 14 | +
|
| 15 | + Returns: |
| 16 | + The converted camelCase string. |
| 17 | + """ |
| 18 | + # First, remove any trailing underscores. This is common for names that |
| 19 | + # conflict with Python keywords, like 'in_' or 'from_'. |
| 20 | + if snake.endswith('_'): |
| 21 | + snake = snake.rstrip('_') |
| 22 | + return to_camel(snake) |
2 | 23 |
|
3 | 24 |
|
4 | 25 | class A2ABaseModel(BaseModel): |
5 | 26 | """Base class for shared behavior across A2A data models. |
6 | 27 |
|
7 | 28 | Provides a common configuration (e.g., alias-based population) and |
8 | 29 | serves as the foundation for future extensions or shared utilities. |
| 30 | +
|
| 31 | + This implementation provides backward compatibility for camelCase aliases |
| 32 | + by lazy-loading an alias map upon first use. Accessing or setting |
| 33 | + attributes via their camelCase alias will raise a DeprecationWarning. |
9 | 34 | """ |
10 | 35 |
|
11 | 36 | model_config = ConfigDict( |
12 | 37 | # SEE: https://docs.pydantic.dev/latest/api/config/#pydantic.config.ConfigDict.populate_by_name |
13 | 38 | validate_by_name=True, |
14 | 39 | validate_by_alias=True, |
| 40 | + serialize_by_alias=True, |
| 41 | + alias_generator=to_camel_custom, |
15 | 42 | ) |
| 43 | + |
| 44 | + # Cache for the alias -> field_name mapping. |
| 45 | + # It starts as None and is populated on first access. |
| 46 | + _alias_to_field_name_map: ClassVar[dict[str, str] | None] = None |
| 47 | + |
| 48 | + @classmethod |
| 49 | + def _get_alias_map(cls) -> dict[str, str]: |
| 50 | + """Lazily builds and returns the alias-to-field-name mapping for the class. |
| 51 | +
|
| 52 | + The map is cached on the class object to avoid re-computation. |
| 53 | + """ |
| 54 | + if cls._alias_to_field_name_map is None: |
| 55 | + cls._alias_to_field_name_map = { |
| 56 | + field.alias: field_name |
| 57 | + for field_name, field in cls.model_fields.items() |
| 58 | + if field.alias is not None |
| 59 | + } |
| 60 | + return cls._alias_to_field_name_map |
| 61 | + |
| 62 | + def __setattr__(self, name: str, value: Any) -> None: |
| 63 | + """Allow setting attributes via their camelCase alias.""" |
| 64 | + # Get the map and find the corresponding snake_case field name. |
| 65 | + field_name = type(self)._get_alias_map().get(name) # noqa: SLF001 |
| 66 | + |
| 67 | + if field_name: |
| 68 | + # An alias was used, issue a warning. |
| 69 | + warnings.warn( |
| 70 | + ( |
| 71 | + f"Setting field '{name}' via its camelCase alias is deprecated and will be removed in version 0.3.0 " |
| 72 | + f"Use the snake_case name '{field_name}' instead." |
| 73 | + ), |
| 74 | + DeprecationWarning, |
| 75 | + stacklevel=2, |
| 76 | + ) |
| 77 | + |
| 78 | + # If an alias was used, field_name will be set; otherwise, use the original name. |
| 79 | + super().__setattr__(field_name or name, value) |
| 80 | + |
| 81 | + def __getattr__(self, name: str) -> Any: |
| 82 | + """Allow getting attributes via their camelCase alias.""" |
| 83 | + # Get the map and find the corresponding snake_case field name. |
| 84 | + field_name = type(self)._get_alias_map().get(name) # noqa: SLF001 |
| 85 | + |
| 86 | + if field_name: |
| 87 | + # An alias was used, issue a warning. |
| 88 | + warnings.warn( |
| 89 | + ( |
| 90 | + f"Accessing field '{name}' via its camelCase alias is deprecated and will be removed in version 0.3.0 " |
| 91 | + f"Use the snake_case name '{field_name}' instead." |
| 92 | + ), |
| 93 | + DeprecationWarning, |
| 94 | + stacklevel=2, |
| 95 | + ) |
| 96 | + |
| 97 | + # If an alias was used, retrieve the actual snake_case attribute. |
| 98 | + return getattr(self, field_name) |
| 99 | + |
| 100 | + # If it's not a known alias, it's a genuine missing attribute. |
| 101 | + raise AttributeError( |
| 102 | + f"'{type(self).__name__}' object has no attribute '{name}'" |
| 103 | + ) |
0 commit comments