Skip to content
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

Fix type-safety of attribute access in BaseModel #8651

Merged
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
121 changes: 61 additions & 60 deletions pydantic/main.py
Expand Up @@ -733,6 +733,7 @@ def __deepcopy__(self: Model, memo: dict[int, Any] | None = None) -> Model:

if not typing.TYPE_CHECKING:
# We put `__getattr__` in a non-TYPE_CHECKING block because otherwise, mypy allows arbitrary attribute access
# The same goes for __setattr__ and __delattr__, see: https://github.com/pydantic/pydantic/issues/8643

def __getattr__(self, item: str) -> Any:
private_attributes = object.__getattribute__(self, '__private_attributes__')
Expand Down Expand Up @@ -766,74 +767,74 @@ def __getattr__(self, item: str) -> Any:
# this is the current error
raise AttributeError(f'{type(self).__name__!r} object has no attribute {item!r}')

def __setattr__(self, name: str, value: Any) -> None:
if name in self.__class_vars__:
raise AttributeError(
f'{name!r} is a ClassVar of `{self.__class__.__name__}` and cannot be set on an instance. '
f'If you want to set a value on the class, use `{self.__class__.__name__}.{name} = value`.'
)
elif not _fields.is_valid_field_name(name):
if self.__pydantic_private__ is None or name not in self.__private_attributes__:
_object_setattr(self, name, value)
else:
attribute = self.__private_attributes__[name]
if hasattr(attribute, '__set__'):
attribute.__set__(self, value) # type: ignore
def __setattr__(self, name: str, value: Any) -> None:
if name in self.__class_vars__:
raise AttributeError(
f'{name!r} is a ClassVar of `{self.__class__.__name__}` and cannot be set on an instance. '
f'If you want to set a value on the class, use `{self.__class__.__name__}.{name} = value`.'
)
elif not _fields.is_valid_field_name(name):
if self.__pydantic_private__ is None or name not in self.__private_attributes__:
_object_setattr(self, name, value)
else:
self.__pydantic_private__[name] = value
return
attribute = self.__private_attributes__[name]
if hasattr(attribute, '__set__'):
attribute.__set__(self, value) # type: ignore
else:
self.__pydantic_private__[name] = value
return

self._check_frozen(name, value)

attr = getattr(self.__class__, name, None)
if isinstance(attr, property):
attr.__set__(self, value)
elif self.model_config.get('validate_assignment', None):
self.__pydantic_validator__.validate_assignment(self, name, value)
elif self.model_config.get('extra') != 'allow' and name not in self.model_fields:
# TODO - matching error
raise ValueError(f'"{self.__class__.__name__}" object has no field "{name}"')
elif self.model_config.get('extra') == 'allow' and name not in self.model_fields:
if self.model_extra and name in self.model_extra:
self.__pydantic_extra__[name] = value # type: ignore
else:
try:
getattr(self, name)
except AttributeError:
# attribute does not already exist on instance, so put it in extra
self._check_frozen(name, value)

attr = getattr(self.__class__, name, None)
if isinstance(attr, property):
attr.__set__(self, value)
elif self.model_config.get('validate_assignment', None):
self.__pydantic_validator__.validate_assignment(self, name, value)
elif self.model_config.get('extra') != 'allow' and name not in self.model_fields:
# TODO - matching error
raise ValueError(f'"{self.__class__.__name__}" object has no field "{name}"')
elif self.model_config.get('extra') == 'allow' and name not in self.model_fields:
if self.model_extra and name in self.model_extra:
self.__pydantic_extra__[name] = value # type: ignore
else:
# attribute _does_ already exist on instance, and was not in extra, so update it
_object_setattr(self, name, value)
else:
self.__dict__[name] = value
self.__pydantic_fields_set__.add(name)

def __delattr__(self, item: str) -> Any:
if item in self.__private_attributes__:
attribute = self.__private_attributes__[item]
if hasattr(attribute, '__delete__'):
attribute.__delete__(self) # type: ignore
return
try:
getattr(self, name)
except AttributeError:
# attribute does not already exist on instance, so put it in extra
self.__pydantic_extra__[name] = value # type: ignore
else:
# attribute _does_ already exist on instance, and was not in extra, so update it
_object_setattr(self, name, value)
else:
self.__dict__[name] = value
self.__pydantic_fields_set__.add(name)

try:
# Note: self.__pydantic_private__ cannot be None if self.__private_attributes__ has items
del self.__pydantic_private__[item] # type: ignore
return
except KeyError as exc:
raise AttributeError(f'{type(self).__name__!r} object has no attribute {item!r}') from exc
def __delattr__(self, item: str) -> Any:
if item in self.__private_attributes__:
attribute = self.__private_attributes__[item]
if hasattr(attribute, '__delete__'):
attribute.__delete__(self) # type: ignore
return

self._check_frozen(item, None)
try:
# Note: self.__pydantic_private__ cannot be None if self.__private_attributes__ has items
del self.__pydantic_private__[item] # type: ignore
return
except KeyError as exc:
raise AttributeError(f'{type(self).__name__!r} object has no attribute {item!r}') from exc

if item in self.model_fields:
object.__delattr__(self, item)
elif self.__pydantic_extra__ is not None and item in self.__pydantic_extra__:
del self.__pydantic_extra__[item]
else:
try:
self._check_frozen(item, None)

if item in self.model_fields:
object.__delattr__(self, item)
except AttributeError:
raise AttributeError(f'{type(self).__name__!r} object has no attribute {item!r}')
elif self.__pydantic_extra__ is not None and item in self.__pydantic_extra__:
del self.__pydantic_extra__[item]
else:
try:
object.__delattr__(self, item)
except AttributeError:
raise AttributeError(f'{type(self).__name__!r} object has no attribute {item!r}')

def _check_frozen(self, name: str, value: Any) -> None:
if self.model_config.get('frozen', None):
Expand Down