Skip to content

Commit f689630

Browse files
committed
Improve support for Enum's
1 parent 9730786 commit f689630

File tree

5 files changed

+136
-32
lines changed

5 files changed

+136
-32
lines changed

servicestack/__init__.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,14 @@
6060
'WebServiceExceptionType',
6161
'to_json',
6262
'from_json',
63+
'convert',
6364
'qsvalue',
6465
'resolve_httpmethod',
66+
'is_optional',
67+
'is_list',
68+
'is_dict',
69+
'generic_args',
70+
'generic_arg',
6571
'index_of',
6672
'last_index_of',
6773
'left_part',
@@ -75,11 +81,18 @@
7581
'from_datetime',
7682
'to_bytearray',
7783
'from_bytearray',
84+
'from_base64url_safe',
85+
'inspect_jwt',
86+
'inspect_vars',
87+
'dump',
88+
'printdump',
89+
'dumptable',
90+
'printdumptable',
7891
'Bytes',
7992
]
8093

8194
from .dtos import *
8295
from .clients import TypeConverters, JsonServiceClient, WebServiceException, WebServiceExceptionType, to_json, \
83-
from_json, qsvalue, resolve_httpmethod
96+
from_json, convert, qsvalue, resolve_httpmethod
8497
from .utils import *
8598
from .fields import *

servicestack/clients.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import inspect
33
import typing
44
from dataclasses import field, fields, asdict, is_dataclass
5-
from enum import Enum
5+
from enum import Enum, IntEnum
66
from typing import Callable, get_args, Type, get_origin, ForwardRef, Union
77
from urllib.parse import urljoin, quote_plus
88

@@ -29,7 +29,9 @@ def _dump(obj):
2929
print("")
3030

3131

32-
def _get_type_vars_map(cls: Type, type_map: Dict[Union[type, TypeVar], type] = {}):
32+
def _get_type_vars_map(cls: Type, type_map=None):
33+
if type_map is None:
34+
type_map = {}
3335
if hasattr(cls, '__orig_bases__'):
3436
for base_cls in cls.__orig_bases__:
3537
_get_type_vars_map(base_cls, type_map)
@@ -178,7 +180,9 @@ def _json_encoder(obj: Any):
178180

179181
def to_json(obj: Any):
180182
if is_dataclass(obj):
181-
return json.dumps(clean_any(obj.to_dict()), default=_json_encoder)
183+
obj_dict = clean_any(obj.to_dict())
184+
print(obj_dict)
185+
return json.dumps(obj_dict, default=_json_encoder)
182186
return json.dumps(obj, default=_json_encoder)
183187

184188

@@ -236,8 +240,8 @@ def sanitize_name(s: str):
236240

237241

238242
def enum_get(cls: Enum, key: Union[str, int]):
239-
if type(key) == int:
240-
return cls[key]
243+
if type(key) == int or issubclass(cls, IntEnum):
244+
return cls(key)
241245
try:
242246
return cls[key]
243247
except Exception as e:
@@ -246,6 +250,9 @@ def enum_get(cls: Enum, key: Union[str, int]):
246250
return cls[upper_snake]
247251
except Exception as e2:
248252
sanitize_key = sanitize_name(key)
253+
for value in cls.__members__.values():
254+
if sanitize_key == sanitize_name(value):
255+
return value
249256
for member in cls.__members__.keys():
250257
if sanitize_key == sanitize_name(member):
251258
return cls[member]
@@ -266,6 +273,10 @@ def convert(into: Type, obj: Any, substitute_types: Dict[Type, type] = None):
266273
if Log.debug_enabled():
267274
Log.debug(f"convert({into}, {substitute_types}, {obj})")
268275

276+
is_type = type(into) == type
277+
if not is_type:
278+
Log.debug(f"type of {into} is not a class")
279+
269280
generic_def = get_origin(into)
270281
if generic_def is not None and is_dataclass(generic_def):
271282
reified_types = _get_type_vars_map(into)
@@ -290,6 +301,8 @@ def convert(into: Type, obj: Any, substitute_types: Dict[Type, type] = None):
290301
key_type = _resolve_type(key_type, substitute_types)
291302
val_type = _resolve_type(val_type, substitute_types)
292303
to = {}
304+
if not hasattr(obj, 'items'):
305+
Log.warn(f"dict {obj} ({type(type)}) does not have items()")
293306
for key, val in obj.items():
294307
to_key = convert(key_type, key, substitute_types)
295308
to_val = convert(val_type, val, substitute_types)
@@ -309,7 +322,7 @@ def convert(into: Type, obj: Any, substitute_types: Dict[Type, type] = None):
309322
except Exception as e:
310323
Log.error(f"into().deserialize(obj) {into}({obj})", e)
311324
raise e
312-
elif issubclass(into, Enum):
325+
elif is_type and issubclass(into, Enum):
313326
try:
314327
return enum_get(into, obj)
315328
except Exception as e:
@@ -320,6 +333,8 @@ def convert(into: Type, obj: Any, substitute_types: Dict[Type, type] = None):
320333
try:
321334
return into(obj)
322335
except Exception as e:
336+
# if into == typing.Dict or get_origin(into) == typing.Dict:
337+
# print("WAS A typing.Dict")
323338
Log.error(f"into(obj) {into}({obj})", e)
324339
raise e
325340

tests/dtos.py

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
""" Options:
2-
Date: 2021-07-11 10:30:38
2+
Date: 2021-07-11 12:33:01
33
Version: 5.111
44
Tip: To override a DTO option, remove "//" prefix before updating
55
BaseUrl: https://test.servicestack.net
66
7-
#GlobalNamespace:
7+
#GlobalNamespace:
88
#MakePropertiesOptional: False
99
#AddServiceStackTypes: True
1010
#AddResponseStatus: False
11-
#AddImplicitVersion:
11+
#AddImplicitVersion:
1212
#AddDescriptionAsComments: True
13-
#IncludeTypes:
14-
#ExcludeTypes:
15-
#DefaultImports: datetime,decimal,marshmallow.fields:*,servicestack:*,typing:*,dataclasses:dataclass/field,dataclasses_json:dataclass_json/LetterCase/Undefined/config,enum:Enum
16-
#DataClass:
17-
#DataClassJson:
13+
#IncludeTypes:
14+
#ExcludeTypes:
15+
#DefaultImports: datetime,decimal,marshmallow.fields:*,servicestack:*,typing:*,dataclasses:dataclass/field,dataclasses_json:dataclass_json/LetterCase/Undefined/config,enum:Enum/IntEnum
16+
#DataClass:
17+
#DataClassJson:
1818
"""
1919

2020
import datetime
@@ -24,7 +24,7 @@
2424
from typing import *
2525
from dataclasses import dataclass, field
2626
from dataclasses_json import dataclass_json, LetterCase, Undefined, config
27-
from enum import Enum
27+
from enum import Enum, IntEnum
2828

2929

3030
@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE)
@@ -172,7 +172,7 @@ class EnumType(str, Enum):
172172

173173

174174
# @Flags()
175-
class EnumTypeFlags(Enum):
175+
class EnumTypeFlags(IntEnum):
176176
VALUE1 = 0
177177
VALUE2 = 1
178178
VALUE3 = 2
@@ -185,15 +185,15 @@ class EnumWithValues(str, Enum):
185185

186186

187187
# @Flags()
188-
class EnumFlags(Enum):
189-
VALUE0 = 'Value0'
190-
VALUE1 = 'Value 1'
191-
VALUE2 = 'Value2'
192-
VALUE3 = 'Value3'
193-
VALUE123 = 'Value123'
188+
class EnumFlags(IntEnum):
189+
VALUE0 = 0
190+
VALUE1 = 1
191+
VALUE2 = 2
192+
VALUE3 = 4
193+
VALUE123 = 7
194194

195195

196-
class EnumAsInt(Enum):
196+
class EnumAsInt(IntEnum):
197197
VALUE1 = 1000
198198
VALUE2 = 2000
199199
VALUE3 = 3000
@@ -295,7 +295,7 @@ class DayOfWeek(str, Enum):
295295
SATURDAY = 'Saturday'
296296

297297

298-
class ScopeType(Enum):
298+
class ScopeType(IntEnum):
299299
GLOBAL_ = 1
300300
SALE = 2
301301

@@ -2005,3 +2005,4 @@ class RealDeleteAuditTenant(IReturn[RockstarWithIdAndCountResponse], IDeleteDb[R
20052005
@dataclass
20062006
class CreateRockstarVersion(RockstarBase, IReturn[RockstarWithIdAndRowVersionResponse], ICreateDb[RockstarVersion]):
20072007
pass
2008+

tests/techstacks_dtos.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
""" Options:
2-
Date: 2021-07-11 08:30:48
2+
Date: 2021-07-11 10:33:03
33
Version: 5.111
44
Tip: To override a DTO option, remove "//" prefix before updating
55
BaseUrl: https://techstacks.io
66
7-
#GlobalNamespace:
7+
#GlobalNamespace:
88
#MakePropertiesOptional: False
99
#AddServiceStackTypes: True
1010
#AddResponseStatus: False
11-
#AddImplicitVersion:
11+
#AddImplicitVersion:
1212
#AddDescriptionAsComments: True
13-
#IncludeTypes:
14-
#ExcludeTypes:
13+
#IncludeTypes:
14+
#ExcludeTypes:
1515
#DefaultImports: datetime,decimal,marshmallow.fields:*,servicestack:*,typing:*,dataclasses:dataclass/field,dataclasses_json:dataclass_json/LetterCase/Undefined/config,enum:Enum
16-
#DataClass:
17-
#DataClassJson:
16+
#DataClass:
17+
#DataClassJson:
1818
"""
1919

2020
import datetime
@@ -2021,3 +2021,4 @@ class QueryPostComments(QueryDb[PostComment], IReturn[QueryResponse[PostComment]
20212021
word_count_below: Optional[int] = None
20222022
report_count_above: Optional[int] = None
20232023
report_count_below: Optional[int] = None
2024+

tests/test_enum_serialization.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""Enum Serialization
2+
"""
3+
4+
import unittest
5+
6+
import requests
7+
from dataclasses_json import config, dataclass_json, Undefined
8+
9+
from .dtos import *
10+
from datetime import datetime, timedelta, timezone
11+
from servicestack import JsonServiceClient, WebServiceException, to_json, convert
12+
from .config import create_test_client
13+
from servicestack.utils import *
14+
from tests.dtos import HelloWithEnum
15+
16+
17+
class TestEnumSerialization(unittest.TestCase):
18+
19+
def assert_HelloWithEnum(self, actual: HelloWithEnum, expected: HelloWithEnum):
20+
self.assertEqual(actual.enum_prop, expected.enum_prop)
21+
self.assertEqual(actual.enum_with_values, expected.enum_with_values)
22+
self.assertEqual(actual.nullable_enum_prop, expected.nullable_enum_prop)
23+
self.assertEqual(actual.enum_flags, expected.enum_flags)
24+
self.assertEqual(actual.enum_style, expected.enum_style)
25+
26+
def test_does_serialize_HelloWithEnum_empty(self):
27+
dto = HelloWithEnum()
28+
json_str = to_json(dto)
29+
json_obj = json.loads(json_str)
30+
from_json_obj = convert(HelloWithEnum, json_obj)
31+
self.assertEqual(from_json_obj, dto)
32+
33+
def test_does_serialize_HelloWithEnum_EnumFlags(self):
34+
dto = HelloWithEnum(enum_flags=EnumFlags.VALUE1)
35+
json_str = to_json(dto)
36+
json_obj = json.loads(json_str)
37+
from_json_obj = convert(HelloWithEnum, json_obj)
38+
self.assertEqual(from_json_obj, dto)
39+
40+
def test_does_serialize_HelloWithEnum_all(self):
41+
dto = HelloWithEnum(
42+
enum_prop=EnumType.VALUE2,
43+
enum_with_values=EnumWithValues.VALUE1,
44+
enum_flags=EnumFlags.VALUE1,
45+
enum_style=EnumStyle.UPPER)
46+
json_str = to_json(dto)
47+
json_obj = json.loads(json_str)
48+
from_json_obj = convert(HelloWithEnum, json_obj)
49+
self.assertEqual(from_json_obj, dto)
50+
51+
def assert_HelloWithEnumMap(self, actual: HelloWithEnumMap, expected: HelloWithEnumMap):
52+
self.assertDictEqual(actual.enum_prop, expected.enum_prop)
53+
self.assertDictEqual(actual.enum_with_values, expected.enum_with_values)
54+
self.assertDictEqual(actual.nullable_enum_prop, expected.nullable_enum_prop)
55+
self.assertDictEqual(actual.enum_flags, expected.enum_flags)
56+
self.assertDictEqual(actual.enum_style, expected.enum_style)
57+
58+
def test_does_serialize_HelloWithEnumMap_empty(self):
59+
dto = HelloWithEnumMap()
60+
json_str = to_json(dto)
61+
json_obj = json.loads(json_str)
62+
from_json_obj = convert(HelloWithEnumMap, json_obj)
63+
self.assertEqual(from_json_obj, dto)
64+
65+
def test_does_serialize_HelloWithEnumMap_all(self):
66+
dto = HelloWithEnumMap(
67+
enum_prop={f"{EnumType.VALUE2}": EnumType.VALUE2},
68+
enum_with_values={f"{EnumWithValues.VALUE1}": EnumWithValues.VALUE1},
69+
enum_flags={f"{EnumFlags.VALUE1}": EnumFlags.VALUE1},
70+
enum_style={f"{EnumStyle.UPPER}": EnumStyle.UPPER})
71+
json_str = to_json(dto)
72+
json_obj = json.loads(json_str)
73+
from_json_obj = convert(HelloWithEnumMap, json_obj)
74+
self.assertEqual(from_json_obj, dto)

0 commit comments

Comments
 (0)