Created
May 15, 2024 23:31
-
-
Save pedro-psb/83008f3d9523610aca125aaa597e7f59 to your computer and use it in GitHub Desktop.
Small experiment with adding Schema typing workaround for Dynaconf. https://github.com/dynaconf/dynaconf/issues/1082
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import typing as t | |
from dataclasses import dataclass, is_dataclass | |
# not strictly necessary, but makes sense | |
T = t.TypeVar("T") # https://mypy.readthedocs.io/en/stable/generics.html | |
class ValidationError(ValueError): | |
... | |
class _Dynaconf: | |
def __init__(self, schema, *args, **kwargs): | |
self.schema = schema | |
self._store = kwargs | |
self._obj_cache = None | |
def __getattr__(self, key): | |
if not self._obj_cache: | |
self.validate() | |
return getattr(self._obj_cache, key) | |
def to_dict(self): | |
return self._store | |
def validate(self): | |
dynaconf_obj = self | |
schema_obj = self.schema | |
errors = BaseSchema._check_dataclass(schema_obj, dynaconf_obj.to_dict()) | |
if errors: | |
raise ValidationError("\n" + "\n".join(errors)) | |
self._obj_cache = BaseSchema._populate_dataclass(schema_obj, self._store) | |
return True | |
def Dynaconf(schema: type[T], *args, **kwargs) -> T: | |
dynaconf = _Dynaconf(schema, *args, **kwargs) | |
return t.cast(T, dynaconf) | |
class BaseSchema: | |
def validate(self) -> bool: | |
return True | |
@staticmethod | |
def _populate_dataclass(dc, dict_data: dict): | |
_dict_data = dict_data.copy() | |
for k, v in dc.__annotations__.items(): | |
try: | |
dict_data[k] | |
except KeyError: | |
raise ValueError("Your data doesnt fit the schema") | |
if is_dataclass(v): | |
if not isinstance(dict_data[k], dict): | |
raise ValidationError( | |
f"Your current value for {k} isn't a dict internally." | |
) | |
_dict_data[k] = BaseSchema._populate_dataclass(v, dict_data[k]) | |
return dc(**_dict_data) | |
@staticmethod | |
def _check_dataclass(dc, dict_data: dict): | |
errors = [] | |
for key, _type in dc.__annotations__.items(): | |
value = dict_data.get(key) | |
if not value: | |
errors.append(f"You don't have a value for {key}.") | |
elif is_dataclass(_type): | |
if not isinstance(value, dict): | |
errors.append( | |
f"Your current value for {key:!} isn't a dict internally." | |
) | |
errors.extend(BaseSchema._check_dataclass(_type, value)) | |
elif not isinstance(value, _type): | |
errors.append( | |
f"Your current value for {key} isn't a {_type}. Its a {type(value)}." | |
) | |
return errors | |
# Usage | |
if __name__ == "__main__": | |
@dataclass | |
class NestedObject: | |
name: str | |
id: int | |
@dataclass | |
class MySchema(BaseSchema): | |
foo: str | |
listy: list | |
dicty: NestedObject | |
data = { | |
"foo": "spam", | |
"listy": ["a", "b", "c"], | |
"dicty": {"name": "john", "id": 123}, | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment