Skip to content

Commit ecfa638

Browse files
committed
added support for types and subclass of types
1 parent 593ae3f commit ecfa638

File tree

6 files changed

+118
-10
lines changed

6 files changed

+118
-10
lines changed

datamodel/abstract.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,8 @@ def _initialize_fields(attrs, annotations, strict):
209209
# check type of field:
210210
if _is_prim:
211211
_type_category = 'primitive'
212+
elif origin == type:
213+
_type_category = 'typing'
212214
elif _is_dc:
213215
_type_category = 'dataclass'
214216
elif _is_typing: # noqa

datamodel/converters.pyx

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -586,12 +586,6 @@ cdef object _parse_typing_type(
586586
"""
587587
cdef tuple type_args = getattr(T, '__args__', ())
588588

589-
print('FIELD > ', field, field.origin)
590-
print('T > ', T)
591-
print('NAME > ', name)
592-
print('DATA > ', data)
593-
print('Type Args > ', type_args)
594-
595589
if field.origin in {dict, Mapping} or name in {'Dict', 'Mapping'}:
596590
if isinstance(data, dict):
597591
if type_args:

datamodel/validation.pyx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,6 @@ cpdef list _validation(object F, str name, object value, object annotated_type,
5050
annotated_type = annotated_type.type
5151
errors = []
5252

53-
print(' ::: F ', F, name, value)
54-
print(' BASE ', F.origin, F.args, field_type)
55-
print('THIS > ', F.origin is Callable)
5653
# first: calling (if exists) custom validator:
5754
fn = F.metadata.get('validator', None)
5855
if fn is not None and callable(fn):
@@ -108,6 +105,15 @@ cpdef list _validation(object F, str name, object value, object annotated_type,
108105
f"Field '{name}': expected an awaitable, but got {type(value)}."
109106
)
110107
elif field_type == 'typing' or hasattr(annotated_type, '__module__') and annotated_type.__module__ == 'typing':
108+
if F.origin is type:
109+
for allowed in F.args:
110+
if isinstance(value, allowed):
111+
break
112+
else:
113+
expected = ', '.join([str(t) for t in F.args])
114+
errors.append(
115+
_create_error(name, value, f'Invalid type for {annotated_type}.{name}, expected a subclass of {expected}', val_type, annotated_type)
116+
)
111117
if F.origin is tuple:
112118
# Check if we are in the homogeneous case: Tuple[T, ...]
113119
if len(F.args) == 2 and F.args[1] is Ellipsis:

datamodel/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
'simple library based on python +3.8 to use Dataclass-syntax'
77
'for interacting with Data'
88
)
9-
__version__ = '0.8.12'
9+
__version__ = '0.8.13'
1010
__copyright__ = 'Copyright (c) 2020-2024 Jesus Lara'
1111
__author__ = 'Jesus Lara'
1212
__author_email__ = '[email protected]'

examples/test_type_user.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from typing import Type, Union
2+
from datamodel import BaseModel, Field
3+
from datamodel.exceptions import ValidationError
4+
5+
# Define a basic user hierarchy.
6+
class User(BaseModel):
7+
username: str = Field(required=True)
8+
email: str = Field(required=True)
9+
10+
class BasicUser(User):
11+
level: str = Field(default="basic")
12+
13+
class ProUser(User):
14+
level: str = Field(default="pro")
15+
perks: list = Field(default_factory=list)
16+
17+
# Employee model whose user_class field must be a type (class)
18+
# that is either BasicUser or ProUser.
19+
class Employee(BaseModel):
20+
# The type hint below means: user_class must be a type (class)
21+
# that is a subclass of either BasicUser or ProUser.
22+
user_class: type[BasicUser | ProUser] = Field(required=True)
23+
24+
25+
try:
26+
# This should raise a ValidationError because the type hint
27+
# specifies that user_class must be a type (class), not an instance.
28+
user = BasicUser(username="user", email="email")
29+
employee = Employee(user_class=user)
30+
print(employee)
31+
except ValidationError as e:
32+
print(e.payload)
33+
34+
try:
35+
# This should raise a ValidationError because the type hint
36+
# specifies that user_class must be a type (class), not an instance.
37+
user = User(username="user", email="email")
38+
employee = Employee(user_class=user)
39+
print(employee)
40+
except ValidationError as e:
41+
print(e.payload)

tests/test_type_user.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# test_employee.py
2+
from typing import Type, Union
3+
import pytest
4+
from datamodel import BaseModel, Field
5+
from datamodel.exceptions import ValidationError
6+
7+
8+
# Define a basic user hierarchy.
9+
class User(BaseModel):
10+
username: str = Field(required=True)
11+
email: str = Field(required=True)
12+
13+
class BasicUser(User):
14+
level: str = Field(default="basic")
15+
16+
class ProUser(User):
17+
level: str = Field(default="pro")
18+
perks: list = Field(default_factory=list)
19+
20+
# Employee model whose user_class field must be a type (class)
21+
# that is either BasicUser or ProUser.
22+
class Employee(BaseModel):
23+
# The type hint below means: user_class must be a type (class)
24+
# that is a subclass of either BasicUser or ProUser.
25+
user_class: type[BasicUser | ProUser] = Field(required=True)
26+
27+
# NOTE: In your converters/validation modules you must support such fields by:
28+
# - Checking that get_origin(annotated_type) is type.
29+
# - Extracting get_args(annotated_type) and verifying that
30+
# the supplied value is a type and a subclass of one of the allowed types.
31+
32+
# Example test functions below:
33+
34+
def test_valid_employee_basic():
35+
"""Test that an Employee can be created with BasicUser as its user_class."""
36+
user = BasicUser(username="user", email="email")
37+
emp = Employee(user_class=user)
38+
# Verify that the field user_class was correctly set to BasicUser
39+
assert isinstance(emp.user_class, BasicUser)
40+
41+
def test_valid_employee_pro():
42+
"""Test that an Employee can be created with ProUser as its user_class."""
43+
user = ProUser(username="user", email="email")
44+
emp = Employee(user_class=user)
45+
assert isinstance(emp.user_class, ProUser)
46+
47+
def test_invalid_employee_team():
48+
"""Test that assigning a type not allowed (TeamUser) raises a validation error."""
49+
with pytest.raises(ValidationError) as excinfo:
50+
user = User(username="user", email="email")
51+
Employee(user_class=user)
52+
errors = excinfo.value.payload
53+
# Check that the error mentions the 'user_class' field and the allowed types.
54+
assert "user_class" in errors
55+
error_message = errors["user_class"][0]["error"]
56+
assert "subclass" in error_message
57+
58+
# For manual testing when running this file directly:
59+
if __name__ == "__main__":
60+
test_valid_employee_basic()
61+
test_valid_employee_pro()
62+
try:
63+
test_invalid_employee_team()
64+
except Exception as e:
65+
print("Expected error:", e)

0 commit comments

Comments
 (0)