@@ -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+ )
0 commit comments