Skip to content

Commit e5e4bd3

Browse files
Merge pull request #244 from phenobarbital/new-exports
New exports
2 parents fc2fa36 + a3625da commit e5e4bd3

File tree

6 files changed

+356
-88
lines changed

6 files changed

+356
-88
lines changed

datamodel/abstract.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
Union,
99
get_args,
1010
get_origin,
11-
ClassVar
11+
ClassVar,
12+
NewType
1213
)
1314
from types import GenericAlias
1415
from collections import OrderedDict
@@ -178,6 +179,10 @@ def _initialize_fields(attrs, annotations, strict):
178179
# Skip InitVar fields;
179180
# they should not be part of the dataclass instance
180181
continue
182+
if isinstance(_type, NewType):
183+
# Get the corresponding type of the NewType.
184+
_type = _type.__supertype__
185+
181186
origin = get_origin(_type)
182187
if origin is ClassVar:
183188
continue
@@ -226,10 +231,7 @@ def _initialize_fields(attrs, annotations, strict):
226231
df.origin = origin
227232
df.args = args
228233
df.type_args = getattr(_type, '__args__', None)
229-
230-
df._typeinfo_ = {
231-
"default_callable": callable(_default)
232-
}
234+
df._default_callable = callable(_default)
233235
# Current Field have an Encoder Function.
234236
custom_encoder = df.metadata.get("encoder")
235237
try:

datamodel/converters.pyx

Lines changed: 138 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ from dataclasses import _MISSING_TYPE, _FIELDS, fields
99
import ciso8601
1010
import orjson
1111
from decimal import Decimal, InvalidOperation
12+
from libc.stdio cimport sprintf, snprintf
13+
from libc.stdlib cimport malloc, free
14+
from cpython.mem cimport PyMem_Malloc, PyMem_Free
1215
cimport cython
1316
from cpython cimport datetime
1417
from cpython.object cimport (
@@ -24,19 +27,42 @@ from cpython.object cimport (
2427
from cpython.ref cimport PyObject
2528
from uuid import UUID
2629
import asyncpg.pgproto.pgproto as pgproto
27-
from .functions import is_empty, is_iterable, is_primitive
30+
from .functions import is_iterable, is_primitive
2831
from .validation import _validation
2932
from .fields import Field
3033
# New converter:
3134
import datamodel.rs_parsers as rc
3235

36+
cdef struct ColumnDef:
37+
const char* name
38+
PyObject* field
3339

3440
cdef bint is_dc(object obj):
3541
"""Returns True if obj is a dataclass or an instance of a
3642
dataclass."""
3743
cls = obj if isinstance(obj, type) and not isinstance(obj, types.GenericAlias) else type(obj)
3844
return PyObject_HasAttr(cls, '__dataclass_fields__')
3945

46+
cdef bint is_empty(object value):
47+
cdef bint result = False
48+
if value is None:
49+
return True
50+
if PyObject_IsInstance(value, _MISSING_TYPE) or value == _MISSING_TYPE:
51+
result = True
52+
elif PyObject_IsInstance(value, str) and value == '':
53+
result = True
54+
elif PyObject_IsInstance(value, (int, float)) and value == 0:
55+
result = False
56+
elif PyObject_IsInstance(value, dict) and value == {}:
57+
result = False
58+
elif PyObject_IsInstance(value, (list, tuple, set)) and value == []:
59+
result = False
60+
elif PyObject_HasAttr(value, 'empty') and PyObject_GetAttr(value, 'empty') == False:
61+
result = False
62+
elif not value:
63+
result = True
64+
return result
65+
4066
cpdef bint has_attribute(object obj, object attr):
4167
"""Returns True if obj has the attribute attr."""
4268
return PyObject_HasAttr(obj, attr)
@@ -597,7 +623,6 @@ cdef object _parse_builtin_type(object field, object T, object data, object enco
597623
raise ValueError(
598624
f"Error parsing type {T}: {e}"
599625
) from e
600-
return data
601626

602627
cpdef object parse_basic(object T, object data, object encoder = None):
603628
"""parse_basic.
@@ -1175,6 +1200,15 @@ cdef object _handle_default_value(
11751200
# Otherwise, return value as-is
11761201
return value
11771202

1203+
cdef dict _build_error(str name, str message, object exp):
1204+
"""
1205+
_build_error.
1206+
1207+
Build a tuple containing an error message and the name of the field.
1208+
"""
1209+
cdef str error_message = message + name + ", Error: " + str(exp)
1210+
return {name: error_message}
1211+
11781212
cpdef dict processing_fields(object obj, list columns):
11791213
"""
11801214
Process the fields (columns) of a dataclass object.
@@ -1190,10 +1224,21 @@ cpdef dict processing_fields(object obj, list columns):
11901224
cdef object meta = obj.Meta
11911225
cdef bint as_objects = meta.as_objects
11921226
cdef bint no_nesting = meta.no_nesting
1227+
cdef tuple type_args = ()
1228+
# Error handling
11931229
cdef dict errors = {}
1230+
# Type Information
11941231
cdef dict _typeinfo = {}
1195-
1196-
for name, f in columns:
1232+
# Column information:
1233+
cdef tuple c_col
1234+
cdef str name
1235+
cdef object f
1236+
cdef object value
1237+
cdef object newval
1238+
1239+
for c_col in columns:
1240+
name = c_col[0]
1241+
f = c_col[1]
11971242
value = getattr(obj, name)
11981243
# Use the precomputed field type category:
11991244
field_category = f._type_category
@@ -1202,92 +1247,91 @@ cpdef dict processing_fields(object obj, list columns):
12021247
# Handle descriptor-specific logic
12031248
try:
12041249
value = f.__get__(obj, type(obj)) # Get the descriptor value
1205-
setattr(obj, name, value)
1250+
PyObject_SetAttr(obj, name, value)
12061251
except Exception as e:
1207-
errors[name] = f"Descriptor error in {name}: {e}"
1252+
errors.update(_build_error(name, f"Descriptor Error on {name}: ", e))
12081253
continue
12091254

12101255
# get type and default:
12111256
_type = f.type
12121257
_default = f.default
12131258
typeinfo = f.typeinfo # cached info (e.g., type_args, default_callable)
1214-
metadata = PyObject_GetAttr(f, "metadata")
1215-
_encoder = metadata.get('encoder')
1216-
_default_callable = typeinfo.get('default_callable', False)
1217-
1218-
if isinstance(_type, NewType):
1219-
_type = _type.__supertype__
1220-
1259+
type_args = f.type_args
12211260
try:
1222-
# Check if object is empty
1223-
if is_empty(value) and not isinstance(value, list):
1224-
if _type == str and value is not "":
1225-
value = f.default_factory if isinstance(_default, (_MISSING_TYPE)) else _default
1226-
setattr(obj, name, value)
1227-
if _default is not None:
1228-
value = _handle_default_value(obj, name, value, _default, _default_callable)
1261+
metadata = f.metadata
1262+
except AttributeError:
1263+
metadata = PyObject_GetAttr(f, "metadata")
1264+
1265+
# _default_callable = typeinfo.get('default_callable', False)
1266+
# Check if object is empty
1267+
if is_empty(value) and not PyObject_IsInstance(value, list):
1268+
if _type == str and value is not "":
1269+
value = f.default_factory if PyObject_IsInstance(_default, (_MISSING_TYPE)) else _default
1270+
# PyObject_SetAttr(obj, name, value)
1271+
obj.__dict__[name] = value
1272+
if _default is not None:
1273+
value = _handle_default_value(obj, name, value, _default, f._default_callable)
12291274

1275+
try:
1276+
_encoder = metadata.get('encoder')
12301277
if f.parser is not None:
12311278
# If a custom parser is attached to Field, use it
12321279
try:
1233-
value = f.parser(value)
1280+
newval = f.parser(value)
1281+
if newval != value:
1282+
obj.__dict__[name] = newval
12341283
except Exception as ex:
1235-
errors[name] = f"Error parsing *{name}* = *{value}*, error: {ex}"
1284+
errors.update(_build_error(name, f"Error parsing *{name}* = *{value}*", ex))
12361285
continue
1237-
12381286
elif field_category == 'primitive':
12391287
try:
1240-
value = parse_basic(_type, value, _encoder)
1288+
newval = parse_basic(_type, value, _encoder)
1289+
if newval != value:
1290+
obj.__dict__[name] = newval
12411291
except ValueError as ex:
1242-
errors[name] = f"Error parsing {name}: {ex}"
1292+
errors.update(_build_error(name, f"Error parsing {name}: ", ex))
12431293
continue
12441294
elif field_category == 'type':
12451295
# TODO: support multiple types
12461296
pass
12471297
elif field_category == 'dataclass':
12481298
if no_nesting is False:
12491299
if as_objects is True:
1250-
value = _handle_dataclass_type(
1300+
newval = _handle_dataclass_type(
12511301
f, name, value, _type, as_objects, obj
12521302
)
12531303
else:
1254-
value = _handle_dataclass_type(
1304+
newval = _handle_dataclass_type(
12551305
f, name, value, _type, as_objects, None
12561306
)
1307+
if newval!= value:
1308+
# PyObject_SetAttr(obj, name, newval)
1309+
obj.__dict__[name] = newval
12571310
elif f.origin in (list, 'list') and f._inner_is_dc:
12581311
if as_objects is True:
1259-
value = _handle_list_of_dataclasses(f, name, value, _type, obj)
1260-
else:
1261-
value = _handle_list_of_dataclasses(f, name, value, _type, None)
1262-
elif isinstance(value, list) and typeinfo.get('type_args'):
1263-
if as_objects is True:
1264-
value = _handle_list_of_dataclasses(f, name, value, _type, obj)
1312+
newval = _handle_list_of_dataclasses(f, name, value, _type, obj)
12651313
else:
1266-
value = _handle_list_of_dataclasses(f, name, value, _type, None)
1314+
newval = _handle_list_of_dataclasses(f, name, value, _type, None)
1315+
obj.__dict__[name] = newval
12671316
elif field_category == 'typing':
12681317
if f.is_dc:
12691318
# means that is_dataclass(T)
1270-
value = _handle_dataclass_type(None, name, value, _type, as_objects, None)
1271-
# If the field is a Union and data is a list, use _parse_union_type.
1272-
if f.origin is Union:
1273-
# e.g. Optional[...] or Union[A, B]
1274-
if len(f.args) == 2 and type(None) in f.args:
1275-
# Handle Optional[...] that is Union[..., None] cases first:
1276-
if f._inner_priv:
1277-
# If Optional but non-None is a primitive
1278-
value = _parse_builtin_type(f, f._inner_type, value, _encoder)
1279-
if f._inner_is_dc:
1280-
# non-None is a Optional Dataclass:
1281-
value = _handle_dataclass_type(
1282-
None, name, value, f._inner_type, as_objects, None
1283-
)
1284-
if f.origin is list:
1319+
newval = _handle_dataclass_type(None, name, value, _type, as_objects, None)
1320+
obj.__dict__[name] = newval
1321+
elif f.origin is list:
12851322
# Other typical case is when is a List of primitives.
12861323
if f._inner_priv:
1287-
value = _parse_list_type(f, _type, value, _encoder, f.args, obj)
1324+
newval = _parse_list_type(
1325+
f,
1326+
_type,
1327+
value,
1328+
_encoder,
1329+
f.args,
1330+
obj
1331+
)
12881332
else:
12891333
try:
1290-
value = _parse_typing(
1334+
newval = _parse_typing(
12911335
f,
12921336
_type,
12931337
value,
@@ -1299,47 +1343,70 @@ cpdef dict processing_fields(object obj, list columns):
12991343
raise ValueError(
13001344
f"Error parsing List: {name}: {e}"
13011345
)
1346+
obj.__dict__[name] = newval
1347+
# If the field is a Union and data is a list, use _parse_union_type.
1348+
elif f.origin is Union:
1349+
# e.g. Optional[...] or Union[A, B]
1350+
if len(f.args) == 2 and type(None) in f.args:
1351+
# Handle Optional[...] that is Union[..., None] cases first:
1352+
if f._inner_priv:
1353+
# If Optional but non-None is a primitive
1354+
newval = _parse_builtin_type(f, f._inner_type, value, _encoder)
1355+
if f._inner_is_dc:
1356+
# non-None is a Optional Dataclass:
1357+
newval = _handle_dataclass_type(
1358+
None, name, value, f._inner_type, as_objects, None
1359+
)
1360+
obj.__dict__[name] = newval
13021361
else:
13031362
try:
1304-
value = _parse_typing(
1363+
newval = _parse_typing(
13051364
f,
13061365
_type,
13071366
value,
13081367
_encoder,
13091368
as_objects,
13101369
obj
13111370
)
1371+
obj.__dict__[name] = newval
13121372
except Exception as e:
13131373
raise ValueError(
13141374
f"Error parsing {f.origin}: {name}: {e}"
13151375
)
1376+
elif isinstance(value, list) and type_args:
1377+
if as_objects is True:
1378+
newval = _handle_list_of_dataclasses(f, name, value, _type, obj)
1379+
else:
1380+
newval = _handle_list_of_dataclasses(f, name, value, _type, None)
1381+
obj.__dict__[name] = newval
13161382
else:
1317-
value = _parse_typing(
1383+
newval = _parse_typing(
13181384
f,
13191385
_type,
13201386
value,
13211387
_encoder,
13221388
as_objects,
13231389
obj
13241390
)
1325-
# Set the value:
1326-
PyObject_SetAttr(obj, name, value)
1391+
obj.__dict__[name] = newval
13271392
# then, call the validation process:
1328-
if (error := _validation_(name, value, f, _type, meta, field_category, as_objects)):
1393+
if (error := _validation_(name, newval, f, _type, meta, field_category, as_objects)):
13291394
errors[name] = error
13301395
except ValueError as ex:
13311396
if meta.strict is True:
13321397
raise
13331398
else:
1334-
errors[name] = f"Wrong Value for {f.name}: {f.type}, error: {ex}"
1399+
errors.update(_build_error(name, f"Wrong Value for {f.name}: {f.type}", ex))
13351400
continue
1401+
except AttributeError:
1402+
raise
13361403
except (TypeError, RuntimeError) as ex:
1337-
errors[name] = f"Wrong Type for {f.name}: {f.type}, error: {ex}"
1404+
errors.update(_build_error(name, f"Wrong Type for {f.name}: {f.type}", ex))
13381405
continue
13391406
# Return Errors (if any)
13401407
return errors
13411408

1342-
cdef dict _validation_(
1409+
cdef object _validation_(
13431410
str name,
13441411
object value,
13451412
object f,
@@ -1353,16 +1420,26 @@ cdef dict _validation_(
13531420
TODO: cover validations as length, not_null, required, max, min, etc
13541421
"""
13551422
cdef object val_type = type(value)
1423+
cdef str error = None
1424+
cdef dict err = {
1425+
"field": name,
1426+
"value": value,
1427+
"error": None
1428+
}
13561429
if val_type == type or value == _type or is_empty(value):
13571430
try:
13581431
_field_checks_(f, name, value, meta)
1359-
return {}
1432+
return None
13601433
except (ValueError, TypeError):
13611434
raise
13621435
# If the field has a cached validator, use it.
13631436
if f.validator is not None:
13641437
try:
1365-
return f.validator(f, name, value, _type)
1438+
error = f.validator(f, name, value, _type)
1439+
if error:
1440+
err["error"] = error
1441+
return err
1442+
return None
13661443
except ValueError:
13671444
raise
13681445
else:

0 commit comments

Comments
 (0)