##// END OF EJS Templates
jsonalchemy: add pickle support to mutation dict so that json...
dan -
r575:ec0a2507 default
parent child Browse files
Show More
@@ -0,0 +1,45 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2016 RhodeCode GmbH
4 #
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
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
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/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import pickle
22 import pytest
23
24 from rhodecode.lib.jsonalchemy import MutationDict, MutationList
25
26
27 def test_mutation_dict_is_picklable():
28 mutation_dict = MutationDict({'key1': 'value1', 'key2': 'value2'})
29 dumped = pickle.dumps(mutation_dict)
30 loaded = pickle.loads(dumped)
31 assert loaded == mutation_dict
32
33 def test_mutation_list_is_picklable():
34 mutation_list = MutationList(['a', 'b', 'c'])
35 dumped = pickle.dumps(mutation_list)
36 loaded = pickle.loads(dumped)
37 assert loaded == mutation_list
38
39 def test_mutation_dict_with_lists_is_picklable():
40 mutation_dict = MutationDict({
41 'key': MutationList(['values', MutationDict({'key': 'value'})])
42 })
43 dumped = pickle.dumps(mutation_dict)
44 loaded = pickle.loads(dumped)
45 assert loaded == mutation_dict
@@ -1,257 +1,265 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import collections
22 22
23 23 import sqlalchemy
24 24 from sqlalchemy import UnicodeText
25 25 from sqlalchemy.ext.mutable import Mutable
26 26
27 27 from rhodecode.lib.ext_json import json
28 28
29 29
30 30 class JsonRaw(unicode):
31 31 """
32 32 Allows interacting with a JSON types field using a raw string.
33 33
34 34 For example::
35 35 db_instance = JsonTable()
36 36 db_instance.enabled = True
37 37 db_instance.json_data = JsonRaw('{"a": 4}')
38 38
39 39 This will bypass serialization/checks, and allow storing
40 40 raw values
41 41 """
42 42 pass
43 43
44 44
45 45 # Set this to the standard dict if Order is not required
46 46 DictClass = collections.OrderedDict
47 47
48 48
49 49 class JSONEncodedObj(sqlalchemy.types.TypeDecorator):
50 50 """
51 51 Represents an immutable structure as a json-encoded string.
52 52
53 53 If default is, for example, a dict, then a NULL value in the
54 54 database will be exposed as an empty dict.
55 55 """
56 56
57 57 impl = UnicodeText
58 58 safe = True
59 59
60 60 def __init__(self, *args, **kwargs):
61 61 self.default = kwargs.pop('default', None)
62 62 self.safe = kwargs.pop('safe_json', self.safe)
63 63 self.dialect_map = kwargs.pop('dialect_map', {})
64 64 super(JSONEncodedObj, self).__init__(*args, **kwargs)
65 65
66 66 def load_dialect_impl(self, dialect):
67 67 if dialect.name in self.dialect_map:
68 68 return dialect.type_descriptor(self.dialect_map[dialect.name])
69 69 return dialect.type_descriptor(self.impl)
70 70
71 71 def process_bind_param(self, value, dialect):
72 72 if isinstance(value, JsonRaw):
73 73 value = value
74 74 elif value is not None:
75 75 value = json.dumps(value)
76 76 return value
77 77
78 78 def process_result_value(self, value, dialect):
79 79 if self.default is not None and (not value or value == '""'):
80 80 return self.default()
81 81
82 82 if value is not None:
83 83 try:
84 84 value = json.loads(value, object_pairs_hook=DictClass)
85 85 except Exception as e:
86 86 if self.safe:
87 87 return self.default()
88 88 else:
89 89 raise
90 90 return value
91 91
92 92
93 93 class MutationObj(Mutable):
94 94 @classmethod
95 95 def coerce(cls, key, value):
96 96 if isinstance(value, dict) and not isinstance(value, MutationDict):
97 97 return MutationDict.coerce(key, value)
98 98 if isinstance(value, list) and not isinstance(value, MutationList):
99 99 return MutationList.coerce(key, value)
100 100 return value
101 101
102 102 @classmethod
103 103 def _listen_on_attribute(cls, attribute, coerce, parent_cls):
104 104 key = attribute.key
105 105 if parent_cls is not attribute.class_:
106 106 return
107 107
108 108 # rely on "propagate" here
109 109 parent_cls = attribute.class_
110 110
111 111 def load(state, *args):
112 112 val = state.dict.get(key, None)
113 113 if coerce:
114 114 val = cls.coerce(key, val)
115 115 state.dict[key] = val
116 116 if isinstance(val, cls):
117 117 val._parents[state.obj()] = key
118 118
119 119 def set(target, value, oldvalue, initiator):
120 120 if not isinstance(value, cls):
121 121 value = cls.coerce(key, value)
122 122 if isinstance(value, cls):
123 123 value._parents[target.obj()] = key
124 124 if isinstance(oldvalue, cls):
125 125 oldvalue._parents.pop(target.obj(), None)
126 126 return value
127 127
128 128 def pickle(state, state_dict):
129 129 val = state.dict.get(key, None)
130 130 if isinstance(val, cls):
131 131 if 'ext.mutable.values' not in state_dict:
132 132 state_dict['ext.mutable.values'] = []
133 133 state_dict['ext.mutable.values'].append(val)
134 134
135 135 def unpickle(state, state_dict):
136 136 if 'ext.mutable.values' in state_dict:
137 137 for val in state_dict['ext.mutable.values']:
138 138 val._parents[state.obj()] = key
139 139
140 140 sqlalchemy.event.listen(parent_cls, 'load', load, raw=True,
141 141 propagate=True)
142 142 sqlalchemy.event.listen(parent_cls, 'refresh', load, raw=True,
143 143 propagate=True)
144 144 sqlalchemy.event.listen(parent_cls, 'pickle', pickle, raw=True,
145 145 propagate=True)
146 146 sqlalchemy.event.listen(attribute, 'set', set, raw=True, retval=True,
147 147 propagate=True)
148 148 sqlalchemy.event.listen(parent_cls, 'unpickle', unpickle, raw=True,
149 149 propagate=True)
150 150
151 151
152 152 class MutationDict(MutationObj, DictClass):
153 153 @classmethod
154 154 def coerce(cls, key, value):
155 155 """Convert plain dictionary to MutationDict"""
156 156 self = MutationDict(
157 157 (k, MutationObj.coerce(key, v)) for (k, v) in value.items())
158 158 self._key = key
159 159 return self
160 160
161 161 def __setitem__(self, key, value):
162 162 # Due to the way OrderedDict works, this is called during __init__.
163 163 # At this time we don't have a key set, but what is more, the value
164 164 # being set has already been coerced. So special case this and skip.
165 165 if hasattr(self, '_key'):
166 166 value = MutationObj.coerce(self._key, value)
167 167 DictClass.__setitem__(self, key, value)
168 168 self.changed()
169 169
170 170 def __delitem__(self, key):
171 171 DictClass.__delitem__(self, key)
172 172 self.changed()
173 173
174 def __setstate__(self, state):
175 self.__dict__ = state
176
177 def __reduce_ex__(self, proto):
178 # support pickling of MutationDicts
179 d = dict(self)
180 return (self.__class__, (d, ))
181
174 182
175 183 class MutationList(MutationObj, list):
176 184 @classmethod
177 185 def coerce(cls, key, value):
178 186 """Convert plain list to MutationList"""
179 187 self = MutationList((MutationObj.coerce(key, v) for v in value))
180 188 self._key = key
181 189 return self
182 190
183 191 def __setitem__(self, idx, value):
184 192 list.__setitem__(self, idx, MutationObj.coerce(self._key, value))
185 193 self.changed()
186 194
187 195 def __setslice__(self, start, stop, values):
188 196 list.__setslice__(self, start, stop,
189 197 (MutationObj.coerce(self._key, v) for v in values))
190 198 self.changed()
191 199
192 200 def __delitem__(self, idx):
193 201 list.__delitem__(self, idx)
194 202 self.changed()
195 203
196 204 def __delslice__(self, start, stop):
197 205 list.__delslice__(self, start, stop)
198 206 self.changed()
199 207
200 208 def append(self, value):
201 209 list.append(self, MutationObj.coerce(self._key, value))
202 210 self.changed()
203 211
204 212 def insert(self, idx, value):
205 213 list.insert(self, idx, MutationObj.coerce(self._key, value))
206 214 self.changed()
207 215
208 216 def extend(self, values):
209 217 list.extend(self, (MutationObj.coerce(self._key, v) for v in values))
210 218 self.changed()
211 219
212 220 def pop(self, *args, **kw):
213 221 value = list.pop(self, *args, **kw)
214 222 self.changed()
215 223 return value
216 224
217 225 def remove(self, value):
218 226 list.remove(self, value)
219 227 self.changed()
220 228
221 229
222 230 def JsonType(impl=None, **kwargs):
223 231 """
224 232 Helper for using a mutation obj, it allows to use .with_variant easily.
225 233 example::
226 234
227 235 settings = Column('settings_json',
228 236 MutationObj.as_mutable(
229 237 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
230 238 """
231 239
232 240 if impl == 'list':
233 241 return JSONEncodedObj(default=list, **kwargs)
234 242 elif impl == 'dict':
235 243 return JSONEncodedObj(default=DictClass, **kwargs)
236 244 else:
237 245 return JSONEncodedObj(**kwargs)
238 246
239 247
240 248 JSON = MutationObj.as_mutable(JsonType())
241 249 """
242 250 A type to encode/decode JSON on the fly
243 251
244 252 sqltype is the string type for the underlying DB column::
245 253
246 254 Column(JSON) (defaults to UnicodeText)
247 255 """
248 256
249 257 JSONDict = MutationObj.as_mutable(JsonType('dict'))
250 258 """
251 259 A type to encode/decode JSON dictionaries on the fly
252 260 """
253 261
254 262 JSONList = MutationObj.as_mutable(JsonType('list'))
255 263 """
256 264 A type to encode/decode JSON lists` on the fly
257 265 """
General Comments 0
You need to be logged in to leave comments. Login now