-
Notifications
You must be signed in to change notification settings - Fork 24
Chore/undefined aware base model #159
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,64 @@ | ||||||||||||||||||
| from enum import Enum | ||||||||||||||||||
| from typing import Any | ||||||||||||||||||
|
|
||||||||||||||||||
| from pydantic import BaseModel, model_validator | ||||||||||||||||||
|
|
||||||||||||||||||
| from aci.common.logging_setup import get_logger | ||||||||||||||||||
|
|
||||||||||||||||||
| logger = get_logger(__name__) | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
| class BehaviorOnDumpWithoutExcludeUnset(Enum): | ||||||||||||||||||
| WARN = "warn" | ||||||||||||||||||
| ERROR = "error" | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
| class UndefinedAwareBaseModel(BaseModel): | ||||||||||||||||||
| """ | ||||||||||||||||||
| A base model that allows all fields to be nullable and use a custom validator to check for | ||||||||||||||||||
| non-nullable fields. | ||||||||||||||||||
| """ | ||||||||||||||||||
|
|
||||||||||||||||||
| _non_nullable_fields: list[str] = [] | ||||||||||||||||||
| _dump_without_exclude_unset_behavior: BehaviorOnDumpWithoutExcludeUnset = ( | ||||||||||||||||||
| BehaviorOnDumpWithoutExcludeUnset.WARN | ||||||||||||||||||
| ) | ||||||||||||||||||
|
|
||||||||||||||||||
| @model_validator(mode="after") | ||||||||||||||||||
| def validate_non_nullable_fields(self) -> "UndefinedAwareBaseModel": | ||||||||||||||||||
| """ | ||||||||||||||||||
| As there is no easy way to differentiate between "None" and "Undefined" with Pydantic. | ||||||||||||||||||
| We don't know whether caller do not provide a value for a field or want to explicitly set | ||||||||||||||||||
| it to None. We use a workaround as follow: | ||||||||||||||||||
| - We allow all fields to be nullable and default to None ewhen defining the pydantic model. | ||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix typo in docstring. "ewhen" should be "when". - - We allow all fields to be nullable and default to None ewhen defining the pydantic model.
+ - We allow all fields to be nullable and default to None when defining the pydantic model.📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
| - We use a custom validator to check for non-nullable fields. | ||||||||||||||||||
| - Only when caller provided a value, field name will be in `model.model_fields_set`. | ||||||||||||||||||
| - When updating to database, we either check `model.model_fields_set` or use | ||||||||||||||||||
| `model_dump(exclude_unset=True)` to exclude unset fields. | ||||||||||||||||||
| - If model_dump is called without exclude_unset, we console warn (default) or raise error. | ||||||||||||||||||
| """ | ||||||||||||||||||
|
|
||||||||||||||||||
| non_nullable_fields = self._non_nullable_fields | ||||||||||||||||||
| for field in self.model_fields_set: | ||||||||||||||||||
| if field in non_nullable_fields and getattr(self, field) is None: | ||||||||||||||||||
| raise ValueError(f"{field} cannot be None if it is provided.") | ||||||||||||||||||
| return self | ||||||||||||||||||
|
Comment on lines
+22
to
+45
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unclear how subclasses populate The validation logic relies on Consider one of these approaches:
class ConfigDict:
non_nullable_fields: list[str] = []
class MyModel(UndefinedAwareBaseModel):
field1: str | None = None
field2: int | None = None
_non_nullable_fields: list[str] = PrivateAttr(default_factory=lambda: ["field1"])
|
||||||||||||||||||
|
|
||||||||||||||||||
| def model_dump(self, **kwargs: Any) -> dict: | ||||||||||||||||||
| # Warn if model_dump is called without exclude_unset | ||||||||||||||||||
| if "exclude_unset" not in kwargs: | ||||||||||||||||||
| match self._dump_without_exclude_unset_behavior: | ||||||||||||||||||
| case BehaviorOnDumpWithoutExcludeUnset.WARN: | ||||||||||||||||||
| logger.warning( | ||||||||||||||||||
| "model_dump is called without providing `exclude_unset` args. This may " | ||||||||||||||||||
| "accidentally include unset fields." | ||||||||||||||||||
| ) | ||||||||||||||||||
| case BehaviorOnDumpWithoutExcludeUnset.ERROR: | ||||||||||||||||||
| raise SyntaxError( | ||||||||||||||||||
| "model_dump is called without providing `exclude_unset` args. This may " | ||||||||||||||||||
| "accidentally include unset fields." | ||||||||||||||||||
| ) | ||||||||||||||||||
|
Comment on lines
+57
to
+60
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use
Apply this diff: - raise SyntaxError(
+ raise ValueError(
"model_dump is called without providing `exclude_unset` args. This may "
"accidentally include unset fields."
)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
| return super().model_dump(**kwargs) | ||||||||||||||||||
|
|
||||||||||||||||||
| # TODO: Do we need to intercept __getattribute__() to protect when directly accessing the field | ||||||||||||||||||
| # via `model.field` ? | ||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use
PrivateAttr()for internal configuration fields.The fields
_non_nullable_fieldsand_dump_without_exclude_unset_behaviorare currently treated as regular Pydantic model fields, which means they:model_dump()outputSince these are internal configuration attributes, they should use
PrivateAttr()from Pydantic v2.Additionally, the mutable default
[]for_non_nullable_fieldsis a Python anti-pattern that could cause modifications in one instance to affect others.Apply this diff to fix the issue:
📝 Committable suggestion
🤖 Prompt for AI Agents