Skip to content

Commit 47465e8

Browse files
Merge pull request #257 from phenobarbital/new-exports
fix on nested lists
2 parents ffd9f01 + c9b733f commit 47465e8

File tree

4 files changed

+262
-50
lines changed

4 files changed

+262
-50
lines changed

datamodel/converters.pyx

Lines changed: 158 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# Copyright (C) 2018-present Jesus Lara
33
#
44
import re
5-
from typing import get_args, get_origin, Union, Optional, List, NewType, Literal, Any
5+
from typing import get_args, get_origin, Union, Optional, List, NewType, Literal, Any, Set
66
from collections.abc import Sequence, Mapping, Callable, Awaitable
77
import types
88
from dataclasses import _MISSING_TYPE, _FIELDS, fields
@@ -45,6 +45,9 @@ cdef bint is_dc(object obj):
4545
cls = obj if isinstance(obj, type) and not isinstance(obj, types.GenericAlias) else type(obj)
4646
return PyObject_HasAttr(cls, '__dataclass_fields__')
4747

48+
cdef bint is_typing(object obj):
49+
return PyObject_HasAttr(obj, '__module__') and obj.__module__ == 'typing'
50+
4851
cdef bint is_empty(object value):
4952
"""
5053
Determines if a value should be considered empty.
@@ -555,6 +558,7 @@ cdef object _parse_set_type(
555558
cdef tuple key = (arg_type, field.name)
556559
cdef object converter = TYPE_PARSERS.get(key) or TYPE_PARSERS.get(arg_type)
557560
cdef object inner_type = field._inner_type if hasattr(field, '_inner_type') else arg_type
561+
cdef bint is_typing_set = hasattr(inner_type, '__origin__') and inner_type.__origin__ is set
558562

559563
if data is None:
560564
return set() # short-circuit
@@ -579,6 +583,31 @@ cdef object _parse_set_type(
579583
else:
580584
result.add(inner_type(d))
581585
return result
586+
elif is_typing_set:
587+
# If we're dealing with typing.Set[str] or similar
588+
inner_element_type = get_args(inner_type)[0] if get_args(inner_type) else Any
589+
# If the inner type is a set, we need to process it differently
590+
try:
591+
for item in data:
592+
if isinstance(item, str):
593+
# String items are individual elements
594+
result.add(item)
595+
elif isinstance(item, (list, tuple, set)):
596+
# Process each element in collections
597+
for element in item:
598+
# Convert the element to the inner type if needed
599+
if inner_element_type in encoders and not isinstance(element, inner_element_type):
600+
converted = encoders[inner_element_type](element)
601+
result.add(converted)
602+
else:
603+
result.add(element)
604+
else:
605+
# Single non-string item
606+
result.add(item)
607+
except Exception as e:
608+
raise ValueError(
609+
f"Error parsing set item of {inner_type}: {e}"
610+
) from e
582611
elif converter:
583612
for item in data:
584613
result.add(
@@ -689,16 +718,42 @@ cdef object _parse_list_type(
689718
cdef object arg_type = args[0]
690719
cdef list result = []
691720
cdef tuple key = (arg_type, field.name)
692-
cdef object converter = TYPE_PARSERS.get(key) or TYPE_PARSERS.get(arg_type)
721+
cdef object converter
693722
cdef object inner_type = field._inner_type or arg_type
694723
cdef bint is_optional = False
724+
cdef object origin = field._inner_origin or get_origin(T)
725+
cdef tuple type_args = field._typing_args or get_args(T)
726+
727+
# Debug information if needed
728+
# print(f"_parse_list_type: field={field.name}, T={T}, data={data}, args={args}")
695729

696730
if data is None:
697731
return [] # short-circuit
698732

733+
# Compute the Inner Type:
734+
if hasattr(field, '_inner_type') and field._inner_type is not None:
735+
inner_type = field._inner_type
736+
# Then try to get it from the type's args
737+
elif type_args and len(type_args) > 0:
738+
inner_type = type_args[0]
739+
# Finally try to get it from args parameter
740+
elif args and isinstance(args, (list, tuple)) and len(args) > 0:
741+
if hasattr(args[0], '__origin__') and args[0].__origin__ is list and hasattr(args[0], '__args__'):
742+
# This is typing.List[T]
743+
arg_type = args[0]
744+
inner_type = arg_type.__args__[0] if arg_type.__args__ else Any
745+
else:
746+
inner_type = args[0]
747+
else:
748+
inner_type = Any
749+
699750
if not isinstance(data, (list, tuple)):
700751
data = [data]
701752

753+
# Get the converter if available
754+
key = (inner_type, field.name)
755+
converter = TYPE_PARSERS.get(key) or TYPE_PARSERS.get(arg_type)
756+
702757
# If it's a dataclass
703758
if is_dc(inner_type):
704759
for d in data:
@@ -721,6 +776,17 @@ cdef object _parse_list_type(
721776
f"Error creating {inner_type.__name__} from {d}: {e}"
722777
) from e
723778
return result
779+
elif is_typing(inner_type):
780+
if isinstance(data, list):
781+
result = _parse_list_typing(
782+
field,
783+
type_args,
784+
data,
785+
encoder,
786+
origin,
787+
args,
788+
None
789+
)
724790
elif converter:
725791
for item in data:
726792
result.append(
@@ -754,6 +820,7 @@ cdef object _parse_list_type(
754820
# If no type is specified, return the list as-is
755821
return data
756822
else:
823+
# Default: process each item with _parse_typing
757824
for item in data:
758825
result.append(
759826
_parse_typing(field, T=inner_type, data=item, encoder=encoder, as_objects=False)
@@ -1244,6 +1311,13 @@ cdef object _parse_union_type(
12441311
cdef object non_none_arg = None
12451312
cdef tuple inner_targs = None
12461313
cdef bint is_typing = False
1314+
cdef bint has_list_type = False
1315+
cdef list errors = [] # Collect all errors to report if all types fail
1316+
1317+
1318+
# First, check for None in Optional types
1319+
if type(None) in targs and data is None:
1320+
return None
12471321

12481322
# If the union includes NoneType, unwrap it and use only the non-None type.
12491323
if origin == Union and type(None) in targs:
@@ -1265,52 +1339,72 @@ cdef object _parse_union_type(
12651339
encoder,
12661340
False
12671341
)
1268-
else:
1269-
pass
12701342

12711343
# First check for dataclasses in the union
12721344
for arg_type in targs:
1345+
subtype_origin = field._inner_origin or get_origin(arg_type)
12731346
if is_dc(arg_type):
12741347
if isinstance(data, dict):
12751348
try:
12761349
return arg_type(**data)
12771350
except Exception as exc:
1278-
error = f"Failed to create dataclass {arg_type.__name__} from dict: {exc}"
1351+
errors.append(f"Failed to create dataclass {arg_type.__name__} from dict: {exc}")
12791352
continue
12801353
elif isinstance(data, arg_type):
12811354
return data
12821355
else:
12831356
# For string inputs, don't accept them as dataclasses
12841357
if isinstance(data, (str, int, float, bool)):
1285-
error = f"Cannot convert {type(data).__name__} to {arg_type.__name__}"
1358+
errors.append(f"Cannot convert {type(data).__name__} to {arg_type.__name__}")
12861359
continue
12871360
try:
12881361
return arg_type(data)
12891362
except Exception as exc:
1290-
error = f"Failed to create {arg_type.__name__} from {type(data).__name__}: {exc}"
1363+
errors.append(f"Failed to create {arg_type.__name__} from {type(data).__name__}: {exc}")
12911364
continue
1292-
if error:
1293-
raise ValueError(
1294-
f"Invalid type for {field.name} with data={data}, error = {error}"
1295-
)
1296-
1297-
for arg_type in targs:
1298-
# Iterate over all subtypes of Union:
1299-
subtype_origin = get_origin(arg_type)
1300-
try:
1301-
if subtype_origin is list or subtype_origin is tuple:
1365+
else:
1366+
if is_primitive(arg_type):
1367+
if isinstance(data, arg_type):
1368+
return data
1369+
continue
1370+
elif subtype_origin is list or subtype_origin is tuple:
13021371
if isinstance(data, list):
1303-
return _parse_list_type(field, arg_type, data, encoder, targs)
1372+
try:
1373+
return _parse_list_type(field, arg_type, data, encoder, targs)
1374+
except ValueError as exc:
1375+
errors.append(str(exc))
1376+
continue
13041377
else:
1305-
error = f"Invalid type for {field_name}: Expected a list, got {type(data).__name__}"
1378+
errors.append(f"Invalid type for {field_name}: Expected a list, got {type(data).__name__}")
13061379
continue
1307-
if subtype_origin is list and field._inner_is_dc == True and isinstance(data, list):
1380+
elif subtype_origin is set:
1381+
if isinstance(data, (list, tuple)):
1382+
return set(data)
1383+
elif isinstance(data, set):
1384+
return data
1385+
else:
1386+
try:
1387+
return _parse_set_type(field, T, data, encoder, targs, None)
1388+
except Exception as exc:
1389+
errors.append(f"Invalid type for {field_name}: Expected a set, got {type(data).__name__}")
1390+
continue
1391+
elif subtype_origin is frozenset:
1392+
if isinstance(data, (list, tuple)):
1393+
return frozenset(data)
1394+
elif isinstance(data, frozenset):
1395+
return data
1396+
else:
1397+
errors.append(f"Invalid type for {field_name}: Expected a frozenset, got {type(data).__name__}")
1398+
continue
1399+
elif subtype_origin is list and field._inner_is_dc == True and isinstance(data, list):
13081400
return _handle_list_of_dataclasses(field, field_name, data, T, None)
13091401
elif subtype_origin is dict:
13101402
if isinstance(data, dict):
13111403
return _parse_dict_type(field, arg_type, data, encoder, targs)
13121404
else:
1313-
error = f"Invalid type for {field_name} Expected a dict, got {type(data).__name__}"
1405+
errors.append(
1406+
f"Invalid type for {field_name} Expected a dict, got {type(data).__name__}"
1407+
)
13141408
continue
13151409
elif arg_type is list:
13161410
if isinstance(data, list):
@@ -1321,43 +1415,60 @@ cdef object _parse_union_type(
13211415
else:
13221416
return _parse_list_type(field, arg_type, data, encoder, targs)
13231417
else:
1324-
error = f"Invalid type for {field_name}: Expected a list, got {type(data).__name__}"
1418+
errors.append(
1419+
f"Invalid type for {field_name}: Expected a list, got {type(data).__name__}"
1420+
)
13251421
continue
13261422
elif arg_type is dict:
13271423
if isinstance(data, dict):
13281424
return _parse_dict_type(field, arg_type, data, encoder, targs)
13291425
else:
1330-
error = f"Invalid type for {field_name} Expected a dict, got {type(data).__name__}"
1426+
errors.append(
1427+
f"Invalid type for {field_name} Expected a dict, got {type(data).__name__}"
1428+
)
13311429
continue
13321430
elif subtype_origin is None:
1333-
if is_dc(arg_type):
1334-
return _handle_dataclass_type(field, name, data, arg_type, False, None)
1335-
elif arg_type in encoders:
1336-
return _parse_builtin_type(field, arg_type, data, encoder)
1337-
elif isinstance(data, arg_type):
1338-
return data
1339-
else:
1340-
# Not matching => record an error
1341-
error = f"Invalid type for {field_name}, Data {data!r} is not an instance of {arg_type}"
1431+
try:
1432+
if is_dc(arg_type):
1433+
return _handle_dataclass_type(field, name, data, arg_type, False, None)
1434+
elif arg_type in encoders:
1435+
return _parse_builtin_type(field, arg_type, data, encoder)
1436+
elif isinstance(data, arg_type):
1437+
return data
1438+
else:
1439+
# Not matching => record an error
1440+
errors.append(
1441+
f"Invalid type for {field_name}, Data {data!r} is not an instance of {arg_type}"
1442+
)
1443+
continue
1444+
except ValueError as exc:
1445+
errors.append(f"{field.name}: {exc}")
13421446
continue
13431447
else:
1344-
# fallback to builtin parse
1345-
return _parse_typing(
1346-
field,
1347-
arg_type,
1348-
data,
1349-
encoder,
1350-
False
1351-
)
1352-
except ValueError as exc:
1353-
error = f"{field.name}: {exc}"
1354-
except Exception as exc:
1355-
error = f"Parse Error on {field.name}, {arg_type}: {exc}"
1448+
try:
1449+
# fallback to builtin parse
1450+
return _parse_typing(
1451+
field,
1452+
arg_type,
1453+
data,
1454+
encoder,
1455+
False
1456+
)
1457+
except ValueError as exc:
1458+
errors.append(f"{field.name}: {exc}")
1459+
continue
1460+
except Exception as exc:
1461+
errors.append(f"Parse Error on {field.name}, {arg_type}: {exc}")
1462+
continue
13561463

13571464
# If we get here, all union attempts failed
1358-
raise ValueError(
1359-
f"Invalid type for {field.name} with data={data}, error = {error}"
1360-
)
1465+
if errors:
1466+
error_msg = f"All Union types failed for {field_name}. Errors: " + "; ".join(errors)
1467+
raise ValueError(error_msg)
1468+
else:
1469+
raise ValueError(
1470+
f"Invalid type for {field_name} with data={data}, no matching type found"
1471+
)
13611472

13621473
cdef object _parse_type(
13631474
object field,

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.10.16'
9+
__version__ = '0.10.17'
1010
__copyright__ = 'Copyright (c) 2020-2024 Jesus Lara'
1111
__author__ = 'Jesus Lara'
1212
__author_email__ = '[email protected]'

0 commit comments

Comments
 (0)