Last active
March 19, 2020 13:30
-
-
Save Invisi/2f7f76ad981d34bf5b95fb2b3a764681 to your computer and use it in GitHub Desktop.
Simple, stupid implementation for (pydantic-like) class-based definitions in Python 3
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 enum | |
import json | |
from pathlib import Path | |
class Encoder(json.JSONEncoder): | |
def default(self, o): | |
if isinstance(o, enum.Enum): | |
return o.value | |
return o.__dict__ | |
class ClassObject: | |
def __init__(self, **kwargs): | |
annotation_keys = self.__annotations__.keys() | |
# Assign values | |
for key, value in kwargs.items(): | |
if key in annotation_keys: | |
# Handle classes | |
if issubclass(self.__annotations__[key], ClassObject) and isinstance( | |
value, dict | |
): | |
# Try to parse class | |
setattr(self, key, self.__annotations__[key](**value)) | |
continue | |
# Handle enums, right now only string enums are supported | |
if issubclass(self.__annotations__[key], enum.Enum) and isinstance( | |
value, str | |
): | |
try: | |
setattr(self, key, self.__annotations__[key](value)) | |
except ValueError: | |
# Set default if the config's value does not exist in the enum | |
setattr(self, key, getattr(self, key)) | |
continue | |
# Type does not match annotation | |
if type(value) is not self.__annotations__[key]: | |
raise TypeError( | |
f"Value of {key} ({value}, {type(value)}) does not match" | |
f" annotation's type ({self.__annotations__[key]})." | |
) | |
setattr(self, key, value) | |
else: | |
raise ValueError(f"Key {key} does not exist in annotation.") | |
# Check if all values are set | |
kwargs_keys = kwargs.keys() | |
missing_keys = [] | |
for key in annotation_keys: | |
# Set default values so that they appear in __dict__ | |
if not hasattr(self, key) and key not in kwargs_keys: | |
missing_keys.append(key) | |
elif key not in kwargs_keys: | |
setattr(self, key, getattr(self, key)) | |
if len(missing_keys) > 0: | |
raise AttributeError( | |
f"{self.__class__.__name__} is missing the following keys: {missing_keys}" | |
) | |
def __setattr__(self, key, value): | |
# Verify type if in annotation | |
if key in self.__annotations__ and type(value) is not self.__annotations__[key]: | |
raise TypeError( | |
f"Value of {key} ({value}, {type(value)}) does not match" | |
f" annotation's type ({self.__annotations__[key]})." | |
) | |
elif key not in self.__annotations__: | |
print( | |
f"Warning: Setting unknown value/key combination in config {key}={value} ({type(value)})" | |
) | |
super().__setattr__(key, value) | |
def __str__(self): | |
sorted_kv = sorted([f"{k}={v}" for k, v in self.__dict__.items()]) | |
return f"<{self.__class__.__name__} {' '.join(sorted_kv)}>" | |
def __repr__(self): | |
return self.__dict__ | |
# Example usage | |
class SubConfig(ClassObject): | |
d: str = "abc" | |
class Config(ClassObject): | |
a: str = "" | |
b: int = 2 | |
c: int | |
sub_config: SubConfig = SubConfig() | |
def save(self): | |
cf = Path("state.json") | |
try: | |
cf.write_text(json.dumps(self, cls=Encoder, sort_keys=True, indent=4)) | |
except OSError: | |
print("Failed to save to state file") | |
raise | |
@staticmethod | |
def load(): | |
cf = Path("state.json") | |
if cf.exists(): | |
cj = json.loads(cf.read_text()) | |
return Config(**cj) | |
else: | |
return Config() | |
if __name__ == "__main__": | |
conf = Config(c=3) | |
print(conf) # <Config c=5 a= b=2 sub_config=<SubConfig d=abc>> | |
Config() # ValueError: Key c does not define a default value and is missing in kwargs. | |
Config( | |
b="2", c=3 | |
) # TypeError: Value of b (2, <class 'str'>) does not match annotation's type (<class 'int'>). |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment