mirror of
https://github.com/openSUSE/osc.git
synced 2025-01-05 22:36:15 +01:00
Merge pull request #1473 from dmach/nested-models
Support nested models + related fixes
This commit is contained in:
commit
01036341d4
@ -13,6 +13,7 @@ import types
|
|||||||
from typing import get_type_hints
|
from typing import get_type_hints
|
||||||
|
|
||||||
# supported types
|
# supported types
|
||||||
|
from enum import Enum
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from typing import List
|
from typing import List
|
||||||
@ -40,6 +41,7 @@ __all__ = (
|
|||||||
"Field",
|
"Field",
|
||||||
"NotSet",
|
"NotSet",
|
||||||
"FromParent",
|
"FromParent",
|
||||||
|
"Enum",
|
||||||
"Dict",
|
"Dict",
|
||||||
"List",
|
"List",
|
||||||
"NewType",
|
"NewType",
|
||||||
@ -125,9 +127,26 @@ class Field(property):
|
|||||||
origin_type = get_origin(self.type) or self.type
|
origin_type = get_origin(self.type) or self.type
|
||||||
if self.is_optional:
|
if self.is_optional:
|
||||||
types = [i for i in self.type.__args__ if i != type(None)]
|
types = [i for i in self.type.__args__ if i != type(None)]
|
||||||
return types[0]
|
return get_origin(types[0]) or types[0]
|
||||||
return origin_type
|
return origin_type
|
||||||
|
|
||||||
|
@property
|
||||||
|
def inner_type(self):
|
||||||
|
if self.is_optional:
|
||||||
|
types = [i for i in self.type.__args__ if i != type(None)]
|
||||||
|
type_ = types[0]
|
||||||
|
else:
|
||||||
|
type_ = self.type
|
||||||
|
|
||||||
|
if get_origin(type_) != list:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not hasattr(type_, "__args__"):
|
||||||
|
return None
|
||||||
|
|
||||||
|
inner_type = [i for i in type_.__args__ if i != type(None)][0]
|
||||||
|
return inner_type
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_optional(self):
|
def is_optional(self):
|
||||||
origin_type = get_origin(self.type) or self.type
|
origin_type = get_origin(self.type) or self.type
|
||||||
@ -137,6 +156,10 @@ class Field(property):
|
|||||||
def is_model(self):
|
def is_model(self):
|
||||||
return inspect.isclass(self.origin_type) and issubclass(self.origin_type, BaseModel)
|
return inspect.isclass(self.origin_type) and issubclass(self.origin_type, BaseModel)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_model_list(self):
|
||||||
|
return inspect.isclass(self.inner_type) and issubclass(self.inner_type, BaseModel)
|
||||||
|
|
||||||
def validate_type(self, value, expected_types=None):
|
def validate_type(self, value, expected_types=None):
|
||||||
if not expected_types and self.is_optional and value is None:
|
if not expected_types and self.is_optional and value is None:
|
||||||
return True
|
return True
|
||||||
@ -176,6 +199,15 @@ class Field(property):
|
|||||||
valid_type = True
|
valid_type = True
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if (
|
||||||
|
inspect.isclass(expected_type)
|
||||||
|
and issubclass(expected_type, Enum)
|
||||||
|
):
|
||||||
|
# test if the value is part of the enum
|
||||||
|
expected_type(value)
|
||||||
|
valid_type = True
|
||||||
|
continue
|
||||||
|
|
||||||
if not isinstance(value, origin_type):
|
if not isinstance(value, origin_type):
|
||||||
msg = f"Field '{self.name}' has type '{self.type}'. Cannot assign a value with type '{type(value).__name__}'."
|
msg = f"Field '{self.name}' has type '{self.type}'. Cannot assign a value with type '{type(value).__name__}'."
|
||||||
raise TypeError(msg)
|
raise TypeError(msg)
|
||||||
@ -241,9 +273,17 @@ class Field(property):
|
|||||||
def set(self, obj, value):
|
def set(self, obj, value):
|
||||||
# if this is a model field, convert dict to a model instance
|
# if this is a model field, convert dict to a model instance
|
||||||
if self.is_model and isinstance(value, dict):
|
if self.is_model and isinstance(value, dict):
|
||||||
new_value = self.origin_type() # pylint: disable=not-callable
|
# initialize a model instance from a dictionary
|
||||||
for k, v in value.items():
|
klass = self.origin_type
|
||||||
setattr(new_value, k, v)
|
value = klass(**value) # pylint: disable=not-callable
|
||||||
|
elif self.is_model_list and isinstance(value, list):
|
||||||
|
new_value = []
|
||||||
|
for i in value:
|
||||||
|
if isinstance(i, dict):
|
||||||
|
klass = self.inner_type
|
||||||
|
new_value.append(klass(**i))
|
||||||
|
else:
|
||||||
|
new_value.append(i)
|
||||||
value = new_value
|
value = new_value
|
||||||
|
|
||||||
self.validate_type(value)
|
self.validate_type(value)
|
||||||
@ -311,12 +351,12 @@ class BaseModel(metaclass=ModelMeta):
|
|||||||
|
|
||||||
if kwargs:
|
if kwargs:
|
||||||
unknown_fields_str = ", ".join([f"'{i}'" for i in kwargs])
|
unknown_fields_str = ", ".join([f"'{i}'" for i in kwargs])
|
||||||
raise TypeError(f"The following kwargs do not match any field: {unknown_fields_str}")
|
raise TypeError(f"The following kwargs of '{self.__class__.__name__}.__init__()' do not match any field: {unknown_fields_str}")
|
||||||
|
|
||||||
if uninitialized_fields:
|
if uninitialized_fields:
|
||||||
uninitialized_fields_str = ", ".join([f"'{i}'" for i in uninitialized_fields])
|
uninitialized_fields_str = ", ".join([f"'{i}'" for i in uninitialized_fields])
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
f"The following fields are not initialized and have no default either: {uninitialized_fields_str}"
|
f"The following fields of '{self.__class__.__name__}' object are not initialized and have no default either: {uninitialized_fields_str}"
|
||||||
)
|
)
|
||||||
|
|
||||||
for name, field in self.__fields__.items():
|
for name, field in self.__fields__.items():
|
||||||
@ -329,8 +369,12 @@ class BaseModel(metaclass=ModelMeta):
|
|||||||
for name, field in self.__fields__.items():
|
for name, field in self.__fields__.items():
|
||||||
if field.exclude:
|
if field.exclude:
|
||||||
continue
|
continue
|
||||||
if field.is_model:
|
value = getattr(self, name)
|
||||||
result[name] = getattr(self, name).dict()
|
if value is not None and field.is_model:
|
||||||
|
result[name] = value.dict()
|
||||||
|
if value is not None and field.is_model_list:
|
||||||
|
result[name] = [i.dict() for i in value]
|
||||||
else:
|
else:
|
||||||
result[name] = getattr(self, name)
|
result[name] = value
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
@ -186,14 +186,91 @@ class Test(unittest.TestCase):
|
|||||||
self.assertEqual(field.is_optional, True)
|
self.assertEqual(field.is_optional, True)
|
||||||
self.assertEqual(field.origin_type, TestSubmodel)
|
self.assertEqual(field.origin_type, TestSubmodel)
|
||||||
self.assertEqual(m.field, None)
|
self.assertEqual(m.field, None)
|
||||||
|
m.dict()
|
||||||
|
|
||||||
m = TestModel(field=TestSubmodel())
|
m = TestModel(field=TestSubmodel())
|
||||||
self.assertIsInstance(m.field, TestSubmodel)
|
self.assertIsInstance(m.field, TestSubmodel)
|
||||||
self.assertEqual(m.field.text, "default")
|
self.assertEqual(m.field.text, "default")
|
||||||
|
m.dict()
|
||||||
|
|
||||||
m = TestModel(field={"text": "text"})
|
m = TestModel(field={"text": "text"})
|
||||||
self.assertNotEqual(m.field, None)
|
self.assertNotEqual(m.field, None)
|
||||||
self.assertEqual(m.field.text, "text")
|
self.assertEqual(m.field.text, "text")
|
||||||
|
m.dict()
|
||||||
|
|
||||||
|
def test_list_submodels(self):
|
||||||
|
class TestSubmodel(BaseModel):
|
||||||
|
text: str = Field(default="default")
|
||||||
|
|
||||||
|
class TestModel(BaseModel):
|
||||||
|
field: List[TestSubmodel] = Field(default=[])
|
||||||
|
|
||||||
|
m = TestModel()
|
||||||
|
|
||||||
|
field = m.__fields__["field"]
|
||||||
|
self.assertEqual(field.is_model, False)
|
||||||
|
self.assertEqual(field.is_model_list, True)
|
||||||
|
self.assertEqual(field.is_optional, False)
|
||||||
|
self.assertEqual(field.origin_type, list)
|
||||||
|
m.dict()
|
||||||
|
|
||||||
|
m = TestModel(field=[TestSubmodel()])
|
||||||
|
self.assertEqual(m.field[0].text, "default")
|
||||||
|
m.dict()
|
||||||
|
|
||||||
|
m = TestModel(field=[{"text": "text"}])
|
||||||
|
self.assertEqual(m.field[0].text, "text")
|
||||||
|
m.dict()
|
||||||
|
|
||||||
|
self.assertRaises(TypeError, getattr(m, "field"))
|
||||||
|
|
||||||
|
def test_optional_list_submodels(self):
|
||||||
|
class TestSubmodel(BaseModel):
|
||||||
|
text: str = Field(default="default")
|
||||||
|
|
||||||
|
class TestModel(BaseModel):
|
||||||
|
field: Optional[List[TestSubmodel]] = Field(default=[])
|
||||||
|
|
||||||
|
m = TestModel()
|
||||||
|
|
||||||
|
field = m.__fields__["field"]
|
||||||
|
self.assertEqual(field.is_model, False)
|
||||||
|
self.assertEqual(field.is_model_list, True)
|
||||||
|
self.assertEqual(field.is_optional, True)
|
||||||
|
self.assertEqual(field.origin_type, list)
|
||||||
|
m.dict()
|
||||||
|
|
||||||
|
m = TestModel(field=[TestSubmodel()])
|
||||||
|
self.assertEqual(m.field[0].text, "default")
|
||||||
|
m.dict()
|
||||||
|
|
||||||
|
m = TestModel(field=[{"text": "text"}])
|
||||||
|
self.assertEqual(m.field[0].text, "text")
|
||||||
|
m.dict()
|
||||||
|
|
||||||
|
m.field = None
|
||||||
|
self.assertEqual(m.field, None)
|
||||||
|
m.dict()
|
||||||
|
|
||||||
|
def test_enum(self):
|
||||||
|
class Numbers(Enum):
|
||||||
|
one = "one"
|
||||||
|
two = "two"
|
||||||
|
|
||||||
|
class TestModel(BaseModel):
|
||||||
|
field: Optional[Numbers] = Field(default=None)
|
||||||
|
|
||||||
|
m = TestModel()
|
||||||
|
field = m.__fields__["field"]
|
||||||
|
self.assertEqual(field.is_model, False)
|
||||||
|
self.assertEqual(field.is_optional, True)
|
||||||
|
self.assertEqual(field.origin_type, Numbers)
|
||||||
|
self.assertEqual(m.field, None)
|
||||||
|
|
||||||
|
m.field = "one"
|
||||||
|
self.assertEqual(m.field, "one")
|
||||||
|
|
||||||
|
self.assertRaises(ValueError, setattr, m, "field", "does-not-exist")
|
||||||
|
|
||||||
def test_parent(self):
|
def test_parent(self):
|
||||||
class ParentModel(BaseModel):
|
class ParentModel(BaseModel):
|
||||||
|
Loading…
Reference in New Issue
Block a user