129 lines
4.3 KiB
Python
129 lines
4.3 KiB
Python
from typing import Any, Optional, Set, Dict
|
|
|
|
from pydantic import BaseModel, PrivateAttr, parse_obj_as, ValidationError
|
|
|
|
from pydantic.error_wrappers import ErrorWrapper
|
|
|
|
from lazy_model.nao import NAO
|
|
|
|
ROOT_KEY = "__root__"
|
|
object_setattr = object.__setattr__
|
|
|
|
|
|
class LazyModel(BaseModel):
|
|
_store: Dict[str, Any] = PrivateAttr(default_factory=dict)
|
|
_lazily_parsed: bool = PrivateAttr(default=False)
|
|
|
|
@classmethod
|
|
def lazy_parse(
|
|
cls,
|
|
data: Dict[str, Any],
|
|
fields: Optional[Set[str]] = None,
|
|
**new_kwargs,
|
|
):
|
|
m = cls.__new__(cls, **new_kwargs) # type: ignore
|
|
if fields is None:
|
|
fields = set()
|
|
fields_values: Dict[str, Any] = {}
|
|
field_alias_map: Dict[str, str] = {}
|
|
for name, field in cls.__fields__.items():
|
|
fields_values[name] = NAO
|
|
if field.alias in fields:
|
|
field_alias_map[field.alias] = name
|
|
object_setattr(m, "__dict__", fields_values)
|
|
_fields_set = set(fields_values.keys())
|
|
object_setattr(m, "__fields_set__", _fields_set)
|
|
m._init_private_attributes()
|
|
|
|
m._store = data
|
|
m._lazily_parsed = True
|
|
|
|
for alias in fields:
|
|
m._set_attr(field_alias_map[alias], data[alias])
|
|
return m
|
|
|
|
def _set_attr(self, name, value):
|
|
"""
|
|
Stolen from Pydantic.
|
|
|
|
# TODO rework
|
|
"""
|
|
field_type = self.__fields__.get(name).annotation
|
|
try:
|
|
value = parse_obj_as(field_type, value)
|
|
except ValidationError as e:
|
|
if value is None and self.__fields__.get(name).required is False:
|
|
value = None
|
|
else:
|
|
raise e
|
|
|
|
new_values = {**self.__dict__, name: value}
|
|
|
|
for validator in self.__pre_root_validators__:
|
|
try:
|
|
new_values = validator(self.__class__, new_values)
|
|
except (ValueError, TypeError, AssertionError) as exc:
|
|
raise ValidationError(
|
|
[ErrorWrapper(exc, loc=ROOT_KEY)], self.__class__
|
|
)
|
|
|
|
known_field = self.__fields__.get(name, None)
|
|
if known_field:
|
|
if not known_field.field_info.allow_mutation:
|
|
raise TypeError(
|
|
f'"{known_field.name}" has allow_mutation set '
|
|
f"to False and cannot be assigned"
|
|
)
|
|
dict_without_original_value = {
|
|
k: v for k, v in self.__dict__.items() if k != name
|
|
}
|
|
value, error_ = known_field.validate(
|
|
value,
|
|
dict_without_original_value,
|
|
loc=name,
|
|
cls=self.__class__,
|
|
)
|
|
if error_:
|
|
raise ValidationError([error_], self.__class__)
|
|
else:
|
|
new_values[name] = value
|
|
|
|
errors = []
|
|
for skip_on_failure, validator in self.__post_root_validators__:
|
|
if skip_on_failure and errors:
|
|
continue
|
|
try:
|
|
new_values = validator(self.__class__, new_values)
|
|
except (ValueError, TypeError, AssertionError) as exc:
|
|
errors.append(ErrorWrapper(exc, loc=ROOT_KEY))
|
|
if errors:
|
|
raise ValidationError(errors, self.__class__)
|
|
|
|
object_setattr(self, "__dict__", new_values)
|
|
|
|
self.__fields_set__.add(name)
|
|
|
|
def parse_store(self):
|
|
for name in self.__fields__:
|
|
self.__getattribute__(name)
|
|
|
|
def __getattribute__(self, item):
|
|
# If __class__ is accessed, return it directly to avoid recursion
|
|
if item == "__class__":
|
|
return super().__getattribute__(item)
|
|
|
|
# If called on the class itself, delegate to super's __getattribute__
|
|
if type(self) is type: # Check if self is a class
|
|
return super(type, self).__getattribute__(item)
|
|
|
|
# For instances, use the object's __getattribute__ to prevent recursion
|
|
res = object.__getattribute__(self, item)
|
|
if res is NAO:
|
|
field_info = self.__fields__.get(item)
|
|
value = self._store.get(field_info.alias, NAO)
|
|
if value is NAO:
|
|
value = field_info.get_default()
|
|
self._set_attr(item, value)
|
|
res = super(LazyModel, self).__getattribute__(item)
|
|
return res
|