Skip to content

Commit 6b630bc

Browse files
committed
fix bug when a user is passed as object to a datamodel
1 parent 657286a commit 6b630bc

File tree

5 files changed

+148
-16
lines changed

5 files changed

+148
-16
lines changed

datamodel/base.py

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from collections.abc import Callable
12
from typing import Any, Optional
23
import inspect
34
import logging
@@ -22,6 +23,9 @@
2223
from .models import ModelMixin
2324

2425

26+
TYPE_CONVERTERS = {} # Maps a type to a conversion callable
27+
28+
2529
class BaseModel(ModelMixin, metaclass=ModelMeta):
2630
"""
2731
BaseModel.
@@ -106,7 +110,12 @@ def _handle_default_value(self, value, f, name) -> Any:
106110
# Otherwise, return value as-is
107111
return value
108112

109-
def _handle_dataclass_type(self, value, _type):
113+
@classmethod
114+
def register_converter(cls, target_type: Any, func: Callable, field_name: str = None):
115+
key = (target_type, field_name) if field_name else target_type
116+
TYPE_CONVERTERS[key] = func
117+
118+
def _handle_dataclass_type(self, name: str, value: Any, _type: Any):
110119
try:
111120
if hasattr(self.Meta, 'no_nesting'):
112121
return value
@@ -116,21 +125,47 @@ def _handle_dataclass_type(self, value, _type):
116125
return _type(**value)
117126
if isinstance(value, list):
118127
return _type(*value)
119-
return value if isinstance(value, (int, str, UUID)) else _type(value)
128+
else:
129+
# If a converter exists for this type, use it:
130+
key = (_type, name)
131+
converter = TYPE_CONVERTERS.get(key) or TYPE_CONVERTERS.get(_type)
132+
if converter:
133+
return converter(name, value, _type)
134+
if getattr(self.Meta, 'as_objects', False) is True:
135+
return _type(**{name: value})
136+
if isinstance(value, (int, str, UUID)):
137+
return value
138+
if inspect.isclass(_type) and hasattr(_type, '__dataclass_fields__'):
139+
return _type(**{name: value})
140+
else:
141+
return _type(value)
120142
except Exception as exc:
121143
raise ValueError(
122144
f"Invalid value for {_type}: {value}, error: {exc}"
123145
)
124146

125-
def _handle_list_of_dataclasses(self, value, _type):
147+
def _handle_list_of_dataclasses(self, name: str, value: Any, _type: Any):
148+
"""
149+
_handle_list_of_dataclasses.
150+
151+
Process a list field that is annotated as List[SomeDataclass].
152+
If there's a registered converter for the sub-dataclass, call it;
153+
otherwise, build the sub-dataclass using default logic.
154+
"""
126155
try:
127156
sub_type = _type.__args__[0]
128157
if is_dataclass(sub_type):
129-
return [
130-
sub_type(
131-
**item
132-
) if isinstance(item, dict) else item for item in value
133-
]
158+
key = (sub_type, name)
159+
converter = TYPE_CONVERTERS.get(key) or TYPE_CONVERTERS.get(_type)
160+
new_list = []
161+
for item in value:
162+
if converter:
163+
new_list.append(converter(name, item, sub_type))
164+
elif isinstance(item, dict):
165+
new_list.append(sub_type(**item))
166+
else:
167+
new_list.append(item)
168+
return new_list
134169
except AttributeError:
135170
pass
136171
return value
@@ -160,13 +195,13 @@ def _process_field_(
160195
new_val = parse_basic(_type, value, _encoder)
161196
return self._validation_(name, new_val, f, _type)
162197
elif field_category == 'dataclass':
163-
new_val = self._handle_dataclass_type(value, _type)
198+
new_val = self._handle_dataclass_type(name, value, _type)
164199
return self._validation_(name, new_val, f, _type)
165200
elif field_category == 'typing':
166201
new_val = parse_type(_type, value, _encoder, field_category)
167202
return self._validation_(name, new_val, f, _type)
168203
elif isinstance(value, list) and hasattr(_type, '__args__'):
169-
new_val = self._handle_list_of_dataclasses(value, _type)
204+
new_val = self._handle_list_of_dataclasses(name, value, _type)
170205
return self._validation_(name, new_val, f, _type)
171206
else:
172207
new_val = parse_type(f.type, value, _encoder, field_category)

datamodel/validation.pyx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,20 +79,27 @@ cpdef list _validation(object F, str name, object value, object annotated_type,
7979
)
8080
elif hasattr(annotated_type, '__module__') and annotated_type.__module__ == 'typing':
8181
# TODO: validation of annotated types
82-
pass
82+
return errors
8383
elif type(annotated_type) is ModelMeta:
8484
# Check if there's a field in the annotated type that matches the name and type
85+
if isinstance(value, annotated_type):
86+
# if value is already a User, no further check needed for columns
87+
return errors
8588
try:
8689
field = annotated_type.get_column(name)
87-
field_type = field.type
88-
if field_type <> val_type:
90+
ftype = field.type
91+
if ftype <> val_type:
8992
errors.append(
90-
_create_error(name, value, f'invalid type for {annotated_type}.{name}, expected {field_type}', val_type, annotated_type)
93+
_create_error(name, value, f'invalid type for {annotated_type}.{name}, expected {ftype}', val_type, annotated_type)
9194
)
9295
except AttributeError as e:
9396
errors.append(
9497
_create_error(name, value, f'{annotated_type} has no column {name}', val_type, annotated_type, e)
9598
)
99+
except Exception as e:
100+
errors.append(
101+
_create_error(name, value, f'Error validating {annotated_type}.{name}', val_type, annotated_type, e)
102+
)
96103
elif is_optional_type(annotated_type):
97104
inner_types = get_args(annotated_type)
98105
for t in inner_types:

datamodel/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
__title__ = 'python-datamodel'
55
__description__ = ('simple library based on python +3.8 to use Dataclass-syntax'
66
'for interacting with Data')
7-
__version__ = '0.8.0'
7+
__version__ = '0.8.1'
88
__author__ = 'Jesus Lara'
99
__author_email__ = '[email protected]'
1010
__license__ = 'BSD'

examples/validate_user.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
from datetime import datetime
2+
from typing import Any
3+
from datamodel import BaseModel, Column
4+
from datamodel.exceptions import ValidationError
5+
6+
7+
class User(BaseModel):
8+
user_id: int = Column(required=True, primary_key=True)
9+
name: str = Column(required=False)
10+
email: str = Column(required=False)
11+
created_at: datetime = Column(required=False, default=datetime.now())
12+
created_by: str = Column(required=False)
13+
14+
class Meta:
15+
name = "users"
16+
schema = "public"
17+
strict = True
18+
connection = None
19+
frozen = False
20+
21+
class Group(BaseModel):
22+
group_id: int = Column(required=True, primary_key=True)
23+
name: str = Column(required=True)
24+
created_at: datetime = Column(required=False, default=datetime.now())
25+
created_by: str = Column(required=False)
26+
27+
class Meta:
28+
name = "groups"
29+
schema = "public"
30+
strict = True
31+
connection = None
32+
frozen = False
33+
34+
class UserGroup(BaseModel):
35+
user_id: User = Column(required=True, primary_key=True)
36+
group_id: Group = Column(required=True, primary_key=True)
37+
created_at: datetime = Column(required=False, default=datetime.now())
38+
created_by: str = Column(required=False)
39+
40+
class Meta:
41+
name = "user_groups"
42+
schema = "public"
43+
strict = True
44+
connection = None
45+
frozen = False
46+
47+
48+
class UserAttributes(BaseModel):
49+
user_id: User = Column(required=True, primary_key=True)
50+
attributes: dict = Column(required=False, default_factory=dict)
51+
52+
class Meta:
53+
name = "users_attributes"
54+
schema = "public"
55+
strict = True
56+
connection = None
57+
frozen = False
58+
59+
60+
# Path: examples/validate_user.py
61+
try:
62+
user = User(user_id=1, name="user1", email="email1")
63+
print(user)
64+
except ValidationError as e:
65+
print(e.payload)
66+
67+
def create_user(name: str, value: Any, target_type: Any):
68+
print('Target: ', target_type, value, name)
69+
args = {
70+
name: value,
71+
"name": "John Doe",
72+
"created_by": "John Doe",
73+
}
74+
return target_type(**args)
75+
76+
BaseModel.register_converter(User, create_user, 'user_id')
77+
78+
try:
79+
user_attributes = UserAttributes(user_id=1, attributes={"key": "value"})
80+
print(user_attributes)
81+
print('export user attributes')
82+
print(user_attributes.to_json())
83+
except ValidationError as e:
84+
print(e.payload)
85+
86+
# UserAttributes.Meta.as_objects = True
87+
# try:
88+
# user_attributes = UserAttributes(user_id=1, attributes={"key": "value"})
89+
# print(user_attributes)
90+
# except ValidationError as e:
91+
# print(e.payload)

tests/test_qsobject.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ class QueryObject(ClassDict):
4545

4646
def test_queryobject():
4747
qry = QueryObject(**data)
48-
print('AQUI >> ', qry.fields)
4948
assert qry.fields == ["store_id", "postpaid_sales", "postpaid_trended", "apd", "postpaid_to_goal", "hours_worked", "hps", "hps_to_goal"]
5049
fields = qry.pop('fields')
5150
assert fields == ["store_id", "postpaid_sales", "postpaid_trended", "apd", "postpaid_to_goal", "hours_worked", "hps", "hps_to_goal"]

0 commit comments

Comments
 (0)