Skip to content

Commit f4099b7

Browse files
committed
reduce complexity of validation
1 parent 43ed684 commit f4099b7

File tree

7 files changed

+269
-189
lines changed

7 files changed

+269
-189
lines changed

datamodel/base.py

Lines changed: 93 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,16 @@
1111
from enum import EnumMeta
1212
from uuid import UUID
1313
from orjson import OPT_INDENT_2
14-
from .converters import parse_type, slugify_camelcase
14+
from .converters import parse_basic, parse_type, slugify_camelcase
1515
from .fields import Field
1616
from .types import JSON_TYPES, Text
17-
from .validation import validator, is_callable, is_empty, is_dataclass
17+
from .validation import (
18+
_validation,
19+
is_callable,
20+
is_empty,
21+
is_dataclass,
22+
is_primitive
23+
)
1824
from .exceptions import ValidationError
1925
from .parsers.encoders import json_encoder
2026
from .abstract import ModelMeta, Meta
@@ -81,13 +87,10 @@ def __post_init__(self) -> None:
8187
for name, f in self.__columns__.items():
8288
try:
8389
value = getattr(self, name)
84-
self._calculate_value_(name, value, f)
85-
error = self._validation_(name, value, f)
86-
if error:
90+
if (error := self._process_field_(name, value, f)):
8791
errors[name] = error
8892
except RuntimeError as err:
8993
logging.exception(err)
90-
9194
if errors:
9295
if self.Meta.strict is True:
9396
raise ValidationError(
@@ -100,15 +103,6 @@ def __post_init__(self) -> None:
100103
else:
101104
object.__setattr__(self, "__valid__", True)
102105

103-
def is_valid(self) -> bool:
104-
"""is_valid.
105-
106-
returns True when current Model is valid under datatype validations.
107-
Returns:
108-
bool: True if current model is valid.
109-
"""
110-
return bool(self.__valid__)
111-
112106
@classmethod
113107
def add_field(cls, name: str, value: Any = None) -> None:
114108
if cls.Meta.strict is True:
@@ -158,52 +152,96 @@ def set(self, name: str, value: Any) -> None:
158152
else:
159153
setattr(self, name, value)
160154

161-
def _calculate_value_(self, name: str, value: Any, f: Field) -> None:
162-
_type = f.type
163-
_encoder = f.metadata.get('encoder')
164-
165-
if f.default is not None and is_callable(value):
166-
return
167-
168-
# Handle dataclass types
169-
if is_dataclass(_type):
170-
new_val = self._handle_dataclass_type(value, _type)
171-
elif _type.__module__ == 'typing':
172-
new_val = parse_type(_type, value, _encoder)
173-
elif isinstance(value, list) and hasattr(_type, '__args__'):
174-
new_val = self._handle_list_of_dataclasses(value, _type)
175-
else:
176-
new_val = self._handle_default_case(value, f)
177-
178-
setattr(self, name, new_val)
155+
def _handle_default_value(self, value, f, name) -> Any:
156+
# Calculate default value
157+
if is_callable(value):
158+
if value.__module__ != 'typing':
159+
try:
160+
new_val = value()
161+
except TypeError:
162+
try:
163+
new_val = f.default()
164+
except TypeError:
165+
new_val = None
166+
setattr(self, name, new_val)
167+
elif is_callable(f.default) and value is None:
168+
# Set the default value first
169+
try:
170+
new_val = f.default()
171+
except (AttributeError, RuntimeError):
172+
new_val = None
173+
setattr(self, name, new_val)
174+
value = new_val # Return the new value
175+
elif not isinstance(f.default, _MISSING_TYPE) and value is None:
176+
setattr(self, name, f.default)
177+
value = f.default
178+
return value
179179

180180
def _handle_dataclass_type(self, value, _type):
181-
if hasattr(self.Meta, 'no_nesting'):
182-
return value
183-
if value is None or is_dataclass(value):
184-
return value
185-
if isinstance(value, dict):
186-
return _type(**value)
187-
if isinstance(value, list):
188-
return _type(*value)
189-
return value if isinstance(value, (int, str, UUID)) else _type(value)
181+
try:
182+
if hasattr(self.Meta, 'no_nesting'):
183+
return value
184+
if value is None or is_dataclass(value):
185+
return value
186+
if isinstance(value, dict):
187+
return _type(**value)
188+
if isinstance(value, list):
189+
return _type(*value)
190+
return value if isinstance(value, (int, str, UUID)) else _type(value)
191+
except Exception as exc:
192+
raise ValueError(
193+
f"Invalid value for {_type}: {value}, error: {exc}"
194+
)
190195

191196
def _handle_list_of_dataclasses(self, value, _type):
192197
try:
193198
sub_type = _type.__args__[0]
194199
if is_dataclass(sub_type):
195-
return [sub_type(**item) if isinstance(item, dict) else item for item in value]
200+
return [
201+
sub_type(**item) if isinstance(item, dict) else item for item in value
202+
]
196203
except AttributeError:
197204
pass
198205
return value
199206

200-
def _handle_default_case(self, value, f):
207+
def _process_field_(self, name: str, value: Any, f: Field) -> dict[Any]:
208+
_type = f.type
209+
_encoder = f.metadata.get('encoder')
210+
new_val = value
211+
201212
if is_empty(value):
202-
return f.default_factory if isinstance(f.default, _MISSING_TYPE) else f.default
203-
try:
204-
return parse_type(f.type, value, f.metadata.get('encoder'))
205-
except (TypeError, ValueError) as ex:
206-
raise ValueError(f"Wrong Type for {f.name}: {f.type}, error: {ex}") from ex
213+
new_val = f.default_factory if isinstance(f.default, (_MISSING_TYPE)) else f.default
214+
setattr(self, name, new_val)
215+
216+
if f.default is not None:
217+
value = self._handle_default_value(value, f, name)
218+
219+
if is_primitive(_type):
220+
try:
221+
new_val = parse_basic(f.type, value, _encoder)
222+
return self._validation_(name, new_val, f, _type)
223+
except (TypeError, ValueError) as ex:
224+
raise ValueError(
225+
f"Wrong Type for {f.name}: {f.type}, error: {ex}"
226+
) from ex
227+
elif isinstance(value, list) and hasattr(_type, '__args__'):
228+
new_val = self._handle_list_of_dataclasses(value, _type)
229+
return self._validation_(name, new_val, f, _type)
230+
elif _type.__module__ == 'typing':
231+
new_val = parse_type(_type, value, _encoder)
232+
return self._validation_(name, new_val, f, _type)
233+
elif is_dataclass(_type):
234+
new_val = self._handle_dataclass_type(value, _type)
235+
return self._validation_(name, new_val, f, _type)
236+
else:
237+
try:
238+
new_val = parse_type(f.type, value, _encoder)
239+
except (TypeError, ValueError) as ex:
240+
raise ValueError(
241+
f"Wrong Type for {f.name}: {f.type}, error: {ex}"
242+
) from ex
243+
# Then validate the value
244+
return self._validation_(name, new_val, f, _type)
207245

208246
def _field_checks_(self, f: Field, name: str, value: Any) -> None:
209247
# Validate Primary Key
@@ -238,43 +276,21 @@ def _field_checks_(self, f: Field, name: str, value: Any) -> None:
238276
except KeyError:
239277
pass
240278

241-
def _validation_(self, name: str, value: Any, f: Field) -> Optional[Any]:
279+
def _validation_(self, name: str, value: Any, f: Field, _type: Any) -> Optional[Any]:
242280
"""
243281
_validation_.
244282
TODO: cover validations as length, not_null, required, max, min, etc
245283
"""
246-
annotated_type = f.type
247284
val_type = type(value)
285+
# Set the current Value
286+
setattr(self, name, value)
248287

249-
# Calculate default value
250-
if f.default is not None:
251-
if is_callable(value):
252-
if value.__module__ != 'typing':
253-
try:
254-
new_val = value()
255-
except TypeError:
256-
try:
257-
new_val = f.default()
258-
except TypeError:
259-
new_val = None
260-
setattr(self, name, new_val)
261-
elif is_callable(f.default) and value is None:
262-
# Set the default value first
263-
try:
264-
new_val = f.default()
265-
except (AttributeError, RuntimeError):
266-
new_val = None
267-
setattr(self, name, new_val)
268-
value = new_val # Return the new value
269-
elif not isinstance(f.default, _MISSING_TYPE) and value is None:
270-
setattr(self, name, f.default)
271-
value = f.default
272-
273-
if val_type == type or value == annotated_type or is_empty(value):
288+
if val_type == type or value == _type or is_empty(value):
274289
self._field_checks_(f, name, value)
275290
else:
276291
# capturing other errors from validator:
277-
return validator(f, name, value, annotated_type)
292+
# return _validation(f, name, value, _type, val_type)
293+
return None
278294

279295
def get_errors(self):
280296
return self.__errors__

datamodel/converters.pyx

Lines changed: 34 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,6 @@ def parse_type(object T, object data, object encoder = None):
389389
f'Unsupported Encoder {t}'
390390
)
391391
pass
392-
# F.type = args[0]
393392
return data
394393
except KeyError:
395394
pass
@@ -400,7 +399,7 @@ def parse_type(object T, object data, object encoder = None):
400399
return encoder(data)
401400
except ValueError as e:
402401
raise ValueError(
403-
f"DataModel: Error parsing type {T}, {e}"
402+
f"Error parsing type {T}, {e}"
404403
)
405404
elif is_dataclass(T):
406405
if isinstance(data, dict):
@@ -410,8 +409,6 @@ def parse_type(object T, object data, object encoder = None):
410409
else:
411410
data = T(data)
412411
return data
413-
elif T == str:
414-
return str(data)
415412
else:
416413
try:
417414
conv = encoders[T]
@@ -432,51 +429,39 @@ def parse_type(object T, object data, object encoder = None):
432429
elif isinstance(data, str):
433430
data = T(data)
434431
except (TypeError, ValueError) as e:
435-
logging.error(f'Conversion Error {T!r}: {e}')
432+
logging.error(
433+
f'Conversion Error {T!r}: {e}'
434+
)
436435
return data
437436
return data
438437

439-
# def parse_type(object T, object data, object encoder = None):
440-
# args = getattr(T, '__args__', None)
441-
442-
# if T.__module__ == 'typing':
443-
# if T._name == 'Dict' and isinstance(data, dict) and args:
444-
# return {k: parse_type(args[1], v) for k, v in data.items()}
445-
# elif T._name == 'List':
446-
# if not isinstance(data, (list, tuple)):
447-
# data = [data]
448-
# return [parse_type(args[0], item) for item in data] if args else data
449-
# elif T._name in ('Optional', 'Union') and args:
450-
# return parse_type(args[0], data)
451-
452-
# if encoder and callable(encoder):
453-
# return encoder(data)
454-
455-
# if is_dataclass(T):
456-
# if isinstance(data, (dict, list, tuple)):
457-
# return T(*data) if isinstance(data, (list, tuple)) else T(**data)
458-
# return T(data)
459-
460-
# if T == str:
461-
# return str(data)
462-
463-
# try:
464-
# return encoders[T](data)
465-
# except KeyError:
466-
# pass
467-
# except (TypeError, ValueError) as e:
468-
# raise ValueError(f"Error type {T}: {e}") from e
469-
470-
# # The check for inspect.isclass(T)
471-
# if inspect.isclass(T):
472-
# try:
473-
# if isinstance(data, dict):
474-
# return T(**data)
475-
# elif isinstance(data, (list, tuple)):
476-
# return T(*data)
477-
# elif isinstance(data, str):
478-
# return T(data)
479-
# except (TypeError, ValueError) as e:
480-
# logging.error(f'Conversion Error {T!r}: {e}')
481-
482-
# return data
438+
cpdef object parse_basic(object T, object data, object encoder = None):
439+
"""parse_type.
440+
441+
Parse a value to primitive types as str or int.
442+
--- (int, float, str, bool, bytes)
443+
"""
444+
if T == str:
445+
return str(data)
446+
elif T == bytes:
447+
return bytes(data)
448+
449+
# Using the encoders for basic types:
450+
try:
451+
return encoders[T](data)
452+
except KeyError:
453+
pass
454+
except (TypeError, ValueError) as e:
455+
raise ValueError(
456+
f"Encoder Error {T}: {e}"
457+
) from e
458+
459+
# function encoder:
460+
if encoder and callable(encoder):
461+
# using a function encoder:
462+
try:
463+
return encoder(data)
464+
except ValueError as e:
465+
raise ValueError(
466+
f"Error parsing type {T}, {e}"
467+
)

datamodel/models.py

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,18 @@ def __getitem__(self, item):
2929
def column(self, name: str) -> Field:
3030
return self.__columns__[name]
3131

32+
# def __repr__(self) -> str:
33+
# nodef_f_vals = (
34+
# (f.name, getattr(self, f.name))
35+
# for f in fields(self)
36+
# if not (getattr(self, f.name) == f.default and not callable(f.default))
37+
# )
38+
# nodef_f_repr = ", ".join(f"{name}={value}" for name, value in nodef_f_vals)
39+
# return f"{self.__class__.__name__}({nodef_f_repr})"
40+
3241
def __repr__(self) -> str:
33-
nodef_f_vals = (
34-
(f.name, attrgetter(f.name)(self))
35-
for f in fields(self)
36-
if attrgetter(f.name)(self) != f.default
37-
)
38-
nodef_f_repr = ", ".join(f"{name}={value}" for name, value in nodef_f_vals)
39-
return f"{self.__class__.__name__}({nodef_f_repr})"
42+
f_repr = ", ".join(f"{f.name}={getattr(self, f.name)}" for f in fields(self))
43+
return f"{self.__class__.__name__}({f_repr})"
4044

4145
def remove_nulls(self, obj: Any) -> dict[str, Any]:
4246
"""Recursively removes any fields with None values from the given object."""
@@ -58,6 +62,15 @@ def json(self, **kwargs):
5862

5963
to_json = json
6064

65+
def is_valid(self) -> bool:
66+
"""is_valid.
67+
68+
returns True when current Model is valid under datatype validations.
69+
Returns:
70+
bool: True if current model is valid.
71+
"""
72+
return bool(self.__valid__)
73+
6174
class Model(ModelMixin, metaclass=ModelMeta):
6275
"""Model.
6376

0 commit comments

Comments
 (0)