I'm using Pydantic for settings managment and now I faced with an issue.
Let's say I have different Settings classes for multiple environments. Now I want to use class as a field attribute for different Settings environments and change them during testing. The problem is that I'm able to fetch class, but not set it explicitly during an execution. Here is a brief example:
In [96]: from typing import TypeVar
In [97]: UserSchemaType = TypeVar("UserSchemaType", bound=BaseModel)
In [98]: from pydantic import BaseModel
In [99]: class User(BaseModel):
...: id: str
...:
In [100]: class CustomUser(BaseModel):
...: id: str
...: first_name: str
...: last_name: str
...:
In [101]: class AppSettings(BaseSettings):
...: # some settings
...: foo: str = 'foo'
...:
...: user_class: UserSchemaType = User
...:
In [102]: class TestAppSettings(AppSettings):
...: # some settings
...: pass
...:
In [103]: test_app_settings = TestAppSettings()
In [104]: test_app_settings.dict()
Out[104]: {'foo': 'foo'}
In [105]: test_app_settings.user_class
Out[105]: __main__.User
In [106]: test_app_settings.user_class = CustomUser
ValueError: "TestAppSettings" object has no field "user_class"
CodePudding user response:
AppSettings.user_class is not typed correctly.
The annotation user_class: UserSchemaType declares that user_class should be an instance of a BaseModel, e.g., user_class: UserSchemaType = User(...).
However, you are assigning a type, not an instance: user_class: UserSchemaType = User.
The correct annotation is user_class: type[UserSchemaType] or, depending on your python version you will need to use from typing import Type and then user_class: Type[UserSchemaType] = User.
If you inspect test_app_settings.__fields__ while using the incorrect type annotation, you'll see that user_class is not there. Pydantic overrides __setattr__ and by default it checks whether an attribute name is in __fields__, if not found it throws the error you experienced.
I very quickly looked for an implementation of __getattr__ and it seems that pydantic doesn't re-implement that, so your attribute access check (test_app_settings.user_class on input 105 in your example) is subject to normal python MRO attribute lookup, which is why it finds it there.
As an aside, you are not really getting anything out of using a TypeVar in your example, as the class isn't generic. You could just as easily use user_class: BaseModel = User.
