##// END OF EJS Templates
ext-json: fixed tests and made the lib more consistent with orjson vs simplejson
super-admin -
r5012:2b0b3872 default
parent child Browse files
Show More
@@ -1,83 +1,86 b''
1 import datetime
1 import datetime
2 import decimal
2 import decimal
3 import functools
3 import functools
4 # we keep simplejson for having dump functionality still
4 # we keep simplejson for having dump functionality still
5 # orjson doesn't support it
5 # orjson doesn't support it
6 import simplejson as sjson
7
6 import orjson
8 import orjson
7 import simplejson
8 import orjson as json
9 import orjson as json
9
10
10
11
11 from rhodecode.lib.datelib import is_aware
12 from rhodecode.lib.datelib import is_aware
12 from rhodecode.lib.str_utils import safe_str
13 from rhodecode.lib.str_utils import safe_str
13
14
14 try:
15 try:
15 import rhodecode.translation
16 import rhodecode.translation
16 except ImportError:
17 except ImportError:
17 rhodecode = None
18 rhodecode = None
18
19
19 __all__ = ['json']
20 __all__ = ['json']
20
21
21
22
22 def _obj_dump(obj):
23 def _obj_dump(obj):
23 """
24 """
24 Custom function for dumping objects to JSON, if obj has __json__ attribute
25 Custom function for dumping objects to JSON, if obj has __json__ attribute
25 or method defined it will be used for serialization
26 or method defined it will be used for serialization
26
27
27 :param obj:
28 :param obj:
28 """
29 """
29
30 # See "Date Time String Format" in the ECMA-262 specification.
30 # See "Date Time String Format" in the ECMA-262 specification.
31 # some code borrowed from django 1.4
31 # some code borrowed from django 1.4
32 if isinstance(obj, set):
32 if isinstance(obj, set):
33 return list(obj)
33 return list(obj)
34 elif isinstance(obj, datetime.datetime):
34 elif isinstance(obj, datetime.datetime):
35 r = obj.isoformat()
35 r = obj.isoformat()
36 if isinstance(obj.microsecond, int):
36 if isinstance(obj.microsecond, int):
37 r = r[:23] + r[26:]
37 r = r[:23] + r[26:]
38 if r.endswith('+00:00'):
38 if r.endswith('+00:00'):
39 r = r[:-6] + 'Z'
39 r = r[:-6] + 'Z'
40 return r
40 return r
41 elif isinstance(obj, datetime.date):
41 elif isinstance(obj, datetime.date):
42 return obj.isoformat()
42 return obj.isoformat()
43 elif isinstance(obj, datetime.time):
43 elif isinstance(obj, datetime.time):
44 if is_aware(obj):
44 if is_aware(obj):
45 raise TypeError("Time-zone aware times are not JSON serializable")
45 raise TypeError("Time-zone aware times are not JSON serializable")
46 r = obj.isoformat()
46 r = obj.isoformat()
47 if isinstance(obj.microsecond, int):
47 if isinstance(obj.microsecond, int):
48 r = r[:12]
48 r = r[:12]
49 return r
49 return r
50 elif hasattr(obj, '__json__'):
50 elif hasattr(obj, '__json__'):
51 if callable(obj.__json__):
51 if callable(obj.__json__):
52 return obj.__json__()
52 return obj.__json__()
53 else:
53 else:
54 return obj.__json__
54 return obj.__json__
55 elif isinstance(obj, decimal.Decimal):
55 elif isinstance(obj, decimal.Decimal):
56 return str(obj)
56 return str(obj)
57 elif isinstance(obj, complex):
57 elif isinstance(obj, complex):
58 return [obj.real, obj.imag]
58 return [obj.real, obj.imag]
59 elif rhodecode and isinstance(obj, rhodecode.translation._LazyString):
59 elif rhodecode and isinstance(obj, rhodecode.translation._LazyString):
60 return obj.eval()
60 return obj.eval()
61 else:
61 else:
62 raise TypeError(repr(obj) + " is not JSON serializable")
62 raise TypeError(repr(obj) + " is not JSON serializable")
63
63
64
64
65 sjson.dumps = functools.partial(sjson.dumps, default=_obj_dump)
66 sjson.dump = functools.partial(sjson.dump, default=_obj_dump)
67
65 json.dumps = functools.partial(json.dumps, default=_obj_dump, option=orjson.OPT_NON_STR_KEYS)
68 json.dumps = functools.partial(json.dumps, default=_obj_dump, option=orjson.OPT_NON_STR_KEYS)
66 json.dump = functools.partial(simplejson.dump, default=_obj_dump)
69 json.dump = functools.partial(sjson.dump, default=_obj_dump)
67
70
68
71
69 def formatted_json(*args, **kwargs):
72 def formatted_json(*args, **kwargs):
70 # alias for formatted json
73 # alias for formatted json
71 opts = orjson.OPT_NON_STR_KEYS | orjson.OPT_INDENT_2 | orjson.OPT_SORT_KEYS
74 opts = orjson.OPT_NON_STR_KEYS | orjson.OPT_INDENT_2 | orjson.OPT_SORT_KEYS
72 return functools.partial(json.dumps, option=opts)(*args, **kwargs)
75 return functools.partial(json.dumps, option=opts)(*args, **kwargs)
73
76
74
77
75 def formatted_str_json(*args, **kwargs):
78 def formatted_str_json(*args, **kwargs):
76 opts = orjson.OPT_NON_STR_KEYS | orjson.OPT_INDENT_2 | orjson.OPT_SORT_KEYS
79 opts = orjson.OPT_NON_STR_KEYS | orjson.OPT_INDENT_2 | orjson.OPT_SORT_KEYS
77 closure = functools.partial(json.dumps, option=opts)
80 closure = functools.partial(json.dumps, option=opts)
78 return safe_str(closure(*args, **kwargs))
81 return safe_str(closure(*args, **kwargs))
79
82
80
83
81 def str_json(*args, **kwargs):
84 def str_json(*args, **kwargs):
82 closure = functools.partial(json.dumps)
85 closure = functools.partial(json.dumps)
83 return safe_str(closure(*args, **kwargs))
86 return safe_str(closure(*args, **kwargs))
@@ -1,166 +1,167 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import io
21 import datetime
22 import datetime
22 import decimal
23 import decimal
23 import io
24 import textwrap
24 import textwrap
25
25
26 import pytest
26 import pytest
27
27
28 from rhodecode.lib.ext_json import json
28 from rhodecode.lib import ext_json
29 from rhodecode.lib.ext_json import formatted_json
30 from rhodecode.translation import _, _pluralize
29 from rhodecode.translation import _, _pluralize
31
30
32
31
33 class Timezone(datetime.tzinfo):
32 class Timezone(datetime.tzinfo):
34 def __init__(self, hours):
33 def __init__(self, hours):
35 self.hours = hours
34 self.hours = hours
36
35
37 def utcoffset(self, unused_dt):
36 def utcoffset(self, unused_dt):
38 return datetime.timedelta(hours=self.hours)
37 return datetime.timedelta(hours=self.hours)
39
38
40
39
40 class SerializableObject(object):
41 def __json__(self):
42 return 'foo'
43
44
41 def test_dumps_set():
45 def test_dumps_set():
42 result = json.dumps(set((1, 2, 3)))
46 result = ext_json.json.dumps(set((1, 2, 3)))
43 # We cannot infer what the order of result is going to be
47 # We cannot infer what the order of result is going to be
44 result = json.loads(result)
48 result = ext_json.json.loads(result)
45 assert isinstance(result, list)
49 assert isinstance(result, list)
46 assert [1, 2, 3] == sorted(result)
50 assert [1, 2, 3] == sorted(result)
47
51
48
52
49 def test_dumps_decimal():
53 def test_dumps_decimal():
50 assert '"1.5"' == json.dumps(decimal.Decimal('1.5'))
54 assert b'"1.5"' == ext_json.json.dumps(decimal.Decimal('1.5'))
51
55
52
56
53 def test_dumps_complex():
57 def test_dumps_complex():
54 assert "[0.0, 1.0]" == json.dumps(1j)
58 assert b"[0.0,1.0]" == ext_json.json.dumps(1j)
55 assert "[1.0, 0.0]" == json.dumps(1 + 0j)
59 assert b"[1.0,0.0]" == ext_json.json.dumps(1 + 0j)
56 assert "[1.1, 1.2]" == json.dumps(1.1 + 1.2j)
60 assert b"[1.1,1.2]" == ext_json.json.dumps(1.1 + 1.2j)
57
61
58
62
59 def test_dumps_object_with_json_method():
63 def test_dumps_object_with_json_method():
60 class SerializableObject(object):
64 assert '"foo"' == ext_json.str_json(SerializableObject())
61 def __json__(self):
62 return 'foo'
63
64 assert '"foo"' == json.dumps(SerializableObject())
65
65
66
66
67 def test_dumps_object_with_json_attribute():
67 def test_dumps_object_with_json_attribute():
68 class SerializableObject(object):
69 __json__ = 'foo'
70
68
71 assert '"foo"' == json.dumps(SerializableObject())
69 assert '"foo"' == ext_json.str_json(SerializableObject())
72
70
73
71
74 def test_dumps_time():
72 def test_dumps_time():
75 assert '"03:14:15.926"' == json.dumps(datetime.time(3, 14, 15, 926535))
73 assert '"03:14:15.926535"' == ext_json.str_json(datetime.time(3, 14, 15, 926535))
76
74
77
75
78 def test_dumps_time_no_microseconds():
76 def test_dumps_time_no_microseconds():
79 assert '"03:14:15"' == json.dumps(datetime.time(3, 14, 15))
77 assert '"03:14:15"' == ext_json.str_json(datetime.time(3, 14, 15))
80
78
81
79
82 def test_dumps_time_with_timezone():
80 def test_dumps_time_with_timezone():
83 with pytest.raises(TypeError) as excinfo:
81 with pytest.raises(TypeError) as excinfo:
84 json.dumps(datetime.time(3, 14, 15, 926535, Timezone(0)))
82 ext_json.json.dumps(datetime.time(3, 14, 15, 926535, Timezone(0)))
85
83
86 error_msg = str(excinfo.value)
84 error_msg = str(excinfo.value)
87 assert 'Time-zone aware times are not JSON serializable' in error_msg
85
86 assert 'timezone library is not supported' in error_msg
87 # only for simplejson
88 #assert 'Time-zone aware times are not JSON serializable' in error_msg
88
89
89
90
90 def test_dumps_date():
91 def test_dumps_date():
91 assert '"1969-07-20"' == json.dumps(datetime.date(1969, 7, 20))
92 assert b'"1969-07-20"' == ext_json.json.dumps(datetime.date(1969, 7, 20))
92
93
93
94
94 def test_dumps_datetime():
95 def test_dumps_datetime():
95 json_data = json.dumps(datetime.datetime(1969, 7, 20, 3, 14, 15, 926535))
96 json_data = ext_json.json.dumps(datetime.datetime(1969, 7, 20, 3, 14, 15, 926535))
96 assert '"1969-07-20T03:14:15.926"' == json_data
97 assert b'"1969-07-20T03:14:15.926535"' == json_data
97
98
98
99
99 def test_dumps_datetime_no_microseconds():
100 def test_dumps_datetime_no_microseconds():
100 json_data = json.dumps(datetime.datetime(1969, 7, 20, 3, 14, 15))
101 json_data = ext_json.json.dumps(datetime.datetime(1969, 7, 20, 3, 14, 15))
101 assert '"1969-07-20T03:14:15"' == json_data
102 assert b'"1969-07-20T03:14:15"' == json_data
102
103
103
104
104 def test_dumps_datetime_with_utc_timezone():
105 def test_dumps_datetime_with_utc_timezone():
105 json_data = json.dumps(
106 json_data = ext_json.json.dumps(
106 datetime.datetime(1969, 7, 20, 3, 14, 15, 926535, Timezone(0)))
107 datetime.datetime(1969, 7, 20, 3, 14, 15, 926535, Timezone(0)))
107 assert '"1969-07-20T03:14:15.926Z"' == json_data
108 assert b'"1969-07-20T03:14:15.926535+00:00"' == json_data
108
109
109
110
110 def test_dumps_datetime_with_plus1_timezone():
111 def test_dumps_datetime_with_plus1_timezone():
111 json_data = json.dumps(
112 json_data = ext_json.json.dumps(
112 datetime.datetime(1969, 7, 20, 3, 14, 15, 926535, Timezone(1)))
113 datetime.datetime(1969, 7, 20, 3, 14, 15, 926535, Timezone(1)))
113 assert '"1969-07-20T03:14:15.926+01:00"' == json_data
114 assert b'"1969-07-20T03:14:15.926535+01:00"' == json_data
114
115
115
116
116 def test_dumps_unserializable_class():
117 def test_dumps_unserializable_class():
117 unserializable_obj = object()
118 unserializable_obj = object()
118 with pytest.raises(TypeError) as excinfo:
119 with pytest.raises(TypeError) as excinfo:
119 json.dumps(unserializable_obj)
120 ext_json.json.dumps(unserializable_obj)
120
121
121 assert repr(unserializable_obj) in str(excinfo.value)
122 assert 'object' in str(excinfo.value)
122 assert 'is not JSON serializable' in str(excinfo.value)
123 assert 'is not JSON serializable' in str(excinfo.value)
123
124
124
125
125 def test_dump_is_like_dumps():
126 def test_dump_is_like_dumps():
126 data = {
127 data = {
127 'decimal': decimal.Decimal('1.5'),
128 'decimal': decimal.Decimal('1.5'),
128 'set': set([1]), # Just one element to guarantee the order
129 'set': set([1]), # Just one element to guarantee the order
129 'complex': 1 - 1j,
130 'complex': 1 - 1j,
130 'datetime': datetime.datetime(1969, 7, 20, 3, 14, 15, 926535),
131 'datetime': datetime.datetime(1969, 7, 20, 3, 14, 15, 926535),
131 'time': datetime.time(3, 14, 15, 926535),
132 'time': datetime.time(3, 14, 15, 926535),
132 'date': datetime.date(1969, 7, 20),
133 'date': datetime.date(1969, 7, 20),
133 }
134 }
134 json_buffer = io.BytesIO()
135 json_buffer = io.StringIO() # StringIO because dump uses simplejson not orjson
135 json.dump(data, json_buffer)
136 ext_json.json.dump(data, json_buffer)
136
137
137 assert json.dumps(data) == json_buffer.getvalue()
138 assert ext_json.sjson.dumps(data) == json_buffer.getvalue()
138
139
139
140
140 def test_formatted_json():
141 def test_formatted_json():
141 data = {
142 data = {
142 'b': {'2': 2, '1': 1},
143 'b': {'2': 2, '1': 1},
143 'a': {'3': 3, '4': 4},
144 'a': {'3': 3, '4': 4},
144 }
145 }
145
146
146 expected_data = textwrap.dedent('''
147 expected_data = textwrap.dedent('''
147 {
148 {
148 "a": {
149 "a": {
149 "3": 3,
150 "3": 3,
150 "4": 4
151 "4": 4
151 },
152 },
152 "b": {
153 "b": {
153 "1": 1,
154 "1": 1,
154 "2": 2
155 "2": 2
155 }
156 }
156 }''').strip()
157 }''').strip()
157
158
158 assert formatted_json(data) == expected_data
159 assert expected_data == ext_json.formatted_str_json(data)
159
160
160
161
161 def test_lazy_translation_string(baseapp):
162 def test_lazy_translation_string(baseapp):
162 data = {'label': _('hello')}
163 data = {'label': _('hello')}
163 data2 = {'label2': _pluralize('singular', 'plural', 1)}
164 data2 = {'label2': _pluralize('singular', 'plural', 1)}
164
165
165 assert json.dumps(data) == '{"label": "hello"}'
166 assert b'{"label":"hello"}' == ext_json.json.dumps(data)
166 assert json.dumps(data2) == '{"label2": "singular"}'
167 assert b'{"label2":"singular"}' == ext_json.json.dumps(data2)
General Comments 0
You need to be logged in to leave comments. Login now