@@ -9,6 +9,9 @@ from dataclasses import _MISSING_TYPE, _FIELDS, fields
9
9
import ciso8601
10
10
import orjson
11
11
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
12
15
cimport cython
13
16
from cpython cimport datetime
14
17
from cpython.object cimport (
@@ -24,19 +27,42 @@ from cpython.object cimport (
24
27
from cpython.ref cimport PyObject
25
28
from uuid import UUID
26
29
import asyncpg.pgproto.pgproto as pgproto
27
- from .functions import is_empty, is_iterable, is_primitive
30
+ from .functions import is_iterable, is_primitive
28
31
from .validation import _validation
29
32
from .fields import Field
30
33
# New converter:
31
34
import datamodel.rs_parsers as rc
32
35
36
+ cdef struct ColumnDef:
37
+ const char * name
38
+ PyObject* field
33
39
34
40
cdef bint is_dc(object obj):
35
41
""" Returns True if obj is a dataclass or an instance of a
36
42
dataclass."""
37
43
cls = obj if isinstance (obj, type ) and not isinstance (obj, types.GenericAlias) else type (obj)
38
44
return PyObject_HasAttr(cls , ' __dataclass_fields__' )
39
45
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
+
40
66
cpdef bint has_attribute(object obj, object attr):
41
67
""" Returns True if obj has the attribute attr."""
42
68
return PyObject_HasAttr(obj, attr)
@@ -597,7 +623,6 @@ cdef object _parse_builtin_type(object field, object T, object data, object enco
597
623
raise ValueError (
598
624
f" Error parsing type {T}: {e}"
599
625
) from e
600
- return data
601
626
602
627
cpdef object parse_basic(object T, object data, object encoder = None ):
603
628
""" parse_basic.
@@ -1175,6 +1200,15 @@ cdef object _handle_default_value(
1175
1200
# Otherwise, return value as-is
1176
1201
return value
1177
1202
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
+
1178
1212
cpdef dict processing_fields(object obj, list columns):
1179
1213
"""
1180
1214
Process the fields (columns) of a dataclass object.
@@ -1190,10 +1224,21 @@ cpdef dict processing_fields(object obj, list columns):
1190
1224
cdef object meta = obj.Meta
1191
1225
cdef bint as_objects = meta.as_objects
1192
1226
cdef bint no_nesting = meta.no_nesting
1227
+ cdef tuple type_args = ()
1228
+ # Error handling
1193
1229
cdef dict errors = {}
1230
+ # Type Information
1194
1231
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 ]
1197
1242
value = getattr (obj, name)
1198
1243
# Use the precomputed field type category:
1199
1244
field_category = f._type_category
@@ -1202,92 +1247,91 @@ cpdef dict processing_fields(object obj, list columns):
1202
1247
# Handle descriptor-specific logic
1203
1248
try :
1204
1249
value = f.__get__ (obj, type (obj)) # Get the descriptor value
1205
- setattr (obj, name, value)
1250
+ PyObject_SetAttr (obj, name, value)
1206
1251
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))
1208
1253
continue
1209
1254
1210
1255
# get type and default:
1211
1256
_type = f.type
1212
1257
_default = f.default
1213
1258
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
1221
1260
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)
1229
1274
1275
+ try :
1276
+ _encoder = metadata.get(' encoder' )
1230
1277
if f.parser is not None :
1231
1278
# If a custom parser is attached to Field, use it
1232
1279
try :
1233
- value = f.parser(value)
1280
+ newval = f.parser(value)
1281
+ if newval != value:
1282
+ obj.__dict__ [name] = newval
1234
1283
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))
1236
1285
continue
1237
-
1238
1286
elif field_category == ' primitive' :
1239
1287
try :
1240
- value = parse_basic(_type, value, _encoder)
1288
+ newval = parse_basic(_type, value, _encoder)
1289
+ if newval != value:
1290
+ obj.__dict__ [name] = newval
1241
1291
except ValueError as ex:
1242
- errors[ name] = f" Error parsing {name}: {ex} "
1292
+ errors.update(_build_error( name, f" Error parsing {name}: " , ex))
1243
1293
continue
1244
1294
elif field_category == ' type' :
1245
1295
# TODO: support multiple types
1246
1296
pass
1247
1297
elif field_category == ' dataclass' :
1248
1298
if no_nesting is False :
1249
1299
if as_objects is True :
1250
- value = _handle_dataclass_type(
1300
+ newval = _handle_dataclass_type(
1251
1301
f, name, value, _type, as_objects, obj
1252
1302
)
1253
1303
else :
1254
- value = _handle_dataclass_type(
1304
+ newval = _handle_dataclass_type(
1255
1305
f, name, value, _type, as_objects, None
1256
1306
)
1307
+ if newval!= value:
1308
+ # PyObject_SetAttr(obj, name, newval)
1309
+ obj.__dict__ [name] = newval
1257
1310
elif f.origin in (list , ' list' ) and f._inner_is_dc:
1258
1311
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)
1265
1313
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
1267
1316
elif field_category == ' typing' :
1268
1317
if f.is_dc:
1269
1318
# 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 :
1285
1322
# Other typical case is when is a List of primitives.
1286
1323
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
+ )
1288
1332
else :
1289
1333
try :
1290
- value = _parse_typing(
1334
+ newval = _parse_typing(
1291
1335
f,
1292
1336
_type,
1293
1337
value,
@@ -1299,47 +1343,70 @@ cpdef dict processing_fields(object obj, list columns):
1299
1343
raise ValueError (
1300
1344
f" Error parsing List: {name}: {e}"
1301
1345
)
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
1302
1361
else :
1303
1362
try :
1304
- value = _parse_typing(
1363
+ newval = _parse_typing(
1305
1364
f,
1306
1365
_type,
1307
1366
value,
1308
1367
_encoder,
1309
1368
as_objects,
1310
1369
obj
1311
1370
)
1371
+ obj.__dict__ [name] = newval
1312
1372
except Exception as e:
1313
1373
raise ValueError (
1314
1374
f" Error parsing {f.origin}: {name}: {e}"
1315
1375
)
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
1316
1382
else :
1317
- value = _parse_typing(
1383
+ newval = _parse_typing(
1318
1384
f,
1319
1385
_type,
1320
1386
value,
1321
1387
_encoder,
1322
1388
as_objects,
1323
1389
obj
1324
1390
)
1325
- # Set the value:
1326
- PyObject_SetAttr(obj, name, value)
1391
+ obj.__dict__ [name] = newval
1327
1392
# 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)):
1329
1394
errors[name] = error
1330
1395
except ValueError as ex:
1331
1396
if meta.strict is True :
1332
1397
raise
1333
1398
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))
1335
1400
continue
1401
+ except AttributeError :
1402
+ raise
1336
1403
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))
1338
1405
continue
1339
1406
# Return Errors (if any)
1340
1407
return errors
1341
1408
1342
- cdef dict _validation_(
1409
+ cdef object _validation_(
1343
1410
str name,
1344
1411
object value,
1345
1412
object f,
@@ -1353,16 +1420,26 @@ cdef dict _validation_(
1353
1420
TODO: cover validations as length, not_null, required, max, min, etc
1354
1421
"""
1355
1422
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
+ }
1356
1429
if val_type == type or value == _type or is_empty(value):
1357
1430
try :
1358
1431
_field_checks_(f, name, value, meta)
1359
- return {}
1432
+ return None
1360
1433
except (ValueError , TypeError ):
1361
1434
raise
1362
1435
# If the field has a cached validator, use it.
1363
1436
if f.validator is not None :
1364
1437
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
1366
1443
except ValueError :
1367
1444
raise
1368
1445
else :
0 commit comments