Skip to content

Instantly share code, notes, and snippets.

@pedro-psb
Created May 15, 2024 23:31
Show Gist options
  • Save pedro-psb/83008f3d9523610aca125aaa597e7f59 to your computer and use it in GitHub Desktop.
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
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